diff options
Diffstat (limited to 'src/op_mode/powerctrl.py')
-rw-r--r-- | src/op_mode/powerctrl.py | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py new file mode 100644 index 0000000..c32a2be --- /dev/null +++ b/src/op_mode/powerctrl.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023-2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from argparse import ArgumentParser +from datetime import datetime +from sys import exit +from time import time + +from vyos.utils.io import ask_yes_no +from vyos.utils.process import call +from vyos.utils.process import run +from vyos.utils.process import STDOUT + +systemd_sched_file = "/run/systemd/shutdown/scheduled" + +def utc2local(datetime): + now = time() + offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) + return datetime + offs + +def parse_time(s): + try: + if re.match(r'^\d{1,9999}$', s): + if (int(s) > 59) and (int(s) < 1440): + s = str(int(s)//60) + ":" + str(int(s)%60) + return datetime.strptime(s, "%H:%M").time() + if (int(s) >= 1440): + return s.split() + else: + return datetime.strptime(s, "%M").time() + else: + return datetime.strptime(s, "%H:%M").time() + except ValueError: + return None + + +def parse_date(s): + for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + # If nothing matched... + return None + + +def get_shutdown_status(): + if os.path.exists(systemd_sched_file): + # Get scheduled from systemd file + with open(systemd_sched_file, 'r') as f: + data = f.read().rstrip('\n') + r_data = {} + for line in data.splitlines(): + tmp_split = line.split("=") + if tmp_split[0] == "USEC": + # Convert USEC to human readable format + r_data['DATETIME'] = datetime.utcfromtimestamp( + int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') + else: + r_data[tmp_split[0]] = tmp_split[1] + return r_data + return None + + +def check_shutdown(): + output = get_shutdown_status() + if output and 'MODE' in output: + dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') + if output['MODE'] == 'reboot': + print("Reboot is scheduled", utc2local(dt)) + elif output['MODE'] == 'poweroff': + print("Poweroff is scheduled", utc2local(dt)) + else: + print("Reboot or poweroff is not scheduled") + + +def cancel_shutdown(): + output = get_shutdown_status() + if output and 'MODE' in output: + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + try: + run('/sbin/shutdown -c --no-wall') + except OSError as e: + exit(f'Could not cancel a reboot or poweroff: {e}') + + mode = output['MODE'] + message = f'Scheduled {mode} has been cancelled {timenow}' + run(f'wall {message} > /dev/null 2>&1') + else: + print("Reboot or poweroff is not scheduled") + +def check_unsaved_config(): + from vyos.config_mgmt import unsaved_commits + from vyos.utils.boot import boot_configuration_success + + if unsaved_commits(allow_missing_config=True) and boot_configuration_success(): + print("Warning: there are unsaved configuration changes!") + print("Run 'save' command if you do not want to lose those changes after reboot/shutdown.") + else: + pass + +def execute_shutdown(time, reboot=True, ask=True): + from vyos.utils.process import cmd + + check_unsaved_config() + + host = cmd("hostname --fqdn") + + action = "reboot" if reboot else "poweroff" + if not ask: + if not ask_yes_no(f"Are you sure you want to {action} this system ({host})?"): + exit(0) + action_cmd = "-r" if reboot else "-P" + + if len(time) == 0: + # T870 legacy reboot job support + chk_vyatta_based_reboots() + ### + + out = cmd(f'/sbin/shutdown {action_cmd} now', stderr=STDOUT) + print(out.split(",", 1)[0]) + return + elif len(time) == 1: + # Assume the argument is just time + ts = parse_time(time[0]) + if ts: + cmd(f'/sbin/shutdown {action_cmd} {time[0]}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') + else: + exit(f'Invalid time "{time[0]}". The valid format is HH:MM') + elif len(time) == 2: + # Assume it's date and time + ts = parse_time(time[0]) + ds = parse_date(time[1]) + if ts and ds: + t = datetime.combine(ds, ts) + td = t - datetime.now() + t2 = 1 + int(td.total_seconds())//60 # Get total minutes + + cmd(f'/sbin/shutdown {action_cmd} {t2}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[1]} {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') + else: + if not ts: + exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format') + else: + exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]') + else: + exit('Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM') + check_shutdown() + + +def chk_vyatta_based_reboots(): + # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed + # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job + # name is the node of scheduled the job, commit-confirm checks for that + + f = r'/var/run/confirm.job' + if os.path.exists(f): + jid = open(f).read().strip() + if jid != 0: + call(f'sudo atrm {jid}') + os.remove(f) + + +def main(): + parser = ArgumentParser() + parser.add_argument("--yes", "-y", + help="Do not ask for confirmation", + action="store_true", + dest="yes") + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--reboot", "-r", + help="Reboot the system", + nargs="*", + metavar="HH:MM") + + action.add_argument("--reboot-in", "-i", + help="Reboot the system", + nargs="*", + metavar="Minutes") + + action.add_argument("--poweroff", "-p", + help="Poweroff the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--cancel", "-c", + help="Cancel pending shutdown", + action="store_true") + + action.add_argument("--check", + help="Check pending shutdown", + action="store_true") + args = parser.parse_args() + + try: + if args.reboot is not None: + for r in args.reboot: + if ':' not in r and '/' not in r and '.' not in r: + print("Incorrect format! Use HH:MM") + exit(1) + execute_shutdown(args.reboot, reboot=True, ask=args.yes) + if args.reboot_in is not None: + for i in args.reboot_in: + if ':' in i: + print("Incorrect format! Use Minutes") + exit(1) + execute_shutdown(args.reboot_in, reboot=True, ask=args.yes) + if args.poweroff is not None: + execute_shutdown(args.poweroff, reboot=False, ask=args.yes) + if args.cancel: + cancel_shutdown() + if args.check: + check_shutdown() + except KeyboardInterrupt: + exit("Interrupted") + +if __name__ == "__main__": + main() |