From be4c46420944531765ecc8bae7305086d71a36d0 Mon Sep 17 00:00:00 2001 From: Marek Szwalkiewicz Date: Thu, 30 Jan 2020 13:49:18 +0000 Subject: Add Artifact Manager service. Adds a micro service that offers gRPC interface for CBA artifacts manipulation. By default the service is attached to py-executor but can be ran as a separate service if needed in the future. Issue-ID: CCSDK-1988 Change-Id: I40e20f085ae1c1e81a48f76dbea181af28d9bd0d Signed-off-by: Marek Szwalkiewicz --- ms/artifact-manager/manager/servicer.py | 237 ++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 ms/artifact-manager/manager/servicer.py (limited to 'ms/artifact-manager/manager/servicer.py') diff --git a/ms/artifact-manager/manager/servicer.py b/ms/artifact-manager/manager/servicer.py new file mode 100644 index 000000000..be740b0e3 --- /dev/null +++ b/ms/artifact-manager/manager/servicer.py @@ -0,0 +1,237 @@ +"""Copyright 2019 Deutsche Telekom. + +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 socket +from datetime import datetime, timezone +from functools import wraps +from logging import Logger +from typing import NoReturn, Union + +from grpc import ServicerContext +from manager.configuration import get_logger +from manager.errors import ArtifactManagerError, InvalidRequestError +from manager.utils import Repository, RepositoryStrategy +from onaplogging.mdcContext import MDC +from proto.BluePrintManagement_pb2 import ( + BluePrintDownloadInput, + BluePrintManagementOutput, + BluePrintRemoveInput, + BluePrintUploadInput, +) +from proto.BluePrintManagement_pb2_grpc import BluePrintManagementServiceServicer + +MDC_DATETIME_FORMAT = r"%Y-%m-%dT%H:%M:%S.%f%z" +COMMON_HEADER_DATETIME_FORMAT = r"%Y-%m-%dT%H:%M:%S.%fZ" + + +def fill_common_header(func): + """Decorator to fill handler's output values which is the same type for each handler. + + It copies commonHeader from request object and set timestamp value. + + :param func: Handler function + :return: _handler decorator callable object + """ + + @wraps(func) + def _decorator( + servicer: "ArtifactManagerServicer", + request: Union[BluePrintDownloadInput, BluePrintRemoveInput, BluePrintUploadInput], + context: ServicerContext, + ) -> BluePrintManagementOutput: + + if not all([request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion]): + raise InvalidRequestError("Request has to have set both BluePrint name and version") + output: BluePrintManagementOutput = func(servicer, request, context) + # Set same values for every handler + output.commonHeader.CopyFrom(request.commonHeader) + output.commonHeader.timestamp = datetime.utcnow().strftime(COMMON_HEADER_DATETIME_FORMAT) + return output + + return _decorator + + +def translate_exception_to_response(func): + """Decorator that translates Artifact Manager exceptions into proper responses. + + :param func: Handler function + :return: _handler decorator callable object + """ + + @wraps(func) + def _handler( + servicer: "ArtifactManagerServicer", + request: Union[BluePrintDownloadInput, BluePrintRemoveInput, BluePrintUploadInput], + context: ServicerContext, + ) -> BluePrintManagementOutput: + try: + output: BluePrintManagementOutput = func(servicer, request, context) + output.status.code = 200 + output.status.message = "success" + except ArtifactManagerError as error: + # If ArtifactManagerError is raises one of defined error occurs. + # Every ArtifactManagerError based exception has status_code paramenter + # which has to be set in output. Use also exception's message to + # set errorMessage of the output. + output: BluePrintManagementOutput = BluePrintManagementOutput() + output.status.code = error.status_code + output.status.message = "failure" + output.status.errorMessage = str(error.message) + + servicer.fill_MDC_timestamps() + servicer.logger.error( + "Error while processing the message - blueprintName={} blueprintVersion={}".format( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ), + extra={"mdc": MDC.result()}, + ) + MDC.clear() + return output + + return _handler + + +def prepare_logging_context(func): + """Decorator that prepares MDC logging context for logs inside the handler. + + :param func: Handler function + :return: _handler decorator callable object + """ + + @wraps(func) + def _decorator( + servicer: "ArtifactManagerServicer", + request: Union[BluePrintDownloadInput, BluePrintRemoveInput, BluePrintUploadInput], + context: ServicerContext, + ) -> BluePrintManagementOutput: + MDC.put("RequestID", request.commonHeader.requestId) + MDC.put("InvocationID", request.commonHeader.subRequestId) + MDC.put("ServiceName", servicer.__class__.__name__) + MDC.put("PartnerName", request.commonHeader.originatorId) + started_at = datetime.utcnow().replace(tzinfo=timezone.utc) + MDC.put("BeginTimestamp", started_at.strftime(MDC_DATETIME_FORMAT)) + + # Adding processing_started_at to the servicer so later we'll have the data to calculate elapsed time. + servicer.processing_started_at = started_at + + MDC.put("TargetEntity", "py-executor") + MDC.put("TargetServiceName", func.__name__) + MDC.put("Server", socket.getfqdn()) + + output: BluePrintManagementOutput = func(servicer, request, context) + MDC.clear() + return output + + return _decorator + + +class ArtifactManagerServicer(BluePrintManagementServiceServicer): + """ArtifactManagerServer class. + + Implements methods defined in proto files to manage artifacts repository. + These methods are: download, upload and remove. + """ + + processing_started_at = None + + def __init__(self) -> NoReturn: + """Instance of ArtifactManagerServer class initialization. + + Create logger for class using class name and set configuration property. + """ + self.logger: Logger = get_logger(self.__class__.__name__) + self.repository: Repository = RepositoryStrategy.get_reporitory() + + def fill_MDC_timestamps(self, status_code: int = 200) -> NoReturn: + """Add MDC context timestamps "in place". + + :param status_code: int with expected response status. Default: 200 (success) + """ + now = datetime.utcnow().replace(tzinfo=timezone.utc) + MDC.put("EndTimestamp", now.strftime(MDC_DATETIME_FORMAT)) + + # Elapsed time measured in miliseconds + MDC.put("ElapsedTime", (now - self.processing_started_at).total_seconds() * 1000) + + MDC.put("StatusCode", status_code) + + @prepare_logging_context + @translate_exception_to_response + @fill_common_header + def downloadBlueprint(self, request: BluePrintDownloadInput, context: ServicerContext) -> BluePrintManagementOutput: + """Download blueprint file request method. + + Currently it only logs when is called and all base class method. + :param request: BluePrintDownloadInput + :param context: ServicerContext + :return: BluePrintManagementOutput + """ + output: BluePrintManagementOutput = BluePrintManagementOutput() + output.fileChunk.chunk = self.repository.download_blueprint( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ) + self.fill_MDC_timestamps() + self.logger.info( + "Blueprint download successfuly processed - blueprintName={} blueprintVersion={}".format( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ), + extra={"mdc": MDC.result()}, + ) + return output + + @prepare_logging_context + @translate_exception_to_response + @fill_common_header + def uploadBlueprint(self, request: BluePrintUploadInput, context: ServicerContext) -> BluePrintManagementOutput: + """Upload blueprint file request method. + + Currently it only logs when is called and all base class method. + :param request: BluePrintUploadInput + :param context: ServicerContext + :return: BluePrintManagementOutput + """ + self.repository.upload_blueprint( + request.fileChunk.chunk, request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ) + self.fill_MDC_timestamps() + self.logger.info( + "Blueprint upload successfuly processed - blueprintName={} blueprintVersion={}".format( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ), + extra={"mdc": MDC.result()}, + ) + return BluePrintManagementOutput() + + @prepare_logging_context + @translate_exception_to_response + @fill_common_header + def removeBlueprint(self, request: BluePrintRemoveInput, context: ServicerContext) -> BluePrintManagementOutput: + """Remove blueprint file request method. + + Currently it only logs when is called and all base class method. + :param request: BluePrintRemoveInput + :param context: ServicerContext + :return: BluePrintManagementOutput + """ + self.repository.remove_blueprint( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ) + self.fill_MDC_timestamps() + self.logger.info( + "Blueprint removal successfuly processed - blueprintName={} blueprintVersion={}".format( + request.actionIdentifiers.blueprintName, request.actionIdentifiers.blueprintVersion + ), + extra={"mdc": MDC.result()}, + ) + return BluePrintManagementOutput() -- cgit 1.2.3-korg