diff --git a/poppy/transport/pecan/controllers/v1/services.py b/poppy/transport/pecan/controllers/v1/services.py index 43ab6bb0..d8818115 100644 --- a/poppy/transport/pecan/controllers/v1/services.py +++ b/poppy/transport/pecan/controllers/v1/services.py @@ -13,6 +13,53 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Pecan router to map service related urls. + +Each class in this module maps to different urls. + + - Purge service [:class:`ServiceAssetsController` ] + - Retrieve analytical metrics for domain [:class:`ServicesAnalyticsController` ] + - GET/Create/Delete/Update service [:class:`ServicesController` ] + +Mappings:- + +Each HTTP method is mapped to Pecan's method as shown below. + +Pecan Method HttpMethod/URL +------------- ------------------ +get_one -> GET /services/ +get_all -> GET /services/ +get -> GET /services/ or GET /services/ +post -> POST /services/ +put -> PUT /services/ +delete -> DELETE /services/ + +Example:- + + - The URL ``{{host}}/v1.0/services/ with HTTP POST`` will be received by + :py:func:`ServicesController.post`` + - The URL ``{{host}}/v1.0/services/ with HTTP POST`` will be received by + :py:func:`ServicesController.get_all`` + +Each class in the module have Enabled Context Hook and Errors Hook. +`Context Hook` checks that `X-Project-ID` and `X-Auth-Token` +are present in the request payload and constructs `base_url`. +`Errors Hook` handles any errors during the request. + +``validate`` decorators are injected into each method of the class +to validate the payload and other dependencies. If any of the +validation fails, operation will be aborted and ``Errors Hook`` +will be responsible for sending error response to the user. + +After doing the base level validations on the request +payload, calls will be delegated to Manager layer to +process the request. The Default Manager layer has +various controllers to handle these requests. + +For more details on how the top level URL mapping is done, refer to + :py:mod:`poppy/poppy/transport/pecan/driver.py` +""" + import ast import json import uuid @@ -45,6 +92,7 @@ class ServiceAssetsController(base.Controller, hooks.HookController): + """Controller to purge the contents of service""" __hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()] @@ -55,6 +103,29 @@ class ServiceAssetsController(base.Controller, hooks.HookController): helpers.abort_with_message) ) def delete(self, service_id): + """Purge contents of a service. + + Purge the content when it is in the provider + network. Based on the ``purge_url`` in the request, + content will be purged. + + For example: When the ``purge_url`` is set to + ``/images/*``, all the images present in the + the path will be purged. + + Note that if the ``purge_url`` is None, all the + contents of the service will be purged. + + The default manager will invoke ``purge taskflow`` + to do this operation. + + :param unicode service_id: ID of the service + :return: Pecan's 200 response if the ``purge taskflow`` + is successfully invoked. 404 if the service not + found. 400 response if the request payload is + incompatible. + :rtype: pecan.Response + """ purge_url = pecan.request.GET.get('url', '/*') purge_all = pecan.request.GET.get('all', False) hard = pecan.request.GET.get('hard', 'True') @@ -96,6 +167,9 @@ def delete(self, service_id): class ServicesAnalyticsController(base.Controller, hooks.HookController): + """Controller to process and return Metrics for a given + service. + """ __hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()] @@ -110,8 +184,28 @@ class ServicesAnalyticsController(base.Controller, hooks.HookController): stoplight_helpers.pecan_getter) ) def get(self, service_id): - '''Get Analytics By Domain Data''' + """Get Metrics for a given service ID. + + The below keys are expected in the payload. + - metricType + - startTime + - endTime + - metrics_controller + + Example return: + Pecan will serialize the below dict and sends along + with 200 response. + + ``{'provider': '', 'flavor':'', 'domain':'', 'metricType': {}}`` + + :param unicode service_id: ID of the service + :return: Pecan's 200 response if successfully retrieved + analytics for the service. 404 response if the service + was not found or Provider details not found. 500 response + for general server side exceptions. + :rtype: pecan.Response + """ call_args = getattr(pecan.request.context, "call_args") domain = call_args.pop('domain') @@ -134,6 +228,11 @@ def get(self, service_id): class ServicesController(base.Controller, hooks.HookController): + """Handles typical CRUD operations on Services. + + When Manager layer returns an output/ or any Exception + raised, It serializes the responses and returns to user. + """ __hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()] @@ -153,6 +252,24 @@ def __init__(self, driver): @pecan.expose('json') def get_all(self): + """Get all the services in Poppy. + + Example URL to create service: + -``{{host}}/v1.0/services/`` with HTTP method as `GET` + + There is a limit on number of services that can + be fetched at a time. This limit can be configured + through ``poppy.conf`` by setting an integer value + for ``max_services_per_page``. + + :return: Dictionary containing lists of links and + serialized Service objects + :rtype: dict + :raise ValueError: If the request `limit` value + is more than configured ``max_services_per_page`` + Or If the request `marker` is not + a valid UUID + """ marker = pecan.request.GET.get('marker', None) limit = pecan.request.GET.get('limit', 10) try: @@ -202,6 +319,19 @@ def get_all(self): helpers.abort_with_message) ) def get_one(self, service_id): + """Get a Service details by its ID. + + Example URL to get service: + -``{{host}}/v1.0/services/`` + with HTTP method as `GET` + + :param unicode service_id: Id of the service + + :return: Service object serialized into OrderedDict + :rtype: collections.OrderedDict + :raise ValueError: If there was not any service + for the given ID. + """ services_controller = self._driver.manager.services_controller try: service_obj = services_controller.get_service( @@ -220,6 +350,68 @@ def get_one(self, service_id): helpers.abort_with_message, stoplight_helpers.pecan_getter)) def post(self): + """Create a new service. + + Example URL to create service: + -``{{host}}/v1.0/services/`` with HTTP method as `POST` + + An example payload for this POST request: + + .. code-block:: python + + { + "name": "ServiceName0001", + "domains": + [ + { + "domain": "test.domain.com", + "protocol": "http" + } + ], + "origins": + [ + { + "origin": "www.example.com", + "port": 80, + "ssl": false + } + ], + "caching": [ + { + "name": "default", + "ttl": 3600 + } + ], + "flavor_id": "premium", + "restrictions": [ + ] + } + + The payload must have at least one domain and one origin. + The request to create a new service will be + delegated to Default Manager service controller + that does the below. + + - A service dictionary gets created in Cassandra + - Async tasks to create DNS mappings + - Async tasks to create provider policies + - Based on `Enqueue` flag, request to create SSL + certificate will be enqueued in mod_san_queue or + a certificate will be created for HTTPS domains. + Enqueue flag is set to `True` by default. + + Create service will be aborted if .. + - All the available shards are exhausted + - No flavor is created in Cassandra + - If the service name already exists + - Services count is exceeding the limit + + In all the above cases, Pecan sends a 400 error. + + :return: Pecan's 200 response if the service was + successfully created, 400 response otherwise. + :rtype: pecan.Response + """ services_controller = self._driver.manager.services_controller service_json_dict = json.loads(pecan.request.body.decode('utf-8')) service_id = None @@ -254,6 +446,22 @@ def post(self): helpers.abort_with_message) ) def delete(self, service_id): + """Delete a service for a given service ID. + + Example URL to create service: + -``{{host}}/v1.0/service/`` + with HTTP method as `DELETE` + + Deleting service will trigger the below tasks + - Deleting service dictionary from Cassandra + - Deleting DNS mappings + - Deleting associated certificates for each domain in the service + + :param unicode service_id: Id of the service to delete + :return: Pecan's 202 response if the delete operation was + successful. Else, Pecan's 400 response will be returned. + :rtype: pecan.Response + """ services_controller = self._driver.manager.services_controller try: @@ -276,6 +484,37 @@ def delete(self, service_id): helpers.abort_with_message, stoplight_helpers.pecan_getter)) def patch_one(self, service_id): + """Update service. + + Example URL to create service: + -``{{host}}/v1.0/services/`` + with HTTP method as `PATCH` + + For payload, refer to :meth:`post()`. + + Updating service is two-step process. It filters out + the payload and detects newly added domains, deleted + old domains. For newly added domains, it follows the + :meth:`post()` workflow. For deleted domains it + follows :meth:`delete()` workflow. If the service + update involves anything other than domains (ex: + renaming the service etc..) it updated the service + dictionary in cassandra. + + The update operation will be aborted if .. + - If any validations failed in request Payload + - No flavor detected in cassandra + - No service found + - Conflict names while renaming service + - If the service is in invalid states + - Exhausted shard domains + + :param unicode service_id: ID of the service to update + :return: Pecan's 200 response if the service was updated + successfully. 404 response if the service was not found. + In other cases, 400 response will be returned. + :rtype: pecan.Response + """ service_updates = json.loads(pecan.request.body.decode('utf-8')) services_controller = self._driver.manager.services_controller