Merge "Module to manage pip packages"
authorRodolfo Alonso Hernandez <rodolfo.alonso.hernandez@intel.com>
Thu, 1 Mar 2018 13:23:54 +0000 (13:23 +0000)
committerGerrit Code Review <gerrit@opnfv.org>
Thu, 1 Mar 2018 13:23:54 +0000 (13:23 +0000)
17 files changed:
requirements.txt
test-requirements.txt
tox.ini
yardstick/common/packages.py [new file with mode: 0644]
yardstick/common/privsep.py [new file with mode: 0644]
yardstick/common/utils.py
yardstick/tests/functional/base.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/README.md [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/setup.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/__init__.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/__init__.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/__init__.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/__init__.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/dummy2.py [new file with mode: 0644]
yardstick/tests/functional/common/fake_pip_package/yardstick_new_plugin-1.0.0.tar.gz [new file with mode: 0644]
yardstick/tests/functional/common/test_packages.py [new file with mode: 0644]
yardstick/tests/unit/common/test_packages.py [new file with mode: 0644]

index aacafdf..d45e4b1 100644 (file)
@@ -37,11 +37,13 @@ os-client-config==1.28.0    # OSI Approved  Apache Software License
 osc-lib==1.7.0          # OSI Approved  Apache Software License
 oslo.config==4.11.1     # OSI Approved  Apache Software License
 oslo.i18n==3.17.0       # OSI Approved  Apache Software License
+oslo.privsep===1.22.1   # OSI Approved  Apache Software License
 oslo.serialization==2.20.1  # OSI Approved  Apache Software License
 oslo.utils==3.28.0      # OSI Approved  Apache Software License
 paramiko==2.2.1         # LGPL; OSI Approved  GNU Library or Lesser General Public License (LGPL)
 pbr==3.1.1              # OSI Approved  Apache Software License; Apache License, Version 2.0
 pika==0.10.0            # BSD; OSI Approved  BSD License
+pip==9.0.1;python_version=='2.7'        # MIT
 positional==1.1.2       # OSI Approved  Apache Software License
 pycrypto==2.6.1         # Public Domain
 pyparsing==2.2.0        # MIT License; OSI Approved  MIT License
index ee9815c..4828e98 100644 (file)
@@ -4,6 +4,7 @@
 
 coverage==4.4.2             # Apache 2.0; OSI Approved  Apache Software License; http://www.apache.org/licenses/LICENSE-2.0; http://www.apache.org/licenses/LICENSE-2.0
 fixtures==3.0.0             # OSI Approved  BSD License; OSI Approved  Apache Software License
+oslotest===2.17.1           # OSI Approved  Apache Software License
 packaging==16.8.0           # BSD or Apache License, Version 2.0
 pyflakes==1.0.0             # MIT; OSI Approved  MIT License
 pylint==1.8.1               # GPLv2
diff --git a/tox.ini b/tox.ini
index 822ffda..313f1ec 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,8 @@ envlist = py{27,3},pep8,functional{,-py3},coverage
 [testenv]
 usedevelop=True
 passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY BRANCH
+setenv =
+   VIRTUAL_ENV={envdir}
 deps =
     -r{toxinidir}/requirements.txt
     -r{toxinidir}/test-requirements.txt
diff --git a/yardstick/common/packages.py b/yardstick/common/packages.py
new file mode 100644 (file)
index 0000000..f20217f
--- /dev/null
@@ -0,0 +1,87 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+import logging
+import re
+
+import pip
+from pip import exceptions as pip_exceptions
+from pip.operations import freeze
+
+from yardstick.common import privsep
+
+
+LOG = logging.getLogger(__name__)
+
+ACTION_INSTALL = 'install'
+ACTION_UNINSTALL = 'uninstall'
+
+
+@privsep.yardstick_root.entrypoint
+def _pip_main(package, action, target=None):
+    if action == ACTION_UNINSTALL:
+        cmd = [action, package, '-y']
+    elif action == ACTION_INSTALL:
+        cmd = [action, package, '--upgrade']
+        if target:
+            cmd.append('--target=%s' % target)
+    return pip.main(cmd)
+
+
+def _pip_execute_action(package, action=ACTION_INSTALL, target=None):
+    """Execute an action with a PIP package.
+
+    According to [1], a package could be a URL, a local directory, a local dist
+    file or a requirements file.
+
+    [1] https://pip.pypa.io/en/stable/reference/pip_install/#argument-handling
+    """
+    try:
+        status = _pip_main(package, action, target)
+    except pip_exceptions.PipError:
+        status = 1
+
+    if not status:
+        LOG.info('Action "%s" executed, package %s', package, action)
+    else:
+        LOG.info('Error executing action "%s", package %s', package, action)
+    return status
+
+
+def pip_remove(package):
+    """Remove an installed PIP package"""
+    return _pip_execute_action(package, action=ACTION_UNINSTALL)
+
+
+def pip_install(package, target=None):
+    """Install a PIP package"""
+    return _pip_execute_action(package, action=ACTION_INSTALL, target=target)
+
+
+def pip_list(pkg_name=None):
+    """Dict of installed PIP packages with version.
+
+    If 'pkg_name' is not None, will return only those packages matching the
+    name."""
+    pip_regex = re.compile(r"(?P<name>.*)==(?P<version>[\w\.]+)")
+    git_regex = re.compile(r".*@(?P<version>[\w]+)#egg=(?P<name>[\w]+)")
+
+    pkg_dict = {}
+    for _pkg in freeze.freeze(local_only=True):
+        match = pip_regex.match(_pkg) or git_regex.match(_pkg)
+        if match and (not pkg_name or (
+                pkg_name and match.group('name').find(pkg_name) != -1)):
+            pkg_dict[match.group('name')] = match.group('version')
+
+    return pkg_dict
diff --git a/yardstick/common/privsep.py b/yardstick/common/privsep.py
new file mode 100644 (file)
index 0000000..4ae5104
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+from oslo_privsep import capabilities as c
+from oslo_privsep import priv_context
+
+yardstick_root = priv_context.PrivContext(
+    "yardstick",
+    cfg_section="yardstick_privileged",
+    pypath=__name__ + ".yardstick_root",
+    capabilities=[c.CAP_SYS_ADMIN]
+)
index 4952901..a77a4ca 100644 (file)
@@ -31,6 +31,7 @@ import six
 from flask import jsonify
 from six.moves import configparser
 from oslo_serialization import jsonutils
+from oslo_utils import encodeutils
 
 import yardstick
 
@@ -106,13 +107,12 @@ def remove_file(path):
             raise
 
 
-def execute_command(cmd):
+def execute_command(cmd, **kwargs):
     exec_msg = "Executing command: '%s'" % cmd
     logger.debug(exec_msg)
 
-    output = subprocess.check_output(cmd.split()).split(os.linesep)
-
-    return output
+    output = subprocess.check_output(cmd.split(), **kwargs)
+    return encodeutils.safe_decode(output, incoming='utf-8').split(os.linesep)
 
 
 def source_env(env_file):
diff --git a/yardstick/tests/functional/base.py b/yardstick/tests/functional/base.py
new file mode 100644 (file)
index 0000000..51be013
--- /dev/null
@@ -0,0 +1,46 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+import abc
+import six
+
+from oslo_config import cfg
+from oslotest import base
+
+
+CONF = cfg.CONF
+
+
+@six.add_metaclass(abc.ABCMeta)
+class BaseFunctionalTestCase(base.BaseTestCase):
+    """Base class for functional tests."""
+
+    def setUp(self):
+        super(BaseFunctionalTestCase, self).setUp()
+
+    def config(self, **kw):
+        """Override some configuration values.
+
+        The keyword arguments are the names of configuration options to
+        override and their values.
+
+        If a group argument is supplied, the overrides are applied to
+        the specified configuration option group.
+
+        All overrides are automatically cleared at the end of the current
+        test by the fixtures cleanup process.
+        """
+        group = kw.pop('group', None)
+        for k, v in kw.items():
+            CONF.set_override(k, v, group)
diff --git a/yardstick/tests/functional/common/fake_directory_package/README.md b/yardstick/tests/functional/common/fake_directory_package/README.md
new file mode 100644 (file)
index 0000000..689e470
--- /dev/null
@@ -0,0 +1,2 @@
+# yardstick_new_plugin
+Yardstick plugin
diff --git a/yardstick/tests/functional/common/fake_directory_package/setup.py b/yardstick/tests/functional/common/fake_directory_package/setup.py
new file mode 100644 (file)
index 0000000..cf938ef
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+from setuptools import setup, find_packages
+
+setup(
+    name='yardstick_new_plugin_2',
+    version='1.0.0',
+    packages=find_packages(),
+    include_package_data=True,
+    url='https://www.opnfv.org',
+    entry_points={
+        'yardstick.scenarios': [
+            'Dummy2 = yardstick_new_plugin.benchmark.scenarios.dummy2.dummy2:'
+            'Dummy2',
+        ]
+    },
+)
diff --git a/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/__init__.py b/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/__init__.py b/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/__init__.py b/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/__init__.py b/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/dummy2.py b/yardstick/tests/functional/common/fake_directory_package/yardstick_new_plugin_2/benchmark/scenarios/dummy2/dummy2.py
new file mode 100644 (file)
index 0000000..a2211ec
--- /dev/null
@@ -0,0 +1,40 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+import logging
+
+from yardstick.benchmark.scenarios import base
+
+
+LOG = logging.getLogger(__name__)
+
+
+class Dummy2(base.Scenario):
+    """Execute Dummy (v2!) echo"""
+    __scenario_type__ = "Dummy2"
+
+    def __init__(self, scenario_cfg, context_cfg):
+        self.scenario_cfg = scenario_cfg
+        self.context_cfg = context_cfg
+        self.setup_done = False
+
+    def setup(self):
+        self.setup_done = True
+
+    def run(self, result):
+        if not self.setup_done:
+            self.setup()
+
+        result["hello"] = "yardstick"
+        LOG.info("Dummy (v2!) echo hello yardstick!")
diff --git a/yardstick/tests/functional/common/fake_pip_package/yardstick_new_plugin-1.0.0.tar.gz b/yardstick/tests/functional/common/fake_pip_package/yardstick_new_plugin-1.0.0.tar.gz
new file mode 100644 (file)
index 0000000..e5379a7
Binary files /dev/null and b/yardstick/tests/functional/common/fake_pip_package/yardstick_new_plugin-1.0.0.tar.gz differ
diff --git a/yardstick/tests/functional/common/test_packages.py b/yardstick/tests/functional/common/test_packages.py
new file mode 100644 (file)
index 0000000..5dead4e
--- /dev/null
@@ -0,0 +1,94 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+import os
+from os import path
+import re
+
+from yardstick.common import packages
+from yardstick.common import utils
+from yardstick.tests.functional import base
+
+
+class PipPackagesTestCase(base.BaseFunctionalTestCase):
+
+    TMP_FOLDER = '/tmp/pip_packages/'
+    PYTHONPATH = 'PYTHONPATH=%s' % TMP_FOLDER
+
+    def setUp(self):
+        super(PipPackagesTestCase, self).setUp()
+        privsep_helper = os.path.join(
+            os.getenv('VIRTUAL_ENV'), 'bin', 'privsep-helper')
+        self.config(
+            helper_command=' '.join(['sudo', '-EH', privsep_helper]),
+            group='yardstick_privileged')
+        self.addCleanup(self._cleanup)
+
+    def _cleanup(self):
+        utils.execute_command('sudo rm -rf %s' % self.TMP_FOLDER)
+
+    def _remove_package(self, package):
+        os.system('%s pip uninstall %s -y' % (self.PYTHONPATH, package))
+
+    def _list_packages(self):
+        pip_list_regex = re.compile(
+            r"(?P<name>[\w\.-]+) \((?P<version>[\w\d_\.\-]+),*.*\)")
+        pkg_dict = {}
+        pkgs = utils.execute_command('pip list',
+                                     env={'PYTHONPATH': self.TMP_FOLDER})
+        for line in pkgs:
+            match = pip_list_regex.match(line)
+            if match and match.group('name'):
+                pkg_dict[match.group('name')] = match.group('version')
+        return pkg_dict
+
+    def test_install_from_folder(self):
+        dirname = path.dirname(__file__)
+        package_dir = dirname + '/fake_directory_package'
+        package_name = 'yardstick-new-plugin-2'
+        self.addCleanup(self._remove_package, package_name)
+        self._remove_package(package_name)
+        self.assertFalse(package_name in self._list_packages())
+
+        self.assertEqual(0, packages.pip_install(package_dir, self.TMP_FOLDER))
+        self.assertTrue(package_name in self._list_packages())
+
+    def test_install_from_pip_package(self):
+        dirname = path.dirname(__file__)
+        package_path = (dirname +
+                        '/fake_pip_package/yardstick_new_plugin-1.0.0.tar.gz')
+        package_name = 'yardstick-new-plugin'
+        self.addCleanup(self._remove_package, package_name)
+        self._remove_package(package_name)
+        self.assertFalse(package_name in self._list_packages())
+
+        self.assertEqual(0, packages.pip_install(package_path, self.TMP_FOLDER))
+        self.assertTrue(package_name in self._list_packages())
+
+    # NOTE(ralonsoh): an stable test plugin project is needed in OPNFV git
+    # server to execute this test.
+    # def test_install_from_url(self):
+
+    def test_pip_freeze(self):
+        # NOTE (ralonsoh): from requirements.txt file. The best way to test
+        # this function is to parse requirements.txt and test-requirements.txt
+        # and check all packages.
+        pkgs_ref = {'Babel': '2.3.4',
+                    'SQLAlchemy': '1.1.12',
+                    'influxdb': '4.1.1',
+                    'netifaces': '0.10.6',
+                    'unicodecsv': '0.14.1'}
+        pkgs = packages.pip_list()
+        for name, version in (pkgs_ref.items()):
+            self.assertEqual(version, pkgs[name])
diff --git a/yardstick/tests/unit/common/test_packages.py b/yardstick/tests/unit/common/test_packages.py
new file mode 100644 (file)
index 0000000..ba59a30
--- /dev/null
@@ -0,0 +1,88 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# Licensed 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.
+
+import mock
+from pip import exceptions as pip_exceptions
+from pip.operations import freeze
+import unittest
+
+from yardstick.common import packages
+
+
+class PipExecuteActionTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self._mock_pip_main = mock.patch.object(packages, '_pip_main')
+        self.mock_pip_main = self._mock_pip_main.start()
+        self.mock_pip_main.return_value = 0
+        self._mock_freeze = mock.patch.object(freeze, 'freeze')
+        self.mock_freeze = self._mock_freeze.start()
+        self.addCleanup(self._cleanup)
+
+    def _cleanup(self):
+        self._mock_pip_main.stop()
+        self._mock_freeze.stop()
+
+    def test_pip_execute_action(self):
+        self.assertEqual(0, packages._pip_execute_action('test_package'))
+
+    def test_remove(self):
+        self.assertEqual(0, packages._pip_execute_action('test_package',
+                                                         action='uninstall'))
+
+    def test_install(self):
+        self.assertEqual(0, packages._pip_execute_action(
+            'test_package', action='install', target='temp_dir'))
+
+    def test_pip_execute_action_error(self):
+        self.mock_pip_main.return_value = 1
+        self.assertEqual(1, packages._pip_execute_action('test_package'))
+
+    def test_pip_execute_action_exception(self):
+        self.mock_pip_main.side_effect = pip_exceptions.PipError
+        self.assertEqual(1, packages._pip_execute_action('test_package'))
+
+    def test_pip_list(self):
+        pkg_input = [
+            'XStatic-Rickshaw==1.5.0.0',
+            'xvfbwrapper==0.2.9',
+            '-e git+https://git.opnfv.org/yardstick@50773a24afc02c9652b662ecca'
+            '2fc5621ea6097a#egg=yardstick',
+            'zope.interface==4.4.3'
+        ]
+        pkg_dict = {
+            'XStatic-Rickshaw': '1.5.0.0',
+            'xvfbwrapper': '0.2.9',
+            'yardstick': '50773a24afc02c9652b662ecca2fc5621ea6097a',
+            'zope.interface': '4.4.3'
+        }
+        self.mock_freeze.return_value = pkg_input
+
+        pkg_output = packages.pip_list()
+        for pkg_name, pkg_version in pkg_output.items():
+            self.assertEqual(pkg_dict.get(pkg_name), pkg_version)
+
+    def test_pip_list_single_package(self):
+        pkg_input = [
+            'XStatic-Rickshaw==1.5.0.0',
+            'xvfbwrapper==0.2.9',
+            '-e git+https://git.opnfv.org/yardstick@50773a24afc02c9652b662ecca'
+            '2fc5621ea6097a#egg=yardstick',
+            'zope.interface==4.4.3'
+        ]
+        self.mock_freeze.return_value = pkg_input
+
+        pkg_output = packages.pip_list(pkg_name='xvfbwrapper')
+        self.assertEqual(1, len(pkg_output))
+        self.assertEqual(pkg_output.get('xvfbwrapper'), '0.2.9')