summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Robinson <matt@puppetlabs.com>2011-03-22 15:46:26 -0700
committerMatt Robinson <matt@puppetlabs.com>2011-03-22 15:46:26 -0700
commit8542160479538fe6575475c2dccf5d4696fa3e5d (patch)
treea2d09b68de0e7101116062d44e622d76b0abd1f8
parent6bf5b7fea8ef4d04809e78eef84029a0773d4747 (diff)
parent0170cebba039378597fdf9f0086339c2766df408 (diff)
downloadpuppet-8542160479538fe6575475c2dccf5d4696fa3e5d.tar.gz
puppet-8542160479538fe6575475c2dccf5d4696fa3e5d.tar.xz
puppet-8542160479538fe6575475c2dccf5d4696fa3e5d.zip
Merge branch 'feature/next/6257-pip_package_provider' into next
* feature/next/6257-pip_package_provider: (#6527) Fix uninstall problem and refactor (#6527) Added pip package provider.
-rw-r--r--lib/puppet/provider/package/pip.rb109
-rw-r--r--spec/unit/provider/package/pip_spec.rb176
2 files changed, 285 insertions, 0 deletions
diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb
new file mode 100644
index 000000000..5abbc135a
--- /dev/null
+++ b/lib/puppet/provider/package/pip.rb
@@ -0,0 +1,109 @@
+# Puppet package provider for Python's `pip` package management frontend.
+# <http://pip.openplans.org/>
+
+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"}
+ result = client.call("package_releases", @resource[:name])
+ result.first
+ 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.
+ # <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=562544>
+ 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
+ end
+ end
+
+end
diff --git a/spec/unit/provider/package/pip_spec.rb b/spec/unit/provider/package/pip_spec.rb
new file mode 100644
index 000000000..6809d3f90
--- /dev/null
+++ b/spec/unit/provider/package/pip_spec.rb
@@ -0,0 +1,176 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
+
+provider_class = Puppet::Type.type(:package).provider(:pip)
+
+describe provider_class do
+
+ before do
+ @resource = Puppet::Resource.new(:package, "sdsfdssdhdfyjymdgfcjdfjxdrssf")
+ @provider = provider_class.new(@resource)
+ end
+
+ describe "parse" do
+
+ it "should return a hash on valid input" do
+ provider_class.parse("Django==1.2.5").should == {
+ :ensure => "1.2.5",
+ :name => "Django",
+ :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("Django==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] = "Django"
+ 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 => "Django",
+ :provider => :pip,
+ })]
+
+ @provider.query.should == {
+ :ensure => "1.2.5",
+ :name => "Django",
+ :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 Django" do
+ @resource[:name] = "Django"
+ @provider.latest.should_not == nil
+ end
+
+ it "should not find a version number for sdsfdssdhdfyjymdgfcjdfjxdrssf" do
+ @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf"
+ @provider.latest.should == nil
+ end
+
+ end
+
+ describe "install" do
+
+ before do
+ @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf"
+ @url = "git+https://example.com/sdsfdssdhdfyjymdgfcjdfjxdrssf.git"
+ end
+
+ it "should install" do
+ @resource[:ensure] = :installed
+ @resource[:source] = nil
+ @provider.expects(:lazy_pip).
+ with("install", '-q', "sdsfdssdhdfyjymdgfcjdfjxdrssf")
+ @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=sdsfdssdhdfyjymdgfcjdfjxdrssf")
+ @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=sdsfdssdhdfyjymdgfcjdfjxdrssf")
+ @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", "sdsfdssdhdfyjymdgfcjdfjxdrssf==0.0.0")
+ @provider.install
+ end
+
+ it "should upgrade" do
+ @resource[:ensure] = :latest
+ @resource[:source] = nil
+ @provider.expects(:lazy_pip).
+ with("install", "-q", "--upgrade", "sdsfdssdhdfyjymdgfcjdfjxdrssf")
+ @provider.install
+ end
+
+ end
+
+ describe "uninstall" do
+
+ it "should uninstall" do
+ @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf"
+ @provider.expects(:lazy_pip).
+ with('uninstall', '-y', '-q', 'sdsfdssdhdfyjymdgfcjdfjxdrssf')
+ @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.stubs(:pip).raises(NoMethodError).returns("/fake/bin/pip")
+ @provider.method(:lazy_pip).call "freeze"
+ end
+
+ it "should fail if pip is missing" do
+ @provider.stubs(:pip).twice.raises(NoMethodError)
+ expect { @provider.method(:lazy_pip).call("freeze") }.to \
+ raise_error(NoMethodError)
+ end
+
+ end
+
+end