1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
|
require_relative '../../../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
if resource[:start]
service_command(:start)
return nil
end
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
if resource[:stop]
service_command(:stop)
return nil
end
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
|