require 'puppet/util/plist'
Puppet::Type.type(:service).provide :launchd, :parent => :base do
desc <<-'EOT'
This provider manages jobs with `launchd`, which is the default service
framework for Mac OS X (and may be available for use on other platforms).
For more information, see the `launchd` man page:
* <https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man8/launchd.8.html>
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 plist's "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".
Note that this provider does not support overriding 'restart'
EOT
include Puppet::Util::Warnings
commands :launchctl => "/bin/launchctl"
defaultfor :operatingsystem => :darwin
confine :operatingsystem => :darwin
confine :feature => :cfpropertylist
has_feature :enableable
has_feature :refreshable
mk_resource_methods
# These are the paths in OS X where a launchd service plist could
# exist. This is a helper method, versus a constant, for easy testing
# and mocking
#
# @api private
def self.launchd_paths
[
"/Library/LaunchAgents",
"/Library/LaunchDaemons",
"/System/Library/LaunchAgents",
"/System/Library/LaunchDaemons"
]
end
# Gets the current Darwin version, example 10.6 returns 9 and 10.10 returns 14
# See https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history
# for more information.
#
# @api private
def self.get_os_version
@os_version ||= Puppet.runtime[:facter].value(:operatingsystemmajrelease).to_i
end
# Defines the path to the overrides plist file where service enabling
# behavior is defined in 10.6 and greater.
#
# With the rewrite of launchd in 10.10+, this moves and slightly changes format.
#
# @api private
def self.launchd_overrides
if self.get_os_version < 14
"/var/db/launchd.db/com.apple.launchd/overrides.plist"
else
"/var/db/com.apple.xpc.launchd/disabled.plist"
end
end
# Caching is enabled through the following three methods. Self.prefetch will
# call self.instances to create an instance for each service. Self.flush will
# clear out our cache when we're done.
def self.prefetch(resources)
instances.each do |prov|
resource = resources[prov.name]
if resource
resource.provider = prov
end
end
end
# Self.instances will return an array with each element being a hash
# containing the name, provider, path, and status of each service on the
# system.
def self.instances
jobs = self.jobsearch
@job_list ||= self.job_list
jobs.keys.collect do |job|
job_status = @job_list.has_key?(job) ? :running : :stopped
new(:name => job, :provider => :launchd, :path => jobs[job], :status => job_status)
end
end
# This method will return a list of files in the passed directory. This method
# does not go recursively down the tree and does not return directories
#
# @param path [String] The directory to glob
#
# @api private
#
# @return [Array] of String instances modeling file paths
def self.return_globbed_list_of_file_paths(path)
array_of_files = Dir.glob(File.join(path, '*')).collect do |filepath|
File.file?(filepath) ? filepath : nil
end
array_of_files.compact
end
# Get a hash of all launchd plists, keyed by label. This value is cached, but
# the cache will be refreshed if refresh is true.
#
# @api private
def self.make_label_to_path_map(refresh=false)
return @label_to_path_map if @label_to_path_map and not refresh
@label_to_path_map = {}
launchd_paths.each do |path|
return_globbed_list_of_file_paths(path).each do |filepath|
Puppet.debug("Reading launchd plist #{filepath}")
job = read_plist(filepath)
next if job.nil?
if job.respond_to?(:key) && job.key?("Label")
@label_to_path_map[job["Label"]] = filepath
else
#TRANSLATORS 'plist' and label' should not be translated
Puppet.debug(_("The %{file} plist does not contain a 'label' key; Puppet is skipping it") % { file: filepath })
next
end
end
end
@label_to_path_map
end
# Sets a class instance variable with a hash of all launchd plist files that
# are found on the system. The key of the hash is the job id and the value
# is the path to the file. If a label is passed, we return the job id and
# path for that specific job.
def self.jobsearch(label=nil)
by_label = make_label_to_path_map
if label
if by_label.has_key? label
return { label => by_label[label] }
else
# try refreshing the map, in case a plist has been added in the interim
by_label = make_label_to_path_map(true)
if by_label.has_key? label
return { label => by_label[label] }
else
raise Puppet::Error, "Unable to find launchd plist for job: #{label}"
end
end
else
# caller wants the whole map
by_label
end
end
# This status method lists out all currently running services.
# This hash is returned at the end of the method.
def self.job_list
@job_list = Hash.new
begin
output = launchctl :list
raise Puppet::Error.new("launchctl list failed to return any data.") if output.nil?
output.split("\n").each do |line|
@job_list[line.split(/\s/).last] = :running
end
rescue Puppet::ExecutionFailure
raise Puppet::Error.new("Unable to determine status of #{resource[:name]}", $!)
end
@job_list
end
# Read a plist, whether its format is XML or in Apple's "binary1"
# format.
def self.read_plist(path)
Puppet::Util::Plist.read_plist_file(path)
end
# Read overrides plist, retrying if necessary
def self.read_overrides
i = 1
overrides = nil
loop do
Puppet.debug(_("Reading overrides plist, attempt %{i}") % {i: i}) if i > 1
overrides = read_plist(launchd_overrides)
break unless overrides.nil?
raise Puppet::Error.new(_('Unable to read overrides plist, too many attempts')) if i == 20
Puppet.info(_('Overrides file could not be read, trying again.'))
Kernel.sleep(0.1)
i += 1
end
overrides
end
# Clean out the @property_hash variable containing the cached list of services
def flush
@property_hash.clear
end
def exists?
Puppet.debug("Puppet::Provider::Launchd:Ensure for #{@property_hash[:name]}: #{@property_hash[:ensure]}")
@property_hash[:ensure] != :absent
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]
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
# when a service includes hasstatus=>false, we override the launchctl
# status mechanism and fall back to the base provider status method.
def status
if @resource && ((@resource[:hasstatus] == :false) || (@resource[:status]))
return super
elsif @property_hash[:status].nil?
# property_hash was flushed so the service changed status
service_name = @resource[:name]
# Updating services with new statuses
job_list = self.class.job_list
# if job is present in job_list, return its status
if job_list.key?(service_name)
job_list[service_name]
# if job is no longer present in job_list, it was stopped
else
:stopped
end
else
@property_hash[:status]
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
return ucommand(:start) if resource[:start]
job_path, _ = plist_from_label(resource[:name])
did_enable_job = false
cmds = []
cmds << :launchctl << :load
# always add -w so it always starts the job, it is a noop if it is not needed, this means we do
# not have to rescan all launchd plists.
cmds << "-w"
if self.enabled? == :false || self.status == :stopped # launchctl won't load disabled jobs
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
return ucommand(:stop) if resource[:stop]
job_path, _ = 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
def restart
Puppet.debug("A restart has been triggered for the #{resource[:name]} service")
Puppet.debug("Stopping the #{resource[:name]} service")
self.stop
Puppet.debug("Starting the #{resource[:name]} service")
self.start
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.
# Starting 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
begin
_, job_plist = plist_from_label(resource[:name])
rescue Puppet::Error => err
# if job does not exist, log the error and return false as on other platforms
Puppet.log_exception(err)
return :false
end
job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled")
overrides = self.class.read_overrides if FileTest.file?(self.class.launchd_overrides)
if overrides
if overrides.has_key?(resource[:name])
if self.class.get_os_version < 14
overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled")
else
overrides_disabled = overrides[resource[:name]]
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.
def enable
overrides = self.class.read_overrides
if self.class.get_os_version < 14
overrides[resource[:name]] = { "Disabled" => false }
else
overrides[resource[:name]] = false
end
Puppet::Util::Plist.write_plist_file(overrides, self.class.launchd_overrides)
end
def disable
overrides = self.class.read_overrides
if self.class.get_os_version < 14
overrides[resource[:name]] = { "Disabled" => true }
else
overrides[resource[:name]] = true
end
Puppet::Util::Plist.write_plist_file(overrides, self.class.launchd_overrides)
end
end
Copyright 2K16 - 2K18 Indonesian Hacker Rulez