From fbffec5f91d1d058a27fb8f974dba77f93279018 Mon Sep 17 00:00:00 2001 From: YuruC3 Date: Sat, 8 Aug 2026 17:45:27 +0200 Subject: [PATCH] First version that is cron-based --- .gitignore | 1 + Dockerfile | 25 +++ Roadmap | 115 ++++++++++ docker-compose.yaml | 32 +++ main.py | 495 ++++++++++++++++++++++++++++++++++++++++++++ manifest.yaml | 112 ++++++++++ requirements.txt | 1 + 7 files changed, 781 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Roadmap create mode 100644 docker-compose.yaml create mode 100644 main.py create mode 100644 manifest.yaml create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5e96db --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1eab9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.14.7-alpine3.24 + + + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN addgroup -g 1600 pyusr \ + && adduser -D -u 1600 -G pyusr pyusr + +RUN mkdir -p /app/pbsStartUpper/ && \ + chown -R 1600:1600 /app/pbsStartUpper/ + +WORKDIR /app/pbsStartUpper/ + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=pyusr:pyusr main.py . + +USER pyusr + +CMD ["python", "main.py"] + diff --git a/Roadmap b/Roadmap new file mode 100644 index 0000000..12469fc --- /dev/null +++ b/Roadmap @@ -0,0 +1,115 @@ + +# Connect to IDRAC + +# Check if power is on + + # chassis power status | grep fourth element + # "Chassis Power is off" + + # if turn on power + + # Send Power on (ipmitool chassis power on) + + # else systemPower is on, continue. + + +# Store current date as PWR_DATE to later compare in while loop + +# Send ping to HOST every 15 seconds for 10 minutes. It can take some time for the server to start + +# while PWR_DATE is less than 15 minutes compared to CURRENT_DATE, ping + # if ping successfull 3 times in a row + # break + +# else, exit with error, + # exit "Host didn't start after 20 minutes. Check the server" + + + + +# SSH to HOST + +# check when verification was last ran + # if not in the last LAST_VERIF_D, run verification + # Verify datastore backups + + # else, backups already verified enough, continue + # continue + +# check if remote exists +# proxmox-backup-manager remote list +# ┌──────┬──────────────┬──────────┬───────────────────────────────────────────┬─────────┐ +# │ name │ host │ userid │ fingerprint │ comment │ +# ╞══════╪══════════════╪══════════╪═══════════════════════════════════════════╪═════════╡ +# │ pbs2 │ pbs2.example │ sync@pam │64:d3:ff:3a:50:38:53:5a:9b:f7:50:...:ab:fe │ │ +# └──────┴──────────────┴──────────┴───────────────────────────────────────────┴─────────┘ + +# for each remote host in REMOTE_PBS + + # if remote host not in "host" collumn from "remote list" command + + # exit with "Please first add a remote to PBS" + +# else + # print All passed remotes found on PBS + + +# Get local and remote stores + + +# Create backup one-shot sync job + +# for syncJob in jobList + + # if AUTO_ADD_NAME in comment collumn of syncJob + # remove sync-job by id + + # elif + # Add sync job + + # else + # idk + +# run one-shop sync jobs + +# for syncJob in jobList + # if AUTO_ADD_NAME in syncJob.comment + # run sync-job + + # if run succesfull + # continue + # else + # print the error message to stderr + + # elif AUTO_ADD_NAME not in syncJob.comment + # skipping a manually addad syncJob + # else + # either no syncJobs or something broke + +# store current date as PWR_OFF_DATE + +# while ongoing syncJobs or ongoing verifyJobs + # if any remaining syncJobs and verifyJobs and garbageCollect finish + + # if PWR_OFF_DATE is longer than 3 hours + # brake in order to send shutdown signal nontheless + # else + # sleep for 30 seconds + +# send power off signal via SSH +# if chassis power status is on after 5 minutes + # send additional "chassis power off" via ipmitool +# else + # exit 0 + + + +# proxmox-backup-manager sync-job create pbs2-local --remote pbs2 --remote-store local --store local --schedule 'Wed 02:30' +# proxmox-backup-manager sync-job update pbs2-local --comment 'offsite' +# proxmox-backup-manager sync-job list +# ┌────────────┬───────┬────────┬──────────────┬───────────┬─────────┐ +# │ id │ store │ remote │ remote-store │ schedule │ comment │ +# ╞════════════╪═══════╪════════╪══════════════╪═══════════╪═════════╡ +# │ pbs2-local │ local │ pbs2 │ local │ Wed 02:30 │ offsite │ +# └────────────┴───────┴────────┴──────────────┴───────────┴─────────┘ +# proxmox-backup-manager sync-job remove pbs2-local diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..bf8b2b5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,32 @@ +--- + +# sudo crontab -e +# 0 2 * * * cd /path/to/docker-compose && /usr/bin/docker compose up -d >> /var/log/pbsupper-docker-compose.log 2>&1 + +services: + pbsupper-1: + container_name: pbsupper + user: 1600:1600 + image: tea.shupogaki.org/yuruc3/pbsupper:v0 + environment: + LOCAL_DATASTORE: somethingSomething + REMOTE_DATASTORE: somethingSomething + REMOTE_PBS: somethingSomething + + HOST: somethingSomething + HOST_SSH_PORT: somethingSomething + HOST_USR: somethingSomething + HOST_USR_PWD: somethingSomething + + HOST_USR_SSHKEY: | + -----BEGIN OPENSSH PRIVATE KEY----- + 123... + -----END OPENSSH PRIVATE KEY----- + HOST_USR_SSHKEY_PASS: somethingSomething + + IDRAC: somethingSomething + IDRAC_USR: somethingSomething + IDRAC_PWD: somethingSomething + + + restart: unless-stopped diff --git a/main.py b/main.py new file mode 100644 index 0000000..06eb585 --- /dev/null +++ b/main.py @@ -0,0 +1,495 @@ +import os, socket, subprocess, paramiko, io, sys, time +from typing import Final +from datetime import datetime + + +IDRAC: Final[str] = os.getenv("IDRAC", None) +IDRAC_USR: Final[str] = os.getenv("IDRAC_USR", None) +IDRAC_PWD: Final[str] = os.getenv("IDRAC_PWD", None) + +# LAST_VERIFY_DAY: Final[int] = os.getenv("LAST_VERIFY_DAY", 7) +# LAST_GARBAGE_DAY: Final[int] = os.getenv("LAST_GARBAGE_DAY", 7) + +HOST: Final[str] = os.getenv("HOST", None) +HOST_SSH_PORT: Final[int] = os.getenv("HOST_SSH_PORT", None) +HOST_USR: Final[str] = os.getenv("HOST_USR", "root") +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_PASS: Final[str] = os.getenv("HOST_USR_SSHKEY_PASS", None) + +LOCAL_DATASTORE: Final[str] = os.getenv("LOCAL_DATASTORE", None) +REMOTE_DATASTORE: Final[str] = os.getenv("HOST_UREMOTE_DATASTORESR_SSHKEY_PASS", None) +REMOTE_PBS: Final[str] = os.getenv("REMOTE_PBS", None) +SYNC_DIR: Final[str] = os.getenv("SYNC_DIR", "push") +PBS_RATE_OUT: Final[str] = os.getenv("PBS_RATE_OUT", "") +PBS_RATE_IN: Final[str] = os.getenv("PBS_RATE_IN", "") +PBS_TASK_LIMIT: Final[int] = os.getenv("PBS_TASK_LIMIT", 50) +PBS_MAX_RUNTIME_MIN: Final[int] = os.getenv("PBS_MAX_RUNTIME_MIN", 180) +PBS_MAX_SHUT_MIN: Final[int] = os.getenv("PBS_MAX_SHUT_MIN", 10) +AUTO_ADD_NAME: Final[str] = os.getenv("AUTO_ADD_NAME", "autoBackupScript_one-shot") + + + + + + +def exceptionator(exception, message): + """Return an exception that can be custom-used""" + return exception(message) + + +def load_private_key(key_data: str, passphrase: str | None = None): + """Take in SSH key, determine type and return paramiko-compatible element""" + key_classes = [ + paramiko.Ed25519Key, + paramiko.ECDSAKey, + paramiko.RSAKey, + # paramiko.DSSKey, + ] + + for key_class in key_classes: + try: + return key_class.from_private_key( + io.StringIO(key_data), + password=passphrase or None, + ) + except (paramiko.SSHException, ValueError): + pass + + raise ValueError("Unsupported or invalid SSH private key") + + +def getCurrentTasks(allTasks: bool = False, limitOfTasks: int =50) -> dict: + """Get what tasks are running/ran on PBS + + Keyword arguments: + allTasks -- if tasks that are finished should be included. (default False) + 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}') + + taskList = stdout.read().decode() + + outTaskList = [] + for line in taskList.splitlines(): + line = line.strip() + + # Only process actual table rows + if not line.startswith("│"): + continue + + columns = [col.strip() for col in line.strip("│").split("│")] + + # Skip the header row + if columns[0] == "starttime": + continue + if len(columns) >= 2: + outTaskList.append(columns[2].split(":")[6]) + # exit() + + + + # print(outTaskList) + + outTaskDict = {} + + for task in outTaskList: + + match task: + case "garbage_collection": + outTaskDict["garbage_collection"] = outTaskDict.get("garbage_collection", 0) + 1 + case "verificationjob": + outTaskDict["verificationjob"] = outTaskDict.get("verificationjob", 0) + 1 + case "syncjob": + outTaskDict["syncjob"] = outTaskDict.get("syncjob", 0) + 1 + case "aptupdate": + outTaskDict["aptupdate"] = outTaskDict.get("aptupdate", 0) + 1 + case "reader": + outTaskDict["reader"] = outTaskDict.get("reader", 0) + 1 + case "logrotate": + outTaskDict["logrotate"] = outTaskDict.get("logrotate", 0) + 1 + case "termproxy": + outTaskDict["termproxy"] = outTaskDict.get("termproxy", 0) + 1 + case _: + outTaskDict["otherjob"] = outTaskDict.get("otherjob", 0) + 1 + + + return outTaskDict + + + + + + + + + + + + + + +if len(AUTO_ADD_NAME) > 32: + raise exceptionator(ValueError, "Maxmimum length of AUTO_ADD_NAME is 32") + sys.exit(1) + + + + + + + + + + +# dt = datetime.now.timestamp() + +# epoch_time = dt.timestamp() +# print(epoch_time) + +# 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) + +try: + # power on if needed + 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 +except exception as e: + print("Most likely wrong password, username or hostname has been passed for idrac.\nExiting\n") + sys.exit(1) +# else: +# continue + + + + + + + +PWR_DATE = datetime.now().timestamp() + +pinging = subprocess.run(["ping", "-c", "3", "-W", "5", HOST], capture_output=True).returncode + +if pinging: + print("not reachable") + + while datetime.now().timestamp() - PWR_DATE <= 1200: + pinging = subprocess.run(["ping", "-c", "3", "-W", "5", HOST], capture_output=True).returncode + + if not pinging: + print("reachable") + break + else: + raise exceptionator(TimeoutError, "Host didn't start after 20 minutes.\nCheck the server.") + sys.exit(1) + +else: + print("reachable") + + + + + + + + + +# Create object of SSHClient and +# connecting to SSH +ssh = paramiko.SSHClient() + +# Adding new host key to the local +# HostKeys object(in case of missing) +# AutoAddPolicy for missing host key to be set before connection setup. +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + +if not HOST_USR_SSHKEY: + + ssh.connect(hostname=HOST, port=HOST_SSH_PORT, username=HOST_USR, password=HOST_USR_PWD, timeout=3, ) + +else: + key = load_private_key( + HOST_USR_SSHKEY, + HOST_USR_SSHKEY_PASS + ) + + ssh.connect( + hostname=HOST, + port=HOST_SSH_PORT, + username=HOST_USR, + pkey=key, + timeout=3, + allow_agent=False, + look_for_keys=False, + + ) + + + + +# Execute command on SSH terminal +# using exec_command +# (stdin, stdout, stderr) = ssh.exec_command('hostname') + +# # redirecting all the output in cmd_output +# # variable +# cmd_output = stdout.read().decode() + +# print(cmd_output) + +(stdin, stdout, stderr) = ssh.exec_command('proxmox-backup-manager remote list') + +remoteList = stdout.read().decode() + +remoteDict = {} + +for line in remoteList.splitlines(): + line = line.strip() + + # Only process actual table rows + if not line.startswith("│"): + continue + + columns = [col.strip() for col in line.strip("│").split("│")] + + # Skip the header row + if columns[0] == "name": + continue + + if len(columns) >= 2: + name = columns[0] + host = columns[1] + + remoteDict[host] = name + + + +if REMOTE_PBS in remoteDict: + print(f"Found {REMOTE_PBS} as {remoteDict[REMOTE_PBS]}") +else: + print(f"\n!!!\nRemote {REMOTE_PBS} not found.\nAdd remote {REMOTE_PBS} manually.\n!!!\n", file=sys.stderr) + REMOTE_PBS.remove(REMOTE_PBS) + sys.exit(1) + + +(stdin, stdout, stderr) = ssh.exec_command('proxmox-backup-manager sync-job list --sync-direction all') + +syncJobList = stdout.read().decode() + + +for line in syncJobList.splitlines(): + line = line.strip() + + # Only process actual table rows + if not line.startswith("│"): + continue + + columns = [col.strip() for col in line.strip("│").split("│")] + + # Skip the header row + if columns[0] == "id": + continue + if len(columns) >= 2: + print("Sync job already on Proxmox Backup Server.\nSkipping sync job creation.") + # print(columns) + break +else: + + match SYNC_DIR: + case "push": + if PBS_RATE_OUT: + (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 push --rate-out {PBS_RATE_OUT} --comment 'automatic startupper script'") + 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 push --comment 'automatic startupper script'") + case "pull": + if PBS_RATE_IN: + (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 --rate-in {PBS_RATE_IN} --comment 'automatic startupper script'") + 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'") + case _: + raise exceptionator(SyncDirectionError, f"No such sync direction as {SYNC_DIR}.\n Either use 'push' or 'pull'\n") + + +# print(stdout.read().decode()) + + + + +(stdin, stdout, stderr) = ssh.exec_command(f"proxmox-backup-manager sync-job run {AUTO_ADD_NAME}") +# ssh.exec_command(f"proxmox-backup-manager sync-job run {AUTO_ADD_NAME}") +# print("here it runs the sync-job") +# print(stdout.read().decode()) +# print(stderr.read().decode()) + + + + +# Maybe add some checks here if the backup was properly scheduled + + + +# Get tasks +# print(getCurrentTasks(limitOfTasks=100)) + + + + +# check ongoing tasks +SYNC_JOB_DATE = datetime.now().timestamp() + +currenttasks = getCurrentTasks(limitOfTasks=PBS_TASK_LIMIT) + +while currenttasks: + + currenttasks = getCurrentTasks(limitOfTasks=PBS_TASK_LIMIT) + + print("Waiting 1 minute before checking tasks again") + time.sleep(60) + + if SYNC_JOB_DATE - datetime.now().timestamp() >= (PBS_MAX_RUNTIME_MIN * 60): + print(SYNC_JOB_DATE - datetime.now().timestamp()) + # 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 + if ( + currenttasks.get("aptupdate", 0) + or currenttasks.get("reader", 0) + or currenttasks.get("otherjob", 0) + ): + continue + # No need to do anything here. It's only to check if these keys exist + ... + break + + +(stdin, stdout, stderr) = ssh.exec_command(f"sudo shutdown") +print("Shuting down PBS") +# print("here it runs a shutdown now") +print(stdout.read().decode()) +print(stderr.read().decode(), file=sys.stderr) + +ssh.close() +# wait for PBS to safely shutdown +time.sleep(PBS_MAX_SHUT_MIN * 60) + + +if subprocess.run(["ping", "-c", "3", "-W", "5", HOST], capture_output=True).returncode: + print("PBS is powered off") + +else: + subprocess.run(["ipmitool", "-I", "lanplus", "-H", IDRAC, "-U", IDRAC_USR, "-P", IDRAC_PWD, "chassis", "power", "off"]) + +sys.exit(0) + + + + + + + +# ▒▒▒▓▓▓▒▒▒▒▓▒▒▒▒▓▓▒▒▓▓▒ ▒ ▒ ▒▒░▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ░░░ ████████ +# ▒▒▒▒▓▓▒▓▒▒▓▒▓▓▒▓▒▒▒▓▓ ▒ ▒░ ▒▒▒▒░▒▒▒▒ ░ ░ ██████ +# ▒▒▓░▒▓▓▓▓▒▒▓▓▒▒▓▓▒▓▒ ▒░ ▒░ ▒▒ ▒▒ ░░ ████ +# ▓ ▒▒▒▒▒▓▓▒▓▒▓▓▒░▒ ▒ ░░ ██ +# ▒▒▓▓▓▒░▒▒▓▒▒▒ ░░ █ +# ░ ▓▒▓ ▒ ░░ +# ░ +# ▒ ▒▓ ░ +# ░ ▓▒▒ ░▓▓▓▓▓ ▒▒ +# ▒▓▓▓░▓▓░▓▓▓▓▓▓ +# ▓▓▓▓▓▓░▓▒░░▒▒ ▒▒▓▓░░░▓▓▓▓▓░▓▓▓▓ +# ▓▓▓░▒▓░▓▓▒▒▒ ▒▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓ +# ░░░░░ ▓▓▓▓▓░▓▒▒▒▒▒▓▓▓▓▒ ▓▓▓▓▓▓░░░▒▓▓▓▓ +# ░░░░░░░░░░░░░░ ▓▓▓▓▓░▒▒ ▒▒▓▓▓▓▓▓▓▓░░▓▒▓▓▓▓▓▓▓▓▓ +# █ ░░░░░░░░░░░░░░░░ ▓▓▓▓▓▒▒ ▒▒▒░▒▓ ▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓ +# ███ ░░░░░░░░░░░░░░░░░ ▒░▒▒ ▒▓▓▓▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ +# ████ ░░░░░░░░░░▓▓▓▓▓▒░░▒▓▓▓▓░▓░▓▓▓▓▓▓▓▓░▒▒░░▓▓▓▓▓ +# ██████ ░▓▓▓▓▓▓▓▓▒▒░▓▓▓▓ ▓▓░▓▓▓▓▒░░▓▒▓▓▓▓▓▓▓░▒ +# ████████ ▓▓▓▓▒▓▓▓▓▓▓▓▒▒░▓▒░▒ ▓▓▒░░▒▓▓▓▓▓░▓▓▓▓▓▓▒░▒ ▒ ▓░▓▓▒ +# ███████████ ▓▓▓▒▓▓▓▓▓▓▓░▒▒▒ ▒▒ ▓▓▓░▓▓▓▓▓▓▓▓▒▓▓▓▓▓▒▒ ▒▓▓░▓▓ ▓▓▓▓▓▓▒▓▓▓▓▓░ +# ████████████ ▓▒ ▒▒▒▒▒ ▓▓▓▓▒▓▓▓▓▓▓▓░▓▒▓▓▒░▓░▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▒░░░▓▓▓▓░ +# ████████████▓ ▓▒ ▒ ▒ ▓▓▓▓░▓▓▓▓▓▓▓▓░▒▓▓▓▓▓░▒▓▓▓░░▒▓ ▒░▒▓▓▓▓▓▓░▓▓▓▓▓ +# ███████████ ▒▒▓▓▓░▓▓▓░▒▓▓▒▓▓▓▓░▒ ▒▒▓▓▓▓▓▒▒▒░▒▓▓▓▓▓▓▒▓▓▓▓ +# ████████▓ ▓▓▓▓▓▓▓▓░ ▒▒▓▓▒▓▓░▓▓▓▓▓▓▓░▓▓▓▓ +# ██████ ▓▓▓▓▓▓▓▓▓▒░░ +# ████ ▓░ ░▓░░░▓▓▓░▓▓▓ +# ██ ░░░░░░░░░░░░░░░░░ ░▓▓▓▓▓▓▓▒▓▓▓ +# ▓ ░░░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ░░░░░░░░░ ▓▓▓▓▓▓▓▓▓░▓▓ +# ░░ ░ ▓▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ░ ▓▓░▓▓▓▒░░▒░▓▓ +# ░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒░ ░░ ▓▒ ▓▓▓▓▓▓▓▓▓▒▓ +# ▓████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ░░░ ░░░ ▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓ ▓ +# ███████ ▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓░ ░░░░░ ░░░░░ ░ ▒▒▒▒▒▒▒▒▒▒▒▒▓ ▒▒▒▒▒▒▓ +# ███████ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▓▒▓▒▒▒▒▒▒▒ ░ +# ███████ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒▒▒▓ ░▒▒▒▒▒▒▒ +# ██████ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▓▒▒▒▓▓▓▓▓▓▓▓▓▒▒▒▓▒░░ ▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▓▒▒▒▓▒▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓ ░▓▒▒▒▓▒▒ +# ██████ ▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓ █▒░▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓ ▒ ▒▒▓▒▒ +# ██████ ▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ██▒▓▓▓▓▒▓▓██▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▒▓▓▓░▓▓▓▓▓▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░▒ ░ ▒▓▓▒ +# █████ ▒▒▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓████ ▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓ ▒▓▓▓▓ +# █████ ▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓█████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓ ▓▓▓ +# █████ ▓▓▓▓▓▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓█▓▓▓▓▓▓▓▓▓▓█▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓ ▓▓▓ +# █████ ▓▓▓▓▓▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓█▓▓▓▓▓▓▓██████ ▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▒▓▓▓▓▓▓█▓▓▒▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▒▓ ▓▓▓ +# ████▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓███████ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓ +# ████▓▓▓▓▓▓▓▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░███████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓ +# █████▓▓▓▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓ ▒▓▓▓▓▓▓▓▓ ████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒░▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓ +# █████▓▓▓▓▒▓▓▒▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓ ▒ ▓▓▓▓▓▓▓▒█████████▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓ ░ ▓ +# █████▓▓▓▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▒█▓ ▓▓▓▒▓▓▓▓▓█▓▓▓█▓██ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▒██▓▓▓▓▓██▒▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓ ▒▓ +# █████▓▓▓▓▒▓▓▒▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓ ▓ ▒▓█████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓████▒▓▓▓████ ▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓ +# █████ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ ▓ ▒▒▓▒███▓ ░ ███ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓ ████░▓▓██▓█▓ ▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓ ░▓ +# ██ ██ ▓▓▓▓▒▓▓▓▓▓▓▓▒▓▓▓▓▓▓ ▒▒ ░▓█▓▓▓████▒▓▓▓ ▒▓▓▓▓▓▒ █ █████▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓▓▓▓▓▓▓█▓ ▓▓▓▓█▓█▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▒▓ +# ▓ ███▓▓▓ ░▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▒▓▓▓██░█████▓▓▓▓▓▓▒▒▒▒▒▓▓▓▓▓▓█████▓▓ ▓▓▓▓▓▓▓▓▓██ █▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓ ▓ ▓ +# █▒ ██▓▓ ██ ▓▒▓▓▓▓▓▓░▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████▓█▒▒▓▓ ▓▓ █▒█ ▒▓▓▒███▒▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓ ▒▓▓ +# ███ ██ ▓░▓██▓▒▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████▓█▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓ ▓ ░▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓ ▓ ▒█ +# ███ ███▓▓▓██▒▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ██████▓▓ ░░░▓▓▓▓▒▓▒▓▓▓▓▓▓▒▓ ▓ ▒ ▒ +# ███████ ▓████▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓ ▓ ░ ▒ +# ██ ████▒█▓██▓▒▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓█▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▓▓▓▓▓▓▒▓ ▓▓ ▒ ▒ +# ██ ██████▒░▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓ ▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓██▓ ▒ +# ██▓▓ ████████▓▓▓▓▒▓▓▓▓▒▓▓▓▓▓▓▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓█▒ +# ██ ▒██████▒▓█░▓▓▓▓▒▒▒▒▒▓▓▓▓▓▓▓▓▒▒ ░ ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒ +# ████░█▒▒███████ ▓▓▓ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓██▓██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ +# ██▒▒▒▒▒███████ ▓▒▓▓ ▓▓▓▓▓▓█▓▓▓▓▓▓▓ ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▓▓▓▓▓▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▓▓▓▒▓▓▓▓▓ ▒ +# ████░░▓█████ ▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓█████▓████████░▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▒▓▓▓▓▓ ▓ ▒ ▓▓ +# ███████████ ▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒ ██████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓░███▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ ░▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▒ ▒▓▓ +# ██████████ ▓▒▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓░░░▒▒▒▒█████████████▓▓▓▓▓▓▓▓▓▓▓▓▓█████████▓▓▓▓▓▓ ░░░ ░▓▓▓▓▓▓▓▒▒▓▓▓▓ ▓█▒▓░ ▒ ▓▓▒▒░▓▓ +# ███████▒▓▓█ ▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░▒▒▒▒▒ ███████████▓▓▓▓▓▓▓▓▓▒██████████████ ░▒ ░░░░ ▓▓▓▓▓▓▓▓▒▓▓▓ ▓ ░ ▒▒▓▓▓▓▓▒▒ ▓ █ +# ███▓▒ ░▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓░░ ▒▒▒▒█▒▒ ████████████████████████████ ▒▒▒▒▒▒ ░░░░▓▓ ▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▒▒▓ ▓ █ +# ▒ ▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▒▒▒▒▒ ████████████████████▒▒▒▒█ ▒▒▒▒▒ █ ▓▓ ▒▒▓▓▓▓▓▓▓ ▒ ▒ ▓▓▓▒▒▒▒▓ █ +# ████████████▓ ██ ▓▓▓▓▓▓ ▒▒▒▒▒▒ ▒▓ ▒▒ ▒▒ ██████████ ▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▓▓ ▒▒▒▒▓▓▓▓▓▒░▒▓ ▓░▓█ ▒░▓▓▓▒▒▓▓▓░▓ █ +# ▒ ▒▓▓▓▒ ███████████████████ ▒▓▓▓▓▓▓▓▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒░ ░░░░░░▒ ▒▓▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▓▒▒░▒░▒▓▒▓░ ▓ ▓▓▓▓▒▓▓▓▓ ▓▓█ +# █▓ ▒▓█▓▓███ ██████▓█████░░▓█████ ░░ ▒▓▓▓▓▓▓▒ ▒▒▒▒▒▒▒▒ ▒ ▒▒▒▒▒▒░░░░░░░░░▒▒░▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒ ▒░▒░▒ ░ ▓ ▒▒▓▒▓▒▓▓▓▓▓ █▓█ +# ██▓▒██░▓██ █████▒▒░▒░██░░░▒█████ ▒▒▒▒▒ ▒▓▓▓▓▓ ▒▒░▒▒ ▒▒▒▒▒▒▒ ▒▒░░░░░░░▒ ▒ ▒▒▒▒░░░ ▒ ▒▒▒▒▒▒▓▓▓▓░░░░░▒ ▓▒▒▓▓▓▒▓▓ █▓░ +# ██░▒█▓ ▒ ██████░░░░▒▒░░░▓█████ ▓▓▓▓▒ ▒ ░░░░░░▒ ▒▒▒▒▒▒░░▒▒ ░░░░░░░▒▒ ░▒▒▒▒▒░░ ▒▒▒▒▓▓▓▓▓▓░░░ ▒ ▓▓▓▓▓▓▒░▓ ▓▓ +# ▓▓ ░ ███████░░░░░░░░██████ ▓▓▓▓▓▓ ░░░░░░░▒ ░░░░░░░░ ▒ ░░░▒▒▒▒░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓ ▒ ▒▓▓▓▓▓▓▒▓▓ ▓ +# ░ ████▒░▒█▒░░░░░▒█░▒▒██ ▓▓▓▓▓▓▓ ░░░░░░░ ░░░░░░░░░░ ▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ █ █▒▒▒▒▒▓▓▒▓▓▓▓▓▓▒▓▓ ▓ +# ██░░░░▒███░░░░██▒▓██ ▓▓▓▓▓▓▓▓▓ ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ █▒▒ ▓▓▓▒▓▓▓▓▓▓▓▓▓▓ ▓ +# ███▒▒█████████████▓ ▓▓▓▓▓▓▓▓▓▓ ░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ ▓ █▒▒▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓ ▓ +# ████████████████ ▒▓▓▓▓▓▓▓▓▓▓▓ ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▒ █ ▒▒▒▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓ ▓ +# ███████████ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ +# █▓ ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓ ▓ ░ ▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓ +# ▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓ ▓ ▓▓ ▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +# █▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ░ ▒▓ ▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +# ███████ ▓██▓▒▒▒ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░ ▒▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓ +# ███████▓ ▒███▒▒▒▒ ▓▓▓░▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░░ ▒▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓ +# ███████████▓ ▒▓▓▓▓▓▓▓▓▓░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓ ░░░ ░▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓ +# ████████▒ ▓▒▓▓▓▓▓▓▓▓▓▓ ░ ▓▓▓▓▓▓▓▓▓▓▒ ▒▓▒▒ ░░ ▓▓▓▒▒▒▒▓▓▒▒▓▓▓▓▓▓▒▓ +# █████▒ ▒░ ▒░▓▓▓▓▓▒▓▓▓▓ ░░ ▒▓▓▓▓▓▓ ▓▓▓▓▒▓▒▓▓▓▒▒▓▒▓▓▓▓ +# ██▓ ▒▒▒▒░ ▓▓▓▓▓▓▓▓▓▓ ░ ░▓▓▓ ░ ▓ ▓ ▒▓▓▓▓▒▓▓▓▓▓▓▓ +# ▒ ▒▒▒▒▒▒░ ▓▒ ▓▓▓▓▓▓▓▓▒▓▒ ░░░░░ ▓▓ ▓ ▒░ ▒▓▓▓▓▓▓▓▓ +# ░▒▒▒▒▒▒░ ▓▓ ▓▓▓▓▓▓▓▓▓▒ ░░░ ░ ▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▓▓▓▓▒ +# ▒▒▒▓█▒▒▒░ ▓ ▓▒▓▓▓▓▓▓▓▓▓▒▓░░ ▒ ▒▓ ▓ ▒▒▒▒▒░▒▓▓▓▓▒ +# ▒▓██▒▒▒▒ ▓▓▓▓▓▓▓▓▓▒▓▓ ▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓ +# ██▓▒▒▒▒ ░▒▓▓▓▓▓▓ ▓▓▓▓▓▒▒▒ ▓▒ +# █▓▒▒▒▒░ ▓▓▓▓▓ ▓▓▓▓▓▒▒▒ ▒▓▓▒ +# ▓▒▒▒▒░ ▓▓ ▒▒▒▒▒▒▒░ ▒▒▓ +# ▒▒▒▒▒ ░▓▓ +# ▒▒▒▒▒ ▓▓ +# ▒▒▒▒▒ ▓▓ +# ▒▒▒▒▒ ▓▓ +# ▒▒▒▒▒ ▒▓ +# ▒▒▒▒▒▒ ▓▓ +# ▒▒▒▒▒▒ ▒ ▓▓ +# ▒▒▒▒▒▒ ▒▓▓ +# ▒▒▒▒▒▒░ ▒ ▓▓▓ +# ▒▒▒▒▒▒▒ ▒▓▓▓ +# ▓▒▒▒▒▒▓▓ ░▓▓▓▒ +# ▓▒▒▒▒▓▓▓▒ ▒▓▓▒▒▓ diff --git a/manifest.yaml b/manifest.yaml new file mode 100644 index 0000000..8284f92 --- /dev/null +++ b/manifest.yaml @@ -0,0 +1,112 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pbsupper-deployment-1-secretmap + namespace: CHANGE_ME + labels: + server: Server-1 + idrac: v7 +type: Opaque +data: + IDRAC_PWD: c29tZXRoaW5nQnV0SW5CYXNlNjQ= + HOST_USR_PWD: c29tZXRoaW5nQnV0SW5CYXNlNjQ= + HOST_USR_SSHKEY_PASS: c29tZXRoaW5nQnV0SW5CYXNlNjQ= + + # Must be base64 of the ENTIRE private key, as one base64 value. + # base64 -w 0 id_ed25519 + HOST_USR_SSHKEY: c29tZXRoaW5nQnV0SW5CYXNlNjQ= + + +# Alternatively you can also use this option instead of PRIVATE_KEY: 123... + +stringData: + PRIVATE_KEY: | + -----BEGIN OPENSSH PRIVATE KEY----- + 123... + -----END OPENSSH PRIVATE KEY----- +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: pbsupper-deployment-1-configmap + namespace: CHANGE_ME + labels: + server: Server-1 + idrac: v7 +data: + IDRAC: somethingSomething + IDRAC_USR: somethingSomething + HOST: somethingSomething + HOST_SSH_PORT: somethingSomething + HOST_USR: somethingSomething + LOCAL_DATASTORE: somethingSomething + REMOTE_DATASTORE: somethingSomething + REMOTE_PBS: somethingSomething + SYNC_DIR: somethingSomething + PBS_RATE_OUT: somethingSomething + PBS_RATE_IN: somethingSomething + PBS_TASK_LIMIT: somethingSomething + PBS_MAX_RUNTIME_MIN: somethingSomething + PBS_MAX_SHUT_MIN: somethingSomething + AUTO_ADD_NAME : somethingSomething + +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: pbsupper + namespace: CHANGE_ME + labels: + server: Server-1 + idrac: v7 + +spec: + schedule: "*/15 * * * *" + timeZone: "Europe/Stockholm" + concurrencyPolicy: Forbid + + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + + jobTemplate: + spec: + backoffLimit: 2 + + template: + spec: + restartPolicy: Never + automountServiceAccountToken: false + + securityContext: + appArmorProfile: + type: RuntimeDefault + + containers: + - name: pbsupper + image: tea.shupogaki.org/yuruc3/pbsupper:v0 + imagePullPolicy: IfNotPresent + + envFrom: + - secretRef: + name: pbsupper-deployment-1-secretmap + - configMapRef: + name: pbsupper-deployment-1-configmap + + securityContext: + runAsUser: 1600 + runAsGroup: 1600 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 100m + memory: 64Mi \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..80db7d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +paramiko==5.0.0 \ No newline at end of file