From 4b46200477216dbcc54611d1c3e6f0cc83757aaf Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 5 Jan 2016 00:01:22 -0500 Subject: [PATCH] Add umask option to pip module On systems with restrictive umasks, the pip module won't allow you to install pip packages that are usable by everyone on the system. This commit adds a umask option to optionally override the umask on a per-package basis. --- packaging/language/pip.py | 250 ++++++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 107 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 484c8bfc0d8..d9b403215bf 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -22,6 +22,7 @@ import tempfile import re import os +import sys DOCUMENTATION = ''' --- @@ -114,6 +115,17 @@ options: version_added: "1.3" required: false default: null + umask: + description: + - The system umask to apply before installing the pip package. This is + useful, for example, when installing on systems that have a very + restrictive umask by default (e.g., 0077) and you want to pip install + packages which are to be used by all users. Note that this requires you + to specify desired umask mode in octal, with a leading 0 (e.g., 0077). + version_added: "2.1" + required: false + default: null + notes: - Please note that virtualenv (U(http://www.virtualenv.org/)) must be installed on the remote host if the virtualenv parameter is specified and the virtualenv needs to be initialized. requirements: [ "virtualenv", "pip" ] @@ -159,6 +171,10 @@ EXAMPLES = ''' # Install (Bottle), forcing reinstallation if it's already installed - pip: name=bottle state=forcereinstall + +# Install (Bottle) while ensuring the umask is 0022 (to ensure other users can use it) +- pip: name=bottle umask=0022 + become: True ''' def _get_cmd_options(module, cmd): @@ -258,6 +274,7 @@ def main(): editable=dict(default='yes', type='bool', required=False), chdir=dict(default=None, required=False, type='path'), executable=dict(default=None, required=False), + umask=dict(reqiured=False,default=None), ), required_one_of=[['name', 'requirements']], mutually_exclusive=[['name', 'requirements']], @@ -271,129 +288,148 @@ def main(): extra_args = module.params['extra_args'] virtualenv_python = module.params['virtualenv_python'] chdir = module.params['chdir'] + umask = module.params['umask'] + + if umask and not isinstance(umask, int): + try: + umask = int(umask, 8) + except Exception: + module.fail_json(msg="umask must be an octal integer", + details=str(sys.exc_info()[1])) + + + old_umask = None + if umask != None: + old_umask = os.umask(umask) + try: + if state == 'latest' and version is not None: + module.fail_json(msg='version is incompatible with state=latest') + + if chdir is None: + # this is done to avoid permissions issues with privilege escalation and virtualenvs + chdir = tempfile.gettempdir() + + err = '' + out = '' + + env = module.params['virtualenv'] + virtualenv_command = module.params['virtualenv_command'] + + if env: + env = os.path.expanduser(env) + if not os.path.exists(os.path.join(env, 'bin', 'activate')): + if module.check_mode: + module.exit_json(changed=True) + + cmd = os.path.expanduser(virtualenv_command) + if os.path.basename(cmd) == cmd: + cmd = module.get_bin_path(virtualenv_command, True) + + if module.params['virtualenv_site_packages']: + cmd += ' --system-site-packages' + else: + cmd_opts = _get_cmd_options(module, cmd) + if '--no-site-packages' in cmd_opts: + cmd += ' --no-site-packages' + + if virtualenv_python: + cmd += ' -p%s' % virtualenv_python + + cmd = "%s %s" % (cmd, env) + rc, out_venv, err_venv = module.run_command(cmd, cwd=chdir) + out += out_venv + err += err_venv + if rc != 0: + _fail(module, cmd, out, err) + + pip = _get_pip(module, env, module.params['executable']) + + cmd = '%s %s' % (pip, state_map[state]) + + # If there's a virtualenv we want things we install to be able to use other + # installations that exist as binaries within this virtualenv. Example: we + # install cython and then gevent -- gevent needs to use the cython binary, + # not just a python package that will be found by calling the right python. + # So if there's a virtualenv, we add that bin/ to the beginning of the PATH + # in run_command by setting path_prefix here. + path_prefix = None + if env: + path_prefix = "/".join(pip.split('/')[:-1]) + + # Automatically apply -e option to extra_args when source is a VCS url. VCS + # includes those beginning with svn+, git+, hg+ or bzr+ + has_vcs = bool(name and re.match(r'(svn|git|hg|bzr)\+', name)) + if has_vcs and module.params['editable']: + args_list = [] # used if extra_args is not used at all + if extra_args: + args_list = extra_args.split(' ') + if '-e' not in args_list: + args_list.append('-e') + # Ok, we will reconstruct the option string + extra_args = ' '.join(args_list) - if state == 'latest' and version is not None: - module.fail_json(msg='version is incompatible with state=latest') - - if chdir is None: - # this is done to avoid permissions issues with privilege escalation and virtualenvs - chdir = tempfile.gettempdir() - - err = '' - out = '' + if extra_args: + cmd += ' %s' % extra_args + if name: + cmd += ' %s' % _get_full_name(name, version) + elif requirements: + cmd += ' -r %s' % requirements - env = module.params['virtualenv'] - virtualenv_command = module.params['virtualenv_command'] - if env: - env = os.path.expanduser(env) - if not os.path.exists(os.path.join(env, 'bin', 'activate')): - if module.check_mode: + if module.check_mode: + if extra_args or requirements or state == 'latest' or not name: + module.exit_json(changed=True) + elif has_vcs: module.exit_json(changed=True) - cmd = os.path.expanduser(virtualenv_command) - if os.path.basename(cmd) == cmd: - cmd = module.get_bin_path(virtualenv_command, True) - - if module.params['virtualenv_site_packages']: - cmd += ' --system-site-packages' - else: - cmd_opts = _get_cmd_options(module, cmd) - if '--no-site-packages' in cmd_opts: - cmd += ' --no-site-packages' + freeze_cmd = '%s freeze' % pip - if virtualenv_python: - cmd += ' -p%s' % virtualenv_python + rc, out_pip, err_pip = module.run_command(freeze_cmd, cwd=chdir) - cmd = "%s %s" % (cmd, env) - rc, out_venv, err_venv = module.run_command(cmd, cwd=chdir) - out += out_venv - err += err_venv if rc != 0: - _fail(module, cmd, out, err) - - pip = _get_pip(module, env, module.params['executable']) - - cmd = '%s %s' % (pip, state_map[state]) - - # If there's a virtualenv we want things we install to be able to use other - # installations that exist as binaries within this virtualenv. Example: we - # install cython and then gevent -- gevent needs to use the cython binary, - # not just a python package that will be found by calling the right python. - # So if there's a virtualenv, we add that bin/ to the beginning of the PATH - # in run_command by setting path_prefix here. - path_prefix = None - if env: - path_prefix = "/".join(pip.split('/')[:-1]) - - # Automatically apply -e option to extra_args when source is a VCS url. VCS - # includes those beginning with svn+, git+, hg+ or bzr+ - has_vcs = bool(name and re.match(r'(svn|git|hg|bzr)\+', name)) - if has_vcs and module.params['editable']: - args_list = [] # used if extra_args is not used at all - if extra_args: - args_list = extra_args.split(' ') - if '-e' not in args_list: - args_list.append('-e') - # Ok, we will reconstruct the option string - extra_args = ' '.join(args_list) - - if extra_args: - cmd += ' %s' % extra_args - if name: - cmd += ' %s' % _get_full_name(name, version) - elif requirements: - cmd += ' -r %s' % requirements - + module.exit_json(changed=True) - if module.check_mode: - if extra_args or requirements or state == 'latest' or not name: - module.exit_json(changed=True) - elif has_vcs: - module.exit_json(changed=True) + out += out_pip + err += err_pip - freeze_cmd = '%s freeze' % pip + is_present = _is_present(name, version, out.split()) - rc, out_pip, err_pip = module.run_command(freeze_cmd, cwd=chdir) + changed = (state == 'present' and not is_present) or (state == 'absent' and is_present) + module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err) - if rc != 0: - module.exit_json(changed=True) + if requirements or has_vcs: + freeze_cmd = '%s freeze' % pip + out_freeze_before = module.run_command(freeze_cmd, cwd=chdir)[1] + else: + out_freeze_before = None + rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=chdir) out += out_pip err += err_pip - - is_present = _is_present(name, version, out.split()) - - changed = (state == 'present' and not is_present) or (state == 'absent' and is_present) or (state == 'forcereinstall' and is_present) - module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err) - - if requirements or has_vcs: - freeze_cmd = '%s freeze' % pip - out_freeze_before = module.run_command(freeze_cmd, cwd=chdir)[1] - else: - out_freeze_before = None - - rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=chdir) - out += out_pip - err += err_pip - if rc == 1 and state == 'absent' and \ - ('not installed' in out_pip or 'not installed' in err_pip): - pass # rc is 1 when attempting to uninstall non-installed package - elif rc != 0: - _fail(module, cmd, out, err) - - if state == 'absent': - changed = 'Successfully uninstalled' in out_pip - else: - if out_freeze_before is None: - changed = 'Successfully installed' in out_pip + if rc == 1 and state == 'absent' and \ + ('not installed' in out_pip or 'not installed' in err_pip): + pass # rc is 1 when attempting to uninstall non-installed package + elif rc != 0: + _fail(module, cmd, out, err) + + if state == 'absent': + changed = 'Successfully uninstalled' in out_pip else: - out_freeze_after = module.run_command(freeze_cmd, cwd=chdir)[1] - changed = out_freeze_before != out_freeze_after - - module.exit_json(changed=changed, cmd=cmd, name=name, version=version, - state=state, requirements=requirements, virtualenv=env, - stdout=out, stderr=err) + if out_freeze_before is None: + changed = 'Successfully installed' in out_pip + else: + if out_freeze_before is None: + changed = 'Successfully installed' in out_pip + else: + out_freeze_after = module.run_command(freeze_cmd, cwd=chdir)[1] + changed = out_freeze_before != out_freeze_after + + module.exit_json(changed=changed, cmd=cmd, name=name, version=version, + state=state, requirements=requirements, virtualenv=env, + stdout=out, stderr=err) + finally: + if old_umask != None: + os.umask(old_umask) # import module snippets from ansible.module_utils.basic import *