diff --git a/meta/ad.murr b/meta/ad.murr new file mode 100644 index 00000000..7288753c Binary files /dev/null and b/meta/ad.murr differ diff --git a/muranoapi/packages/__init__.py b/muranoapi/packages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/muranoapi/packages/application_package.py b/muranoapi/packages/application_package.py new file mode 100644 index 00000000..f2ce95a1 --- /dev/null +++ b/muranoapi/packages/application_package.py @@ -0,0 +1,218 @@ +# Copyright (c) 2014 Mirantis Inc. +# +# 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 imghdr +import os +import shutil +import sys +import tarfile +import tempfile +import yaml + +import muranoapi.packages.exceptions as e +import muranoapi.packages.versions.v1 + + +class PackageTypes(object): + Library = 'Library' + Application = 'Application' + ALL = [Library, Application] + + +class ApplicationPackage(object): + def __init__(self, source_directory, yaml_content): + self._source_directory = source_directory + self._full_name = None + self._package_type = None + self._display_name = None + self._description = None + self._author = None + self._tags = None + self._classes = None + self._ui = None + self._logo = None + self._format = yaml_content.get('Format') + self._ui_cache = None + self._raw_ui_cache = None + self._logo_cache = None + self._classes_cache = {} + + @property + def full_name(self): + return self._full_name + + @property + def package_type(self): + return self._package_type + + @property + def display_name(self): + return self._display_name + + @property + def description(self): + return self._description + + @property + def author(self): + return self._author + + @property + def tags(self): + return tuple(self._tags) + + @property + def classes(self): + return tuple(self._classes.keys()) + + @property + def ui(self): + if not self._ui_cache: + self._load_ui(True) + return self._ui_cache + + @property + def raw_ui(self): + if not self._raw_ui_cache: + self._load_ui(False) + return self._raw_ui_cache + + @property + def logo(self): + if not self._logo_cache: + self._load_logo(False) + return self._logo_cache + + def get_class(self, name): + if name not in self._classes_cache: + self._load_class(name) + return self._classes_cache[name] + + def validate(self): + self._classes_cache.clear() + for class_name in self._classes: + self.get_class(class_name) + self._load_ui(True) + self._load_logo(True) + + # Private methods + def _load_ui(self, load_yaml=False): + if self._raw_ui_cache and load_yaml: + self._ui_cache = yaml.load(self._raw_ui_cache) + else: + ui_file = self._ui + full_path = os.path.join(self._source_directory, 'UI', ui_file) + if not os.path.isfile(full_path): + self._raw_ui_cache = None + self._ui_cache = None + return + try: + with open(full_path) as stream: + self._raw_ui_cache = stream.read() + if load_yaml: + self._ui_cache = yaml.load(self._raw_ui_cache) + except Exception as ex: + trace = sys.exc_info()[2] + raise e.PackageUILoadError(str(ex)), None, trace + + def _load_logo(self, validate=False): + logo_file = self._logo or 'logo.png' + full_path = os.path.join(self._source_directory, logo_file) + if not os.path.isfile(full_path) and logo_file == 'logo.png': + self._logo_cache = None + return + try: + if validate: + if imghdr.what(full_path) != 'png': + raise e.PackageLoadError("Logo is not in PNG format") + with open(full_path) as stream: + self._logo_cache = stream.read() + except Exception as ex: + trace = sys.exc_info()[2] + raise e.PackageLoadError( + "Unable to load logo: " + str(ex)), None, trace + + def _load_class(self, name): + if name not in self._classes: + raise e.PackageClassLoadError(name, 'Class not defined ' + 'in this package') + def_file = self._classes[name] + full_path = os.path.join(self._source_directory, 'Classes', def_file) + if not os.path.isfile(full_path): + raise e.PackageClassLoadError(name, 'File with class ' + 'definition not found') + try: + with open(full_path) as stream: + self._classes_cache[name] = yaml.load(stream) + except Exception as ex: + trace = sys.exc_info()[2] + msg = 'Unable to load class definition due to "{0}"'.format( + str(ex)) + raise e.PackageClassLoadError(name, msg), None, trace + + +def load_from_dir(source_directory, filename='manifest.yaml', preload=False): + formats = {'1.0': muranoapi.packages.versions.v1} + + if not os.path.isdir(source_directory) or not os.path.exists( + source_directory): + raise e.PackageLoadError('Invalid package directory') + full_path = os.path.join(source_directory, filename) + if not os.path.isfile(full_path): + raise e.PackageLoadError('Unable to find package manifest') + + try: + with open(full_path) as stream: + content = yaml.load(stream) + except Exception as ex: + trace = sys.exc_info()[2] + raise e.PackageLoadError( + "Unable to load due to '{0}'".format(str(ex))), None, trace + if content: + p_format = str(content.get('Format')) + if not p_format or p_format not in formats: + raise e.PackageFormatError( + 'Unknown or missing format version') + package = ApplicationPackage(source_directory, content) + formats[p_format].load(package, content) + if preload: + package.validate() + return package + + +def load_from_file(archive_path, target_dir=None, drop_dir=False): + if not os.path.isfile(archive_path): + raise e.PackageLoadError('Unable to find package file') + created = False + if not target_dir: + target_dir = tempfile.mkdtemp() + created = True + elif not os.path.exists(target_dir): + os.mkdir(target_dir) + created = True + else: + if os.listdir(target_dir): + raise e.PackageLoadError('Target directory is not empty') + + try: + package = tarfile.open(archive_path) + package.extractall(path=target_dir) + return load_from_dir(target_dir, preload=True) + finally: + if drop_dir: + if created: + shutil.rmtree(target_dir) + else: + for f in os.listdir(target_dir): + os.unlink(os.path.join(target_dir, f)) diff --git a/muranoapi/packages/exceptions.py b/muranoapi/packages/exceptions.py new file mode 100644 index 00000000..4e2c47f7 --- /dev/null +++ b/muranoapi/packages/exceptions.py @@ -0,0 +1,44 @@ +# Copyright (c) 2014 Mirantis Inc. +# +# 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 muranoapi.openstack.common.exception as e + + +class PackageClassLoadError(e.Error): + def __init__(self, class_name, message=None): + msg = 'Unable to load class "{0}" from package'.format(class_name) + if message: + msg += ": " + message + super(PackageClassLoadError, self).__init__(msg) + + +class PackageUILoadError(e.Error): + def __init__(self, message=None): + msg = 'Unable to load ui definition from package' + if message: + msg += ": " + message + super(PackageUILoadError, self).__init__(msg) + + +class PackageLoadError(e.Error): + pass + + +class PackageFormatError(PackageLoadError): + def __init__(self, message=None): + msg = 'Incorrect package format' + if message: + msg += ': ' + message + super(PackageFormatError, self).__init__(msg) diff --git a/muranoapi/packages/versions/__init__.py b/muranoapi/packages/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/muranoapi/packages/versions/v1.py b/muranoapi/packages/versions/v1.py new file mode 100644 index 00000000..ce64a9dd --- /dev/null +++ b/muranoapi/packages/versions/v1.py @@ -0,0 +1,50 @@ +# Copyright (c) 2014 Mirantis Inc. +# +# 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 re + +import muranoapi.packages.application_package +import muranoapi.packages.exceptions as e + + +# noinspection PyProtectedMember +def load(package, yaml_content): + package._full_name = yaml_content.get('FullName') + if not package._full_name: + raise muranoapi.packages.exceptions.PackageFormatError( + 'FullName not specified') + _check_full_name(package._full_name) + package._package_type = yaml_content.get('Type') + if not package._package_type or package._package_type not in \ + muranoapi.packages.application_package.PackageTypes.ALL: + raise e.PackageFormatError('Invalid Package Type') + package._display_name = yaml_content.get('Name', package._full_name) + package._description = yaml_content.get('Description') + package._author = yaml_content.get('Author') + package._classes = yaml_content.get('Classes') + package._ui = yaml_content.get('UI', 'ui.yaml') + package._logo = yaml_content.get('Logo') + + +def _check_full_name(full_name): + error = muranoapi.packages.exceptions.PackageFormatError( + 'Invalid FullName') + if re.match(r'^[\w\.]+$', full_name): + if full_name.startswith('.') or full_name.endswith('.'): + raise error + if '..' in full_name: + raise error + else: + raise error