├── LICENSE ├── README.md ├── lib └── puppet │ └── provider │ └── package │ └── pip.rb ├── puppet-pip.gemspec ├── spec └── unit │ └── provider │ └── package │ └── pip_spec.rb └── test.pp /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Richard Crowley. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY RICHARD CROWLEY ``AS IS'' AND ANY EXPRESS 16 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL RICHARD CROWLEY OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation 28 | are those of the authors and should not be interpreted as representing 29 | official policies, either expressed or implied, of Richard Crowley. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | puppet-pip 2 | ========== 3 | 4 | Puppet provider of Python packages via `pip`. Alliteration FTW. 5 | 6 | * Puppet: 7 | * Puppet `apt` package provider: 8 | * `pip`: 9 | * PyPI: 10 | 11 | Installation 12 | ------------ 13 | 14 | gem install puppet-pip 15 | 16 | Example 17 | ------- 18 | 19 | Resource: 20 | 21 | package { "httplib2": 22 | ensure => latest, 23 | provider => pip, 24 | } 25 | 26 | Usage: 27 | 28 | sudo env RUBYLIB="$GEM_HOME/1.8/gems/puppet-pip-0.0.5/lib" puppet apply test.pp 29 | -------------------------------------------------------------------------------- /lib/puppet/provider/package/pip.rb: -------------------------------------------------------------------------------- 1 | # Puppet package provider for Python's `pip` package management frontend. 2 | # 3 | 4 | require 'puppet/provider/package' 5 | require 'xmlrpc/client' 6 | 7 | Puppet::Type.type(:package).provide :pip, 8 | :parent => ::Puppet::Provider::Package do 9 | 10 | desc "Python packages via `pip`." 11 | 12 | has_feature :installable, :uninstallable, :upgradeable, :versionable 13 | 14 | # Parse lines of output from `pip freeze`, which are structured as 15 | # _package_==_version_. 16 | def self.parse(line) 17 | if line.chomp =~ /^([^=]+)==([^=]+)$/ 18 | {:ensure => $2, :name => $1, :provider => name} 19 | else 20 | nil 21 | end 22 | end 23 | 24 | # Return an array of structured information about every installed package 25 | # that's managed by `pip` or an empty array if `pip` is not available. 26 | def self.instances 27 | packages = [] 28 | pip_cmd = which('pip') or return [] 29 | execpipe "#{pip_cmd} freeze" do |process| 30 | process.collect do |line| 31 | next unless options = parse(line) 32 | packages << new(options) 33 | end 34 | end 35 | packages 36 | end 37 | 38 | # Return structured information about a particular package or `nil` if 39 | # it is not installed or `pip` itself is not available. 40 | def query 41 | self.class.instances.each do |provider_pip| 42 | return provider_pip.properties if @resource[:name] == provider_pip.name 43 | end 44 | return nil 45 | end 46 | 47 | # Ask the PyPI API for the latest version number. There is no local 48 | # cache of PyPI's package list so this operation will always have to 49 | # ask the web service. 50 | def latest 51 | client = XMLRPC::Client.new2("http://pypi.python.org/pypi") 52 | client.http_header_extra = {"Content-Type" => "text/xml"} 53 | result = client.call("package_releases", @resource[:name]) 54 | result.first 55 | end 56 | 57 | # Install a package. The ensure parameter may specify installed, 58 | # latest, a version number, or, in conjunction with the source 59 | # parameter, an SCM revision. In that case, the source parameter 60 | # gives the fully-qualified URL to the repository. 61 | def install 62 | args = %w{install -q} 63 | if @resource[:source] 64 | args << "-e" 65 | if String === @resource[:ensure] 66 | args << "#{@resource[:source]}@#{@resource[:ensure]}#egg=#{ 67 | @resource[:name]}" 68 | else 69 | args << "#{@resource[:source]}#egg=#{@resource[:name]}" 70 | end 71 | else 72 | case @resource[:ensure] 73 | when String 74 | args << "#{@resource[:name]}==#{@resource[:ensure]}" 75 | when :latest 76 | args << "--upgrade" << @resource[:name] 77 | else 78 | args << @resource[:name] 79 | end 80 | end 81 | lazy_pip *args 82 | end 83 | 84 | # Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu 85 | # unless this issue gets fixed. 86 | # 87 | def uninstall 88 | lazy_pip "uninstall", "-y", "-q", @resource[:name] 89 | end 90 | 91 | def update 92 | install 93 | end 94 | 95 | # Execute a `pip` command. If Puppet doesn't yet know how to do so, 96 | # try to teach it and if even that fails, raise the error. 97 | private 98 | def lazy_pip(*args) 99 | pip *args 100 | rescue NoMethodError => e 101 | if pathname = which('pip') 102 | self.class.commands :pip => pathname 103 | pip *args 104 | else 105 | raise e 106 | end 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /puppet-pip.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "puppet-pip" 3 | s.version = "1.0.0" 4 | s.date = "2011-04-18" 5 | s.authors = ["Richard Crowley"] 6 | s.email = "r@rcrowley.org" 7 | s.summary = "Puppet provider of Python packages via pip." 8 | s.homepage = "http://github.com/rcrowley/puppet-pip" 9 | s.description = "Puppet provider of Python packages via pip." 10 | s.files = [ 11 | "lib/puppet/provider/package/pip.rb", 12 | "spec/unit/provider/package/pip_spec.rb", 13 | ] 14 | s.add_dependency "puppet" 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/provider/package/pip_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | require 'spec_helper' 3 | 4 | provider_class = Puppet::Type.type(:package).provider(:pip) 5 | 6 | describe provider_class do 7 | 8 | before do 9 | @resource = Puppet::Resource.new(:package, "sdsfdssdhdfyjymdgfcjdfjxdrssf") 10 | @provider = provider_class.new(@resource) 11 | end 12 | 13 | describe "parse" do 14 | 15 | it "should return a hash on valid input" do 16 | provider_class.parse("Django==1.2.5").should == { 17 | :ensure => "1.2.5", 18 | :name => "Django", 19 | :provider => :pip, 20 | } 21 | end 22 | 23 | it "should return nil on invalid input" do 24 | provider_class.parse("foo").should == nil 25 | end 26 | 27 | end 28 | 29 | describe "instances" do 30 | 31 | it "should return an array when pip is present" do 32 | provider_class.expects(:which).with('pip').returns("/fake/bin/pip") 33 | p = stub("process") 34 | p.expects(:collect).yields("Django==1.2.5") 35 | provider_class.expects(:execpipe).with("/fake/bin/pip freeze").yields(p) 36 | provider_class.instances 37 | end 38 | 39 | it "should return an empty array when pip is missing" do 40 | provider_class.expects(:which).with('pip').returns nil 41 | provider_class.instances.should == [] 42 | end 43 | 44 | end 45 | 46 | describe "query" do 47 | 48 | before do 49 | @resource[:name] = "Django" 50 | end 51 | 52 | it "should return a hash when pip and the package are present" do 53 | provider_class.expects(:instances).returns [provider_class.new({ 54 | :ensure => "1.2.5", 55 | :name => "Django", 56 | :provider => :pip, 57 | })] 58 | 59 | @provider.query.should == { 60 | :ensure => "1.2.5", 61 | :name => "Django", 62 | :provider => :pip, 63 | } 64 | end 65 | 66 | it "should return nil when the package is missing" do 67 | provider_class.expects(:instances).returns [] 68 | @provider.query.should == nil 69 | end 70 | 71 | end 72 | 73 | describe "latest" do 74 | 75 | it "should find a version number for Django" do 76 | @resource[:name] = "Django" 77 | @provider.latest.should_not == nil 78 | end 79 | 80 | it "should not find a version number for sdsfdssdhdfyjymdgfcjdfjxdrssf" do 81 | @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf" 82 | @provider.latest.should == nil 83 | end 84 | 85 | end 86 | 87 | describe "install" do 88 | 89 | before do 90 | @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf" 91 | @url = "git+https://example.com/sdsfdssdhdfyjymdgfcjdfjxdrssf.git" 92 | end 93 | 94 | it "should install" do 95 | @resource[:ensure] = :installed 96 | @resource[:source] = nil 97 | @provider.expects(:lazy_pip). 98 | with("install", '-q', "sdsfdssdhdfyjymdgfcjdfjxdrssf") 99 | @provider.install 100 | end 101 | 102 | it "should install from SCM" do 103 | @resource[:ensure] = :installed 104 | @resource[:source] = @url 105 | @provider.expects(:lazy_pip). 106 | with("install", '-q', '-e', "#{@url}#egg=sdsfdssdhdfyjymdgfcjdfjxdrssf") 107 | @provider.install 108 | end 109 | 110 | it "should install a particular SCM revision" do 111 | @resource[:ensure] = "0123456" 112 | @resource[:source] = @url 113 | @provider.expects(:lazy_pip). 114 | with("install", "-q", "-e", "#{@url}@0123456#egg=sdsfdssdhdfyjymdgfcjdfjxdrssf") 115 | @provider.install 116 | end 117 | 118 | it "should install a particular version" do 119 | @resource[:ensure] = "0.0.0" 120 | @resource[:source] = nil 121 | @provider.expects(:lazy_pip).with("install", "-q", "sdsfdssdhdfyjymdgfcjdfjxdrssf==0.0.0") 122 | @provider.install 123 | end 124 | 125 | it "should upgrade" do 126 | @resource[:ensure] = :latest 127 | @resource[:source] = nil 128 | @provider.expects(:lazy_pip). 129 | with("install", "-q", "--upgrade", "sdsfdssdhdfyjymdgfcjdfjxdrssf") 130 | @provider.install 131 | end 132 | 133 | end 134 | 135 | describe "uninstall" do 136 | 137 | it "should uninstall" do 138 | @resource[:name] = "sdsfdssdhdfyjymdgfcjdfjxdrssf" 139 | @provider.expects(:lazy_pip). 140 | with('uninstall', '-y', '-q', 'sdsfdssdhdfyjymdgfcjdfjxdrssf') 141 | @provider.uninstall 142 | end 143 | 144 | end 145 | 146 | describe "update" do 147 | 148 | it "should just call install" do 149 | @provider.expects(:install).returns(nil) 150 | @provider.update 151 | end 152 | 153 | end 154 | 155 | describe "lazy_pip" do 156 | 157 | it "should succeed if pip is present" do 158 | @provider.stubs(:pip).returns(nil) 159 | @provider.method(:lazy_pip).call "freeze" 160 | end 161 | 162 | it "should retry if pip has not yet been found" do 163 | @provider.expects(:pip).twice.with('freeze').raises(NoMethodError).then.returns(nil) 164 | @provider.expects(:which).with('pip').returns("/fake/bin/pip") 165 | @provider.method(:lazy_pip).call "freeze" 166 | end 167 | 168 | it "should fail if pip is missing" do 169 | @provider.expects(:pip).with('freeze').raises(NoMethodError) 170 | @provider.expects(:which).with('pip').returns(nil) 171 | expect { @provider.method(:lazy_pip).call("freeze") }.to raise_error(NoMethodError) 172 | end 173 | 174 | end 175 | 176 | end 177 | -------------------------------------------------------------------------------- /test.pp: -------------------------------------------------------------------------------- 1 | stage { "pre": before => Stage["main"] } 2 | class pre { 3 | package { 4 | "python": ensure => latest; 5 | "python-dev": ensure => latest; 6 | "python-setuptools": ensure => latest; 7 | } 8 | exec { "easy_install pip": 9 | path => "/usr/local/bin:/usr/bin:/bin", 10 | require => Package["python-setuptools"], 11 | subscribe => Package["python-setuptools"], 12 | unless => "which pip", 13 | } 14 | } 15 | class { "pre": stage => "pre" } 16 | 17 | package { 18 | "httplib2": 19 | ensure => latest, 20 | provider => pip; 21 | "socialregistration": 22 | ensure => "317a0dbed71b660c8ec7f7994f3ae42dadf2e992", 23 | provider => pip, 24 | source => "git+https://github.com/itmustbejj/django-socialregistration.git"; 25 | } 26 | --------------------------------------------------------------------------------