diff --git a/lib/puppet/provider/service/launchd.rb b/lib/puppet/provider/service/launchd.rb index 970359539..b296e0a38 100644 --- a/lib/puppet/provider/service/launchd.rb +++ b/lib/puppet/provider/service/launchd.rb @@ -1,256 +1,263 @@ require 'facter/util/plist' Puppet::Type.type(:service).provide :launchd, :parent => :base do desc "launchd service management framework. This provider manages launchd jobs, the default service framework for Mac OS X, that has also been open sourced by Apple for possible use on other platforms. See: * http://developer.apple.com/macosx/launchd.html * http://launchd.macosforge.org/ This provider reads plists out of the following directories: * /System/Library/LaunchDaemons * /System/Library/LaunchAgents * /Library/LaunchDaemons * /Library/LaunchAgents and builds up a list of services based upon each plists \"Label\" entry. This provider supports: * ensure => running/stopped, * enable => true/false * status * restart Here is how the Puppet states correspond to launchd states: * stopped => job unloaded * started => job loaded * enabled => 'Disable' removed from job plist file * disabled => 'Disable' added to job plist file Note that this allows you to do something launchctl can't do, which is to be in a state of \"stopped/enabled\ or \"running/disabled\". " commands :launchctl => "/bin/launchctl" commands :sw_vers => "/usr/bin/sw_vers" + commands :plutil => "/usr/bin/plutil" defaultfor :operatingsystem => :darwin confine :operatingsystem => :darwin has_feature :enableable Launchd_Paths = ["/Library/LaunchAgents", "/Library/LaunchDaemons", "/System/Library/LaunchAgents", "/System/Library/LaunchDaemons",] Launchd_Overrides = "/var/db/launchd.db/com.apple.launchd/overrides.plist" + # Read a plist, whether its format is XML or in Apple's "binary1" + # format. + def self.read_plist(path) + Plist::parse_xml(plutil('-convert', 'xml1', '-o', '-', path)) + end + # returns a label => path map for either all jobs, or just a single # job if the label is specified def self.jobsearch(label=nil) label_to_path_map = {} Launchd_Paths.each do |path| if FileTest.exists?(path) Dir.entries(path).each do |f| next if f =~ /^\..*$/ next if FileTest.directory?(f) fullpath = File.join(path, f) - job = Plist::parse_xml(fullpath) - if job and job.has_key?("Label") + if FileTest.file?(fullpath) and job = read_plist(fullpath) and job.has_key?("Label") if job["Label"] == label return { label => fullpath } else label_to_path_map[job["Label"]] = fullpath end end end end end # if we didn't find the job above and we should have, error. raise Puppet::Error.new("Unable to find launchd plist for job: #{label}") if label # if returning all jobs label_to_path_map end def self.instances jobs = self.jobsearch jobs.keys.collect do |job| new(:name => job, :provider => :launchd, :path => jobs[job]) end end def self.get_macosx_version_major return @macosx_version_major if defined?(@macosx_version_major) begin # Make sure we've loaded all of the facts Facter.loadfacts if Facter.value(:macosx_productversion_major) product_version_major = Facter.value(:macosx_productversion_major) else # TODO: remove this code chunk once we require Facter 1.5.5 or higher. Puppet.warning("DEPRECATION WARNING: Future versions of the launchd provider will require Facter 1.5.5 or newer.") product_version = Facter.value(:macosx_productversion) fail("Could not determine OS X version from Facter") if product_version.nil? product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") end fail("#{product_version_major} is not supported by the launchd provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major) @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail fail("Could not determine OS X version: #{detail}") end end # finds the path for a given label and returns the path and parsed plist # as an array of [path, plist]. Note plist is really a Hash here. def plist_from_label(label) job = self.class.jobsearch(label) job_path = job[label] - job_plist = Plist::parse_xml(job_path) - raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}") if not job_plist + if FileTest.file?(job_path) + job_plist = self.class.read_plist(job_path) + else + raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}") + end [job_path, job_plist] end def status # launchctl list exits zero if the job is loaded # and non-zero if it isn't. Simple way to check... but is only # available on OS X 10.5 unfortunately, so we grab the whole list # and check if our resource is included. The output formats differ # between 10.4 and 10.5, thus the necessity for splitting begin output = launchctl :list raise Puppet::Error.new("launchctl list failed to return any data.") if output.nil? output.split("\n").each do |j| return :running if j.split(/\s/).last == resource[:name] end return :stopped rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to determine status of #{resource[:name]}") end end # start the service. To get to a state of running/enabled, we need to # conditionally enable at load, then disable by modifying the plist file # directly. def start job_path, job_plist = plist_from_label(resource[:name]) did_enable_job = false cmds = [] cmds << :launchctl << :load if self.enabled? == :false # launchctl won't load disabled jobs cmds << "-w" did_enable_job = true end cmds << job_path begin execute(cmds) rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to start service: #{resource[:name]} at path: #{job_path}") end # As load -w clears the Disabled flag, we need to add it in after self.disable if did_enable_job and resource[:enable] == :false end def stop job_path, job_plist = plist_from_label(resource[:name]) did_disable_job = false cmds = [] cmds << :launchctl << :unload if self.enabled? == :true # keepalive jobs can't be stopped without disabling cmds << "-w" did_disable_job = true end cmds << job_path begin execute(cmds) rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to stop service: #{resource[:name]} at path: #{job_path}") end # As unload -w sets the Disabled flag, we need to add it in after self.enable if did_disable_job and resource[:enable] == :true end # launchd jobs are enabled by default. They are only disabled if the key # "Disabled" is set to true, but it can also be set to false to enable it. # In 10.6, the Disabled key in the job plist is consulted, but only if there # is no entry in the global overrides plist. # We need to draw a distinction between undefined, true and false for both # locations where the Disabled flag can be defined. def enabled? job_plist_disabled = nil overrides_disabled = nil job_path, job_plist = plist_from_label(resource[:name]) job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled") if self.class.get_macosx_version_major == "10.6": - overrides = Plist::parse_xml(Launchd_Overrides) - - unless overrides.nil? + if FileTest.file?(Launchd_Overrides) and overrides = self.class.read_plist(Launchd_Overrides) if overrides.has_key?(resource[:name]) overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled") end end end if overrides_disabled.nil? if job_plist_disabled.nil? or job_plist_disabled == false return :true end elsif overrides_disabled == false return :true end :false end # enable and disable are a bit hacky. We write out the plist with the appropriate value # rather than dealing with launchctl as it is unable to change the Disabled flag # without actually loading/unloading the job. # In 10.6 we need to write out a disabled key to the global overrides plist, in earlier # versions this is stored in the job plist itself. def enable if self.class.get_macosx_version_major == "10.6" - overrides = Plist::parse_xml(Launchd_Overrides) + overrides = self.class.read_plist(Launchd_Overrides) overrides[resource[:name]] = { "Disabled" => false } Plist::Emit.save_plist(overrides, Launchd_Overrides) else job_path, job_plist = plist_from_label(resource[:name]) if self.enabled? == :false job_plist.delete("Disabled") Plist::Emit.save_plist(job_plist, job_path) end end end def disable if self.class.get_macosx_version_major == "10.6" - overrides = Plist::parse_xml(Launchd_Overrides) + overrides = self.class.read_plist(Launchd_Overrides) overrides[resource[:name]] = { "Disabled" => true } Plist::Emit.save_plist(overrides, Launchd_Overrides) else job_path, job_plist = plist_from_label(resource[:name]) job_plist["Disabled"] = true Plist::Emit.save_plist(job_plist, job_path) end end end diff --git a/spec/unit/provider/service/launchd_spec.rb b/spec/unit/provider/service/launchd_spec.rb index 320ee3ace..05d76972d 100755 --- a/spec/unit/provider/service/launchd_spec.rb +++ b/spec/unit/provider/service/launchd_spec.rb @@ -1,200 +1,201 @@ #!/usr/bin/env ruby # # Unit testing for the launchd service provider # require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet' provider_class = Puppet::Type.type(:service).provider(:launchd) describe provider_class do before :each do # Create a mock resource @resource = stub 'resource' @provider = provider_class.new @joblabel = "com.foo.food" @jobplist = {} # A catch all; no parameters set @resource.stubs(:[]).returns(nil) # But set name, ensure and enable @resource.stubs(:[]).with(:name).returns @joblabel @resource.stubs(:[]).with(:ensure).returns :enabled @resource.stubs(:[]).with(:enable).returns :true @resource.stubs(:ref).returns "Service[#{@joblabel}]" # stub out the provider methods that actually touch the filesystem # or execute commands @provider.stubs(:plist_from_label).returns([@joblabel, @jobplist]) @provider.stubs(:execute).returns("") @provider.stubs(:resource).returns @resource # We stub this out for the normal case as 10.6 is "special". provider_class.stubs(:get_macosx_version_major).returns("10.5") end it "should have a start method for #{@provider.object_id}" do @provider.should respond_to(:start) end it "should have a stop method" do @provider.should respond_to(:stop) end it "should have an enabled? method" do @provider.should respond_to(:enabled?) end it "should have an enable method" do @provider.should respond_to(:enable) end it "should have a disable method" do @provider.should respond_to(:disable) end it "should have a status method" do @provider.should respond_to(:status) end describe "when checking status" do it "should call the external command 'launchctl list' once" do @provider.expects(:launchctl).with(:list).returns("rotating-strawberry-madonnas") @provider.status end it "should return stopped if not listed in launchctl list output" do @provider.stubs(:launchctl).with(:list).returns("rotating-strawberry-madonnas") @provider.status.should == :stopped end it "should return running if listed in launchctl list output" do @provider.stubs(:launchctl).with(:list).returns(@joblabel) @provider.status.should == :running end end describe "when checking whether the service is enabled" do it "should return true if the job plist says disabled is false" do @provider.stubs(:plist_from_label).returns(["foo", {"Disabled" => false}]) @provider.enabled?.should == :true end it "should return true if the job plist has no disabled key" do @provider.stubs(:plist_from_label).returns(["foo", {}]) @provider.enabled?.should == :true end it "should return false if the job plist says disabled is true" do @provider.stubs(:plist_from_label).returns(["foo", {"Disabled" => true}]) @provider.enabled?.should == :false end end describe "when checking whether the service is enabled on OS X 10.6" do + confine "Not running on OSX" => (Facter.value(:operatingsystem) == "Darwin") it "should return true if the job plist says disabled is true and the global overrides says disabled is false" do provider_class.stubs(:get_macosx_version_major).returns("10.6") @provider.stubs(:plist_from_label).returns(["foo", {"Disabled" => true}]) - Plist.stubs(:parse_xml).returns({@resource[:name] => {"Disabled" => false}}) + @provider.class.stubs(:read_plist).returns({@resource[:name] => {"Disabled" => false}}) @provider.enabled?.should == :true end it "should return false if the job plist says disabled is false and the global overrides says disabled is true" do provider_class.stubs(:get_macosx_version_major).returns("10.6") @provider.stubs(:plist_from_label).returns(["foo", {"Disabled" => false}]) - Plist.stubs(:parse_xml).returns({@resource[:name] => {"Disabled" => true}}) + @provider.class.stubs(:read_plist).returns({@resource[:name] => {"Disabled" => true}}) @provider.enabled?.should == :false end it "should return true if the job plist and the global overrides have no disabled keys" do provider_class.stubs(:get_macosx_version_major).returns("10.6") @provider.stubs(:plist_from_label).returns(["foo", {}]) - Plist.stubs(:parse_xml).returns({}) + @provider.class.stubs(:read_plist).returns({}) @provider.enabled?.should == :true end end describe "when starting the service" do it "should look for the relevant plist once" do @provider.expects(:plist_from_label).once @provider.start end it "should execute 'launchctl load' once without writing to the plist if the job is enabled" do @provider.stubs(:enabled?).returns :true @provider.expects(:execute).with([:launchctl, :load, @resource[:name]]).once @provider.start end it "should execute 'launchctl load' with writing to the plist once if the job is disabled" do @provider.stubs(:enabled?).returns :false @provider.expects(:execute).with([:launchctl, :load, "-w", @resource[:name]]).once @provider.start end it "should disable the job once if the job is disabled and should be disabled at boot" do @provider.stubs(:enabled?).returns :false @resource.stubs(:[]).with(:enable).returns :false @provider.expects(:disable).once @provider.start end end describe "when stopping the service" do it "should look for the relevant plist once" do @provider.expects(:plist_from_label).once @provider.stop end it "should execute 'launchctl unload' once without writing to the plist if the job is disabled" do @provider.stubs(:enabled?).returns :false @provider.expects(:execute).with([:launchctl, :unload, @resource[:name]]).once @provider.stop end it "should execute 'launchctl unload' with writing to the plist once if the job is enabled" do @provider.stubs(:enabled?).returns :true @provider.expects(:execute).with([:launchctl, :unload, "-w", @resource[:name]]).once @provider.stop end it "should enable the job once if the job is enabled and should be enabled at boot" do @provider.stubs(:enabled?).returns :true @resource.stubs(:[]).with(:enable).returns :true @provider.expects(:enable).once @provider.stop end end describe "when enabling the service" do it "should look for the relevant plist once" do @provider.expects(:plist_from_label).once @provider.stop end it "should check if the job is enabled once" do @provider.expects(:enabled?).once @provider.stop end end describe "when disabling the service" do it "should look for the relevant plist once" do @provider.expects(:plist_from_label).once @provider.stop end end describe "when enabling the service on OS X 10.6" do it "should write to the global launchd overrides file once" do provider_class.stubs(:get_macosx_version_major).returns("10.6") - Plist.stubs(:parse_xml).returns({}) + @provider.class.stubs(:read_plist).returns({}) Plist::Emit.expects(:save_plist).once @provider.enable end end describe "when disabling the service on OS X 10.6" do it "should write to the global launchd overrides file once" do provider_class.stubs(:get_macosx_version_major).returns("10.6") - Plist.stubs(:parse_xml).returns({}) + @provider.class.stubs(:read_plist).returns({}) Plist::Emit.expects(:save_plist).once @provider.enable end end end