summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarek Szwalkiewicz <marek.szwalkiewicz@external.t-mobile.pl>2020-01-30 13:49:18 +0000
committerMarek Szwalkiewicz <marek.szwalkiewicz@external.t-mobile.pl>2020-01-30 13:52:07 +0000
commitbe4c46420944531765ecc8bae7305086d71a36d0 (patch)
treebe9309a134a50e964b1257395d74c41c2da512ef
parentda25e1649c9a10f998c8dde068641d7601a3f00a (diff)
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 <marek.szwalkiewicz@external.t-mobile.pl>
-rw-r--r--ms/artifact-manager/README199
-rw-r--r--ms/artifact-manager/manager/__init__.py14
-rw-r--r--ms/artifact-manager/manager/configuration.py139
-rw-r--r--ms/artifact-manager/manager/errors.py64
-rw-r--r--ms/artifact-manager/manager/servicer.py237
-rw-r--r--ms/artifact-manager/manager/utils.py176
-rw-r--r--ms/artifact-manager/requirements/docker.txt2
-rw-r--r--ms/artifact-manager/requirements/local.txt2
-rw-r--r--ms/artifact-manager/requirements/shared.txt3
-rw-r--r--ms/artifact-manager/requirements/test.txt (renamed from ms/py-executor/test-requirements.txt)2
-rw-r--r--ms/artifact-manager/server.py47
-rw-r--r--ms/artifact-manager/setup.py25
-rw-r--r--ms/artifact-manager/tests/base_test.py19
-rw-r--r--ms/artifact-manager/tests/configuration-test.ini4
-rw-r--r--ms/artifact-manager/tests/configuration_test.py47
-rw-r--r--ms/artifact-manager/tests/servicer_test.py196
-rw-r--r--ms/artifact-manager/tests/utils_test.py59
-rw-r--r--ms/artifact-manager/tox.ini27
-rw-r--r--ms/configuration-local.ini (renamed from ms/py-executor/configuration-local.ini)8
-rw-r--r--ms/configuration.ini (renamed from ms/py-executor/configuration.ini)8
-rw-r--r--ms/logging.yaml14
-rw-r--r--ms/py-executor/blueprints_grpc/__init__.py4
-rw-r--r--ms/py-executor/blueprints_grpc/blueprint_processing_server.py2
-rw-r--r--ms/py-executor/blueprints_grpc/executor_utils.py28
-rw-r--r--ms/py-executor/blueprints_grpc/script_executor_configuration.py4
-rw-r--r--ms/py-executor/client.py6
-rw-r--r--ms/py-executor/docker/Dockerfile2
-rwxr-xr-xms/py-executor/docker/distribution.xml33
-rwxr-xr-xms/py-executor/docker/start.sh19
-rw-r--r--ms/py-executor/requirements/docker.txt4
-rw-r--r--ms/py-executor/requirements/local.txt4
-rw-r--r--ms/py-executor/requirements/shared.txt (renamed from ms/py-executor/requirements.txt)2
-rw-r--r--ms/py-executor/requirements/test.txt3
-rw-r--r--ms/py-executor/resource_resolution/README8
-rw-r--r--ms/py-executor/resource_resolution/client.py7
-rw-r--r--ms/py-executor/server.py29
-rw-r--r--ms/py-executor/tox.ini8
-rw-r--r--py-modules/common/proto/BluePrintCommon_pb2.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2.py)0
-rw-r--r--py-modules/common/proto/BluePrintCommon_pb2_grpc.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2_grpc.py)0
-rw-r--r--py-modules/common/proto/BluePrintManagement_pb2.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2.py)2
-rw-r--r--py-modules/common/proto/BluePrintManagement_pb2_grpc.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2_grpc.py)2
-rw-r--r--py-modules/common/proto/BluePrintProcessing_pb2.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2.py)2
-rw-r--r--py-modules/common/proto/BluePrintProcessing_pb2_grpc.py (renamed from ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2_grpc.py)2
-rw-r--r--py-modules/common/proto/__init__.py (renamed from ms/py-executor/blueprints_grpc/proto/__init__.py)0
-rw-r--r--py-modules/common/setup.py25
45 files changed, 1442 insertions, 46 deletions
diff --git a/ms/artifact-manager/README b/ms/artifact-manager/README
new file mode 100644
index 000000000..290dadfde
--- /dev/null
+++ b/ms/artifact-manager/README
@@ -0,0 +1,199 @@
+# CDS Artifact Manager
+
+Artifact Manager is a very simple gRPC service that lets you upload, download and delete CBA archives. It can be ran as a standalone micro service (using `server.py`) or you can include it's methods in a service like `py-executor`.
+
+## Configuration
+Configuration is stored in `.ini` file, you can specify a path and name of a file using `CONFIGURATION` env variable.
+For possible variables please see example below (with inline comments):
+```
+[artifactManagerServer]
+port=50052 # A port on which the server will be listening
+logFile=server.log # Path to a log file
+maxWorkers=20 # Max number of concurent workers
+debug=true # Debug flag
+logConfig=logging.yaml # A special MDC logger config
+fileRepositoryBasePath=/tmp/ # A FS path where we should store CBA files
+```
+
+## Methods
+Below is a list of gRPC methods handled by the service. The `proto` files are available in `artifact-manager/manager/proto` directory.
+
+All methods expect `CommonHeader` with:
+* `timestamp` - datetime in UTC with this: `%Y-%m-%dT%H:%M:%S.%fZ` format
+* `originatorId` - name of the component (eg. `CDS`)
+* `requestId` - ID of the request
+* `subRequestId` - Sub ID of the request
+* `flag` - TBD
+
+and an `ActionIdentifiers` with following fields:
+* `blueprintName` - name of the blueprint
+* `blueprintVersion` - version number of blueprint (as string)
+* `actionName` - TBD
+* `mode` - TBD
+
+### Upload
+
+Upload `CBA.zip` file for storage in artifact manager. File needs to be sent as a binary data in `fileChunk` field.
+
+#### Example
+
+```
+stub: BluePrintManagementServiceStub = BluePrintManagementServiceStub(channel)
+with open(file_path, "rb") as cba_file:
+ msg: BluePrintUploadInput = BluePrintUploadInput()
+ msg.actionIdentifiers.blueprintName = "Test"
+ msg.actionIdentifiers.blueprintVersion = "0.0.1"
+ msg.fileChunk.chunk = cba_file.read()
+return stub.uploadBlueprint(msg)
+```
+
+### Download
+
+Download existing `CBA.zip` file.
+
+#### Example
+
+```
+stub: BluePrintManagementServiceStub = BluePrintManagementServiceStub(channel)
+msg: BluePrintDownloadInput = BluePrintDownloadInput()
+msg.actionIdentifiers.blueprintName = "Test"
+msg.actionIdentifiers.blueprintVersion = "0.0.1"
+return stub.downloadBlueprint(msg)
+```
+### Remove
+
+Delete existing `CBA.zip` file.
+
+#### Example
+
+```
+stub: BluePrintManagementServiceStub = BluePrintManagementServiceStub(channel)
+msg: BluePrintRemoveInput = BluePrintRemoveInput()
+msg.actionIdentifiers.blueprintName = "Test"
+msg.actionIdentifiers.blueprintVersion = "0.0.1"
+return stub.removeBlueprint(msg)
+```
+
+## Full gRPC Client Example
+
+```
+import logging
+import sys
+from argparse import ArgumentParser, FileType, Namespace
+from configparser import ConfigParser
+from datetime import datetime
+from pathlib import Path
+
+import zipfile
+
+from grpc import Channel, ChannelCredentials, insecure_channel, secure_channel, ssl_channel_credentials
+
+from proto.BluePrintManagement_pb2 import (
+ BluePrintDownloadInput,
+ BluePrintRemoveInput,
+ BluePrintUploadInput,
+ BluePrintManagementOutput,
+)
+from proto.BluePrintManagement_pb2_grpc import BluePrintManagementServiceStub
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class ClientArgumentParser(ArgumentParser):
+ """Client argument parser.
+
+ It has two arguments:
+ - config_file - provide a path to configuration file. Default is ./configuration-local.ini
+ - actions - list of actions to do by client. It have to be a list of given values: upload, download, remove.
+ """
+
+ DEFAULT_CONFIG_PATH: str = str(Path(__file__).resolve().with_name("configuration-local.ini"))
+
+ def __init__(self, *args, **kwargs):
+ """Initialize argument parser."""
+ super().__init__(*args, **kwargs)
+ self.description: str = "Artifact Manager client example"
+
+ self.add_argument(
+ "--config_file",
+ type=FileType("r"),
+ default=self.DEFAULT_CONFIG_PATH,
+ help="Path to the client configuration file. By default it's `configuration-local.ini` file from Artifact Manager directory",
+ )
+ self.add_argument(
+ "--actions", nargs="+", default=["upload", "download", "remove"], choices=["upload", "download", "remove"]
+ )
+
+
+class Client:
+ """Client class.
+
+ Implements methods which can be called to server.
+ """
+
+ def __init__(self, channel: Channel, config: ConfigParser) -> None:
+ """Initialize client class.
+
+ :param channel: gprc channel object
+ :param config: ConfigParser object with "client" section
+ """
+ self.channel: Channel = channel
+ self.stub: BluePrintManagementServiceStub = BluePrintManagementServiceStub(self.channel)
+ self.config = config
+
+ def upload(self) -> BluePrintManagementOutput:
+ """Prepare upload message and send it to server."""
+ logging.info("Call upload client method")
+ with open(self.config.get("client", "cba_file"), "rb") as cba_file:
+ msg: BluePrintUploadInput = BluePrintUploadInput()
+ msg.actionIdentifiers.blueprintName = "Test"
+ msg.actionIdentifiers.blueprintVersion = "0.0.1"
+ msg.fileChunk.chunk = cba_file.read()
+ return self.stub.uploadBlueprint(msg)
+
+ def download(self) -> BluePrintManagementOutput:
+ """Prepare download message and send it to server."""
+ logging.info("Call download client method")
+ msg: BluePrintDownloadInput = BluePrintDownloadInput()
+ msg.actionIdentifiers.blueprintName = "Test"
+ msg.actionIdentifiers.blueprintVersion = "0.0.1"
+ return self.stub.downloadBlueprint(msg)
+
+ def remove(self) -> BluePrintManagementOutput:
+ """Prepare remove message and send it to server."""
+ logging.info("Call remove client method")
+ msg: BluePrintRemoveInput = BluePrintRemoveInput()
+ msg.actionIdentifiers.blueprintName = "Test"
+ msg.actionIdentifiers.blueprintVersion = "0.0.1"
+ return self.stub.removeBlueprint(msg)
+
+
+if __name__ == "__main__":
+ arg_parser: ClientArgumentParser = ClientArgumentParser()
+ args: Namespace = arg_parser.parse_args()
+
+ config_parser: ConfigParser = ConfigParser()
+ config_parser.read_file(args.config_file)
+
+ server_address: str = f"{config_parser.get('client', 'address')}:{config_parser.get('client', 'port')}"
+ if config_parser.getboolean("client", "use_ssl", fallback=False):
+ logging.info(f"Create secure connection on {server_address}")
+ with open(config_parser.get("client", "private_key_file"), "rb") as private_key_file, open(
+ config_parser.get("client", "certificate_chain_file"), "rb"
+ ) as certificate_chain_file:
+ ssl_credentials: ChannelCredentials = ssl_channel_credentials(
+ private_key=private_key_file.read(), certificate_chain=certificate_chain_file.read()
+ )
+ channel: Channel = secure_channel(server_address, ssl_credentials)
+ else:
+ logging.info(f"Create insecure connection on {server_address}")
+ channel: Channel = insecure_channel(server_address)
+
+ with channel:
+ client: Client = Client(channel, config_parser)
+ for action in args.actions:
+ logging.info("Get response")
+ logging.info(getattr(client, action)())
+
+``` \ No newline at end of file
diff --git a/ms/artifact-manager/manager/__init__.py b/ms/artifact-manager/manager/__init__.py
new file mode 100644
index 000000000..21236908e
--- /dev/null
+++ b/ms/artifact-manager/manager/__init__.py
@@ -0,0 +1,14 @@
+"""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.
+"""
diff --git a/ms/artifact-manager/manager/configuration.py b/ms/artifact-manager/manager/configuration.py
new file mode 100644
index 000000000..0af2c22cc
--- /dev/null
+++ b/ms/artifact-manager/manager/configuration.py
@@ -0,0 +1,139 @@
+"""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 logging
+import os
+from configparser import ConfigParser, SectionProxy
+from distutils.util import strtobool
+from logging import Logger
+from pathlib import Path, PurePath
+from typing import NoReturn
+
+from onaplogging import monkey
+from onaplogging.mdcformatter import MDCFormatter # noqa
+
+monkey.patch_loggingYaml()
+
+
+DEFAUL_CONFIGURATION_FILE: str = str(PurePath(Path().absolute(), "../configuration.ini"))
+SUPPLIED_CONFIGURATION_FILE: str = os.environ.get("CONFIGURATION")
+CONFIGURATION_FILE: str = str(os.path.expanduser(Path(SUPPLIED_CONFIGURATION_FILE or DEFAUL_CONFIGURATION_FILE)))
+
+
+class ArtifactManagerConfiguration:
+ """ServerConfiguration class loads configuration from config INI files."""
+
+ def __init__(self, config_file_path: str) -> NoReturn:
+ """Initialize configuration class instance.
+
+ Configuration is loaded from file provided as a parameter. Environment variables are loaded also.
+ Logger for object is created with the name same as the class name.
+ :param config_file_path: Path to configuration file.
+ """
+ self.config_file_path = config_file_path
+ self.config = ConfigParser(os.environ)
+ self.config.read(config_file_path, encoding="utf-8")
+
+ @property
+ def configuration_directory(self) -> str:
+ """Get directory path to a directory with configuration ini file.
+
+ This is used to handle relative file paths in config file.
+ """
+ return os.path.dirname(self.config_file_path)
+
+ def get_section(self, section_name: str) -> SectionProxy:
+ """Get the section from configuration file.
+
+ :param section_name: Name of the section to get
+ :raises: KeyError
+ :return: SectionProxy object for given section name
+ """
+ return self.config[section_name]
+
+ def __getitem__(self, key: str) -> SectionProxy:
+ """Get the section from configuration file.
+
+ This method just calls the get_section method but allows us to use it as key lookup
+
+ :param section_name: Name of the section to get
+ :raises: KeyError
+ :return: SectionProxy object for given section name
+ """
+ return self.get_section(key)
+
+ def get_property(self, section_name: str, property_name: str) -> str:
+ """Get the property value from *section_name* section.
+
+ :param section_name: Name of the section config file section on which property is set
+ :param property_name: Name of the property to get
+ :raises: configparser.NoOptionError
+ :return: String value of the property
+ """
+ return self.config.get(section_name, property_name)
+
+ def artifact_manager_property(self, property_name: str) -> str:
+ """Get the property value from *artifactManagerServer* section.
+
+ :param property_name: Name of the property to get
+ :raises: configparser.NoOptionError
+ :return: String value of the property
+ """
+ return self.config.get("artifactManagerServer", property_name)
+
+
+config = ArtifactManagerConfiguration(CONFIGURATION_FILE)
+
+
+def prepare_logger(log_file_path: str, development_mode: bool, config: ArtifactManagerConfiguration) -> callable:
+ """Base MDC logger configuration.
+
+ Level depends on the *development_mode* flag: DEBUG if development_mode is set or INFO otherwise.
+ Console handler is created from MDC settings from onappylog library.
+
+ :param log_file_path: Path to the log file, where logs are going to be saved.
+ :param development_mode: Boolean type value which means if logger should be setup in development mode or not
+ :param config: Configuration class so we can fetch app settings (paths) to logger.
+ :return: callable
+ """
+ logging_level: int = logging.DEBUG if development_mode else logging.INFO
+ logging.basicConfig(filename=log_file_path, level=logging_level)
+ logging.config.yamlConfig(
+ filepath=Path(config.configuration_directory, config["artifactManagerServer"]["logConfig"])
+ )
+
+ console: logging.StreamHandler = logging.StreamHandler()
+ console.setLevel(logging_level)
+ formatter: logging.Formatter = MDCFormatter(
+ fmt="%(asctime)s:[%(name)s] %(created)f %(module)s %(funcName)s %(pathname)s %(process)d %(levelno)s :[ %(threadName)s %(thread)d]: [%(mdc)s]: [%(filename)s]-[%(lineno)d] [%(levelname)s]:%(message)s", # noqa
+ mdcfmt="{RequestID} {InvocationID} {ServiceName} {PartnerName} {BeginTimestamp} {EndTimestamp} {ElapsedTime} {StatusCode} {TargetEntity} {TargetServiceName} {Server}", # noqa
+ # Important: We cannot use %f here because this datetime format is used by time library, not datetime.
+ datefmt="%Y-%m-%dT%H:%M:%S%z",
+ )
+ console.setFormatter(formatter)
+
+ def get_logger(name: str) -> Logger:
+ """Get a new logger with predefined MDC handler."""
+ logger: Logger = logging.getLogger(name)
+ logger.addHandler(console)
+ return logger
+
+ return get_logger
+
+
+get_logger = prepare_logger(
+ config.artifact_manager_property("logFile"),
+ strtobool(config["artifactManagerServer"].get("debug", "false")),
+ config,
+)
diff --git a/ms/artifact-manager/manager/errors.py b/ms/artifact-manager/manager/errors.py
new file mode 100644
index 000000000..feafd7668
--- /dev/null
+++ b/ms/artifact-manager/manager/errors.py
@@ -0,0 +1,64 @@
+"""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.
+"""
+
+
+class ArtifactManagerError(Exception):
+ """Base Artifact Manager exception class."""
+
+ status_code: int = 0
+ message: str = "Error"
+
+ def __init__(self, message: str = None) -> None:
+ """Initialize exception with optional message."""
+ if message:
+ self.message: str = message
+
+ @property
+ def status_code(self) -> int:
+ """Artifact Manager error class status code.
+
+ Base class has no and shouldn't have any status code.
+ """
+ if self.status_code == 0:
+ raise NotImplementedError
+ return self.status_code
+
+
+class InvalidRequestError(ArtifactManagerError):
+ """Raised when request has invalid or incomplete data."""
+
+ status_code: int = 500
+ message: str = "Invalid request"
+
+
+class ArtifactNotFoundError(ArtifactManagerError):
+ """Raised when requested artifact doesn't exist in system."""
+
+ status_code: int = 500
+ message: str = "Artifact not found"
+
+
+class ArtifactIOError(ArtifactManagerError):
+ """Raised on input/output error."""
+
+ status_code: int = 500
+ message: str = "Artifact is corrupted"
+
+
+class ArtifactOverwriteError(ArtifactManagerError):
+ """Raised when we cannot remove old artifact to save new."""
+
+ status_code: int = 500
+ message: str = "Artifact already exists and cannot be overwritten"
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()
diff --git a/ms/artifact-manager/manager/utils.py b/ms/artifact-manager/manager/utils.py
new file mode 100644
index 000000000..da4cd9f9d
--- /dev/null
+++ b/ms/artifact-manager/manager/utils.py
@@ -0,0 +1,176 @@
+"""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 os
+import shutil
+from abc import ABC, abstractmethod
+from io import BytesIO
+from pathlib import Path
+from zipfile import ZipFile, is_zipfile
+
+from manager.configuration import config
+from manager.errors import ArtifactNotFoundError, ArtifactOverwriteError, InvalidRequestError
+
+
+class Repository(ABC):
+ """Abstract repository class.
+
+ Defines repository methods.
+ """
+
+ @abstractmethod
+ def upload_blueprint(self, file: bytes, name: str, version: str) -> None:
+ """Store blueprint file in the repository.
+
+ :param file: File to save
+ :param name: Blueprint name
+ :param version: Blueprint version
+ """
+
+ @abstractmethod
+ def download_blueprint(self, name: str, version: str) -> bytes:
+ """Download blueprint file from repository.
+
+ :param name: Blueprint name
+ :param version: Blueprint version
+ :return: Zipped Blueprint file bytes
+ """
+
+ @abstractmethod
+ def remove_blueprint(self, name: str, version: str) -> None:
+ """Remove blueprint file from repository.
+
+ :param name: Blueprint name
+ :param version: Blueprint version
+ """
+
+
+class FileRepository(Repository):
+ """Store blueprints on local directory."""
+
+ base_path = None
+
+ def __init__(self, base_path: Path) -> None:
+ """Initialize the repository while passing the needed path.
+
+ :param base_path: Local OS path on which blueprint files reside.
+ """
+ self.base_path = base_path
+
+ def __remove_directory_tree(self, full_path: str) -> None:
+ """Remove specified path.
+
+ :param full_path: Full path to a directory.
+ :raises: FileNotFoundError
+ """
+ try:
+ shutil.rmtree(full_path, ignore_errors=False)
+ except OSError:
+ raise ArtifactNotFoundError
+
+ def __create_directory_tree(self, full_path: str, mode: int = 0o744, retry_on_error: bool = True) -> None:
+ """Create directory or overwrite existing one.
+
+ This method will handle a directory tree creation. If there is a collision
+ in directory structure - old directory tree will be removed
+ and creation will be attempted one more time. If the creation fails for the second time
+ the exception will be raised.
+
+ :param full_path: Full directory tree path (eg. one/two/tree) as string.
+ :param mode: Permission mask for the directories.
+ :param retry_on_error: Flag that indicates if there should be a attempt to retry the operation.
+ """
+ try:
+ os.makedirs(full_path, mode=mode)
+ except FileExistsError:
+ # In this case we know that cba of same name and version need to be overwritten
+ if retry_on_error:
+ self.__remove_directory_tree(full_path)
+ self.__create_directory_tree(full_path, mode=mode, retry_on_error=False)
+ else:
+ # This way we won't try for ever if something goes wrong
+ raise ArtifactOverwriteError
+
+ def upload_blueprint(self, cba_bytes: bytes, name: str, version: str) -> None:
+ """Store blueprint file in the repository.
+
+ :param cba_bytes: Bytes to save
+ :param name: Blueprint name
+ :param version: Blueprint version
+ """
+ temporary_file: BytesIO = BytesIO(cba_bytes)
+
+ if not is_zipfile(temporary_file):
+ raise InvalidRequestError
+
+ target_path: str = str(Path(self.base_path.absolute(), name, version))
+ self.__create_directory_tree(target_path)
+
+ with ZipFile(temporary_file, "r") as zip_file: # type: ZipFile
+ zip_file.extractall(target_path)
+
+ def download_blueprint(self, name: str, version: str) -> bytes:
+ """Download blueprint file from repository.
+
+ This method does the in-memory zipping the files and returns bytes
+
+ :param name: Blueprint name
+ :param version: Blueprint version
+ :return: Zipped Blueprint file bytes
+ """
+ temporary_file: BytesIO = BytesIO()
+ files_path: str = str(Path(self.base_path.absolute(), name, version))
+ if not os.path.exists(files_path):
+ raise ArtifactNotFoundError
+
+ with ZipFile(temporary_file, "w") as zip_file: # type: ZipFile
+ for directory_name, subdirectory_names, filenames in os.walk(files_path): # type: str, list, list
+ for filename in filenames: # type: str
+ zip_file.write(Path(directory_name, filename))
+
+ # Rewind the fake file to allow reading
+ temporary_file.seek(0)
+
+ zip_as_bytes: bytes = temporary_file.read()
+ temporary_file.close()
+ return zip_as_bytes
+
+ def remove_blueprint(self, name: str, version: str) -> None:
+ """Remove blueprint file from repository.
+
+ :param name: Blueprint name
+ :param version: Blueprint version
+ :raises: FileNotFoundError
+ """
+ files_path: str = str(Path(self.base_path.absolute(), name, version))
+ self.__remove_directory_tree(files_path)
+
+
+class RepositoryStrategy(ABC):
+ """Strategy class.
+
+ It has only one public method `get_repository`, which returns valid repository
+ instance for the the configuration value.
+ You can create many Repository subclasses, but repository clients doesn't have
+ to know which one you use.
+ """
+
+ @classmethod
+ def get_reporitory(cls) -> Repository:
+ """Get the valid repository instance for the configuration value.
+
+ Currently it returns FileRepository because it is an only Repository implementation.
+ """
+ return FileRepository(Path(config["artifactManagerServer"]["fileRepositoryBasePath"]))
diff --git a/ms/artifact-manager/requirements/docker.txt b/ms/artifact-manager/requirements/docker.txt
new file mode 100644
index 000000000..67f68dd22
--- /dev/null
+++ b/ms/artifact-manager/requirements/docker.txt
@@ -0,0 +1,2 @@
+-r shared.txt
+/opt/app/onap/dependencies/common \ No newline at end of file
diff --git a/ms/artifact-manager/requirements/local.txt b/ms/artifact-manager/requirements/local.txt
new file mode 100644
index 000000000..6a06e16f3
--- /dev/null
+++ b/ms/artifact-manager/requirements/local.txt
@@ -0,0 +1,2 @@
+-r shared.txt
+../../py-modules/common \ No newline at end of file
diff --git a/ms/artifact-manager/requirements/shared.txt b/ms/artifact-manager/requirements/shared.txt
new file mode 100644
index 000000000..971d4a82e
--- /dev/null
+++ b/ms/artifact-manager/requirements/shared.txt
@@ -0,0 +1,3 @@
+grpcio-tools==1.25.0
+onappylog==1.0.9
+click==7.0
diff --git a/ms/py-executor/test-requirements.txt b/ms/artifact-manager/requirements/test.txt
index 79ed6ee95..5304eac63 100644
--- a/ms/py-executor/test-requirements.txt
+++ b/ms/artifact-manager/requirements/test.txt
@@ -1,3 +1,3 @@
pytest==5.3.1
pytest-grpc==0.7.0
--r requirements.txt \ No newline at end of file
+-r local.txt \ No newline at end of file
diff --git a/ms/artifact-manager/server.py b/ms/artifact-manager/server.py
new file mode 100644
index 000000000..fe70c8855
--- /dev/null
+++ b/ms/artifact-manager/server.py
@@ -0,0 +1,47 @@
+"""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.
+"""
+
+from concurrent.futures import ThreadPoolExecutor
+
+import click
+from grpc import server as grpc_server
+from manager.configuration import config
+from manager.servicer import ArtifactManagerServicer
+from proto.BluePrintManagement_pb2_grpc import add_BluePrintManagementServiceServicer_to_server
+
+
+@click.command()
+def run_server():
+ """Run Artifact Manager gRPC server.
+
+ Values like 'maxWorkers' and 'port' must be specified in a config file in .ini format.
+
+ Config file path is specified by 'CONFIGURATION' environment variable.
+
+ """
+ max_workers: int = int(config["artifactManagerServer"]["maxWorkers"])
+ server: grpc_server = grpc_server(ThreadPoolExecutor(max_workers=max_workers))
+
+ add_BluePrintManagementServiceServicer_to_server(ArtifactManagerServicer(), server)
+ port_number: int = int(config["artifactManagerServer"]["port"])
+ server.add_insecure_port(f"[::]:{port_number}")
+
+ click.echo(f"Server starts on port {port_number} using {max_workers} workers.")
+ server.start()
+ server.wait_for_termination()
+
+
+if __name__ == "__main__":
+ run_server()
diff --git a/ms/artifact-manager/setup.py b/ms/artifact-manager/setup.py
new file mode 100644
index 000000000..b5b4487de
--- /dev/null
+++ b/ms/artifact-manager/setup.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+"""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.
+"""
+
+from distutils.core import setup
+
+
+setup(
+ name="manager",
+ version="0.1",
+ description="CDS Artifact Manager",
+ packages=["manager"],
+)
diff --git a/ms/artifact-manager/tests/base_test.py b/ms/artifact-manager/tests/base_test.py
new file mode 100644
index 000000000..4466b33dc
--- /dev/null
+++ b/ms/artifact-manager/tests/base_test.py
@@ -0,0 +1,19 @@
+"""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.
+"""
+
+
+def test_mock_function():
+ """Basic mock test that allways work."""
+ assert True
diff --git a/ms/artifact-manager/tests/configuration-test.ini b/ms/artifact-manager/tests/configuration-test.ini
new file mode 100644
index 000000000..e28e6f1c4
--- /dev/null
+++ b/ms/artifact-manager/tests/configuration-test.ini
@@ -0,0 +1,4 @@
+[testSection]
+testValue: 123
+[artifactManagerServer]
+artifactManagerValue=123
diff --git a/ms/artifact-manager/tests/configuration_test.py b/ms/artifact-manager/tests/configuration_test.py
new file mode 100644
index 000000000..219908325
--- /dev/null
+++ b/ms/artifact-manager/tests/configuration_test.py
@@ -0,0 +1,47 @@
+"""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.
+"""
+from configparser import NoOptionError
+from pathlib import Path, PurePath
+from typing import NoReturn
+
+from pytest import raises
+
+from manager.configuration import ArtifactManagerConfiguration
+
+
+TEST_CONFIGURATION_FILE_PATH = str(
+ PurePath(Path(__file__).parent.absolute(), "configuration-test.ini")
+)
+
+
+def test_server_configuration_configuration_file_path() -> NoReturn:
+ """Test ArtifactManagerConfiguration class.
+
+ Test checks if configuration file is loaded properly and returns valid values.
+ If invalid section or option is provided it should raises KeyError or configparser.NoOptionError exceptions.
+ :return: NoReturn
+ """
+ configuration: ArtifactManagerConfiguration = ArtifactManagerConfiguration(
+ TEST_CONFIGURATION_FILE_PATH
+ )
+ assert configuration.get_section("testSection")
+ with raises(KeyError):
+ configuration.get_section("invalidSection")
+ assert configuration.get_property("testSection", "testValue") == "123"
+ with raises(NoOptionError):
+ configuration.get_property("testSection", "invalidValue")
+ assert configuration.artifact_manager_property("artifactManagerValue") == "123"
+ with raises(NoOptionError):
+ configuration.artifact_manager_property("invalidValue")
diff --git a/ms/artifact-manager/tests/servicer_test.py b/ms/artifact-manager/tests/servicer_test.py
new file mode 100644
index 000000000..131e6fb2c
--- /dev/null
+++ b/ms/artifact-manager/tests/servicer_test.py
@@ -0,0 +1,196 @@
+"""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 os
+import shutil
+import zipfile
+from unittest.mock import patch
+
+import manager.utils
+from manager.servicer import ArtifactManagerServicer
+from proto.BluePrintCommon_pb2 import ActionIdentifiers, CommonHeader
+from proto.BluePrintManagement_pb2 import (
+ BluePrintDownloadInput,
+ BluePrintManagementOutput,
+ BluePrintRemoveInput,
+ BluePrintUploadInput,
+ FileChunk,
+)
+from proto.BluePrintManagement_pb2_grpc import (
+ BluePrintManagementServiceStub,
+ add_BluePrintManagementServiceServicer_to_server,
+)
+from pytest import fixture
+
+ZIP_FILE_BINARY = b"PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+
+
+class MockZipFile(zipfile.ZipFile):
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def extractall(self, path: str) -> None:
+ pass
+
+ def write(self, *arg, **kwargs) -> None:
+ pass
+
+
+@fixture(scope="module")
+def grpc_add_to_server():
+ """pytest-grpcio required function."""
+ return add_BluePrintManagementServiceServicer_to_server
+
+
+@fixture(scope="module")
+def grpc_servicer():
+ """pytest-grpcio required function."""
+ return ArtifactManagerServicer()
+
+
+@fixture(scope="module") # noqa
+def grpc_stub_cls(grpc_channel):
+ """pytest-grpcio required function."""
+ return BluePrintManagementServiceStub
+
+
+def test_servicer_upload_handler_header_failure(grpc_stub):
+ """Test servicer upload handler."""
+ request: BluePrintUploadInput = BluePrintUploadInput()
+ output: BluePrintManagementOutput = grpc_stub.uploadBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Request has to have set both BluePrint name and version"
+
+
+def test_servicer_download_handler_header_failure(grpc_stub):
+ """Test servicer download handler."""
+ request: BluePrintDownloadInput = BluePrintDownloadInput()
+ output: BluePrintManagementOutput = grpc_stub.downloadBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Request has to have set both BluePrint name and version"
+
+
+def test_servicer_remove_handler_header_failure(grpc_stub):
+ """Test servicer remove handler."""
+ request: BluePrintRemoveInput = BluePrintRemoveInput()
+ output: BluePrintManagementOutput = grpc_stub.removeBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Request has to have set both BluePrint name and version"
+
+
+def test_servicer_upload_handler_failure(grpc_stub):
+ """Test servicer upload handler."""
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "1.0.0"
+ request: BluePrintUploadInput = BluePrintUploadInput(actionIdentifiers=action_identifiers)
+ output: BluePrintManagementOutput = grpc_stub.uploadBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Invalid request"
+
+
+def test_servicer_download_handler_failure(grpc_stub):
+ """Test servicer download handler."""
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "2.0.0"
+ request: BluePrintDownloadInput = BluePrintDownloadInput(actionIdentifiers=action_identifiers)
+ output: BluePrintManagementOutput = grpc_stub.downloadBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Artifact not found"
+
+
+def test_servicer_remove_handler_failure(grpc_stub):
+ """Test servicer remove handler."""
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "1.0.0"
+ request: BluePrintRemoveInput = BluePrintRemoveInput(actionIdentifiers=action_identifiers)
+ output: BluePrintManagementOutput = grpc_stub.removeBlueprint(request)
+ assert output.status.code == 500
+ assert output.status.message == "failure"
+ assert output.status.errorMessage == "Artifact not found"
+
+
+def test_servicer_upload_handler_success(grpc_stub):
+ """Test servicer upload handler."""
+ header: CommonHeader = CommonHeader()
+ header.requestId = "1234"
+ header.subRequestId = "1234-1"
+ header.originatorId = "CDS"
+
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "1.0.0"
+ action_identifiers.actionName = "SampleScript"
+
+ file_chunk = FileChunk()
+ file_chunk.chunk = ZIP_FILE_BINARY
+
+ # fmt: off
+ with patch.object(os, "makedirs", return_value=None), \
+ patch.object(manager.utils, 'ZipFile', return_value=MockZipFile()):
+ request: BluePrintUploadInput = BluePrintUploadInput(
+ commonHeader=header, fileChunk=file_chunk, actionIdentifiers=action_identifiers
+ )
+ output: BluePrintManagementOutput = grpc_stub.uploadBlueprint(request)
+ # fmt: on
+ assert output.status.code == 200
+ assert output.status.message == "success"
+
+
+def test_servicer_download_handler_success(grpc_stub):
+ """Test servicer download handler."""
+ header: CommonHeader = CommonHeader()
+ header.requestId = "1234"
+ header.subRequestId = "1234-1"
+ header.originatorId = "CDS"
+
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "1.0.0"
+ action_identifiers.actionName = "SampleScript"
+
+ with patch.object(os.path, "exists", return_value=True):
+ request: BluePrintDownloadInput = BluePrintDownloadInput(
+ commonHeader=header, actionIdentifiers=action_identifiers
+ )
+ output: BluePrintManagementOutput = grpc_stub.downloadBlueprint(request)
+ assert output.status.code == 200
+ assert output.status.message == "success"
+ assert output.fileChunk.chunk == ZIP_FILE_BINARY
+
+
+def test_servicer_remove_handler_success(grpc_stub):
+ """Test servicer remove handler."""
+ header: CommonHeader = CommonHeader()
+ header.requestId = "1234"
+ header.subRequestId = "1234-1"
+ header.originatorId = "CDS"
+
+ action_identifiers: ActionIdentifiers = ActionIdentifiers()
+ action_identifiers.blueprintName = "sample-cba"
+ action_identifiers.blueprintVersion = "1.0.0"
+ action_identifiers.actionName = "SampleScript"
+
+ with patch.object(shutil, "rmtree", return_value=None) as mock_rmtree:
+ request: BluePrintRemoveInput = BluePrintRemoveInput(commonHeader=header, actionIdentifiers=action_identifiers)
+ output: BluePrintManagementOutput = grpc_stub.removeBlueprint(request)
+ assert output.status.code == 200
+ assert output.status.message == "success"
diff --git a/ms/artifact-manager/tests/utils_test.py b/ms/artifact-manager/tests/utils_test.py
new file mode 100644
index 000000000..75d8b4c19
--- /dev/null
+++ b/ms/artifact-manager/tests/utils_test.py
@@ -0,0 +1,59 @@
+import os
+import shutil
+import zipfile
+from unittest.mock import patch
+
+import manager.utils
+from manager.utils import FileRepository, Repository, RepositoryStrategy
+
+
+class MockZipFile(zipfile.ZipFile):
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def extractall(self, path: str) -> None:
+ pass
+
+ def write(self, *arg, **kwargs) -> None:
+ pass
+
+
+def test_fetch_proper_repository():
+ repo: Repository = RepositoryStrategy.get_reporitory()
+ assert repo.__class__ is FileRepository
+
+
+def test_blueprint_upload():
+ repo: Repository = RepositoryStrategy.get_reporitory()
+ # fmt: off
+ with patch.object(manager.utils, "is_zipfile", return_value=True) as mock_is_zip, \
+ patch.object(os, "makedirs", return_value=None) as mock_mkdirs, \
+ patch.object(manager.utils, 'ZipFile', return_value=MockZipFile()
+ ):
+ repo.upload_blueprint(b"abcd", "test_cba", "1.0.a")
+ mock_is_zip.assert_called_once()
+ mock_mkdirs.assert_called_once_with('/tmp/test_cba/1.0.a', mode=0o744)
+ # fmt: on
+
+
+def test_blueprint_download():
+ repo: Repository = RepositoryStrategy.get_reporitory()
+ mock_path = [
+ ("test_cba", ["1.0.a"], []),
+ ("test_cba/1.0.a", [], ["file.txt"]),
+ ]
+ # fmt: off
+ with patch.object(os, "walk", return_value=mock_path) as mock_walk, \
+ patch.object(manager.utils, 'ZipFile', return_value=MockZipFile()), \
+ patch.object(os.path, 'exists', return_value=True
+ ):
+ repo.download_blueprint("test_cba", "1.0.a")
+ mock_walk.assert_called_once_with('/tmp/test_cba/1.0.a')
+ # fmt: on
+
+
+def test_remove_blueprint():
+ repo: Repository = RepositoryStrategy.get_reporitory()
+ with patch.object(shutil, "rmtree", return_value=None) as mock_rmtree:
+ repo.remove_blueprint("cba", "1.0a")
+ mock_rmtree.assert_called_once()
diff --git a/ms/artifact-manager/tox.ini b/ms/artifact-manager/tox.ini
new file mode 100644
index 000000000..a95267823
--- /dev/null
+++ b/ms/artifact-manager/tox.ini
@@ -0,0 +1,27 @@
+[tox]
+envlist=py37,py38
+skipsdist=True
+[testenv]
+setenv =
+ PYTHONPATH = {toxinidir}
+ CONFIGURATION = {toxinidir}/../configuration-local.ini
+deps =
+ -rrequirements/test.txt
+commands = pytest
+[testenv:codelint]
+deps =
+ black
+commands = black -l 120 --check {posargs:.}
+[testenv:doclint]
+deps =
+ flake8-docstrings
+commands = flake8 --doctest --docstring-convention google --max-line-length 120 --exclude .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,*test.py --select=D {posargs:.}
+[testenv:coverage]
+basepython = python3.7
+setenv =
+ PYTHONPATH = {toxinidir}
+ CONFIGURATION = {toxinidir}/../configuration-local.ini
+deps =
+ -rrequirements/test.txt
+ pytest-cov
+commands = pytest --cov=manager --cov-fail-under=60 --cov-config={toxinidir}/.coveragerc .
diff --git a/ms/py-executor/configuration-local.ini b/ms/configuration-local.ini
index c98746afe..431ef4c78 100644
--- a/ms/py-executor/configuration-local.ini
+++ b/ms/configuration-local.ini
@@ -11,3 +11,11 @@ maxWorkers=20
blueprintDeployPath=test/resources
blueprintArchivePath=target/blueprints/archive
blueprintWorkingPath=target/blueprints/work
+
+[artifactManagerServer]
+port=50052
+logFile=server.log
+maxWorkers=20
+debug=true
+logConfig=logging.yaml
+fileRepositoryBasePath=/tmp/ \ No newline at end of file
diff --git a/ms/py-executor/configuration.ini b/ms/configuration.ini
index 612c62899..a5c16c790 100644
--- a/ms/py-executor/configuration.ini
+++ b/ms/configuration.ini
@@ -14,3 +14,11 @@ maxWorkers=20
blueprintDeployPath=/opt/app/onap/blueprints/deploy
blueprintArchivePath=/opt/app/onap/blueprints/archive
blueprintWorkingPath=/opt/app/onap/blueprints/work
+
+[artifactManagerServer]
+port=%(ARTIFACT_MANAGER_PORT)s
+logFile=%(ARTIFACT_MANAGER_SERVER_LOG_FILE)s
+maxWorkers=20
+debug=false
+logConfig=logging.yaml
+fileRepositoryBasePath=/tmp/ \ No newline at end of file
diff --git a/ms/logging.yaml b/ms/logging.yaml
new file mode 100644
index 000000000..3a31ca85c
--- /dev/null
+++ b/ms/logging.yaml
@@ -0,0 +1,14 @@
+version: 1
+
+disable_existing_loggers: True
+
+formatters:
+ mdcFormatter:
+ format: "%(asctime)s:[%(name)s] %(created)f %(module)s %(funcName)s %(pathname)s %(process)d %(levelno)s :[ %(threadName)s %(thread)d]: [%(mdc)s]: [%(filename)s]-[%(lineno)d] [%(levelname)s]:%(message)s"
+ mdcfmt: "{key1} {key2} {key3} {key4} dwdawdwa "
+ datefmt: "%Y-%m-%d %H:%M:%S"
+ (): onaplogging.mdcformatter.MDCFormatter
+ standard:
+ format: '%(asctime)s:[%(name)s]:[%(filename)s]-[%(lineno)d]
+ [%(levelname)s]:%(message)s '
+ datefmt: "%Y-%m-%d %H:%M:%S" \ No newline at end of file
diff --git a/ms/py-executor/blueprints_grpc/__init__.py b/ms/py-executor/blueprints_grpc/__init__.py
index 726849f2d..8a2db9e1e 100644
--- a/ms/py-executor/blueprints_grpc/__init__.py
+++ b/ms/py-executor/blueprints_grpc/__init__.py
@@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from .proto.BluePrintCommon_pb2 import *
-from .proto.BluePrintProcessing_pb2 import *
+from proto.BluePrintCommon_pb2 import *
+from proto.BluePrintProcessing_pb2 import *
from .script_executor_configuration import *
from .executor_utils import *
from .blueprint_processing_server import * \ No newline at end of file
diff --git a/ms/py-executor/blueprints_grpc/blueprint_processing_server.py b/ms/py-executor/blueprints_grpc/blueprint_processing_server.py
index f1f29a035..322d34b0b 100644
--- a/ms/py-executor/blueprints_grpc/blueprint_processing_server.py
+++ b/ms/py-executor/blueprints_grpc/blueprint_processing_server.py
@@ -16,7 +16,7 @@
import logging
from google.protobuf.json_format import MessageToJson
-from .proto import BluePrintProcessing_pb2_grpc as BluePrintProcessing_pb2_grpc
+from proto import BluePrintProcessing_pb2_grpc as BluePrintProcessing_pb2_grpc
from .script_executor_configuration import ScriptExecutorConfiguration
from .executor_utils import instance_for_input
diff --git a/ms/py-executor/blueprints_grpc/executor_utils.py b/ms/py-executor/blueprints_grpc/executor_utils.py
index 44b6d8e87..701958fff 100644
--- a/ms/py-executor/blueprints_grpc/executor_utils.py
+++ b/ms/py-executor/blueprints_grpc/executor_utils.py
@@ -14,14 +14,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from google.protobuf.timestamp_pb2 import Timestamp
-from google.protobuf import struct_pb2
-from google.protobuf import json_format
-import sys, importlib, importlib.util, json, time, datetime
+import datetime
+import importlib
+import importlib.util
+import json
import logging
-from .proto.BluePrintProcessing_pb2 import ExecutionServiceInput, ExecutionServiceOutput
-from .proto.BluePrintCommon_pb2 import Status, EVENT_COMPONENT_TRACE, EVENT_COMPONENT_PROCESSING, \
- EVENT_COMPONENT_EXECUTED, EVENT_COMPONENT_NOTIFICATION
+import sys
+import time
+
+from google.protobuf import json_format, struct_pb2
+from google.protobuf.timestamp_pb2 import Timestamp
+from proto.BluePrintCommon_pb2 import (
+ EVENT_COMPONENT_EXECUTED,
+ EVENT_COMPONENT_NOTIFICATION,
+ EVENT_COMPONENT_PROCESSING,
+ EVENT_COMPONENT_TRACE,
+ Status
+)
+from proto.BluePrintProcessing_pb2 import (
+ ExecutionServiceInput,
+ ExecutionServiceOutput,
+)
+
from .script_executor_configuration import ScriptExecutorConfiguration
logger = logging.getLogger("Utils")
diff --git a/ms/py-executor/blueprints_grpc/script_executor_configuration.py b/ms/py-executor/blueprints_grpc/script_executor_configuration.py
index 2f0553b62..9f7460748 100644
--- a/ms/py-executor/blueprints_grpc/script_executor_configuration.py
+++ b/ms/py-executor/blueprints_grpc/script_executor_configuration.py
@@ -42,7 +42,9 @@ class ScriptExecutorConfiguration:
if __name__ == '__main__':
- config_file = str(PurePath(Path().absolute())) + '/../configuration.ini'
+ default_configuration_file = str(PurePath(Path().absolute(), "../../configuration.ini"))
+ supplied_configuration_file = os.environ.get('CONFIGURATION')
+ config_file = str(os.path.expanduser(Path(supplied_configuration_file or default_configuration_file)))
scriptExecutorConfiguration = ScriptExecutorConfiguration(config_file)
blueprintDeployPath = scriptExecutorConfiguration.get_property('blueprintsprocessor', 'blueprintDeployPath')
print(blueprintDeployPath)
diff --git a/ms/py-executor/client.py b/ms/py-executor/client.py
index c5bdc43c8..482bcbdd3 100644
--- a/ms/py-executor/client.py
+++ b/ms/py-executor/client.py
@@ -25,9 +25,9 @@
# limitations under the License.
import grpc
-from blueprints_grpc.proto.BluePrintProcessing_pb2_grpc import BluePrintProcessingServiceStub
-from blueprints_grpc.proto.BluePrintProcessing_pb2 import ExecutionServiceInput
-from blueprints_grpc.proto.BluePrintCommon_pb2 import CommonHeader, ActionIdentifiers
+from proto.BluePrintCommon_pb2 import ActionIdentifiers, CommonHeader
+from proto.BluePrintProcessing_pb2 import ExecutionServiceInput
+from proto.BluePrintProcessing_pb2_grpc import BluePrintProcessingServiceStub
def generate_messages():
diff --git a/ms/py-executor/docker/Dockerfile b/ms/py-executor/docker/Dockerfile
index 9e86cc87e..043e15d53 100644
--- a/ms/py-executor/docker/Dockerfile
+++ b/ms/py-executor/docker/Dockerfile
@@ -8,7 +8,7 @@ RUN tar -xzf /source.tar.gz -C /tmp \
&& rm -rf /source.tar.gz \
&& rm -rf /tmp/@project.build.finalName@
-RUN pip install --no-cache-dir -r /opt/app/onap/python/requirements.txt
+RUN pip install --no-cache-dir -r /opt/app/onap/python/requirements/docker.txt
VOLUME /opt/app/onap/blueprints/deploy/
diff --git a/ms/py-executor/docker/distribution.xml b/ms/py-executor/docker/distribution.xml
index 6d09be8b2..558ce2f17 100755
--- a/ms/py-executor/docker/distribution.xml
+++ b/ms/py-executor/docker/distribution.xml
@@ -27,17 +27,42 @@
<directory>${project.basedir}</directory>
<outputDirectory>opt/app/onap/python</outputDirectory>
<includes>
- <include>blueprints_grpc/**/*.py</include>
+ <include>blueprints_grpc/*.py</include>
<include>*.py</include>
</includes>
<useDefaultExcludes>true</useDefaultExcludes>
</fileSet>
<fileSet>
+ <directory>${project.basedir}/../../py-modules</directory>
+ <outputDirectory>opt/app/onap/dependencies/</outputDirectory>
+ <includes>
+ <include>common/**/*.py</include>
+ </includes>
+ <useDefaultExcludes>true</useDefaultExcludes>
+ </fileSet>
+ <fileSet>
+ <directory>${project.basedir}/..</directory>
+ <outputDirectory>opt/app/onap/dependencies</outputDirectory>
+ <includes>
+ <include>artifact-manager/**/*.*</include>
+ </includes>
+ <useDefaultExcludes>true</useDefaultExcludes>
+ </fileSet>
+ <fileSet>
+ <directory>${project.basedir}/..</directory>
+ <outputDirectory>opt/app/onap</outputDirectory>
+ <includes>
+ <include>logging.yaml</include>
+ <include>configuration.ini</include>
+ </includes>
+ <useDefaultExcludes>true</useDefaultExcludes>
+ <fileMode>0666</fileMode>
+ </fileSet>
+ <fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>opt/app/onap/python</outputDirectory>
<includes>
- <include>requirements.txt</include>
- <include>configuration.ini</include>
+ <include>requirements/*.txt</include>
</includes>
<useDefaultExcludes>true</useDefaultExcludes>
<fileMode>0666</fileMode>
@@ -58,4 +83,4 @@
<fileMode>0755</fileMode>
</fileSet>
</fileSets>
-</assembly> \ No newline at end of file
+</assembly>
diff --git a/ms/py-executor/docker/start.sh b/ms/py-executor/docker/start.sh
index 3a3a9cb70..6868ba0e7 100755
--- a/ms/py-executor/docker/start.sh
+++ b/ms/py-executor/docker/start.sh
@@ -22,12 +22,24 @@ then
export APP_PORT=50052
fi
+if [ -z "${ARTIFACT_MANAGER_PORT}" ]
+then
+ echo "ARTIFACT_MANAGER_PORT environment variable is not set, using default."
+ export ARTIFACT_MANAGER_PORT=50053
+fi
+
if [ -z "${LOG_FILE}" ]
then
echo "LOG_FILE environment variable is not set, using default."
export LOG_FILE="application.log"
fi
+if [ -z "${ARTIFACT_MANAGER_SERVER_LOG_FILE}" ]
+then
+ echo "ARTIFACT_MANAGER_SERVER_LOG_FILE environment variable is not set, using default."
+ export ARTIFACT_MANAGER_SERVER_LOG_FILE="artifacts.log"
+fi
+
if [ "${http_proxy}" ]
then
echo "Setting http_proxy: ${http_proxy}"
@@ -38,5 +50,12 @@ then
echo "Setting https_proxy: ${https_proxy}"
fi
+if [ -z "${CONFIGURATION}" ]
+then
+ echo "CONFIGURATION environment variable is not set, using default."
+ export CONFIGURATION="/opt/app/onap/configuration.ini"
+fi
+
+
cd /opt/app/onap/python/
python server.py \ No newline at end of file
diff --git a/ms/py-executor/requirements/docker.txt b/ms/py-executor/requirements/docker.txt
new file mode 100644
index 000000000..58469c9d7
--- /dev/null
+++ b/ms/py-executor/requirements/docker.txt
@@ -0,0 +1,4 @@
+-r shared.txt
+/opt/app/onap/dependencies/common
+-r /opt/app/onap/dependencies/artifact-manager/requirements/docker.txt
+/opt/app/onap/dependencies/artifact-manager \ No newline at end of file
diff --git a/ms/py-executor/requirements/local.txt b/ms/py-executor/requirements/local.txt
new file mode 100644
index 000000000..74cc76efa
--- /dev/null
+++ b/ms/py-executor/requirements/local.txt
@@ -0,0 +1,4 @@
+-r shared.txt
+../../py-modules/common
+-r ../../artifact-manager/requirements/local.txt
+../artifact-manager \ No newline at end of file
diff --git a/ms/py-executor/requirements.txt b/ms/py-executor/requirements/shared.txt
index 14b4c29a0..228320120 100644
--- a/ms/py-executor/requirements.txt
+++ b/ms/py-executor/requirements/shared.txt
@@ -3,4 +3,4 @@ grpcio-tools==1.25.0
configparser==4.0.2
requests==2.22.0
ncclient==0.6.6
-ansible==2.8.5 \ No newline at end of file
+ansible==2.8.5
diff --git a/ms/py-executor/requirements/test.txt b/ms/py-executor/requirements/test.txt
new file mode 100644
index 000000000..5304eac63
--- /dev/null
+++ b/ms/py-executor/requirements/test.txt
@@ -0,0 +1,3 @@
+pytest==5.3.1
+pytest-grpc==0.7.0
+-r local.txt \ No newline at end of file
diff --git a/ms/py-executor/resource_resolution/README b/ms/py-executor/resource_resolution/README
index a2d1542ab..222dae499 100644
--- a/ms/py-executor/resource_resolution/README
+++ b/ms/py-executor/resource_resolution/README
@@ -5,8 +5,8 @@
### Insecure channel
```
-from blueprints_grpc.proto.BluePrintCommon_pb2_grpc import ActionIdentifiers, CommonHeader
-from blueprints_grpc.proto.BluePrintProcessing_pb2_grpc import ExecutionServiceInput
+from proto.BluePrintCommon_pb2_grpc import ActionIdentifiers, CommonHeader
+from proto.BluePrintProcessing_pb2_grpc import ExecutionServiceInput
from resource_resolution.client import Client as ResourceResolutionClient
@@ -43,8 +43,8 @@ if __name__ == "__main__":
### Secure channel
```
-from blueprints_grpc.proto.BluePrintCommon_pb2_grpc import ActionIdentifiers, CommonHeader
-from blueprints_grpc.proto.BluePrintProcessing_pb2_grpc import ExecutionServiceInput
+from proto.BluePrintCommon_pb2_grpc import ActionIdentifiers, CommonHeader
+from proto.BluePrintProcessing_pb2_grpc import ExecutionServiceInput
from resource_resolution.client import Client as ResourceResolutionClient
diff --git a/ms/py-executor/resource_resolution/client.py b/ms/py-executor/resource_resolution/client.py
index 913b0ed66..89087745c 100644
--- a/ms/py-executor/resource_resolution/client.py
+++ b/ms/py-executor/resource_resolution/client.py
@@ -12,14 +12,13 @@ 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 logging import getLogger, Logger
+from logging import Logger, getLogger
from types import TracebackType
from typing import Iterable, List, Optional, Type
from grpc import Channel, insecure_channel, secure_channel, ssl_channel_credentials
-
-from blueprints_grpc.proto.BluePrintProcessing_pb2 import ExecutionServiceInput, ExecutionServiceOutput
-from blueprints_grpc.proto.BluePrintProcessing_pb2_grpc import BluePrintProcessingServiceStub
+from proto.BluePrintProcessing_pb2 import ExecutionServiceInput, ExecutionServiceOutput
+from proto.BluePrintProcessing_pb2_grpc import BluePrintProcessingServiceStub
class Client:
diff --git a/ms/py-executor/server.py b/ms/py-executor/server.py
index f506e9446..7f6537011 100644
--- a/ms/py-executor/server.py
+++ b/ms/py-executor/server.py
@@ -15,16 +15,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from builtins import KeyboardInterrupt
-from concurrent import futures
import logging
+import os
import time
-import grpc
+from builtins import KeyboardInterrupt
+from concurrent import futures
from pathlib import Path, PurePath
-from blueprints_grpc import BluePrintProcessing_pb2_grpc
-from blueprints_grpc.request_header_validator_interceptor import RequestHeaderValidatorInterceptor
+
+import grpc
+from manager.servicer import ArtifactManagerServicer
+from proto.BluePrintManagement_pb2_grpc import add_BluePrintManagementServiceServicer_to_server
+
+from blueprints_grpc import BluePrintProcessing_pb2_grpc, ScriptExecutorConfiguration
from blueprints_grpc.blueprint_processing_server import BluePrintProcessingServer
-from blueprints_grpc import ScriptExecutorConfiguration
+from blueprints_grpc.request_header_validator_interceptor import RequestHeaderValidatorInterceptor
logger = logging.getLogger("Server")
@@ -53,7 +57,9 @@ def serve(configuration: ScriptExecutorConfiguration):
# create server
server = grpc.server(futures.ThreadPoolExecutor(max_workers=int(maxWorkers)))
BluePrintProcessing_pb2_grpc.add_BluePrintProcessingServiceServicer_to_server(
- BluePrintProcessingServer(configuration), server)
+ BluePrintProcessingServer(configuration), server
+ )
+ add_BluePrintManagementServiceServicer_to_server(ArtifactManagerServicer(), server)
# add secure port using credentials
server.add_secure_port('[::]:' + port, server_credentials)
@@ -68,7 +74,9 @@ def serve(configuration: ScriptExecutorConfiguration):
server = grpc.server(futures.ThreadPoolExecutor(max_workers=int(maxWorkers)),
interceptors=(header_validator,))
BluePrintProcessing_pb2_grpc.add_BluePrintProcessingServiceServicer_to_server(
- BluePrintProcessingServer(configuration), server)
+ BluePrintProcessingServer(configuration), server
+ )
+ add_BluePrintManagementServiceServicer_to_server(ArtifactManagerServicer(), server)
server.add_insecure_port('[::]:' + port)
server.start()
@@ -83,7 +91,10 @@ def serve(configuration: ScriptExecutorConfiguration):
if __name__ == '__main__':
- config_file = str(PurePath(Path().absolute())) + '/configuration.ini'
+ default_configuration_file = str(PurePath(Path().absolute(), "../../configuration.ini"))
+ supplied_configuration_file = os.environ.get("CONFIGURATION")
+ config_file = str(os.path.expanduser(Path(supplied_configuration_file or default_configuration_file)))
+
configuration = ScriptExecutorConfiguration(config_file)
logging_formater = '%(asctime)s - %(name)s - %(threadName)s - %(levelname)s - %(message)s'
logging.basicConfig(filename=configuration.script_executor_property('logFile'),
diff --git a/ms/py-executor/tox.ini b/ms/py-executor/tox.ini
index 8cf1776ba..d722ed45d 100644
--- a/ms/py-executor/tox.ini
+++ b/ms/py-executor/tox.ini
@@ -3,9 +3,9 @@ envlist=py37,py38
skipsdist=True
[testenv]
setenv =
- CONFIGURATION = configuration-local.ini
+ CONFIGURATION = {toxinidir}/../configuration-local.ini
deps =
- -rtest-requirements.txt
+ -rrequirements/test.txt
commands = pytest resource_resolution/
[testenv:codelint]
deps =
@@ -18,8 +18,8 @@ commands = flake8 --doctest --docstring-convention google --max-line-length 120
[testenv:coverage]
basepython = python3.7
setenv =
- CONFIGURATION = configuration-local.ini
+ CONFIGURATION = {toxinidir}/../configuration-local.ini
deps =
- -rtest-requirements.txt
+ -rrequirements/test.txt
pytest-cov
commands = pytest --cov=manager --cov=resource_resolution --cov-fail-under=60 --cov-config={toxinidir}/.coveragerc resource_resolution/
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2.py b/py-modules/common/proto/BluePrintCommon_pb2.py
index db78b2b48..db78b2b48 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2.py
+++ b/py-modules/common/proto/BluePrintCommon_pb2.py
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2_grpc.py b/py-modules/common/proto/BluePrintCommon_pb2_grpc.py
index 0129ff944..0129ff944 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintCommon_pb2_grpc.py
+++ b/py-modules/common/proto/BluePrintCommon_pb2_grpc.py
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2.py b/py-modules/common/proto/BluePrintManagement_pb2.py
index a2e0ec227..746861d6e 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2.py
+++ b/py-modules/common/proto/BluePrintManagement_pb2.py
@@ -28,7 +28,7 @@ _sym_db = _symbol_database.Default()
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
-from blueprints_grpc.proto import BluePrintCommon_pb2 as BluePrintCommon__pb2
+from proto import BluePrintCommon_pb2 as BluePrintCommon__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2_grpc.py b/py-modules/common/proto/BluePrintManagement_pb2_grpc.py
index 423a46003..10226abd6 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintManagement_pb2_grpc.py
+++ b/py-modules/common/proto/BluePrintManagement_pb2_grpc.py
@@ -15,7 +15,7 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
-from blueprints_grpc.proto import BluePrintManagement_pb2 as BluePrintManagement__pb2
+from proto import BluePrintManagement_pb2 as BluePrintManagement__pb2
class BluePrintManagementServiceStub(object):
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2.py b/py-modules/common/proto/BluePrintProcessing_pb2.py
index 36f8c4155..8a8d04c60 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2.py
+++ b/py-modules/common/proto/BluePrintProcessing_pb2.py
@@ -29,7 +29,7 @@ _sym_db = _symbol_database.Default()
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
-from blueprints_grpc.proto import BluePrintCommon_pb2 as BluePrintCommon__pb2
+from proto import BluePrintCommon_pb2 as BluePrintCommon__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
name='BluePrintProcessing.proto',
diff --git a/ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2_grpc.py b/py-modules/common/proto/BluePrintProcessing_pb2_grpc.py
index a482f5652..2349f171e 100644
--- a/ms/py-executor/blueprints_grpc/proto/BluePrintProcessing_pb2_grpc.py
+++ b/py-modules/common/proto/BluePrintProcessing_pb2_grpc.py
@@ -15,7 +15,7 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
-from blueprints_grpc.proto import BluePrintProcessing_pb2 as BluePrintProcessing__pb2
+from proto import BluePrintProcessing_pb2 as BluePrintProcessing__pb2
class BluePrintProcessingServiceStub(object):
diff --git a/ms/py-executor/blueprints_grpc/proto/__init__.py b/py-modules/common/proto/__init__.py
index b63c94c9c..b63c94c9c 100644
--- a/ms/py-executor/blueprints_grpc/proto/__init__.py
+++ b/py-modules/common/proto/__init__.py
diff --git a/py-modules/common/setup.py b/py-modules/common/setup.py
new file mode 100644
index 000000000..1ff59a6de
--- /dev/null
+++ b/py-modules/common/setup.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+"""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.
+"""
+
+from distutils.core import setup
+
+setup(
+ name="cdscommon",
+ version="0.1",
+ description="CDS Common Python Modules",
+ packages=["proto"],
+ install_requires=["grpcio-tools"],
+)