cookbooks/fb_timers/resources/setup.rb (165 lines of code) (raw):

# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright (c) 2016-present, Facebook, Inc. # All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # action :run do # Delete old jobs Dir.glob("#{node['fb_timers']['_timer_path']}/*").each do |path| # this doubles as the unit name. fname = ::File.basename(path) # This is managed by the cookbook, skip it. next if fname == 'README' # Don't delete any directories next if ::File.directory?(path) exp = /^([\w:\-.\\@]+)\.(service|timer)$/ m = exp.match(fname) if m name = m[1] type = m[2] if node['fb_timers']['jobs'][name] # It might be defined, but disabled by an only_if conf = node['fb_timers']['jobs'][name] if conf['only_if'].nil? next else unless conf['only_if'].instance_of?(Proc) fail "fb_timers's only_if requires a Proc for #{name}" end next if conf['only_if'].call end end end # If there's a link in systemd's unit path, delete it too # We have to do this first cause you can't disable a unit who's file has # disappeared off the filesystem possible_link = "/etc/systemd/system/#{fname}" if ::File.symlink?(possible_link) && # ~FC023 ::File.readlink(possible_link) == path # systemd can get confused if you delete the file without disabling # the unit first. Disabling a linked unit removes the symlink anyway. service fname do action [:stop, :disable] end end Chef::Log.info("fb_timers: Removing unknown #{type} file #{path}") file path do action :delete end end optional_keys = node['fb_timers']['optional_keys'] # Setup current jobs node['fb_timers']['jobs'].to_hash.each_pair do |name, conf| conf = FB::Systemd::TIMER_DEFAULTS.merge(conf.merge('name' => name)) node.default['fb_timers']['jobs'][name] = conf # Do this early so we can rely on commands being filled in if conf['command'] if conf['commands'] Chef::Log.warn("fb_timers: [#{conf['name']}] You shouldn't mix " + '`command` and `commands`') else conf['commands'] = [] end conf['commands'] << conf['command'] end unless conf['description'] conf['description'] = "Run scheduled task #{conf['name']}" end unknown_keys = conf.keys - FB::Systemd::TIMER_COOKBOOK_KEYS - optional_keys if unknown_keys.any? Chef::Log.warn( "fb_timers: Unknown keys for timer #{name}: #{unknown_keys}", ) if unknown_keys.find { |key| key.casecmp('user').zero? } Chef::Log.warn('fb_timers: To set a user ' + "{ 'timer_options' => {'User' => 'nobody' }") end end missing_keys = FB::Systemd::REQUIRED_TIMER_KEYS - conf.keys # calendar is not entirely mandatory, one can use On... if missing_keys.include?('calendar') && !( conf['timer_options'].keys & FB::Systemd::ALTERNATE_CALENDAR_KEYS ).empty? missing_keys.delete('calendar') end if missing_keys.any? fail "fb_timers: Missing required key for timer #{name}: #{missing_keys}" end unless conf['only_if'].nil? unless conf['only_if'].instance_of?(Proc) fail "fb_timers's only_if requires a Proc for #{name}" end unless conf['only_if'].call Chef::Log.debug("fb_timers: Not including #{conf['name']}" + 'due to only_if') node.rm('fb_timers', 'jobs', conf['name']) next end end %w{service timer}.each do |type| filename = "#{node['fb_timers']['_timer_path']}/#{conf['name']}.#{type}" template filename do source "#{type}.erb" mode '0644' owner 'root' group 'root' # Use of variables within templates is heavily discouraged. # It's safe to use here since it's in a provider and isn't used # directly. variables :conf => conf notifies :reload_needed, 'fb_timers_setup[fb_timers system setup]', :immediately end execute "link unit file #{filename}" do not_if do ::File.exist?("/etc/systemd/system/#{conf['name']}.#{type}") || !conf['autostart'] end command "systemctl link #{filename}" # Don't notify systemd to reload; you're already talking to systemd end end end # Reload systemd, but only if required if Chef::VERSION.to_i >= 16 notify_group 'reloading systemd' do only_if { node['fb_timers']['_reload_needed'] } action :run notifies :run, 'fb_systemd_reload[system instance]', :immediately end else # rubocop:disable Lint/UnneededCopDisableDirective # rubocop:disable ChefDeprecations/LogResourceNotifications log 'reloading systemd' do only_if { node['fb_timers']['_reload_needed'] } notifies :run, 'fb_systemd_reload[system instance]', :immediately end # rubocop:enable ChefDeprecations/LogResourceNotifications # rubocop:enable Lint/UnneededCopDisableDirective end directory '/etc/systemd/system/timers.target.wants' do only_if do FB::Version.new(node['packages']['systemd'][ 'version']) <= FB::Version.new('201') end owner 'root' group 'root' mode '0755' end # Setup services node['fb_timers']['jobs'].to_hash.each_pair do |_name, conf| timer_name = "#{conf['name']}.timer" service "#{timer_name} enable/start" do only_if do conf['autostart'] && FB::Version.new(node['packages']['systemd'][ 'version']) > FB::Version.new('201') end service_name timer_name action [:enable, :start] end # Versions prior to 201 did not support enablement of unit symlinks. # Workaround is to create the following symlink. link "/etc/systemd/system/timers.target.wants/#{timer_name}" do only_if do conf['autostart'] && FB::Version.new(node['packages']['systemd'][ 'version']) <= FB::Version.new('201') end to lazy { "#{node['fb_timers']['_timer_path']}/#{conf['name']}.timer" } end service "#{timer_name} start only" do only_if do conf['autostart'] && FB::Version.new(node['packages']['systemd'][ 'version']) <= FB::Version.new('201') end service_name timer_name action [:start] end end # Delete any dead symlinks to timers within /etc/systemd/system dead_links = Dir.glob("#{FB::Systemd::UNIT_PATH}/*").select do |unit| # only delete symlinks ::File.symlink?(unit) && # whose targets are timer files ::File.readlink(unit).start_with?(node['fb_timers']['_timer_path']) && # whose targets don't exist !::File.exist?(::File.readlink(unit)) end dead_links.each do |unit| Chef::Log.info("fb_timers: Removing dead link #{unit}") # we can't use systemctl disable here because it's already deleted link unit do action :delete end end end action :reload_needed do node.default['fb_timers']['_reload_needed'] = true end