From 4f3f6c523d59569ac8cde26785a7c55360b45e51 Mon Sep 17 00:00:00 2001 From: Phil Dibowitz Date: Thu, 13 Nov 2025 18:14:32 +1000 Subject: [PATCH] [chefctl] Add a new hook: `skip_run?` This adds a new hook point (and sample plugin usage) that allows the Chef run to be skipped based on some local criteria. Example usage might be: - Device is on battery - Device is not connected to VPN/backhaul/etc. - Some global service meant to disable runs during an emergency Previously I did this in pre_run or pre_start, but the problem with that is that the only way is to force `exit`, which causes the logs to get messed up because we never update the links. This provides a clean way to skip the run but still update the chef.{cur,last} links so that it's clear what has happened. Sample output: ``` $ sudo chefctl -iv [2025-11-13 18:27:44 +1000] DEBUG chefctl: Loading plugin at /etc/cinc/chefctl_hooks.rb. [2025-11-13 18:27:44 +1000] DEBUG chefctl: Including registered plugin KrHook [2025-11-13 18:27:44 +1000] DEBUG chefctl: Trying lock /var/lock/subsys/chefctl [2025-11-13 18:27:44 +1000] DEBUG chefctl: Lock acquired: /var/lock/subsys/chefctl [2025-11-13 18:27:44 +1000] INFO chefctl: taste-tester mode ends in < 1 hour, extending back to 1 hour [2025-11-13 18:27:44 +1000] DEBUG chefctl: Skippinbg battery check due to --immediate flag ``` and ``` $ sudo chefctl [2025-11-13 18:27:22 +1000] INFO chefctl: taste-tester mode ends in < 1 hour, extending back to 1 hour [2025-11-13 18:27:22 +1000] WARN chefctl: Running on battery power, skipping Chef run [2025-11-13 18:27:22 +1000] INFO chefctl: Plugin requested skipping chef run. ``` Signed-off-by: Phil Dibowitz --- chefctl/sample_hooks/skip_on_battery.rb | 50 +++++++++++++++++++++++++ chefctl/src/chefctl.rb | 37 +++++++++++++++--- 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 chefctl/sample_hooks/skip_on_battery.rb diff --git a/chefctl/sample_hooks/skip_on_battery.rb b/chefctl/sample_hooks/skip_on_battery.rb new file mode 100644 index 0000000..ca51d8d --- /dev/null +++ b/chefctl/sample_hooks/skip_on_battery.rb @@ -0,0 +1,50 @@ +# Copyright 2025-present Facebook +# Copyright 2025-present Phil Dibowitz +# +# 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. + +# This hook file is located at '/etc/chef/chefctl_hooks.rb' +# You can change this location by passing `-p/--plugin-path` to `chefctl`, +# or by setting `plugin_path` in `chefctl-config.rb` + +# Below is a sample hook file that will skip runs when the device is on +# battery. +# +# Hook descriptions and empty hooks have been removed. Refer to the hook +# documentation for descriptions of each method. + +module SkipOnBattery + def skip_run? + # If a human is requesting the run, they want the run, so don't worry + # about the battery check. + # + # Another option would be to use the cli_options hook to add an + # override flag, if desired. + if Chefctl::Config.immediate + Chefctl.logger.debug('Skipping battery check due to --immediate flag') + return false + end + + if File.exist?('/sys/class/power_supply/BAT0/status') + bat_status = File.read( + '/sys/class/power_supply/BAT0/status', + ).strip + Chefctl.logger.debug("Battery status: #{bat_status}") + if bat_status == 'Discharging' + Chefctl.logger.warn('Running on battery power, skipping Chef run') + return true + end + end + false + end +end diff --git a/chefctl/src/chefctl.rb b/chefctl/src/chefctl.rb index c9c5d2e..5edf0fe 100755 --- a/chefctl/src/chefctl.rb +++ b/chefctl/src/chefctl.rb @@ -269,6 +269,25 @@ def generate_certs end end + # Called after the lock is acquired, before pre_run (and this before + # the chef run is started). This hook is intended to allow suppressing + # Chef runs under specific conditions. Examples might include: + # + # - Device is on battery + # - Device is not connected to VPN/backhaul/etc. + # - Some global service meant to disable runs during an emergency + # + # Defaults, of course, to false. Should be used with care. While we _will_ + # log that the chef run was skipped at the request of the plugin, the plugin + # should log _why_ it was skipped so that it appears in the log. + # + # Note that if the return value is an Integer, that integer is used as + # the exit value of chefctl. If it's `true`, the exit value is 0, and + # if it is `false`, chef runs normally. + def skip_run? + false + end + # Called after the lock is acquired, before the chef run is started. # Parameters: # - output is the path to the log file for the chef run @@ -885,11 +904,19 @@ def chef_run symlink_output(:chef_cur) - do_splay unless Chefctl::Config.immediate - - plugin.pre_run(@paths[:out]) - - retval = do_chef_runs + ret = plugin.skip_run? + if ret.is_a?(FalseClass) + do_splay unless Chefctl::Config.immediate + plugin.pre_run(@paths[:out]) + retval = do_chef_runs + else + Chefctl.logger.info('Plugin requested skipping chef run.') + # if it's an integer, use that as the exit code, otherwise + # we keep it 0, indicating success + if ret.is_a?(Integer) + retval = ret + end + end plugin.post_run(@paths[:out], retval)