diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb index 5f6806e6f..b6b608928 100644 --- a/lib/puppet/provider/package/pip.rb +++ b/lib/puppet/provider/package/pip.rb @@ -1,108 +1,111 @@ # 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('pip') or return [] execpipe "#{pip_cmd} freeze" do |process| process.collect do |line| next unless options = parse(line) packages << new(options) end end packages 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] == provider_pip.name 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}"; 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] args << "-e" 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('pip') self.class.commands :pip => pathname pip *args else raise e, 'Could not locate the pip command.' end end end diff --git a/spec/unit/provider/package/pip_spec.rb b/spec/unit/provider/package/pip_spec.rb index 7849ec49c..2de0eb4fc 100755 --- a/spec/unit/provider/package/pip_spec.rb +++ b/spec/unit/provider/package/pip_spec.rb @@ -1,187 +1,193 @@ #!/usr/bin/env rspec require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(: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) + @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 "instances" do it "should return an array when pip is present" do provider_class.expects(:which).with('pip').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 when pip is missing" do provider_class.expects(:which).with('pip').returns nil provider_class.instances.should == [] 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 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 "should install from SCM" do @resource[:ensure] = :installed @resource[:source] = @url @provider.expects(:lazy_pip). with("install", '-q', '-e', "#{@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", "-e", "#{@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 it "should succeed if pip is present" do @provider.stubs(:pip).returns(nil) @provider.method(:lazy_pip).call "freeze" end it "should retry if pip has not yet been found" do @provider.expects(:pip).twice.with('freeze').raises(NoMethodError).then.returns(nil) @provider.expects(:which).with('pip').returns("/fake/bin/pip") @provider.method(:lazy_pip).call "freeze" end it "should fail if pip is missing" do @provider.expects(:pip).with('freeze').raises(NoMethodError) @provider.expects(:which).with('pip').returns(nil) expect { @provider.method(:lazy_pip).call("freeze") }.to raise_error(NoMethodError) end it "should output a useful error message if pip is missing" do @provider.expects(:pip).with('freeze').raises(NoMethodError) @provider.expects(:which).with('pip').returns(nil) expect { @provider.method(:lazy_pip).call("freeze") }. to raise_error(NoMethodError, 'Could not locate the pip command.') end end end