X-Git-Url: https://gerrit.onap.org/r/gitweb?a=blobdiff_plain;f=azure%2Faria%2Faria-extension-cloudify%2Fsrc%2Faria%2Faria%2Forchestrator%2Fplugin.py;fp=azure%2Faria%2Faria-extension-cloudify%2Fsrc%2Faria%2Faria%2Forchestrator%2Fplugin.py;h=756a28ed0c1acb35e6ae9f5eced660cac86bb7f6;hb=7409dfb144cf2a06210400134d822a1393462b1f;hp=0000000000000000000000000000000000000000;hpb=9e65649dfff8f00dc0a0ef6b10d020ae0e2255ba;p=multicloud%2Fazure.git diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/orchestrator/plugin.py b/azure/aria/aria-extension-cloudify/src/aria/aria/orchestrator/plugin.py new file mode 100644 index 0000000..756a28e --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/orchestrator/plugin.py @@ -0,0 +1,171 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +""" +Plugin management. +""" + +import os +import tempfile +import subprocess +import sys +import zipfile +from datetime import datetime + +import wagon + +from . import exceptions +from ..utils import process as process_utils + +_IS_WIN = os.name == 'nt' + + +class PluginManager(object): + + def __init__(self, model, plugins_dir): + """ + :param plugins_dir: root directory in which to install plugins + """ + self._model = model + self._plugins_dir = plugins_dir + + def install(self, source): + """ + Install a wagon plugin. + """ + metadata = wagon.show(source) + cls = self._model.plugin.model_cls + + os_props = metadata['build_server_os_properties'] + + plugin = cls( + name=metadata['package_name'], + archive_name=metadata['archive_name'], + supported_platform=metadata['supported_platform'], + supported_py_versions=metadata['supported_python_versions'], + distribution=os_props.get('distribution'), + distribution_release=os_props['distribution_version'], + distribution_version=os_props['distribution_release'], + package_name=metadata['package_name'], + package_version=metadata['package_version'], + package_source=metadata['package_source'], + wheels=metadata['wheels'], + uploaded_at=datetime.now() + ) + if len(self._model.plugin.list(filters={'package_name': plugin.package_name, + 'package_version': plugin.package_version})): + raise exceptions.PluginAlreadyExistsError( + 'Plugin {0}, version {1} already exists'.format(plugin.package_name, + plugin.package_version)) + self._install_wagon(source=source, prefix=self.get_plugin_dir(plugin)) + self._model.plugin.put(plugin) + return plugin + + def load_plugin(self, plugin, env=None): + """ + Load the plugin into an environment. + + Loading the plugin means the plugin's code and binaries paths will be appended to the + environment's ``PATH`` and ``PYTHONPATH``, thereby allowing usage of the plugin. + + :param plugin: plugin to load + :param env: environment to load the plugin into; If ``None``, :obj:`os.environ` will be + used + """ + env = env or os.environ + plugin_dir = self.get_plugin_dir(plugin) + + # Update PATH environment variable to include plugin's bin dir + bin_dir = 'Scripts' if _IS_WIN else 'bin' + process_utils.append_to_path(os.path.join(plugin_dir, bin_dir), env=env) + + # Update PYTHONPATH environment variable to include plugin's site-packages + # directories + if _IS_WIN: + pythonpath_dirs = [os.path.join(plugin_dir, 'Lib', 'site-packages')] + else: + # In some linux environments, there will be both a lib and a lib64 directory + # with the latter, containing compiled packages. + pythonpath_dirs = [os.path.join( + plugin_dir, 'lib{0}'.format(b), + 'python{0}.{1}'.format(sys.version_info[0], sys.version_info[1]), + 'site-packages') for b in ('', '64')] + + process_utils.append_to_pythonpath(*pythonpath_dirs, env=env) + + def get_plugin_dir(self, plugin): + return os.path.join( + self._plugins_dir, + '{0}-{1}'.format(plugin.package_name, plugin.package_version)) + + @staticmethod + def validate_plugin(source): + """ + Validate a plugin archive. + + A valid plugin is a `wagon `__ in the zip format + (suffix may also be ``.wgn``). + """ + if not zipfile.is_zipfile(source): + raise exceptions.InvalidPluginError( + 'Archive {0} is of an unsupported type. Only ' + 'zip/wgn is allowed'.format(source)) + with zipfile.ZipFile(source, 'r') as zip_file: + infos = zip_file.infolist() + try: + package_name = infos[0].filename[:infos[0].filename.index('/')] + package_json_path = "{0}/{1}".format(package_name, 'package.json') + zip_file.getinfo(package_json_path) + except (KeyError, ValueError, IndexError): + raise exceptions.InvalidPluginError( + 'Failed to validate plugin {0} ' + '(package.json was not found in archive)'.format(source)) + + def _install_wagon(self, source, prefix): + pip_freeze_output = self._pip_freeze() + file_descriptor, constraint_path = tempfile.mkstemp(prefix='constraint-', suffix='.txt') + os.close(file_descriptor) + try: + with open(constraint_path, 'wb') as constraint: + constraint.write(pip_freeze_output) + # Install the provided wagon. + # * The --prefix install_arg will cause the plugin to be installed under + # plugins_dir/{package_name}-{package_version}, So different plugins don't step on + # each other and don't interfere with the current virtualenv + # * The --constraint flag points a file containing the output of ``pip freeze``. + # It is required, to handle cases where plugins depend on some python package with + # a different version than the one installed in the current virtualenv. Without this + # flag, the existing package will be **removed** from the parent virtualenv and the + # new package will be installed under prefix. With the flag, the existing version will + # remain, and the version requested by the plugin will be ignored. + wagon.install( + source=source, + install_args='--prefix="{prefix}" --constraint="{constraint}"'.format( + prefix=prefix, + constraint=constraint.name), + venv=os.environ.get('VIRTUAL_ENV')) + finally: + os.remove(constraint_path) + + @staticmethod + def _pip_freeze(): + """Run pip freeze in current environment and return the output""" + bin_dir = 'Scripts' if os.name == 'nt' else 'bin' + pip_path = os.path.join(sys.prefix, bin_dir, + 'pip{0}'.format('.exe' if os.name == 'nt' else '')) + pip_freeze = subprocess.Popen([pip_path, 'freeze'], stdout=subprocess.PIPE) + pip_freeze_output, _ = pip_freeze.communicate() + assert not pip_freeze.poll() + return pip_freeze_output