diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb
index ddbbacdd0..6b4538c14 100644
--- a/lib/puppet/provider/package/pip.rb
+++ b/lib/puppet/provider/package/pip.rb
@@ -1,119 +1,118 @@
# Puppet package provider for Python's `pip` package management frontend.
#
require 'puppet/provider/package'
require 'xmlrpc/client'
Puppet::Type.type(:package).provide :pip,
:parent => ::Puppet::Provider::Package do
desc "Python packages via `pip`."
has_feature :installable, :uninstallable, :upgradeable, :versionable
# Parse lines of output from `pip freeze`, which are structured as
# _package_==_version_.
def self.parse(line)
if line.chomp =~ /^([^=]+)==([^=]+)$/
{:ensure => $2, :name => $1, :provider => name}
else
nil
end
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
packages = []
pip_cmd = which(cmd) or return []
execpipe "#{pip_cmd} freeze" do |process|
process.collect do |line|
next unless options = parse(line)
packages << new(options)
end
end
packages
end
def self.cmd
- case Facter.value(:osfamily)
- when "RedHat"
- "pip-python"
- else
- "pip"
+ if Facter.value(:osfamily) == "RedHat" and Facter.value(:operatingsystemmajrelease).to_i < 7
+ "pip-python"
+ else
+ "pip"
end
end
# Return structured information about a particular package or `nil` if
# it is not installed or `pip` itself is not available.
def query
self.class.instances.each do |provider_pip|
return provider_pip.properties if @resource[:name].downcase == provider_pip.name.downcase
end
return nil
end
# Ask the PyPI API for the latest version number. There is no local
# cache of PyPI's package list so this operation will always have to
# ask the web service.
def latest
client = XMLRPC::Client.new2("http://pypi.python.org/pypi")
client.http_header_extra = {"Content-Type" => "text/xml"}
client.timeout = 10
result = client.call("package_releases", @resource[:name])
result.first
rescue Timeout::Error => detail
raise Puppet::Error, "Timeout while contacting pypi.python.org: #{detail}", detail.backtrace
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
args = %w{install -q}
if @resource[:source]
if String === @resource[:ensure]
args << "#{@resource[:source]}@#{@resource[:ensure]}#egg=#{
@resource[:name]}"
else
args << "#{@resource[:source]}#egg=#{@resource[:name]}"
end
else
case @resource[:ensure]
when String
args << "#{@resource[:name]}==#{@resource[:ensure]}"
when :latest
args << "--upgrade" << @resource[:name]
else
args << @resource[:name]
end
end
lazy_pip *args
end
# Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu
# unless this issue gets fixed.
#
def uninstall
lazy_pip "uninstall", "-y", "-q", @resource[:name]
end
def update
install
end
# Execute a `pip` command. If Puppet doesn't yet know how to do so,
# try to teach it and if even that fails, raise the error.
private
def lazy_pip(*args)
pip *args
rescue NoMethodError => e
if pathname = which(self.class.cmd)
self.class.commands :pip => pathname
pip *args
else
raise e, 'Could not locate the pip command.', e.backtrace
end
end
end
diff --git a/spec/unit/provider/package/pip_spec.rb b/spec/unit/provider/package/pip_spec.rb
index 81f614791..397d8d05f 100755
--- a/spec/unit/provider/package/pip_spec.rb
+++ b/spec/unit/provider/package/pip_spec.rb
@@ -1,247 +1,259 @@
#! /usr/bin/env ruby
require 'spec_helper'
provider_class = Puppet::Type.type(:package).provider(:pip)
-osfamilies = { 'RedHat' => 'pip-python', 'Not RedHat' => 'pip' }
+osfamilies = { ['RedHat', '6'] => 'pip-python', ['RedHat', '7'] => 'pip', ['Not RedHat', nil] => 'pip' }
describe provider_class do
before do
@resource = Puppet::Resource.new(:package, "fake_package")
@provider = provider_class.new(@resource)
@client = stub_everything('client')
@client.stubs(:call).with('package_releases', 'real_package').returns(["1.3", "1.2.5", "1.2.4"])
@client.stubs(:call).with('package_releases', 'fake_package').returns([])
XMLRPC::Client.stubs(:new2).returns(@client)
end
describe "parse" do
it "should return a hash on valid input" do
provider_class.parse("real_package==1.2.5").should == {
:ensure => "1.2.5",
:name => "real_package",
:provider => :pip,
}
end
it "should return nil on invalid input" do
provider_class.parse("foo").should == nil
end
end
describe "cmd" do
- it "should return pip-python on RedHat systems" do
+ it "should return pip-python on RedHat < 7 systems" do
Facter.stubs(:value).with(:osfamily).returns("RedHat")
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns("6")
provider_class.cmd.should == 'pip-python'
end
+ it "should return pip on RedHat >= 7 systems" do
+ Facter.stubs(:value).with(:osfamily).returns("RedHat")
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns("7")
+ provider_class.cmd.should == 'pip'
+ end
+
it "should return pip by default" do
Facter.stubs(:value).with(:osfamily).returns("Not RedHat")
provider_class.cmd.should == 'pip'
end
end
describe "instances" do
osfamilies.each do |osfamily, pip_cmd|
it "should return an array on #{osfamily} when #{pip_cmd} is present" do
- Facter.stubs(:value).with(:osfamily).returns(osfamily)
+ Facter.stubs(:value).with(:osfamily).returns(osfamily.first)
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns(osfamily.last)
provider_class.expects(:which).with(pip_cmd).returns("/fake/bin/pip")
p = stub("process")
p.expects(:collect).yields("real_package==1.2.5")
provider_class.expects(:execpipe).with("/fake/bin/pip freeze").yields(p)
provider_class.instances
end
it "should return an empty array on #{osfamily} when #{pip_cmd} is missing" do
- Facter.stubs(:value).with(:osfamily).returns(osfamily)
+ Facter.stubs(:value).with(:osfamily).returns(osfamily.first)
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns(osfamily.last)
provider_class.expects(:which).with(pip_cmd).returns nil
provider_class.instances.should == []
end
end
end
describe "query" do
before do
@resource[:name] = "real_package"
end
it "should return a hash when pip and the package are present" do
provider_class.expects(:instances).returns [provider_class.new({
:ensure => "1.2.5",
:name => "real_package",
:provider => :pip,
})]
@provider.query.should == {
:ensure => "1.2.5",
:name => "real_package",
:provider => :pip,
}
end
it "should return nil when the package is missing" do
provider_class.expects(:instances).returns []
@provider.query.should == nil
end
it "should be case insensitive" do
@resource[:name] = "Real_Package"
provider_class.expects(:instances).returns [provider_class.new({
:ensure => "1.2.5",
:name => "real_package",
:provider => :pip,
})]
@provider.query.should == {
:ensure => "1.2.5",
:name => "real_package",
:provider => :pip,
}
end
end
describe "latest" do
it "should find a version number for real_package" do
@resource[:name] = "real_package"
@provider.latest.should_not == nil
end
it "should not find a version number for fake_package" do
@resource[:name] = "fake_package"
@provider.latest.should == nil
end
it "should handle a timeout gracefully" do
@resource[:name] = "fake_package"
@client.stubs(:call).raises(Timeout::Error)
lambda { @provider.latest }.should raise_error(Puppet::Error)
end
end
describe "install" do
before do
@resource[:name] = "fake_package"
@url = "git+https://example.com/fake_package.git"
end
it "should install" do
@resource[:ensure] = :installed
@resource[:source] = nil
@provider.expects(:lazy_pip).
with("install", '-q', "fake_package")
@provider.install
end
it "omits the -e flag (GH-1256)" do
# The -e flag makes the provider non-idempotent
@resource[:ensure] = :installed
@resource[:source] = @url
@provider.expects(:lazy_pip).with() do |*args|
not args.include?("-e")
end
@provider.install
end
it "should install from SCM" do
@resource[:ensure] = :installed
@resource[:source] = @url
@provider.expects(:lazy_pip).
with("install", '-q', "#{@url}#egg=fake_package")
@provider.install
end
it "should install a particular SCM revision" do
@resource[:ensure] = "0123456"
@resource[:source] = @url
@provider.expects(:lazy_pip).
with("install", "-q", "#{@url}@0123456#egg=fake_package")
@provider.install
end
it "should install a particular version" do
@resource[:ensure] = "0.0.0"
@resource[:source] = nil
@provider.expects(:lazy_pip).with("install", "-q", "fake_package==0.0.0")
@provider.install
end
it "should upgrade" do
@resource[:ensure] = :latest
@resource[:source] = nil
@provider.expects(:lazy_pip).
with("install", "-q", "--upgrade", "fake_package")
@provider.install
end
end
describe "uninstall" do
it "should uninstall" do
@resource[:name] = "fake_package"
@provider.expects(:lazy_pip).
with('uninstall', '-y', '-q', 'fake_package')
@provider.uninstall
end
end
describe "update" do
it "should just call install" do
@provider.expects(:install).returns(nil)
@provider.update
end
end
describe "lazy_pip" do
after(:each) do
Puppet::Type::Package::ProviderPip.instance_variable_set(:@confine_collection, nil)
end
it "should succeed if pip is present" do
@provider.stubs(:pip).returns(nil)
@provider.method(:lazy_pip).call "freeze"
end
osfamilies.each do |osfamily, pip_cmd|
it "should retry on #{osfamily} if #{pip_cmd} has not yet been found" do
- Facter.stubs(:value).with(:osfamily).returns(osfamily)
+ Facter.stubs(:value).with(:osfamily).returns(osfamily.first)
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns(osfamily.last)
@provider.expects(:pip).twice.with('freeze').raises(NoMethodError).then.returns(nil)
@provider.expects(:which).with(pip_cmd).returns("/fake/bin/pip")
@provider.method(:lazy_pip).call "freeze"
end
it "should fail on #{osfamily} if #{pip_cmd} is missing" do
- Facter.stubs(:value).with(:osfamily).returns(osfamily)
+ Facter.stubs(:value).with(:osfamily).returns(osfamily.first)
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns(osfamily.last)
@provider.expects(:pip).with('freeze').raises(NoMethodError)
@provider.expects(:which).with(pip_cmd).returns(nil)
expect { @provider.method(:lazy_pip).call("freeze") }.to raise_error(NoMethodError)
end
it "should output a useful error message on #{osfamily} if #{pip_cmd} is missing" do
- Facter.stubs(:value).with(:osfamily).returns(osfamily)
+ Facter.stubs(:value).with(:osfamily).returns(osfamily.first)
+ Facter.stubs(:value).with(:operatingsystemmajrelease).returns(osfamily.last)
@provider.expects(:pip).with('freeze').raises(NoMethodError)
@provider.expects(:which).with(pip_cmd).returns(nil)
expect { @provider.method(:lazy_pip).call("freeze") }.
to raise_error(NoMethodError, 'Could not locate the pip command.')
end
end
end
end