Passed
Pull Request — master (#3462)
by Lakshmi
06:54 queued 47s
created

create_virtualenv()   C

Complexity

Conditions 8

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 0
loc 55
rs 6.2085

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