itchef/cookbooks/fb_helpers/resources/reboot.rb (173 lines of code) (raw):

# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright (c) 2013-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. resource_name :fb_helpers_reboot provides :fb_helpers_reboot, :os => ['darwin', 'linux'] # description 'Use the fb_helpers_reboot resource if you need to indicate to an' # ' external service that the host needs to be rebooted and when' # ' that reboot action should be handled.' default_action :now property :required, [true, false], :default => true # :description => 'Control whether the Chef run should fail until the ' # 'host has been rebooted successfully.' property :wakeup_time_secs, Integer, :default => 120 # :description => 'The number of seconds we should wait before waking ' # 'the system back up using the RTC.' property :__fb_helpers_internal_allow_process_deferred, [true, false], :default => false # :description => 'Internal property; :process_deferred will fail ' # 'unless this is set to true to prevent accidental ' # 'misuse.' property :prefix, ['/tmp', '/dev/shm'] # :description => 'Directory prefix for the reboot flag files; used ' # 'only via load_current_value' # Signals future chef-client runs to abort until after reboot REBOOT_OVERRIDE = 'chef_reboot_override'.freeze # Signals :process_deferred that there are reboots enqueued. REBOOT_TRIGGER = 'chef_reboot_trigger'.freeze # Signals :process_deferred that it should fail the run if it can't reboot REBOOT_REQUIRED = 'chef_reboot_required'.freeze NOT_ALLOWED_MSG = 'Was asked to reboot, but reboot is not allowed!'.freeze load_current_value do # macOS doesn't have /dev/shm, so use /tmp instead which is wiped on boot. prefix node.macos? ? '/tmp' : '/dev/shm' end action_class do def load_reboot_reason reason = 'not specified' if ::File.exist?(::File.join(current_resource.prefix, REBOOT_OVERRIDE)) reason = ::File.read(::File.join(current_resource.prefix, REBOOT_OVERRIDE)).chomp end reason end def set_reboot_override(reboot_type) unless reboot_type fail 'set_reboot_override: reboot_type was not set, aborting!' end file ::File.join(current_resource.prefix, REBOOT_OVERRIDE) do owner node.root_user group node.root_group mode '0644' content "#{reboot_type} reboot '#{new_resource.name}' requested by " + "recipe #{cookbook_name}::#{new_resource.recipe_name}" end end def set_reboot_trigger file ::File.join(current_resource.prefix, REBOOT_TRIGGER) do owner node.root_user group node.root_group mode '0644' end end def set_reboot_required file ::File.join(current_resource.prefix, REBOOT_REQUIRED) do owner node.root_user group node.root_group mode '0644' end end def do_managed_reboot msg = '*** Reboot required to proceed' node['fb_helpers']['managed_reboot_callback']&.call(node) ruby_block 'Managed reboot' do block do fail msg end action :nothing end ruby_block 'Schedule failure for reboot' do # rubocop:disable Lint/EmptyBlock block {} # rubocop:enable Lint/EmptyBlock notifies :run, 'ruby_block[Managed reboot]' end end def reboot_allowed(node) node['fb_helpers']['reboot_allowed_callback'].nil? ? node['fb_helpers']['reboot_allowed'] : node['fb_helpers']['reboot_allowed_callback']&.call(node) end end action :now do # TODO (t15830562) - this action should observe required and override the # same way as the :deferred action if reboot_allowed(node) if node.firstboot_any_phase? set_reboot_override('immediate') do_managed_reboot else command = execute 'reboot' do # ~FB026 command 'reboot' action :nothing end node['fb_helpers']['reboot_logging_callback']&.call( node, "reboot reason: '#{new_resource.name}' requested by " + "recipe #{cookbook_name}::#{new_resource.recipe_name}", ) command.run_action(:run) fail 'Reboot requested, aborting chef run and rebooting' end elsif new_resource.required fail NOT_ALLOWED_MSG else Chef::Log.error(NOT_ALLOWED_MSG) end end action :managed_now do if node.firstboot_any_phase? set_reboot_override('managed') do_managed_reboot else Chef::Log.info('Managed reboot supported only during provisioning') end end action :deferred do set_reboot_trigger if new_resource.required # If a reboot is required, Chef will persistently fail Chef until the # reboot happens. set_reboot_override('deferred') set_reboot_required end end # This is an internal action that's only used at the end of # fb_helpers to collate and synchronize all the reboot requests. action :process_deferred do unless new_resource.__fb_helpers_internal_allow_process_deferred fail 'You didn\'t say the magic word...' end # TODO do we need another flag file for this? or can we use REBOOT_OVERRIDE? if ::File.exist?(::File.join(current_resource.prefix, REBOOT_TRIGGER)) if node.firstboot_any_phase? set_reboot_override('process_deferred') do_managed_reboot elsif ::File.exist?(::File.join(current_resource.prefix, REBOOT_REQUIRED)) if reboot_allowed(node) node['fb_helpers']['reboot_logging_callback']&.call( node, load_reboot_reason, ) reboot 'reboot' do # ~FB026 action :request_reboot end else fail NOT_ALLOWED_MSG end else Chef::Log.info('Reboot requested, but not required. Chef will succeed,' + ' but host is pending reboot by an external entity.') end else Chef::Log.info('Trigger not found, no reboots to process') end end action :rtc_wakeup do message = 'RTC can wake' verify_rtc_cap = execute 'verify_rtc_cap' do # Sometimes the RTC message has fallen out of `dmesg`, but it's still # in /var/log/dmesg. So check both if we have to. command "grep -q '#{message}' /var/log/dmesg 2>/dev/null || " + "dmesg | grep -q '#{message}'" action :nothing end set_wakeup = execute 'set_wakeup' do command "rtcwake -m no -s #{new_resource.wakeup_time_secs}" action :nothing end poweroff = execute 'poweroff' do # ~FB026 command 'shutdown -P now' action :nothing end if node.firstboot_any_phase? Chef::Log.info('Not rebooting because we are in firstboot') elsif reboot_allowed(node) set_reboot_override('rtc_wakeup') verify_rtc_cap.run_action(:run) set_wakeup.run_action(:run) node['fb_helpers']['reboot_logging_callback']&.call( node, "poweroff reason: '#{new_resource.name}' (#{message}) requested by " + "recipe #{cookbook_name}::#{new_resource.recipe_name}", 'poweroff', ) poweroff.run_action(:run) fail "Hard shutdown requested, aborting chef and shutting down. Server should wake up in #{new_resource.wakeup_time_secs} seconds." else fail NOT_ALLOWED_MSG end end