| # 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 <http://github.com/cloudify-cosmo/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 |