v0.1 of APscheduler implementation

This commit is contained in:
2026-08-08 19:44:19 +02:00
parent fbffec5f91
commit 8660793ec2
2 changed files with 264 additions and 205 deletions

132
main.py
View File

@@ -1,35 +1,37 @@
import os, socket, subprocess, paramiko, io, sys, time import os, socket, subprocess, paramiko, io, sys, time
from typing import Final from typing import Final
from datetime import datetime from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
IDRAC: Final[str] = os.getenv("IDRAC", None) IDRAC: Final[str] = os.getenv("IDRAC", None)
IDRAC_USR: Final[str] = os.getenv("IDRAC_USR", None) IDRAC_USR: Final[str] = os.getenv("IDRAC_USR", None)
IDRAC_PWD: Final[str] = os.getenv("IDRAC_PWD", None) IDRAC_PWD: Final[str] = os.getenv("IDRAC_PWD", None)
# LAST_VERIFY_DAY: Final[int] = os.getenv("LAST_VERIFY_DAY", 7) # LAST_VERIFY_DAY: Final[int] = int(os.getenv("LAST_VERIFY_DAY", 7))
# LAST_GARBAGE_DAY: Final[int] = os.getenv("LAST_GARBAGE_DAY", 7) # LAST_GARBAGE_DAY: Final[int] = int(os.getenv("LAST_GARBAGE_DAY", 7))
HOST: Final[str] = os.getenv("HOST", None) HOST: Final[str] = os.getenv("HOST", None)
HOST_SSH_PORT: Final[int] = os.getenv("HOST_SSH_PORT", None) HOST_SSH_PORT: Final[int] = int(os.getenv("HOST_SSH_PORT", 22))
HOST_USR: Final[str] = os.getenv("HOST_USR", "root") HOST_USR: Final[str] = os.getenv("HOST_USR", "root")
HOST_USR_PWD: Final[str] = os.getenv("HOST_USR_PWD", None) HOST_USR_PWD: Final[str] = os.getenv("HOST_USR_PWD", None)
HOST_USR_SSHKEY: Final[str] = os.getenv("HOST_USR_SSHKEY", None) HOST_USR_SSHKEY: Final[str] = os.getenv("HOST_USR_SSHKEY", None)
HOST_USR_SSHKEY_PASS: Final[str] = os.getenv("HOST_USR_SSHKEY_PASS", None) HOST_USR_SSHKEY_PASS: Final[str] = os.getenv("HOST_USR_SSHKEY_PASS", None)
LOCAL_DATASTORE: Final[str] = os.getenv("LOCAL_DATASTORE", None) LOCAL_DATASTORE: Final[str] = os.getenv("LOCAL_DATASTORE", None)
REMOTE_DATASTORE: Final[str] = os.getenv("HOST_UREMOTE_DATASTORESR_SSHKEY_PASS", None) REMOTE_DATASTORE: Final[str] = os.getenv("REMOTE_DATASTORE", None)
REMOTE_PBS: Final[str] = os.getenv("REMOTE_PBS", None) REMOTE_PBS: Final[str] = os.getenv("REMOTE_PBS", None)
SYNC_DIR: Final[str] = os.getenv("SYNC_DIR", "push") SYNC_DIR: Final[str] = os.getenv("SYNC_DIR", "push")
PBS_RATE_OUT: Final[str] = os.getenv("PBS_RATE_OUT", "") PBS_RATE_OUT: Final[str] = os.getenv("PBS_RATE_OUT", "")
PBS_RATE_IN: Final[str] = os.getenv("PBS_RATE_IN", "") PBS_RATE_IN: Final[str] = os.getenv("PBS_RATE_IN", "")
PBS_TASK_LIMIT: Final[int] = os.getenv("PBS_TASK_LIMIT", 50) PBS_TASK_LIMIT: Final[int] = int(os.getenv("PBS_TASK_LIMIT", 50))
PBS_MAX_RUNTIME_MIN: Final[int] = os.getenv("PBS_MAX_RUNTIME_MIN", 180) PBS_MAX_RUNTIME_MIN: Final[int] = int(os.getenv("PBS_MAX_RUNTIME_MIN", 180))
PBS_MAX_SHUT_MIN: Final[int] = os.getenv("PBS_MAX_SHUT_MIN", 10) PBS_MAX_SHUT_MIN: Final[int] = int(os.getenv("PBS_MAX_SHUT_MIN", 10))
AUTO_ADD_NAME: Final[str] = os.getenv("AUTO_ADD_NAME", "autoBackupScript_one-shot") AUTO_ADD_NAME: Final[str] = os.getenv("AUTO_ADD_NAME", "autoBackupScript_one-shot")
RUN_SCHEDULE: Final[str] = os.getenv("RUN_SCHEDULE", "45 17 * * 5")
RUN_SCHEDULE_TZ: Final[str] = os.getenv("RUN_SCHEDULE_TZ", "Europe/Stockholm")
@@ -59,15 +61,16 @@ def load_private_key(key_data: str, passphrase: str | None = None):
raise ValueError("Unsupported or invalid SSH private key") raise ValueError("Unsupported or invalid SSH private key")
def getCurrentTasks(allTasks: bool = False, limitOfTasks: int =50) -> dict: def getCurrentTasks(sshThingy, allTasks: bool = False, limitOfTasks: int =50) -> dict:
"""Get what tasks are running/ran on PBS """Get what tasks are running/ran on PBS
Keyword arguments: Keyword arguments:
sshThingy -- a paramiko SSH object (no default)
allTasks -- if tasks that are finished should be included. (default False) allTasks -- if tasks that are finished should be included. (default False)
limitOfTasks -- how many tasks to ask PBS for (default 50) limitOfTasks -- how many tasks to ask PBS for (default 50)
""" """
(stdin, stdout, stderr) = ssh.exec_command(f'proxmox-backup-manager task list --limit {limitOfTasks} --all {allTasks}') (stdin, stdout, stderr) = sshThingy.exec_command(f'proxmox-backup-manager task list --limit {limitOfTasks} --all {allTasks}')
taskList = stdout.read().decode() taskList = stdout.read().decode()
@@ -84,8 +87,11 @@ def getCurrentTasks(allTasks: bool = False, limitOfTasks: int =50) -> dict:
# Skip the header row # Skip the header row
if columns[0] == "starttime": if columns[0] == "starttime":
continue continue
if len(columns) >= 2: if len(columns) >= 3:
try:
outTaskList.append(columns[2].split(":")[6]) outTaskList.append(columns[2].split(":")[6])
except IndexError:
raise exceptionator(ConnectionError, "Incorrect message received from PBS")
# exit() # exit()
@@ -132,7 +138,7 @@ def getCurrentTasks(allTasks: bool = False, limitOfTasks: int =50) -> dict:
if len(AUTO_ADD_NAME) > 32: if len(AUTO_ADD_NAME) > 32:
raise exceptionator(ValueError, "Maxmimum length of AUTO_ADD_NAME is 32") raise exceptionator(ValueError, "Maxmimum length of AUTO_ADD_NAME is 32")
sys.exit(1) # sys.exit(1)
@@ -143,21 +149,35 @@ if len(AUTO_ADD_NAME) > 32:
def theCodeBasically():
print("\nStarting backup sequence\n")
# dt = datetime.now.timestamp() # dt = datetime.now.timestamp()
# epoch_time = dt.timestamp() # epoch_time = dt.timestamp()
# print(epoch_time) # print(epoch_time)
# Check if power is on # Check if power is on
PWR_STATE = subprocess.run(["ipmitool", "-I", "lanplus","-H", IDRAC,"-U", IDRAC_USR,"-P", IDRAC_PWD,"chassis", "power", "status",],capture_output=True,text=True,)
if PWR_STATE.returncode != 0:
raise RuntimeError(f"ipmitool failed: {PWR_STATE.stderr.strip()}")
if "off" in PWR_STATE.stdout.lower():
subprocess.run(["ipmitool", "-I", "lanplus","-H", IDRAC,"-U", IDRAC_USR,"-P", IDRAC_PWD,"chassis", "power", "on",],check=True,)
# PWR_STATE = subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "status"], capture_output=True) # PWR_STATE = subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "status"], capture_output=True)
try: # try:
# power on if needed # # power on if needed
if "off" in str(PWR_STATE.stdout.decode()[-4:]): # if "off" in str(PWR_STATE.stdout.decode()[-4:]):
subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "on"]) # , capture_output=True # subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "on"]) # , capture_output=True
except exception as e: # except Exception as e:
print("Most likely wrong password, username or hostname has been passed for idrac.\nExiting\n") # print("Most likely wrong password, username or hostname has been passed for idrac.\nExiting\n")
sys.exit(1) # sys.exit(1)
# else: # else:
# continue # continue
@@ -182,7 +202,7 @@ if pinging:
break break
else: else:
raise exceptionator(TimeoutError, "Host didn't start after 20 minutes.\nCheck the server.") raise exceptionator(TimeoutError, "Host didn't start after 20 minutes.\nCheck the server.")
sys.exit(1) # sys.exit(1)
else: else:
print("reachable") print("reachable")
@@ -270,31 +290,34 @@ if REMOTE_PBS in remoteDict:
print(f"Found {REMOTE_PBS} as {remoteDict[REMOTE_PBS]}") print(f"Found {REMOTE_PBS} as {remoteDict[REMOTE_PBS]}")
else: else:
print(f"\n!!!\nRemote {REMOTE_PBS} not found.\nAdd remote {REMOTE_PBS} manually.\n!!!\n", file=sys.stderr) print(f"\n!!!\nRemote {REMOTE_PBS} not found.\nAdd remote {REMOTE_PBS} manually.\n!!!\n", file=sys.stderr)
REMOTE_PBS.remove(REMOTE_PBS) raise RuntimeError(f"Remote {REMOTE_PBS} not found.\nAdd remote {REMOTE_PBS} manually.")
sys.exit(1)
(stdin, stdout, stderr) = ssh.exec_command('proxmox-backup-manager sync-job list --sync-direction all') (stdin, stdout, stderr) = ssh.exec_command('proxmox-backup-manager sync-job list --sync-direction all')
syncJobList = stdout.read().decode() syncJobList = stdout.read().decode()
sync_job_exists = False
for line in syncJobList.splitlines(): for line in syncJobList.splitlines():
line = line.strip() line = line.strip()
# Only process actual table rows
if not line.startswith(""): if not line.startswith(""):
continue continue
columns = [col.strip() for col in line.strip("").split("")] columns = [col.strip() for col in line.strip("").split("")]
# Skip the header row
if columns[0] == "id": if columns[0] == "id":
continue continue
if len(columns) >= 2:
print("Sync job already on Proxmox Backup Server.\nSkipping sync job creation.") if columns[0] == AUTO_ADD_NAME:
# print(columns) sync_job_exists = True
break break
if sync_job_exists:
print(f"Sync job {AUTO_ADD_NAME} already exists.")
# create it
else: else:
match SYNC_DIR: match SYNC_DIR:
@@ -309,10 +332,14 @@ else:
else: else:
(stdin, stdout, stderr) = ssh.exec_command(f"proxmox-backup-manager sync-job create {AUTO_ADD_NAME} --remote {remoteDict[REMOTE_PBS]} --remote-store {REMOTE_DATASTORE} --store {LOCAL_DATASTORE} --sync-direction pull --comment 'automatic startupper script'") (stdin, stdout, stderr) = ssh.exec_command(f"proxmox-backup-manager sync-job create {AUTO_ADD_NAME} --remote {remoteDict[REMOTE_PBS]} --remote-store {REMOTE_DATASTORE} --store {LOCAL_DATASTORE} --sync-direction pull --comment 'automatic startupper script'")
case _: case _:
raise exceptionator(SyncDirectionError, f"No such sync direction as {SYNC_DIR}.\n Either use 'push' or 'pull'\n") raise ValueError(f"Invalid sync direction: {SYNC_DIR}. Use 'push' or 'pull'.")
# print(stdout.read().decode())
syncJobCreateExitCode = stdout.channel.recv_exit_status()
if syncJobCreateExitCode != 0:
raise RuntimeError(f"Failed to create sync job: {stderr.read().decode()}")
@@ -323,7 +350,13 @@ else:
# print(stdout.read().decode()) # print(stdout.read().decode())
# print(stderr.read().decode()) # print(stderr.read().decode())
syncJobRunExitCode = stdout.channel.recv_exit_status()
if syncJobRunExitCode != 0:
raise RuntimeError(f"Sync job failed: {stderr.read().decode()}")
# Give some time for PBS to "start" the sync-job task
time.sleep(10)
# Maybe add some checks here if the backup was properly scheduled # Maybe add some checks here if the backup was properly scheduled
@@ -337,19 +370,20 @@ else:
# check ongoing tasks # check ongoing tasks
SYNC_JOB_DATE = datetime.now().timestamp() SYNC_JOB_DATE = time.monotonic() #datetime.now().timestamp()
currenttasks = getCurrentTasks(limitOfTasks=PBS_TASK_LIMIT) currenttasks = getCurrentTasks(ssh, limitOfTasks=PBS_TASK_LIMIT)
while currenttasks: while currenttasks:
currenttasks = getCurrentTasks(limitOfTasks=PBS_TASK_LIMIT)
print("Waiting 1 minute before checking tasks again") print("Waiting 1 minute before checking tasks again")
time.sleep(60) time.sleep(60)
if SYNC_JOB_DATE - datetime.now().timestamp() >= (PBS_MAX_RUNTIME_MIN * 60): currenttasks = getCurrentTasks(ssh, limitOfTasks=PBS_TASK_LIMIT)
print(SYNC_JOB_DATE - datetime.now().timestamp())
if time.monotonic() - SYNC_JOB_DATE >= (PBS_MAX_RUNTIME_MIN * 60):
# print(SYNC_JOB_DATE - time.monotonic())
# Forcefully skip if these tasks are running. reader task is, I think, PVE backup # Forcefully skip if these tasks are running. reader task is, I think, PVE backup
# Also otherjob is something else and it is better to wait if there is one otherjob # Also otherjob is something else and it is better to wait if there is one otherjob
if ( if (
@@ -380,7 +414,31 @@ if subprocess.run(["ping", "-c", "3", "-W", "5", HOST], capture_output=True).ret
else: else:
subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "off"]) subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "off"])
sys.exit(0) # sys.exit(0)
scheduler = BlockingScheduler()
scheduler.add_job(
theCodeBasically,
CronTrigger.from_crontab(
RUN_SCHEDULE,
timezone=RUN_SCHEDULE_TZ,
),
max_instances=1,
coalesce=True,
)
scheduler.start()

View File

@@ -1 +1,2 @@
paramiko==5.0.0 paramiko==5.0.0
APScheduler==3.11.3