vFW and vDNS support added to azure-plugin
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / orchestrator / plugin.py
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 (file)
index 0000000..756a28e
--- /dev/null
@@ -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 <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