vFW and vDNS support added to azure-plugin
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / orchestrator / plugin.py
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
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15
16 """
17 Plugin management.
18 """
19
20 import os
21 import tempfile
22 import subprocess
23 import sys
24 import zipfile
25 from datetime import datetime
26
27 import wagon
28
29 from . import exceptions
30 from ..utils import process as process_utils
31
32 _IS_WIN = os.name == 'nt'
33
34
35 class PluginManager(object):
36
37     def __init__(self, model, plugins_dir):
38         """
39         :param plugins_dir: root directory in which to install plugins
40         """
41         self._model = model
42         self._plugins_dir = plugins_dir
43
44     def install(self, source):
45         """
46         Install a wagon plugin.
47         """
48         metadata = wagon.show(source)
49         cls = self._model.plugin.model_cls
50
51         os_props = metadata['build_server_os_properties']
52
53         plugin = cls(
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()
66         )
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)
74         return plugin
75
76     def load_plugin(self, plugin, env=None):
77         """
78         Load the plugin into an environment.
79
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.
82
83         :param plugin: plugin to load
84         :param env: environment to load the plugin into; If ``None``, :obj:`os.environ` will be
85          used
86         """
87         env = env or os.environ
88         plugin_dir = self.get_plugin_dir(plugin)
89
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)
93
94         # Update PYTHONPATH environment variable to include plugin's site-packages
95         # directories
96         if _IS_WIN:
97             pythonpath_dirs = [os.path.join(plugin_dir, 'Lib', 'site-packages')]
98         else:
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')]
105
106         process_utils.append_to_pythonpath(*pythonpath_dirs, env=env)
107
108     def get_plugin_dir(self, plugin):
109         return os.path.join(
110             self._plugins_dir,
111             '{0}-{1}'.format(plugin.package_name, plugin.package_version))
112
113     @staticmethod
114     def validate_plugin(source):
115         """
116         Validate a plugin archive.
117
118         A valid plugin is a `wagon <http://github.com/cloudify-cosmo/wagon>`__ in the zip format
119         (suffix may also be ``.wgn``).
120         """
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()
127             try:
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))
135
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)
140         try:
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.
153             wagon.install(
154                 source=source,
155                 install_args='--prefix="{prefix}" --constraint="{constraint}"'.format(
156                     prefix=prefix,
157                     constraint=constraint.name),
158                 venv=os.environ.get('VIRTUAL_ENV'))
159         finally:
160             os.remove(constraint_path)
161
162     @staticmethod
163     def _pip_freeze():
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