Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b843113b6 | |||
| d2965d4439 |
@@ -1,32 +0,0 @@
|
||||
# Edit this file to introduce tasks to be run by cron.
|
||||
#
|
||||
# Each task to run has to be defined through a single line
|
||||
# indicating with different fields when the task will be run
|
||||
# and what command to run for the task
|
||||
#
|
||||
# To define the time you can provide concrete values for
|
||||
# minute (m), hour (h), day of month (dom), month (mon),
|
||||
# and day of week (dow) or use '*' in these fields (for 'any').
|
||||
#
|
||||
# Notice that tasks will be started based on the cron's system
|
||||
# daemon's notion of time and timezones.
|
||||
#
|
||||
# Output of the crontab jobs (including errors) is sent through
|
||||
# email to the user the crontab file belongs to (unless redirected).
|
||||
#
|
||||
# For example, you can run a backup of all your user accounts
|
||||
# at 5 a.m every week with:
|
||||
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
|
||||
#
|
||||
# For more information see the manual pages of crontab(5) and cron(8)
|
||||
#
|
||||
# m h dom mon dow command
|
||||
30 0 * * * /cron_jobs/power/ipmi_poweroff.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
30 0 * * * /cron_jobs/power/ipmi_poweroff.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
0 1 * * * /cron_jobs/power/ilo_poweroff.py -H 192.168.129.242 -a ForceOff --no-serverinfo >> /var/log/cron_scripts.log 2>&1
|
||||
0 5 * * * /cron_jobs/system/ilo_poweron.py -H 192.168.129.242 -a On --no-serverinfo >> /var/log/cron_scripts.log 2>&1
|
||||
10 5 * * * /cron_jobs/power/ipmi_poweron.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
10 5 * * * /cron_jobs/power/ipmi_poweron.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
35 0 * * * /cron_jobs/system/update_system.py >> /var/log/cron_scripts.log 2>&1
|
||||
*/3 * * * * /cron_jobs/fancontrol/set_fanspeed.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
*/3 * * * * /cron_jobs/fancontrol/set_fanspeed.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import datetime
|
||||
import argparse
|
||||
|
||||
# IPMI settings
|
||||
IPMIUSER = "<username>"
|
||||
IPMIPW = "<password>"
|
||||
IPMIEK = "0000000000000000000000000000000000000000"
|
||||
|
||||
# Fan / temperature settings
|
||||
FANSPEED = 20 # percent
|
||||
MAXTEMP = 37 # Celsius
|
||||
|
||||
def run_ipmi(args):
|
||||
base = ["ipmitool", "-I", "lanplus", "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK]
|
||||
cmd = base + args
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError(f"ipmitool failed: {' '.join(cmd)}\n{p.stderr.strip()}")
|
||||
return p.stdout
|
||||
|
||||
def log_systemd(tag, msg):
|
||||
try:
|
||||
subprocess.run(["systemd-cat", "-t", tag], input=msg + "\n", text=True, check=True)
|
||||
except Exception:
|
||||
# fallback to stdout
|
||||
ts_print(msg)
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def get_ambient_temp():
|
||||
out = run_ipmi(["sdr", "type", "temperature"])
|
||||
# find lines like "Exhuat Temp | 29 degrees C"
|
||||
lines = [l for l in out.splitlines() if "Exhaust" in l and "degrees" in l]
|
||||
if not lines:
|
||||
raise RuntimeError("Exhaust temperature line not found in ipmitool output")
|
||||
# Extract last number found in the last matching line (like the original)
|
||||
field = lines[-1].split("|")[4]
|
||||
#ts_print("Debug: field =", field)
|
||||
m = re.search(r"(\d{1,3})", field)
|
||||
if not m:
|
||||
raise RuntimeError("Could not parse temperature from line: " + lines[-1])
|
||||
return int(m.group(1))
|
||||
|
||||
def main():
|
||||
# parse CLI args (IPMI host can be overridden with -H/--host)
|
||||
parser = argparse.ArgumentParser(description="Set fan speed via IPMI")
|
||||
parser.add_argument("-H", "--host", default="127.0.0.1", help="IPMI host/IP (default: %(default)s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
global IPMIHOST
|
||||
# set global IPMIHOST so run_ipmi() uses the provided host
|
||||
IPMIHOST = args.host
|
||||
|
||||
ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}")
|
||||
try:
|
||||
temp = get_ambient_temp()
|
||||
ts_print(f"{IPMIHOST}: Exhaust temperature: {temp} C")
|
||||
except Exception as e:
|
||||
ts_print(f"{IPMIHOST}: Error reading temperature: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
speed_hex = format(FANSPEED, "x")
|
||||
|
||||
if temp > MAXTEMP:
|
||||
msg = f"{IPMIHOST}:Warning: Temperature is too high! Activating dynamic fan control! ({temp} C)"
|
||||
log_systemd("R710-IPMI-TEMP", msg)
|
||||
ts_print(msg)
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
|
||||
else:
|
||||
okmsg = f"{IPMIHOST}: Temperature is OK ({temp} C)"
|
||||
log_systemd("R710-IPMI-TEMP", okmsg)
|
||||
ts_print(okmsg)
|
||||
ts_print(f"{IPMIHOST}: Activating manual fan speeds! (requested %d%%)" % FANSPEED)
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x02", "0xff", "0x" + speed_hex])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import getpass
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def run(cmd):
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Run iLO REST commands (login, serverinfo, reboot)")
|
||||
p.add_argument("-H", "--host", required=True, help="iLO host/IP")
|
||||
p.add_argument("-a", "--action", default="ForceOff", choices=["ForceOff", "GracefulRestart", "Reset"],
|
||||
help="reboot action to run (default: ForceOff)")
|
||||
p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo")
|
||||
args = p.parse_args()
|
||||
|
||||
# login
|
||||
ts_print(f"Sending {args.action} to {args.host}")
|
||||
run(['ilorest', 'login', args.host, '-u', '<username>', '-p', '<password>'])
|
||||
# optional serverinfo
|
||||
if not args.no_serverinfo:
|
||||
run(['ilorest', 'serverinfo'])
|
||||
# power off / reboot action
|
||||
run(['ilorest', 'reboot', args.action])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import getpass
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def run(cmd):
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Login to iLO, show serverinfo and power on")
|
||||
p.add_argument("-H", "--host", required=True, help="iLO host/IP")
|
||||
p.add_argument("-a", "--action", default="On", choices=["On","Off","GracefulRestart","ForceOff","Reset"],
|
||||
help="power/reboot action to run (default: On)")
|
||||
p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo")
|
||||
args = p.parse_args()
|
||||
|
||||
ts_print(f"Sending {args.action} to {args.host}")
|
||||
run(['ilorest', 'login', args.host, '-u', '<username>', '-p', '<password>'])
|
||||
if not args.no_serverinfo:
|
||||
run(['ilorest', 'serverinfo'])
|
||||
run(['ilorest', 'reboot', args.action])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
IPMIUSER = "<username>"
|
||||
IPMIPW = "<password>"
|
||||
IPMIEK = "0000000000000000000000000000000000000000"
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
args = p.parse_args()
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", IPMIUSER,
|
||||
"-P", IPMIPW,
|
||||
"-y", IPMIEK,
|
||||
"power", "off",
|
||||
]
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
IPMIUSER = "<username>"
|
||||
IPMIPW = "<password>"
|
||||
IPMIEK = "0000000000000000000000000000000000000000"
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
args = p.parse_args()
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", IPMIUSER,
|
||||
"-P", IPMIPW,
|
||||
"-y", IPMIEK,
|
||||
"power", "on",
|
||||
]
|
||||
print(f"Sending power on to {args.host}")
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
import subprocess
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def generate_timestamp(fmt: Optional[str] = None, utc: bool = False) -> str:
|
||||
"""Return a timestamp string.
|
||||
|
||||
- If `fmt` is provided, return `datetime.strftime(fmt)`.
|
||||
- If `fmt` is None, return an ISO 8601 timestamp. When `utc` is True
|
||||
the timestamp ends with 'Z' (UTC). When `utc` is False it returns
|
||||
the local time ISO format.
|
||||
|
||||
Examples:
|
||||
- `generate_timestamp()` -> '2025-11-20T12:34:56Z'
|
||||
- `generate_timestamp("%Y-%m-%d %H:%M:%S")` -> '2025-11-20 12:34:56'
|
||||
"""
|
||||
if fmt:
|
||||
dt = datetime.now(timezone.utc) if utc else datetime.now()
|
||||
return dt.strftime(fmt)
|
||||
|
||||
dt = datetime.now(timezone.utc) if utc else datetime.now()
|
||||
if utc:
|
||||
# force trailing Z for UTC instead of +00:00
|
||||
return dt.replace(tzinfo=timezone.utc).isoformat().replace('+00:00', 'Z')
|
||||
return dt.isoformat()
|
||||
|
||||
print(f"Starting system update at {generate_timestamp()}")
|
||||
# Update package lists
|
||||
print(f"{generate_timestamp()}: Updating package lists...")
|
||||
subprocess.run(['sudo', 'apt', 'update'], check=True)
|
||||
# Upgrade packages
|
||||
print(f"{generate_timestamp()}: Upgrading packages...")
|
||||
subprocess.run(['sudo', 'apt', 'upgrade', '-y'], check=True)
|
||||
|
||||
# Check if reboot is required
|
||||
if os.path.exists('/var/run/reboot-required'):
|
||||
print(f"{generate_timestamp()}: Reboot required. Rebooting now...")
|
||||
subprocess.run(['sudo', 'reboot'])
|
||||
else:
|
||||
print(f"{generate_timestamp()}: No reboot required.")
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# IPMI IP address
|
||||
IPMIHOST=<hostname>
|
||||
# IPMI Username
|
||||
IPMIUSER=<username
|
||||
# IPMI Password
|
||||
IPMIPW=<password>
|
||||
# Your IPMI Encryption Key
|
||||
IPMIEK=0<IPMI Encryption Key>
|
||||
# Fan Speed / utilisation in percentage, for example 9 for 9% utilisation
|
||||
# Please note that each fan can have a different rpm and will not all be the same speed
|
||||
FANSPEED=10
|
||||
|
||||
# TEMPERATURE
|
||||
# Change this to the temperature in Celsius you are comfortable with.
|
||||
# If the temperature goes above the set degrees it will send raw IPMI command to enable dynamic fan control
|
||||
MAXTEMP=30
|
||||
|
||||
# This variable sends a IPMI command to get the temperature, and outputs it as two digits.
|
||||
# Do not edit unless you know what you do.
|
||||
# Side note, if you are running ipmitool on the system you are controlling, you don't need to specify -H,-U,-P - from the OS installed on the host, ipmitool is assumed permitted. You only need host/user/pass for remote access.
|
||||
TEMP=$(ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK sdr type temperature |grep Ambient |grep degrees |grep -Po '\d{2}' | tail -1)
|
||||
|
||||
# Dont edit this, this converts decimal to hex
|
||||
SPEEDHEX=$( printf "%x" $FANSPEED )
|
||||
|
||||
if [[ $TEMP > $MAXTEMP ]];
|
||||
then
|
||||
printf "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP
|
||||
echo "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)"
|
||||
# This sets the fans to auto mode, so the motherboard will set it to a speed that it will need do cool the server down
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x01
|
||||
else
|
||||
printf "Temperature is OK ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP
|
||||
printf "Activating manual fan speeds! (1560 RPM)"
|
||||
# This sets the fans to manual mode
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x00
|
||||
# This is where we set the slower, quite speed
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x02 0xff 0x$SPEEDHEX
|
||||
fi
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power off
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power on
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Client Type: $1"
|
||||
echo "Client IDs: $2"
|
||||
client_string=$2
|
||||
client_list=()
|
||||
|
||||
IFS=', ' read -a array <<< "${client_string}"; echo "${array[@]}"
|
||||
|
||||
for index in "${!array[@]}"
|
||||
do
|
||||
echo "Starting container: ${array[index]}"
|
||||
lxc-start ${array[index]}
|
||||
done
|
||||
@@ -12,11 +12,11 @@ def get_time():
|
||||
|
||||
def client_shutdown(client_type, clientid):
|
||||
lxc_filepath = "/usr/bin/lxc-stop"
|
||||
qemu_filepath = "/usr/sbin/qm"
|
||||
qemu_filepath = f"/usr/sbin/qm shutdown {clientid}"
|
||||
try:
|
||||
print(f'Stopping {clientid}')
|
||||
if client_type == 'vm':
|
||||
process_id = os.spawnv(os.P_NOWAIT, qemu_filepath, ['shutdown', clientid])
|
||||
process_id = os.spawnv(os.P_NOWAIT, qemu_filepath, ['--skip-lock', 'true'])
|
||||
elif client_type == 'lxc':
|
||||
process_id = os.spawnv(os.P_NOWAIT, lxc_filepath, [client_type, clientid])
|
||||
print(process_id)
|
||||
@@ -37,6 +37,7 @@ def main():
|
||||
print(f'Start time: {timestamp}')
|
||||
for clientid in client_list:
|
||||
client_shutdown(client_type, clientid)
|
||||
sleep(1)
|
||||
else:
|
||||
print(f'Successfully stopped client list.')
|
||||
|
||||
@@ -47,4 +48,4 @@ def main():
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# sys.exit(0)
|
||||
# sys.exit(1)
|
||||
# sys.exit(1)
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/python3
|
||||
"""
|
||||
Parallel rsync launcher.
|
||||
|
||||
Requires rsync on PATH (e.g. via MSYS/Cygwin on Windows).
|
||||
Runs multiple rsync invocations in parallel (one per file/entry).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
#DEFAULT_RSYNC_ARGS = ["-av", "--partial", "--compress", "--info=progress2"]
|
||||
DEFAULT_RSYNC_ARGS = ["-avP", "--partial"]
|
||||
|
||||
|
||||
def build_file_list(src: str) -> List[Path]:
|
||||
p = Path(src)
|
||||
if p.is_dir():
|
||||
# sync each top-level entry separately (files and immediate subdirs)
|
||||
return [x for x in p.iterdir()]
|
||||
# allow glob patterns
|
||||
if any(ch in src for ch in ["*", "?"]):
|
||||
return list(Path(".").glob(src))
|
||||
# single file
|
||||
return [p]
|
||||
|
||||
|
||||
def run_rsync(item: Path, dest: str, rsync_args: List[str], dry_run: bool) -> (Path, int, str, str):
|
||||
cmd = ["rsync", *rsync_args]
|
||||
if dry_run:
|
||||
cmd.append("--dry-run")
|
||||
# preserve relative path by sending the item name, not its parent
|
||||
cmd += [str(item), dest]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return item, proc.returncode, proc.stdout, proc.stderr
|
||||
except FileNotFoundError:
|
||||
return item, 127, "", "rsync not found on PATH"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Transfer files using rsync in parallel.")
|
||||
ap.add_argument("src", help="Source file, directory or glob pattern")
|
||||
ap.add_argument("dest", help="Rsync destination (local path or user@host:/path)")
|
||||
ap.add_argument("-w", "--workers", type=int, default=4, help="Parallel rsync workers")
|
||||
ap.add_argument("-r", "--rsync-args", default=" ".join(DEFAULT_RSYNC_ARGS),
|
||||
help="Additional rsync args (quoted). Default: -a --partial --compress --progress")
|
||||
ap.add_argument("--dry-run", action="store_true", help="Don't transfer, just show what would happen")
|
||||
args = ap.parse_args()
|
||||
|
||||
items = build_file_list(args.src)
|
||||
if not items:
|
||||
print("No files found for:", args.src, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
rsync_args = args.rsync_args.split()
|
||||
results = []
|
||||
print(f"Starting {len(items)} rsync tasks with {args.workers} workers...")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.workers) as ex:
|
||||
futures = {ex.submit(run_rsync, item, args.dest, rsync_args, args.dry_run): item for item in items}
|
||||
for fut in as_completed(futures):
|
||||
item, code, out, err = fut.result()
|
||||
results.append((item, code, out, err))
|
||||
status = "OK" if code == 0 else f"ERR({code})"
|
||||
print(f"[{status}] {item}")
|
||||
|
||||
# summary
|
||||
failed = [r for r in results if r[1] != 0]
|
||||
print(f"\nCompleted: {len(results)} tasks, {len(failed)} failed.")
|
||||
if failed:
|
||||
print("Failures detail:")
|
||||
for item, code, out, err in failed:
|
||||
print(f"--- {item} (exit {code}) ---")
|
||||
if out:
|
||||
print(out.strip())
|
||||
if err:
|
||||
print(err.strip())
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user