diff options
author | Marek Szwalkiewicz <marek.szwalkiewicz@external.t-mobile.pl> | 2020-01-30 13:49:18 +0000 |
---|---|---|
committer | Marek Szwalkiewicz <marek.szwalkiewicz@external.t-mobile.pl> | 2020-01-30 13:52:07 +0000 |
commit | be4c46420944531765ecc8bae7305086d71a36d0 (patch) | |
tree | be9309a134a50e964b1257395d74c41c2da512ef /ms/artifact-manager/manager/utils.py | |
parent | da25e1649c9a10f998c8dde068641d7601a3f00a (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>
Diffstat (limited to 'ms/artifact-manager/manager/utils.py')
-rw-r--r-- | ms/artifact-manager/manager/utils.py | 176 |
1 files changed, 176 insertions, 0 deletions
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"])) |