Passed
Pull Request — master (#3922)
by Anthony
09:35
created

create_virtualenv()   D

Complexity

Conditions 9

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
dl 0
loc 61
rs 4.8709
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# Licensed to the StackStorm, Inc ('StackStorm') 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
Pack virtual environment related utility functions.
18
"""
19
20
from __future__ import absolute_import
21
import os
22
import re
23
import shutil
24
25
from oslo_config import cfg
26
27
from st2common import log as logging
28
from st2common.constants.pack import PACK_REF_WHITELIST_REGEX
29
from st2common.constants.pack import BASE_PACK_REQUIREMENTS
30
from st2common.util.shell import run_command
31
from st2common.util.shell import quote_unix
32
from st2common.util.pack import get_pack_metadata
33
from st2common.util.compat import to_ascii
34
from st2common.content.utils import get_packs_base_paths
35
from st2common.content.utils import get_pack_directory
36
37
__all__ = [
38
    'setup_pack_virtualenv'
39
]
40
41
LOG = logging.getLogger(__name__)
42
43
44
def setup_pack_virtualenv(pack_name, update=False, logger=None, include_pip=True,
45
                          include_setuptools=True, include_wheel=True, proxy_config=None):
46
47
    """
48
    Setup virtual environment for the provided pack.
49
50
    :param pack_name: Name of the pack to setup the virtualenv for.
51
    :type pack_name: ``str``
52
53
    :param update: True to update dependencies inside the virtual environment.
54
    :type update: ``bool``
55
56
    :param logger: Optional logger instance to use. If not provided it defaults to the module
57
                   level logger.
58
    """
59
    logger = logger or LOG
60
    three = False  # TODO: Change default Python version to a global setting
61
    if not re.match(PACK_REF_WHITELIST_REGEX, pack_name):
62
        raise ValueError('Invalid pack name "%s"' % (pack_name))
63
64
    base_virtualenvs_path = os.path.join(cfg.CONF.system.base_path, 'virtualenvs/')
65
    virtualenv_path = os.path.join(base_virtualenvs_path, quote_unix(pack_name))
66
67
    # Ensure pack directory exists in one of the search paths
68
    pack_path = get_pack_directory(pack_name=pack_name)
69
70
    logger.debug('Setting up virtualenv for pack "%s" (%s)' % (pack_name, pack_path))
71
72
    if not pack_path:
73
        packs_base_paths = get_packs_base_paths()
74
        search_paths = ', '.join(packs_base_paths)
75
        msg = 'Pack "%s" is not installed. Looked in: %s' % (pack_name, search_paths)
76
        raise Exception(msg)
77
78
    try:
79
        pack_meta = get_pack_metadata(pack_path)
80
        has_pack_meta = True
81
    except ValueError:
82
        # Pack is missing meta file
83
        has_pack_meta = False
84
85
    if has_pack_meta:
86
        logger.debug('Checking pack specific Python version.')
87
        if 'system' in pack_meta.keys() and 'python3' in pack_meta['system'].keys():
88
            three = bool(pack_meta['system']['python3'])
89
            logger.debug('Using Python %s in virtualenv' % (3 if three else 2))
90
91
    # 1. Create virtualenv if it doesn't exist
92
    if not update or not os.path.exists(virtualenv_path):
93
        # 0. Delete virtual environment if it exists
94
        remove_virtualenv(virtualenv_path=virtualenv_path, logger=logger)
95
96
        # 1. Create virtual environment
97
        logger.debug('Creating virtualenv for pack "%s" in "%s"' % (pack_name, virtualenv_path))
98
        create_virtualenv(virtualenv_path=virtualenv_path, logger=logger, include_pip=include_pip,
99
                          include_setuptools=include_setuptools, include_wheel=include_wheel,
100
                          three=three)
101
102
    # 2. Install base requirements which are common to all the packs
103
    logger.debug('Installing base requirements')
104
    for requirement in BASE_PACK_REQUIREMENTS:
105
        install_requirement(virtualenv_path=virtualenv_path, requirement=requirement,
106
                            proxy_config=proxy_config, logger=logger)
107
108
    # 3. Install pack-specific requirements
109
    requirements_file_path = os.path.join(pack_path, 'requirements.txt')
110
    has_requirements = os.path.isfile(requirements_file_path)
111
112
    if has_requirements:
113
        logger.debug('Installing pack specific requirements from "%s"' %
114
                     (requirements_file_path))
115
        install_requirements(virtualenv_path=virtualenv_path,
116
                             requirements_file_path=requirements_file_path,
117
                             proxy_config=proxy_config,
118
                             logger=logger)
119
    else:
120
        logger.debug('No pack specific requirements found')
121
122
    action = 'updated' if update else 'created'
123
    logger.debug('Virtualenv for pack "%s" successfully %s in "%s"' %
124
                 (pack_name, action, virtualenv_path))
125
126
127
def create_virtualenv(virtualenv_path, logger=None, include_pip=True, include_setuptools=True,
128
                      include_wheel=True, three=False):
129
    """
130
    :param include_pip: Include pip binary and package in the newely created virtual environment.
131
    :type include_pip: ``bool``
132
133
    :param include_setuptools: Include setuptools binary and package in the newely created virtual
134
                               environment.
135
    :type include_setuptools: ``bool``
136
137
    :param include_wheel: Include wheel in the newely created virtual environment.
138
    :type include_wheel : ``bool``
139
140
    :param three: Use Python 3 binary
141
    :type  three: ``bool``
142
    """
143
144
    logger = logger or LOG
145
146
    if three:
147
        python_binary = cfg.CONF.actionrunner.python3_binary
148
    else:
149
        python_binary = cfg.CONF.actionrunner.python_binary
150
    virtualenv_binary = cfg.CONF.actionrunner.virtualenv_binary
151
    virtualenv_opts = cfg.CONF.actionrunner.virtualenv_opts
152
153
    if not os.path.isfile(python_binary):
154
        raise Exception('Python binary "%s" doesn\'t exist' % (python_binary))
155
156
    if not os.path.isfile(virtualenv_binary):
157
        raise Exception('Virtualenv binary "%s" doesn\'t exist.' % (virtualenv_binary))
158
159
    logger.debug('Creating virtualenv in "%s" using Python binary "%s"' %
160
                 (virtualenv_path, python_binary))
161
162
    cmd = [virtualenv_binary, '-p', python_binary]
163
    cmd.extend(virtualenv_opts)
164
165
    if not include_pip:
166
        cmd.append('--no-pip')
167
168
    if not include_setuptools:
169
        cmd.append('--no-setuptools')
170
171
    if not include_wheel:
172
        cmd.append('--no-wheel')
173
174
    cmd.extend([virtualenv_path])
175
    logger.debug('Running command "%s" to create virtualenv.', ' '.join(cmd))
176
177
    try:
178
        exit_code, _, stderr = run_command(cmd=cmd)
179
    except OSError as e:
180
        raise Exception('Error executing command %s. %s.' % (' '.join(cmd),
181
                                                             e.message))
182
183
    if exit_code != 0:
184
        raise Exception('Failed to create virtualenv in "%s": %s' %
185
                        (virtualenv_path, stderr))
186
187
    return True
188
189
190
def remove_virtualenv(virtualenv_path, logger=None):
191
    """
192
    Remove the provided virtualenv.
193
    """
194
    logger = logger or LOG
195
196
    if not os.path.exists(virtualenv_path):
197
        logger.info('Virtualenv path "%s" doesn\'t exist' % virtualenv_path)
198
        return True
199
200
    logger.debug('Removing virtualenv in "%s"' % virtualenv_path)
201
    try:
202
        shutil.rmtree(virtualenv_path)
203
    except Exception as e:
204
        logger.error('Error while removing virtualenv at "%s": "%s"' % (virtualenv_path, e))
205
        raise e
206
207
    return True
208
209
210
def install_requirements(virtualenv_path, requirements_file_path, proxy_config=None, logger=None):
211
    """
212
    Install requirements from a file.
213
    """
214
    logger = logger or LOG
215
    pip_path = os.path.join(virtualenv_path, 'bin/pip')
216
    cmd = [pip_path]
217
218
    if proxy_config:
219
        cert = proxy_config.get('proxy_ca_bundle_path', None)
220
        https_proxy = proxy_config.get('https_proxy', None)
221
        http_proxy = proxy_config.get('http_proxy', None)
222
223
        if http_proxy:
224
            cmd.extend(['--proxy', http_proxy])
225
226
        if https_proxy:
227
            cmd.extend(['--proxy', https_proxy])
228
229
        if cert:
230
            cmd.extend(['--cert', cert])
231
232
    cmd.extend(['install', '-U', '-r', requirements_file_path])
233
    env = get_env_for_subprocess_command()
234
235
    logger.debug('Installing requirements from file %s with command %s.',
236
                 requirements_file_path, ' '.join(cmd))
237
    exit_code, stdout, stderr = run_command(cmd=cmd, env=env)
238
239
    if exit_code != 0:
240
        stdout = to_ascii(stdout)
241
        stderr = to_ascii(stderr)
242
243
        raise Exception('Failed to install requirements from "%s": %s (stderr: %s)' %
244
                        (requirements_file_path, stdout, stderr))
245
246
    return True
247
248
249
def install_requirement(virtualenv_path, requirement, proxy_config=None, logger=None):
250
    """
251
    Install a single requirement.
252
253
    :param requirement: Requirement specifier.
254
    """
255
    logger = logger or LOG
256
    pip_path = os.path.join(virtualenv_path, 'bin/pip')
257
    cmd = [pip_path]
258
259
    if proxy_config:
260
        cert = proxy_config.get('proxy_ca_bundle_path', None)
261
        https_proxy = proxy_config.get('https_proxy', None)
262
        http_proxy = proxy_config.get('http_proxy', None)
263
264
        if http_proxy:
265
            cmd.extend(['--proxy', http_proxy])
266
267
        if https_proxy:
268
            cmd.extend(['--proxy', https_proxy])
269
270
        if cert:
271
            cmd.extend(['--cert', cert])
272
273
    cmd.extend(['install', requirement])
274
    env = get_env_for_subprocess_command()
275
    logger.debug('Installing requirement %s with command %s.',
276
                 requirement, ' '.join(cmd))
277
    exit_code, stdout, stderr = run_command(cmd=cmd, env=env)
278
279
    if exit_code != 0:
280
        raise Exception('Failed to install requirement "%s": %s' %
281
                        (requirement, stdout))
282
283
    return True
284
285
286
def get_env_for_subprocess_command():
287
    """
288
    Retrieve environment to be used with the subprocess command.
289
290
    Note: We remove PYTHONPATH from the environment so the command works
291
    correctly with the newely created virtualenv.
292
    """
293
    env = os.environ.copy()
294
295
    if 'PYTHONPATH' in env:
296
        del env['PYTHONPATH']
297
298
    return env
299