view modules/python/manifests/pip.pp @ 478:adf6fe9bbc17

Update Puppet modules to latest versions
author IBBoard <dev@ibboard.co.uk>
date Thu, 29 Aug 2024 18:47:29 +0100
parents 66c075c5f54a
children
line wrap: on
line source

# @summary Installs and manages packages from pip.
#
# @param name must be unique
# @param pkgname the name of the package.
# @param ensure Require pip to be available.
# @param virtualenv virtualenv to run pip in.
# @param pip_provider version of pip you wish to use.
# @param url URL to install from.
# @param owner The owner of the virtualenv being manipulated.
# @param group The group of the virtualenv being manipulated.
# @param index Base URL of Python package index.
# @param extra_index Base URL of extra Python package index.
# @param proxy Proxy server to use for outbound connections.
# @param editable If true the package is installed as an editable resource.
# @param environment Additional environment variables required to install the packages.
# @param extras Extra features provided by the package which should be installed.
# @param timeout The maximum time in seconds the "pip install" command should take.
# @param install_args Any additional installation arguments that will be supplied when running pip install.
# @param uninstall_args Any additional arguments that will be supplied when running pip uninstall.
# @param log_dir Log directory
# @param egg The egg name to use
# @param umask
#
# @example Install Flask to /var/www/project1 using a proxy
#   python::pip { 'flask':
#     virtualenv => '/var/www/project1',
#     proxy      => 'http://proxy.domain.com:3128',
#     index      => 'http://www.example.com/simple/',
#   }
# @example Install cx_Oracle with pip
#   python::pip { 'cx_Oracle' :
#     pkgname       => 'cx_Oracle',
#     ensure        => '5.1.2',
#     virtualenv    => '/var/www/project1',
#     owner         => 'appuser',
#     proxy         => 'http://proxy.domain.com:3128',
#     environment   => ['ORACLE_HOME=/usr/lib/oracle/11.2/client64'],
#     install_args  => '-e',
#     timeout       => 1800,
#   }
# @example Install Requests with pip3
#   python::pip { 'requests' :
#     ensure        => 'present',
#     pkgname       => 'requests',
#     pip_provider  => 'pip3',
#     virtualenv    => '/var/www/project1',
#     owner         => 'root',
#     timeout       => 1800
#   }
#
define python::pip (
  String[1]                                         $pkgname        = $name,
  Variant[Enum[present, absent, latest], String[1]] $ensure         = present,
  Variant[Enum['system'], Stdlib::Absolutepath]     $virtualenv     = 'system',
  String[1]                                         $pip_provider   = 'pip',
  Variant[Boolean, String]                          $url            = false,
  String[1]                                         $owner          = 'root',
  Optional[String[1]]                               $group          = getvar('python::params::group'),
  Optional[Python::Umask]                           $umask          = undef,
  Variant[Boolean,String[1]]                        $index          = false,
  Variant[Boolean,String[1]]                        $extra_index    = false,
  Optional[Stdlib::HTTPUrl]                         $proxy          = undef,
  Any                                               $egg            = false,
  Boolean                                           $editable       = false,
  Array                                             $environment    = [],
  Array                                             $extras         = [],
  Optional[String[1]]                               $install_args   = undef,
  Optional[String[1]]                               $uninstall_args = undef,
  Numeric                                           $timeout        = 1800,
  String[1]                                         $log_dir        = '/tmp',
  Array[String]                                     $path           = ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'],
  String[1]                                         $exec_provider  = 'shell',
) {
  $python_provider = getparam(Class['python'], 'provider')
  $python_version  = getparam(Class['python'], 'version')

  if $virtualenv != 'system' {
    Python::Pyvenv <| |> -> Python::Pip[$name]
  }

  # Get SCL exec prefix
  # NB: this will not work if you are running puppet from scl enabled shell
  $exec_prefix = $python_provider ? {
    'scl'   => "scl enable ${python_version} -- ",
    'rhscl' => "scl enable ${python_version} -- ",
    default => '',
  }

  $_path = $python_provider ? {
    'anaconda' => concat(["${python::anaconda_install_path}/bin"], $path),
    default    => $path,
  }

  # Parameter validation
  if $virtualenv == 'system' and $owner != 'root' {
    fail('python::pip: root user must be used when virtualenv is system')
  }

  $cwd = $virtualenv ? {
    'system' => '/',
    default  => $virtualenv,
  }

  $log = $virtualenv ? {
    'system' => $log_dir,
    default  => $virtualenv,
  }

  $pip_env = $virtualenv ? {
    'system' => "${exec_prefix}${pip_provider}",
    default  => "${exec_prefix}${virtualenv}/bin/${pip_provider}",
  }

  $pypi_index = $index ? {
    false   => '',
    default => "--index-url=${index}",
  }

  $pypi_extra_index = $extra_index ? {
    false   => '',
    default => "--extra-index-url=${extra_index}",
  }

  $proxy_flag = $proxy ? {
    undef    => '',
    default  => "--proxy=${proxy}",
  }

  if $editable == true {
    $install_editable = ' -e '
  }
  else {
    $install_editable = ''
  }

  # TODO: Do more robust argument checking, but below is a start
  if ($ensure == absent) and $install_args {
    fail('python::pip cannot provide install_args with ensure => absent')
  }

  if ($ensure == present) and $uninstall_args {
    fail('python::pip cannot provide uninstall_args with ensure => present')
  }

  if $pkgname =~ /==/ {
    $parts        = split($pkgname, '==')
    $real_pkgname = $parts[0]

    $_ensure = $ensure ? {
      'absent' => 'absent',
      default => $parts[1],
    }
  } else {
    $real_pkgname = $pkgname
    $_ensure      = $ensure
  }

  # Check if searching by explicit version.
  if $_ensure =~ /^((19|20)[0-9][0-9]-(0[1-9]|1[1-2])-([0-2][1-9]|3[0-1])|[0-9]+\.\w+\+?\w*(\.\w+)*)$/ {
    $grep_regex = "^${real_pkgname}[[:space:]]\\+(\\?${_ensure}\\()$\\|$\\|, \\|[[:space:]]\\)"
  } else {
    $grep_regex = "^${real_pkgname}[[:space:]].*$"
  }

  $extras_string = empty($extras) ? {
    true    => '',
    default => sprintf('[%s]',join($extras,',')),
  }

  $egg_name = $egg ? {
    false   => "${real_pkgname}${extras_string}",
    default => $egg
  }

  $source = $url ? {
    false                                                                     => "${real_pkgname}${extras_string}",
    /^(\/|[a-zA-Z]\:)/                                                        => "'${url}'",
    /^(git\+|hg\+|bzr\+|svn\+)(http|https|ssh|svn|sftp|ftp|lp|git)(:\/\/).+$/ => "'${url}'",
    default                                                                   => "'${url}#egg=${egg_name}'",
  }

  $pip_install     = "${pip_env} --log ${log}/pip.log install"
  $pip_common_args = "${pypi_index} ${pypi_extra_index} ${proxy_flag} ${install_editable} ${source}"

  # Explicit version out of VCS when PIP supported URL is provided
  if $source =~ /^'(git\+|hg\+|bzr\+|svn\+)(http|https|ssh|svn|sftp|ftp|lp|git)(:\/\/).+'$/ {
    if $_ensure != present and $_ensure != latest {
      $command        = "${pip_install} ${install_args} ${pip_common_args}@${_ensure}#egg=${egg_name}"
      $unless_command = "${pip_env} list | grep -i -e '${grep_regex}'"
    } else {
      $command        = "${pip_install} ${install_args} ${pip_common_args}"
      $unless_command = "${pip_env} list | grep -i -e '${grep_regex}'"
    }
  } else {
    case $_ensure {
      /^((19|20)[0-9][0-9]-(0[1-9]|1[1-2])-([0-2][1-9]|3[0-1])|[0-9]+\.\w+\+?\w*(\.\w+)*)$/: {
        # Version formats as per http://guide.python-distribute.org/specification.html#standard-versioning-schemes
        # Explicit version.
        $command        = "${pip_install} ${install_args} ${pip_common_args}==${_ensure}"
        $unless_command = "${pip_env} list | grep -i -e '${grep_regex}'"
      }

      'present': {
        # Whatever version is available.
        $command        = "${pip_install} ${install_args} ${pip_common_args}"
        $unless_command = "${pip_env} list | grep -i -e '${grep_regex}'"
      }

      'latest': {
        $pip_version = $facts['pip_version']
        if $pip_version and versioncmp($pip_version, '21.1') == -1 and versioncmp($pip_version, '20.2.4') == 1 {
          $legacy_resolver = '--use-deprecated=legacy-resolver'
        } else {
          $legacy_resolver = ''
        }

        # Unfortunately this is the smartest way of getting the latest available package version with pip as of now
        # Note: we DO need to repeat ourselves with "from version" in both grep and sed as on some systems pip returns
        # more than one line with paretheses.
        $latest_version = join([
            "${pip_install} ${legacy_resolver} ${pypi_index} ${pypi_extra_index} ${proxy_flag}",
            " ${install_args} ${install_editable} ${real_pkgname}==notreallyaversion 2>&1",
            " | sed -nE 's/.*\\(from versions: (.*, )*(.*)\\)/\\2/p'",
            ' | tr -d "[:space:]"',
        ])

        # Packages with underscores in their names are listed with dashes in their place in `pip freeze` output
        $pkgname_with_dashes            = regsubst($real_pkgname, '_', '-', 'G')
        $grep_regex_pkgname_with_dashes = "^${pkgname_with_dashes}=="
        $installed_version              = join(["${pip_env} freeze --all", " | grep -i -e ${grep_regex_pkgname_with_dashes} | cut -d= -f3", " | tr -d '[:space:]'",])

        $command        = "${pip_install} --upgrade ${install_args} ${pip_common_args}"
        $unless_command = "[ \$(${latest_version}) = \$(${installed_version}) ]"
      }

      default: {
        # Anti-action, uninstall.
        $command        = "echo y | ${pip_env} uninstall ${uninstall_args} ${proxy_flag} ${real_pkgname}"
        $unless_command = "! ${pip_env} list | grep -i -e '${grep_regex}'"
      }
    }
  }

  $pip_installer = ($ensure == 'absent') ? {
    true  => "pip_uninstall_${name}",
    false => "pip_install_${name}",
  }

  exec { $pip_installer:
    command     => $command,
    unless      => $unless_command,
    user        => $owner,
    group       => $group,
    umask       => $umask,
    cwd         => $cwd,
    environment => $environment,
    timeout     => $timeout,
    path        => $_path,
    provider    => $exec_provider,
  }
}