# 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.
# rubocop:disable Chef/Meta/FBUtilReboot

resource_name :fb_helpers_reboot

provides :fb_helpers_reboot, :os => ['darwin', 'linux']
unified_mode(false) if Chef::VERSION >= 18 # TODO(T144966423)

# 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 # rubocop:disable Chef/Meta/FBUtilReboot
        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 # rubocop:disable Chef/Meta/FBUtilReboot
          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 # rubocop:disable Chef/Meta/FBUtilReboot
    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
