diff --git a/lib/puppet/provider/package/appdmg.rb b/lib/puppet/provider/package/appdmg.rb index 5b027a4b5..83915375f 100644 --- a/lib/puppet/provider/package/appdmg.rb +++ b/lib/puppet/provider/package/appdmg.rb @@ -1,109 +1,109 @@ # Jeff McCune # Changed to app.dmg by: Udo Waechter # Mac OS X Package Installer which handles application (.app) # bundles inside an Apple Disk Image. # # Motivation: DMG files provide a true HFS file system # and are easier to manage. # # Note: the 'apple' Provider checks for the package name # in /L/Receipts. Since we possibly install multiple apps's from # a single source, we treat the source .app.dmg file as the package name. # As a result, we store installed .app.dmg file names # in /var/db/.puppet_appdmg_installed_ require 'puppet/provider/package' Puppet::Type.type(:package).provide(:appdmg, :parent => Puppet::Provider::Package) do desc "Package management which copies application bundles to a target." confine :operatingsystem => :darwin commands :hdiutil => "/usr/bin/hdiutil" commands :curl => "/usr/bin/curl" commands :ditto => "/usr/bin/ditto" # JJM We store a cookie for each installed .app.dmg in /var/db def self.instances_by_name Dir.entries("/var/db").find_all { |f| f =~ /^\.puppet_appdmg_installed_/ }.collect do |f| name = f.sub(/^\.puppet_appdmg_installed_/, '') yield name if block_given? name end end def self.instances instances_by_name.collect do |name| new(:name => name, :provider => :appdmg, :ensure => :installed) end end def self.installapp(source, name, orig_source) appname = File.basename(source); ditto "--rsrc", source, "/Applications/#{appname}" File.open("/var/db/.puppet_appdmg_installed_#{name}", "w") do |t| t.print "name: '#{name}'\n" t.print "source: '#{orig_source}'\n" end end def self.installpkgdmg(source, name) require 'open-uri' require 'facter/util/plist' cached_source = source tmpdir = Dir.mktmpdir begin if %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ cached_source cached_source = File.join(tmpdir, name) begin - curl "-o", cached_source, "-C", "-", "-k", "-L", "-s", "--url", source + curl "-o", cached_source, "-C", "-", "-L", "-s", "--url", source Puppet.debug "Success: curl transfered [#{name}]" rescue Puppet::ExecutionFailure Puppet.debug "curl did not transfer [#{name}]. Falling back to slower open-uri transfer methods." cached_source = source end end open(cached_source) do |dmg| xml_str = hdiutil "mount", "-plist", "-nobrowse", "-readonly", "-mountrandom", "/tmp", dmg.path ptable = Plist::parse_xml xml_str # JJM Filter out all mount-paths into a single array, discard the rest. mounts = ptable['system-entities'].collect { |entity| entity['mount-point'] }.select { |mountloc|; mountloc } begin found_app = false mounts.each do |fspath| Dir.entries(fspath).select { |f| f =~ /\.app$/i }.each do |pkg| found_app = true installapp("#{fspath}/#{pkg}", name, source) end end Puppet.debug "Unable to find .app in .appdmg. #{name} will not be installed." if !found_app ensure hdiutil "eject", mounts[0] end end ensure FileUtils.remove_entry_secure(tmpdir, true) end end def query Puppet::FileSystem.exist?("/var/db/.puppet_appdmg_installed_#{@resource[:name]}") ? {:name => @resource[:name], :ensure => :present} : nil end def install source = nil unless source = @resource[:source] self.fail "Mac OS X PKG DMG's must specify a package source." end unless name = @resource[:name] self.fail "Mac OS X PKG DMG's must specify a package name." end self.class.installpkgdmg(source,name) end end diff --git a/lib/puppet/provider/package/pkgdmg.rb b/lib/puppet/provider/package/pkgdmg.rb index 44c0922be..8932a7ce1 100644 --- a/lib/puppet/provider/package/pkgdmg.rb +++ b/lib/puppet/provider/package/pkgdmg.rb @@ -1,149 +1,149 @@ # # Motivation: DMG files provide a true HFS file system # and are easier to manage and .pkg bundles. # # Note: the 'apple' Provider checks for the package name # in /L/Receipts. Since we install multiple pkg's from a single # source, we treat the source .pkg.dmg file as the package name. # As a result, we store installed .pkg.dmg file names # in /var/db/.puppet_pkgdmg_installed_ require 'puppet/provider/package' require 'facter/util/plist' require 'puppet/util/http_proxy' Puppet::Type.type(:package).provide :pkgdmg, :parent => Puppet::Provider::Package do desc "Package management based on Apple's Installer.app and DiskUtility.app. This provider works by checking the contents of a DMG image for Apple pkg or mpkg files. Any number of pkg or mpkg files may exist in the root directory of the DMG file system, and Puppet will install all of them. Subdirectories are not checked for packages. This provider can also accept plain .pkg (but not .mpkg) files in addition to .dmg files. Notes: * The `source` attribute is mandatory. It must be either a local disk path or an HTTP, HTTPS, or FTP URL to the package. * The `name` of the resource must be the filename (without path) of the DMG file. * When installing the packages from a DMG, this provider writes a file to disk at `/var/db/.puppet_pkgdmg_installed_NAME`. If that file is present, Puppet assumes all packages from that DMG are already installed. * This provider is not versionable and uses DMG filenames to determine whether a package has been installed. Thus, to install new a version of a package, you must create a new DMG with a different filename." confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin commands :installer => "/usr/sbin/installer" commands :hdiutil => "/usr/bin/hdiutil" commands :curl => "/usr/bin/curl" # JJM We store a cookie for each installed .pkg.dmg in /var/db def self.instance_by_name Dir.entries("/var/db").find_all { |f| f =~ /^\.puppet_pkgdmg_installed_/ }.collect do |f| name = f.sub(/^\.puppet_pkgdmg_installed_/, '') yield name if block_given? name end end def self.instances instance_by_name.collect do |name| new(:name => name, :provider => :pkgdmg, :ensure => :installed) end end def self.installpkg(source, name, orig_source) installer "-pkg", source, "-target", "/" # Non-zero exit status will throw an exception. File.open("/var/db/.puppet_pkgdmg_installed_#{name}", "w") do |t| t.print "name: '#{name}'\n" t.print "source: '#{orig_source}'\n" end end def self.installpkgdmg(source, name) http_proxy_host = Puppet::Util::HttpProxy.http_proxy_host http_proxy_port = Puppet::Util::HttpProxy.http_proxy_port unless source =~ /\.dmg$/i || source =~ /\.pkg$/i raise Puppet::Error.new("Mac OS X PKG DMG's must specify a source string ending in .dmg or flat .pkg file") end require 'open-uri' # Dead code; this is never used. The File.open call 20-ish lines south of here used to be Kernel.open but changed in '09. -NF cached_source = source tmpdir = Dir.mktmpdir ext = /(\.dmg|\.pkg)$/i.match(source)[0] begin if %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ cached_source cached_source = File.join(tmpdir, "#{name}#{ext}") - args = [ "-o", cached_source, "-C", "-", "-k", "-L", "-s", "--fail", "--url", source ] + args = [ "-o", cached_source, "-C", "-", "-L", "-s", "--fail", "--url", source ] if http_proxy_host and http_proxy_port args << "--proxy" << "#{http_proxy_host}:#{http_proxy_port}" elsif http_proxy_host and not http_proxy_port args << "--proxy" << http_proxy_host end begin curl *args Puppet.debug "Success: curl transfered [#{name}] (via: curl #{args.join(" ")})" rescue Puppet::ExecutionFailure Puppet.debug "curl #{args.join(" ")} did not transfer [#{name}]. Falling back to local file." # This used to fall back to open-uri. -NF cached_source = source end end if source =~ /\.dmg$/i # If you fix this to use open-uri again, you must update the docs above. -NF File.open(cached_source) do |dmg| xml_str = hdiutil "mount", "-plist", "-nobrowse", "-readonly", "-noidme", "-mountrandom", "/tmp", dmg.path hdiutil_info = Plist::parse_xml(xml_str) raise Puppet::Error.new("No disk entities returned by mount at #{dmg.path}") unless hdiutil_info.has_key?("system-entities") mounts = hdiutil_info["system-entities"].collect { |entity| entity["mount-point"] }.compact begin mounts.each do |mountpoint| Dir.entries(mountpoint).select { |f| f =~ /\.m{0,1}pkg$/i }.each do |pkg| installpkg("#{mountpoint}/#{pkg}", name, source) end end ensure mounts.each do |mountpoint| hdiutil "eject", mountpoint end end end else installpkg(cached_source, name, source) end ensure FileUtils.remove_entry_secure(tmpdir, true) end end def query if Puppet::FileSystem.exist?("/var/db/.puppet_pkgdmg_installed_#{@resource[:name]}") Puppet.debug "/var/db/.puppet_pkgdmg_installed_#{@resource[:name]} found" return {:name => @resource[:name], :ensure => :present} else return nil end end def install source = nil unless source = @resource[:source] raise Puppet::Error.new("Mac OS X PKG DMG's must specify a package source.") end unless name = @resource[:name] raise Puppet::Error.new("Mac OS X PKG DMG's must specify a package name.") end self.class.installpkgdmg(source,name) end end diff --git a/spec/unit/provider/package/appdmg_spec.rb b/spec/unit/provider/package/appdmg_spec.rb index ec118792c..380c27662 100755 --- a/spec/unit/provider/package/appdmg_spec.rb +++ b/spec/unit/provider/package/appdmg_spec.rb @@ -1,42 +1,42 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:package).provider(:appdmg) do let(:resource) { Puppet::Type.type(:package).new(:name => 'foo', :provider => :appdmg) } let(:provider) { described_class.new(resource) } describe "when installing an appdmg" do let(:fake_mountpoint) { "/tmp/dmg.foo" } let(:empty_hdiutil_plist) { Plist::Emit.dump({}) } let(:fake_hdiutil_plist) { Plist::Emit.dump({"system-entities" => [{"mount-point" => fake_mountpoint}]}) } before do fh = mock 'filehandle' fh.stubs(:path).yields "/tmp/foo" resource[:source] = "foo.dmg" described_class.stubs(:open).yields fh Dir.stubs(:mktmpdir).returns "/tmp/testtmp123" FileUtils.stubs(:remove_entry_secure) end describe "from a remote source" do let(:tmpdir) { "/tmp/good123" } before :each do resource[:source] = "http://fake.puppetlabs.com/foo.dmg" end it "should call tmpdir and use the returned directory" do Dir.expects(:mktmpdir).returns tmpdir Dir.stubs(:entries).returns ["foo.app"] described_class.expects(:curl).with do |*args| - args[0] == "-o" and args[1].include? tmpdir + args[0] == "-o" && args[1].include?(tmpdir) && ! args.include?("-k") end described_class.stubs(:hdiutil).returns fake_hdiutil_plist described_class.expects(:installapp) provider.install end end end end diff --git a/spec/unit/provider/package/pkgdmg_spec.rb b/spec/unit/provider/package/pkgdmg_spec.rb index 8dfa235b6..deafc5e20 100644 --- a/spec/unit/provider/package/pkgdmg_spec.rb +++ b/spec/unit/provider/package/pkgdmg_spec.rb @@ -1,144 +1,144 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:package).provider(:pkgdmg) do let(:resource) { Puppet::Type.type(:package).new(:name => 'foo', :provider => :pkgdmg) } let(:provider) { described_class.new(resource) } it { should_not be_versionable } it { should_not be_uninstallable } describe "when installing it should fail when" do before :each do Puppet::Util.expects(:execute).never end it "no source is specified" do expect { provider.install }.to raise_error(Puppet::Error, /must specify a package source/) end it "the source does not end in .dmg or .pkg" do resource[:source] = "bar" expect { provider.install }.to raise_error(Puppet::Error, /must specify a source string ending in .*dmg.*pkg/) end end # These tests shouldn't be this messy. The pkgdmg provider needs work... describe "when installing a pkgdmg" do let(:fake_mountpoint) { "/tmp/dmg.foo" } let(:empty_hdiutil_plist) { Plist::Emit.dump({}) } let(:fake_hdiutil_plist) { Plist::Emit.dump({"system-entities" => [{"mount-point" => fake_mountpoint}]}) } before do fh = mock 'filehandle' fh.stubs(:path).yields "/tmp/foo" resource[:source] = "foo.dmg" File.stubs(:open).yields fh Dir.stubs(:mktmpdir).returns "/tmp/testtmp123" FileUtils.stubs(:remove_entry_secure) end it "should fail when a disk image with no system entities is mounted" do described_class.stubs(:hdiutil).returns(empty_hdiutil_plist) expect { provider.install }.to raise_error(Puppet::Error, /No disk entities/) end it "should call hdiutil to mount and eject the disk image" do Dir.stubs(:entries).returns [] provider.class.expects(:hdiutil).with("eject", fake_mountpoint).returns 0 provider.class.expects(:hdiutil).with("mount", "-plist", "-nobrowse", "-readonly", "-noidme", "-mountrandom", "/tmp", nil).returns fake_hdiutil_plist provider.install end it "should call installpkg if a pkg/mpkg is found on the dmg" do Dir.stubs(:entries).returns ["foo.pkg"] provider.class.stubs(:hdiutil).returns fake_hdiutil_plist provider.class.expects(:installpkg).with("#{fake_mountpoint}/foo.pkg", resource[:name], "foo.dmg").returns "" provider.install end describe "from a remote source" do let(:tmpdir) { "/tmp/good123" } before :each do resource[:source] = "http://fake.puppetlabs.com/foo.dmg" end it "should call tmpdir and then call curl with that directory" do Dir.expects(:mktmpdir).returns tmpdir Dir.stubs(:entries).returns ["foo.pkg"] described_class.expects(:curl).with do |*args| - args[0] == "-o" and args[1].include? tmpdir and args.include? "--fail" + args[0] == "-o" && args[1].include?(tmpdir) && args.include?("--fail") && ! args.include?("-k") end described_class.stubs(:hdiutil).returns fake_hdiutil_plist described_class.expects(:installpkg) provider.install end it "should use an http proxy host and port if specified" do Puppet::Util::HttpProxy.expects(:http_proxy_host).returns 'some_host' Puppet::Util::HttpProxy.expects(:http_proxy_port).returns 'some_port' Dir.expects(:mktmpdir).returns tmpdir Dir.stubs(:entries).returns ["foo.pkg"] described_class.expects(:curl).with do |*args| args.should be_include 'some_host:some_port' args.should be_include '--proxy' end described_class.stubs(:hdiutil).returns fake_hdiutil_plist described_class.expects(:installpkg) provider.install end it "should use an http proxy host only if specified" do Puppet::Util::HttpProxy.expects(:http_proxy_host).returns 'some_host' Puppet::Util::HttpProxy.expects(:http_proxy_port).returns nil Dir.expects(:mktmpdir).returns tmpdir Dir.stubs(:entries).returns ["foo.pkg"] described_class.expects(:curl).with do |*args| args.should be_include 'some_host' args.should be_include '--proxy' end described_class.stubs(:hdiutil).returns fake_hdiutil_plist described_class.expects(:installpkg) provider.install end end end describe "when installing flat pkg file" do describe "with a local source" do it "should call installpkg if a flat pkg file is found instead of a .dmg image" do resource[:source] = "/tmp/test.pkg" resource[:name] = "testpkg" provider.class.expects(:installpkgdmg).with("/tmp/test.pkg", "testpkg").returns "" provider.install end end describe "with a remote source" do let(:remote_source) { 'http://fake.puppetlabs.com/test.pkg' } let(:tmpdir) { '/path/to/tmpdir' } let(:tmpfile) { File.join(tmpdir, 'testpkg.pkg') } before do resource[:name] = 'testpkg' resource[:source] = remote_source Dir.stubs(:mktmpdir).returns tmpdir end it "should call installpkg if a flat pkg file is found instead of a .dmg image" do described_class.expects(:curl).with do |*args| args.should be_include tmpfile args.should be_include remote_source end provider.class.expects(:installpkg).with(tmpfile, 'testpkg', remote_source) provider.install end end end end