1 # Licensed to the Apache Software Foundation (ASF) under one or more
2 # contributor license agreements. See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
25 from datetime import datetime
29 from . import exceptions
30 from ..utils import process as process_utils
32 _IS_WIN = os.name == 'nt'
35 class PluginManager(object):
37 def __init__(self, model, plugins_dir):
39 :param plugins_dir: root directory in which to install plugins
42 self._plugins_dir = plugins_dir
44 def install(self, source):
46 Install a wagon plugin.
48 metadata = wagon.show(source)
49 cls = self._model.plugin.model_cls
51 os_props = metadata['build_server_os_properties']
54 name=metadata['package_name'],
55 archive_name=metadata['archive_name'],
56 supported_platform=metadata['supported_platform'],
57 supported_py_versions=metadata['supported_python_versions'],
58 distribution=os_props.get('distribution'),
59 distribution_release=os_props['distribution_version'],
60 distribution_version=os_props['distribution_release'],
61 package_name=metadata['package_name'],
62 package_version=metadata['package_version'],
63 package_source=metadata['package_source'],
64 wheels=metadata['wheels'],
65 uploaded_at=datetime.now()
67 if len(self._model.plugin.list(filters={'package_name': plugin.package_name,
68 'package_version': plugin.package_version})):
69 raise exceptions.PluginAlreadyExistsError(
70 'Plugin {0}, version {1} already exists'.format(plugin.package_name,
71 plugin.package_version))
72 self._install_wagon(source=source, prefix=self.get_plugin_dir(plugin))
73 self._model.plugin.put(plugin)
76 def load_plugin(self, plugin, env=None):
78 Load the plugin into an environment.
80 Loading the plugin means the plugin's code and binaries paths will be appended to the
81 environment's ``PATH`` and ``PYTHONPATH``, thereby allowing usage of the plugin.
83 :param plugin: plugin to load
84 :param env: environment to load the plugin into; If ``None``, :obj:`os.environ` will be
87 env = env or os.environ
88 plugin_dir = self.get_plugin_dir(plugin)
90 # Update PATH environment variable to include plugin's bin dir
91 bin_dir = 'Scripts' if _IS_WIN else 'bin'
92 process_utils.append_to_path(os.path.join(plugin_dir, bin_dir), env=env)
94 # Update PYTHONPATH environment variable to include plugin's site-packages
97 pythonpath_dirs = [os.path.join(plugin_dir, 'Lib', 'site-packages')]
99 # In some linux environments, there will be both a lib and a lib64 directory
100 # with the latter, containing compiled packages.
101 pythonpath_dirs = [os.path.join(
102 plugin_dir, 'lib{0}'.format(b),
103 'python{0}.{1}'.format(sys.version_info[0], sys.version_info[1]),
104 'site-packages') for b in ('', '64')]
106 process_utils.append_to_pythonpath(*pythonpath_dirs, env=env)
108 def get_plugin_dir(self, plugin):
111 '{0}-{1}'.format(plugin.package_name, plugin.package_version))
114 def validate_plugin(source):
116 Validate a plugin archive.
118 A valid plugin is a `wagon <http://github.com/cloudify-cosmo/wagon>`__ in the zip format
119 (suffix may also be ``.wgn``).
121 if not zipfile.is_zipfile(source):
122 raise exceptions.InvalidPluginError(
123 'Archive {0} is of an unsupported type. Only '
124 'zip/wgn is allowed'.format(source))
125 with zipfile.ZipFile(source, 'r') as zip_file:
126 infos = zip_file.infolist()
128 package_name = infos[0].filename[:infos[0].filename.index('/')]
129 package_json_path = "{0}/{1}".format(package_name, 'package.json')
130 zip_file.getinfo(package_json_path)
131 except (KeyError, ValueError, IndexError):
132 raise exceptions.InvalidPluginError(
133 'Failed to validate plugin {0} '
134 '(package.json was not found in archive)'.format(source))
136 def _install_wagon(self, source, prefix):
137 pip_freeze_output = self._pip_freeze()
138 file_descriptor, constraint_path = tempfile.mkstemp(prefix='constraint-', suffix='.txt')
139 os.close(file_descriptor)
141 with open(constraint_path, 'wb') as constraint:
142 constraint.write(pip_freeze_output)
143 # Install the provided wagon.
144 # * The --prefix install_arg will cause the plugin to be installed under
145 # plugins_dir/{package_name}-{package_version}, So different plugins don't step on
146 # each other and don't interfere with the current virtualenv
147 # * The --constraint flag points a file containing the output of ``pip freeze``.
148 # It is required, to handle cases where plugins depend on some python package with
149 # a different version than the one installed in the current virtualenv. Without this
150 # flag, the existing package will be **removed** from the parent virtualenv and the
151 # new package will be installed under prefix. With the flag, the existing version will
152 # remain, and the version requested by the plugin will be ignored.
155 install_args='--prefix="{prefix}" --constraint="{constraint}"'.format(
157 constraint=constraint.name),
158 venv=os.environ.get('VIRTUAL_ENV'))
160 os.remove(constraint_path)
164 """Run pip freeze in current environment and return the output"""
165 bin_dir = 'Scripts' if os.name == 'nt' else 'bin'
166 pip_path = os.path.join(sys.prefix, bin_dir,
167 'pip{0}'.format('.exe' if os.name == 'nt' else ''))
168 pip_freeze = subprocess.Popen([pip_path, 'freeze'], stdout=subprocess.PIPE)
169 pip_freeze_output, _ = pip_freeze.communicate()
170 assert not pip_freeze.poll()
171 return pip_freeze_output