1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
|
# Puppet package provider for Python's `pip` package management frontend.
# <http://pip.pypa.io/>
require_relative '../../../puppet/util/package/version/pip'
require_relative '../../../puppet/util/package/version/range'
require_relative '../../../puppet/provider/package_targetable'
Puppet::Type.type(:package).provide :pip, :parent => ::Puppet::Provider::Package::Targetable do
desc "Python packages via `pip`.
This provider supports the `install_options` attribute, which allows command-line flags to be passed to pip.
These options should be specified as an array where each element is either a string or a hash."
has_feature :installable, :uninstallable, :upgradeable, :versionable, :version_ranges, :install_options, :targetable
PIP_VERSION = Puppet::Util::Package::Version::Pip
PIP_VERSION_RANGE = Puppet::Util::Package::Version::Range
# Override the specificity method to return 1 if pip is not set as default provider
def self.specificity
match = default_match
length = match ? match.length : 0
return 1 if length == 0
super
end
# Define the default provider package command name when the provider is targetable.
# Required by Puppet::Provider::Package::Targetable::resource_or_provider_command
def self.provider_command
# Ensure pip can upgrade pip, which usually puts pip into a new path /usr/local/bin/pip (compared to /usr/bin/pip)
self.cmd.map { |c| which(c) }.find { |c| c != nil }
end
def self.cmd
if Puppet::Util::Platform.windows?
["pip.exe"]
else
["pip", "pip-python", "pip2", "pip-2"]
end
end
def self.pip_version(command)
version = nil
execpipe [quote(command), '--version'] do |process|
process.collect do |line|
md = line.strip.match(/^pip (\d+\.\d+\.?\d*).*$/)
if md
version = md[1]
break
end
end
end
raise Puppet::Error, _("Cannot resolve pip version") unless version
version
end
# Return an array of structured information about every installed package
# that's managed by `pip` or an empty array if `pip` is not available.
def self.instances(target_command = nil)
if target_command
command = target_command
self.validate_command(command)
else
command = provider_command
end
packages = []
return packages unless command
command_options = ['freeze']
command_version = self.pip_version(command)
if compare_pip_versions(command_version, '8.1.0') >= 0
command_options << '--all'
end
execpipe [quote(command), command_options] do |process|
process.collect do |line|
pkg = parse(line)
next unless pkg
pkg[:command] = command
packages << new(pkg)
end
end
# Pip can also upgrade pip, but it's not listed in freeze so need to special case it
# Pip list would also show pip installed version, but "pip list" doesn't exist for older versions of pip (E.G v1.0)
# Not needed when "pip freeze --all" is available.
if compare_pip_versions(command_version, '8.1.0') == -1
packages << new({:ensure => command_version, :name => File.basename(command), :provider => name, :command => command})
end
packages
end
# Parse lines of output from `pip freeze`, which are structured as:
# _package_==_version_ or _package_===_version_
def self.parse(line)
if line.chomp =~ /^([^=]+)===?([^=]+)$/
{:ensure => $2, :name => $1, :provider => name}
end
end
# Return structured information about a particular package or `nil`
# if the package is not installed or `pip` itself is not available.
def query
command = resource_or_provider_command
self.class.validate_command(command)
self.class.instances(command).each do |pkg|
return pkg.properties if @resource[:name].casecmp(pkg.name).zero?
end
return nil
end
# Return latest version available for current package
def latest
command = resource_or_provider_command
self.class.validate_command(command)
command_version = self.class.pip_version(command)
if self.class.compare_pip_versions(command_version, '1.5.4') == -1
available_versions_with_old_pip.last
else
available_versions_with_new_pip(command_version).last
end
end
def self.compare_pip_versions(x, y)
begin
Puppet::Util::Package::Version::Pip.compare(x, y)
rescue PIP_VERSION::ValidationFailure => ex
Puppet.debug("Cannot compare #{x} and #{y}. #{ex.message} Falling through default comparison mechanism.")
Puppet::Util::Package.versioncmp(x, y)
end
end
# Use pip CLI to look up versions from PyPI repositories,
# honoring local pip config such as custom repositories.
def available_versions
command = resource_or_provider_command
self.class.validate_command(command)
command_version = self.class.pip_version(command)
if self.class.compare_pip_versions(command_version, '1.5.4') == -1
available_versions_with_old_pip
else
available_versions_with_new_pip(command_version)
end
end
def available_versions_with_new_pip(command_version)
command = resource_or_provider_command
self.class.validate_command(command)
command_and_options = [self.class.quote(command), 'install', "#{@resource[:name]}==versionplease"]
extra_arg = list_extra_flags(command_version)
command_and_options << extra_arg if extra_arg
command_and_options << install_options if @resource[:install_options]
execpipe command_and_options do |process|
process.collect do |line|
# PIP OUTPUT: Could not find a version that satisfies the requirement example==versionplease (from versions: 1.2.3, 4.5.6)
if line =~ /from versions: (.+)\)/
versionList = $1.split(', ').sort do |x,y|
self.class.compare_pip_versions(x, y)
end
return versionList
end
end
end
[]
end
def available_versions_with_old_pip
command = resource_or_provider_command
self.class.validate_command(command)
Dir.mktmpdir("puppet_pip") do |dir|
command_and_options = [self.class.quote(command), 'install', "#{@resource[:name]}", '-d', "#{dir}", '-v']
command_and_options << install_options if @resource[:install_options]
execpipe command_and_options do |process|
process.collect do |line|
# PIP OUTPUT: Using version 0.10.1 (newest of versions: 1.2.3, 4.5.6)
if line =~ /Using version .+? \(newest of versions: (.+?)\)/
versionList = $1.split(', ').sort do |x,y|
self.class.compare_pip_versions(x, y)
end
return versionList
end
end
end
return []
end
end
# Finds the most suitable version available in a given range
def best_version(should_range)
included_available_versions = []
available_versions.each do |version|
version = PIP_VERSION.parse(version)
included_available_versions.push(version) if should_range.include?(version)
end
included_available_versions.sort!
return included_available_versions.last unless included_available_versions.empty?
Puppet.debug("No available version for package #{@resource[:name]} is included in range #{should_range}")
should_range
end
def get_install_command_options()
should = @resource[:ensure]
command_options = %w{install -q}
command_options += install_options if @resource[:install_options]
if @resource[:source]
if String === should
command_options << "#{@resource[:source]}@#{should}#egg=#{@resource[:name]}"
else
command_options << "#{@resource[:source]}#egg=#{@resource[:name]}"
end
return command_options
end
if should == :latest
command_options << "--upgrade" << @resource[:name]
return command_options
end
unless String === should
command_options << @resource[:name]
return command_options
end
begin
should_range = PIP_VERSION_RANGE.parse(should, PIP_VERSION)
rescue PIP_VERSION_RANGE::ValidationFailure, PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{should} as a pip version range, falling through.")
command_options << "#{@resource[:name]}==#{should}"
return command_options
end
if should_range.is_a?(PIP_VERSION_RANGE::Eq)
command_options << "#{@resource[:name]}==#{should}"
return command_options
end
should = best_version(should_range)
if should == should_range
# when no suitable version for the given range was found, let pip handle
if should.is_a?(PIP_VERSION_RANGE::MinMax)
command_options << "#{@resource[:name]} #{should.split.join(',')}"
else
command_options << "#{@resource[:name]} #{should}"
end
else
command_options << "#{@resource[:name]}==#{should}"
end
command_options
end
# Install a package. The ensure parameter may specify installed,
# latest, a version number, or, in conjunction with the source
# parameter, an SCM revision. In that case, the source parameter
# gives the fully-qualified URL to the repository.
def install
command = resource_or_provider_command
self.class.validate_command(command)
command_options = get_install_command_options
execute([command, command_options])
end
# Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu unless this issue gets fixed.
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=562544
def uninstall
command = resource_or_provider_command
self.class.validate_command(command)
command_options = ["uninstall", "-y", "-q", @resource[:name]]
execute([command, command_options])
end
def update
install
end
def install_options
join_options(@resource[:install_options])
end
def insync?(is)
return false unless is && is != :absent
begin
should = @resource[:ensure]
should_range = PIP_VERSION_RANGE.parse(should, PIP_VERSION)
rescue PIP_VERSION_RANGE::ValidationFailure, PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{should} as a pip version range")
return false
end
begin
is_version = PIP_VERSION.parse(is)
rescue PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{is} as a pip version")
return false
end
should_range.include?(is_version)
end
# Quoting is required if the path to the pip command contains spaces.
# Required for execpipe() but not execute(), as execute() already does this.
def self.quote(path)
if path.include?(" ")
"\"#{path}\""
else
path
end
end
private
def list_extra_flags(command_version)
klass = self.class
if klass.compare_pip_versions(command_version, '20.2.4') == 1 &&
klass.compare_pip_versions(command_version, '21.1') == -1
'--use-deprecated=legacy-resolver'
end
end
end
|