Compare commits

2 Commits

56 changed files with 63 additions and 10042 deletions
-26
View File
@@ -1,26 +0,0 @@
# PyInstaller
build/
dist/
*.spec
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info/
.eggs/
# Virtual environments
.venv/
venv/
env/
# Credentials (never commit these)
credentials
*.credentials
# OS
.DS_Store
Thumbs.db
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent
CRON_SCRIPTS = REPO_ROOT / "personal" / "cron_scripts"
BUILD_SCRIPTS = [
CRON_SCRIPTS / "build_multi_powercycle.py",
CRON_SCRIPTS / "build_set_fanspeed.py",
CRON_SCRIPTS / "build_ilo_status.py",
CRON_SCRIPTS / "build_ilo_powercycle.py",
CRON_SCRIPTS / "build_ipmi_poweron.py",
CRON_SCRIPTS / "build_ipmi_poweroff.py",
]
def main():
failures = []
for script in BUILD_SCRIPTS:
print(f"\n{'='*60}")
print(f"Building: {script.name}")
print(f"{'='*60}")
result = subprocess.run([sys.executable, str(script)], cwd=REPO_ROOT)
if result.returncode != 0:
failures.append(script.name)
print(f"\n{'='*60}")
if failures:
print(f"FAILED builds ({len(failures)}/{len(BUILD_SCRIPTS)}):")
for name in failures:
print(f" - {name}")
raise SystemExit(1)
else:
print(f"All {len(BUILD_SCRIPTS)} builds completed successfully.")
print(f"Executables are in: {REPO_ROOT / 'dist'}")
if __name__ == "__main__":
main()
-13
View File
@@ -1,13 +0,0 @@
[credentials]
username=<username>
password=<password>
domain=<domain>
[ipmi_credentials]
username=<username>
password=<password>
IPMIEK=0000000000000000000000000000000000000000
[ilo_credentials]
username=<username>
password=<password>
-6
View File
@@ -1,6 +0,0 @@
[ipmi]
#list of IPMI hosts to power cycle
[ilo]
#list of iLO hosts to power cycle
-149
View File
@@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
set_password.py — Encrypt and store a password in any section of credentials.ini
Usage:
python set_password.py # interactive prompts
python set_password.py -s ipmi_credentials # pre-select section
python set_password.py --credentials /path/to/credentials.ini
python set_password.py --keyfile /path/to/secret.key
The first run generates a secret.key file next to credentials.ini.
Keep that key file secure (chmod 600); without it, passwords cannot be decrypted.
Decrypting at runtime (in other scripts):
from cryptography.fernet import Fernet
key = open("secret.key", "rb").read()
fernet = Fernet(key)
plaintext = fernet.decrypt(encrypted_value.encode()).decode()
"""
import argparse
import configparser
import getpass
import os
import sys
try:
from cryptography.fernet import Fernet
except ImportError:
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_CREDENTIALS = os.path.join(SCRIPT_DIR, "credentials.ini")
DEFAULT_KEYFILE = os.path.join(SCRIPT_DIR, "secret.key")
def load_or_create_key(keyfile: str) -> bytes:
"""Load an existing Fernet key or generate and save a new one."""
if os.path.exists(keyfile):
with open(keyfile, "rb") as fh:
return fh.read()
key = Fernet.generate_key()
with open(keyfile, "wb") as fh:
fh.write(key)
# Restrict permissions on Unix-like systems
try:
os.chmod(keyfile, 0o600)
except OSError:
pass
print(f"New encryption key created: {keyfile}")
print("Keep this file secure — it is required to decrypt passwords.")
return key
def encrypt_password(plaintext: str, fernet: Fernet) -> str:
return fernet.encrypt(plaintext.encode()).decode()
def main():
parser = argparse.ArgumentParser(
description="Encrypt and store a password in credentials.ini"
)
parser.add_argument(
"--credentials",
default=DEFAULT_CREDENTIALS,
help=f"Path to credentials file (default: {DEFAULT_CREDENTIALS})",
)
parser.add_argument(
"--keyfile",
default=DEFAULT_KEYFILE,
help=f"Path to encryption key file (default: {DEFAULT_KEYFILE})",
)
parser.add_argument(
"-s",
"--section",
help="Section to update (skips interactive section prompt)",
)
args = parser.parse_args()
config = configparser.ConfigParser()
if not os.path.exists(args.credentials):
print(f"Credentials file not found. Creating: {args.credentials}")
os.makedirs(os.path.dirname(os.path.abspath(args.credentials)), exist_ok=True)
# Seed with the standard sections and placeholder values
config["credentials"] = {"username": "", "password": "", "domain": ""}
config["ipmi_credentials"] = {
"username": "",
"password": "",
"IPMIEK": "0000000000000000000000000000000000000000",
}
config["ilo_credentials"] = {"username": "", "password": ""}
with open(args.credentials, "w") as fh:
config.write(fh)
try:
os.chmod(args.credentials, 0o600)
except OSError:
pass
print(f"Created {args.credentials} — fill in usernames and re-run to set passwords.")
else:
config.read(args.credentials)
sections = config.sections()
if not sections:
sys.exit(f"Error: no sections found in {args.credentials}")
# Resolve section
if args.section:
section = args.section
if section not in sections:
sys.exit(
f"Error: section [{section}] not found in {args.credentials}\n"
f"Available sections: {', '.join(sections)}"
)
else:
print("Available sections:")
for i, s in enumerate(sections, 1):
print(f" {i}. {s}")
choice = input("Select section number: ").strip()
try:
section = sections[int(choice) - 1]
except (ValueError, IndexError):
sys.exit("Error: invalid selection")
# Prompt for new password (hidden input, confirmed)
while True:
password = getpass.getpass(f"New password for [{section}]: ")
confirm = getpass.getpass("Confirm password: ")
if password == confirm:
break
print("Passwords do not match. Try again.")
# Load/create key and encrypt
key = load_or_create_key(args.keyfile)
fernet = Fernet(key)
encrypted = encrypt_password(password, fernet)
config.set(section, "password", encrypted)
with open(args.credentials, "w") as fh:
config.write(fh)
print(f"Password updated and encrypted in [{section}] of {args.credentials}")
if __name__ == "__main__":
main()
-642
View File
@@ -1,642 +0,0 @@
mqtt:
enabled: false
detectors:
coral:
type: edgetpu
device: pci
ffmpeg:
hwaccel_args: preset-nvidia
record:
enabled: true
alerts:
retain:
days: 30
mode: motion
detections:
retain:
days: 30
mode: motion
continuous:
days: 0
motion:
days: 3
go2rtc:
ffmpeg:
bin: ffmpeg
# Nvidia NVDEC hardware decode templates (used when #hardware is in stream URL)
h264: "-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid {input} -c:v copy -f rtsp {output}"
h265: "-hwaccel cuda -hwaccel_output_format cuda -c:v hevc_cuvid {input} -c:v copy -f rtsp {output}"
streams:
# Backbasement:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.222:554/live/ch0#video=copy#audio=copy#audio=aac#hardware
# SubBackBasement:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.222:554/live/ch1#video=copy#audio=copy#audio=aac#hardware
# FrontBasement:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.223:554/live/ch0?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
# SubFrontBasement:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.223:554/live/ch1?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
# Eningeering:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.221:554/live/ch0?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
# SubEngineering:
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.221:554/live/ch1?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
Driveway:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch12_0.264#video=copy#audio=copy#audio=aac#hardware
SubDriveway:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch12_1.264#video=copy#audio=copy#audio=aac#hardware
Porch:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch8_0.264#video=copy#audio=copy#audio=aac#hardware
SubPorch:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch8_1.264#video=copy#audio=copy#audio=aac#hardware
FenceGate:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch1_0.264#video=copy#audio=copy#audio=aac#hardware
SubFenceGate:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch1_1.264#video=copy#audio=copy#audio=aac#hardware
GardenArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch2_0.264#video=copy#audio=copy#audio=aac#hardware
SubGardenArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch2_1.264#video=copy#audio=copy#audio=aac#hardware
Backyard1:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch3_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackyard1:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch3_1.264#video=copy#audio=copy#audio=aac#hardware
Backyard2:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch4_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackyard2:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch4_1.264#video=copy#audio=copy#audio=aac#hardware
Backyard3:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch0_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackyard3:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch0_1.264#video=copy#audio=copy#audio=aac#hardware
Backyard4:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch10_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackyard4:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch10_1.264#video=copy#audio=copy#audio=aac#hardware
Backyard5:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch7_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackyard5:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch7_1.264#video=copy#audio=copy#audio=aac#hardware
PatioArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch6_0.264#video=copy#audio=copy#audio=aac#hardware
SubPatioArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch6_1.264#video=copy#audio=copy#audio=aac#hardware
PoolArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch9_0.264#video=copy#audio=copy#audio=aac#hardware
SubPoolArea:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch9_1.264#video=copy#audio=copy#audio=aac#hardware
SideHouse:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch11_0.264#video=copy#audio=copy#audio=aac#hardware
SubSideHouse:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch11_1.264#video=copy#audio=copy#audio=aac#hardware
BackSlider:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch5_0.264#video=copy#audio=copy#audio=aac#hardware
SubBackSlider:
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch5_1.264#video=copy#audio=copy#audio=aac#hardware
cameras:
# name_of_your_camera: # <------ Name the camera
# enabled: True
# ffmpeg:
# inputs:
# - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection
# roles:
# - detect
# detect:
# enabled: False # <---- disable detection until you have a working camera feed
# width: 1280
# height: 720
# back_basement:
# enabled: true
# ffmpeg:
# inputs:
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/BackBasement
# #input_args: preset-rtsp-restream
# roles:
# - record
# - path: rtsp://127.0.0.1:8554/SubBackBasement
# #input_args: preset-rtsp-restream
# roles:
# - detect
# detect:
# enabled: true # <---- disable detection until you have a working camera feed
# width: 1280
# height: 720
# fps: 5
# motion:
# enabled: true
# threshold: 30
# contour_area: 30
# mask: 0.683,0.006,0.683,0.056,0.766,0.055,0.996,0.054,0.996,0.006
# improve_contrast: true
# snapshots:
# enabled: true
# timestamp: true
# bounding_box: true
# retain:
# objects:
# person: 7
# front_basement:
# enabled: true
# ffmpeg:
# inputs:
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/FrontBasement
# #input_args: preset-rtsp-restream
# roles:
# - record
# - path: rtsp://127.0.0.1:8554/SubFrontBasement
# #input_args: preset-rtsp-restream
# roles:
# - detect
# detect:
# enabled: true # <---- disable detection until you have a working camera feed
# width: 1280
# height: 720
# fps: 5
# motion:
# enabled: true
# threshold: 30
# contour_area: 30
# mask: 0.723,0,0.724,0.05,1,0.048,0.999,0.001
# improve_contrast: true
# snapshots:
# enabled: true
# timestamp: true
# bounding_box: true
# retain:
# objects:
# person: 7
# record:
# enabled: false
# engineering:
# enabled: true
# ffmpeg:
# inputs:
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Engineering
# #input_args: preset-rtsp-restream
# roles:
# - record
# - path: rtsp://127.0.0.1:8554/SubEngineering
# #input_args: preset-rtsp-restream
# roles:
# - detect
# detect:
# enabled: true # <---- disable detection until you have a working camera feed
# width: 1280
# height: 720
# fps: 5
# motion:
# enabled: true
# threshold: 14
# contour_area: 15
# improve_contrast: true
# snapshots:
# enabled: true
# timestamp: true
# bounding_box: true
# retain:
# objects:
# person: 7
# record:
# enabled: false
driveway:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Driveway
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubDriveway
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
zones:
drivewayview:
coordinates: 0,0.373,0,1,1,1,1,0.381,0.47,0.343,0.134,0.352
loitering_time: 0
friendly_name: DrivewayView
porch:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Porch
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubPorch
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
fence_gate:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/FenceGate
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubFenceGate
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
zones:
fencegate:
coordinates:
0,0.395,0.318,0.259,0.598,0.175,0.916,0.237,1,0.296,1,1,0.003,0.996
loitering_time: 0
friendly_name: FenceGate
garden_area:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/GardenArea
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubGardenArea
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
backyard_1:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Backyard1
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackyard1
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
zones:
backyard1:
coordinates:
0.001,0.274,0.124,0.162,0.228,0.143,0.382,0.131,0.555,0.133,0.714,0.16,0.854,0.196,0.998,0.253,0.998,0.996,0,0.992
loitering_time: 0
friendly_name: Backyard1
backyard_2:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Backyard2
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackyard2
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
zones:
backyard2:
coordinates:
0.002,0.147,0.086,0.106,0.172,0.091,0.271,0.078,0.386,0.077,0.499,0.077,0.601,0.086,0.698,0.099,0.787,0.117,0.859,0.087,0.995,0.155,0.998,0.992,0.001,0.995
loitering_time: 0
friendly_name: Backyard2
backyard_3:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Backyard3
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackyard3
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
zones:
backyard3:
coordinates:
0.001,0.337,0.166,0.332,0.327,0.342,0.419,0.344,0.51,0.355,0.603,0.363,0.692,0.374,0.771,0.381,0.848,0.389,0.951,0.405,0.997,0.423,0.998,0.996,0.003,0.994
loitering_time: 0
friendly_name: Backyard3
motion:
mask: 0.79,0.539,0.845,0.541,0.82,0.858,0.772,0.844
backyard_4:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Backyard4
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackyard4
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
motion:
mask:
- 0.866,0.001,0.866,0.209,1,0.202,1,0
- 0.001,0,0.249,0.001,0.249,0.045,0.002,0.044
threshold: 60
contour_area: 10
improve_contrast: true
backyard_5:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/Backyard5
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackyard5
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
motion:
mask:
- 0.029,0.31,0.13,0.305,0.134,0.54,0.032,0.544
- 0.004,0.002,0.004,0.044,0.245,0.044,0.244,0
threshold: 60
contour_area: 10
improve_contrast: true
zones:
backyard5:
coordinates:
0.001,0.305,0.043,0.264,0.113,0.194,0.19,0.128,0.271,0.109,0.371,0.089,0.525,0.057,0.709,0.057,0.89,0.076,0.997,0.122,0.997,0.995,0.002,0.998
loitering_time: 0
friendly_name: Backyard5
patio_area:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/PatioArea
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubPatioArea
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
pool_area:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/PoolArea
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubPoolArea
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
motion:
mask:
0.43,0.146,0.704,0.194,0.532,0.808,0.199,0.704,0.002,0.559,0.252,0.282
back_slider:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/BackSlider
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubBackSlider
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
side_house:
enabled: true
ffmpeg:
inputs:
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
# - path: rtsp://127.0.0.1:8554/SideHouse
# input_args: preset-rtsp-restream
# roles:
# - record
- path: rtsp://127.0.0.1:8554/SubSideHouse
input_args: preset-rtsp-restream
roles:
- record
- detect
detect:
enabled: true # <---- disable detection until you have a working camera feed
width: 1280
height: 720
fps: 5
snapshots:
enabled: true
timestamp: true
bounding_box: true
retain:
objects:
person: 7
record:
enabled: true
motion:
threshold: 60
contour_area: 40
improve_contrast: true
mask:
0.001,0.859,0.085,0.717,0.203,0.637,0.383,0.736,0.65,0.421,0.642,0.997,0.003,0.999
zones:
sidehouse:
coordinates: 0.661,0.001,0.662,0.996,1,1,1,0
loitering_time: 0
friendly_name: SideHouse
objects:
filters:
person: {}
version: 0.17-0
-43
View File
@@ -1,43 +0,0 @@
api:
listen: ":1984"
log:
level: "info"
format: "color"
output: "file:go2rtc.log"
rtsp:
listen: ":8554" # RTSP Server TCP port, default - 8554
#username: "admin" # optional, default - disabled
#password: "pass" # optional, default - disabled
#default_query: "video=h264" # optional, default codecs filters
ffmpeg:
bin: ffmpeg
# Nvidia NVDEC hardware decode templates (used when #hardware is in stream URL)
h264: "-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid {input} -c:v copy -f rtsp {output}"
h265: "-hwaccel cuda -hwaccel_output_format cuda -c:v hevc_cuvid {input} -c:v copy -f rtsp {output}"
streams:
porch_steps: rtsp://admin:Optimus0329@192.168.128.11:80/ch8_0.264#video=h264#hardware=cuda
driveway: rtsp://admin:Optimus0329@192.168.128.11:80/ch12_0.264#video=h264#hardware=cuda
fence_gate: rtsp://admin:Optimus0329@192.168.128.11:80/ch1_0.264#video=h264#hardware=cuda
shed_area: rtsp://admin:Optimus0329@192.168.128.11:80/ch13_0.264#video=h264#hardware=cuda
garden_area: rtsp://admin:Optimus0329@192.168.128.11:80/ch2_0.264#video=h264#hardware=cuda
backyard_1: rtsp://admin:Optimus0329@192.168.128.11:80/ch3_0.264#video=h264#hardware=cuda
backyard_2: rtsp://admin:Optimus0329@192.168.128.11:80/ch4_0.264#video=h264#hardware=cuda
backyard_3: rtsp://admin:Optimus0329@192.168.128.11:80/ch0_0.264#video=h264#hardware=cuda
backyard_4: rtsp://admin:Optimus0329@192.168.128.11:80/ch10_0.264#video=h264#hardware=cuda
backyard_5: rtsp://admin:Optimus0329@192.168.128.11:80/ch7_0.264#video=h264#hardware=cuda
inside_garage: rtsp://admin:Optimus0329@192.168.128.11:80/ch6_0.264#video=h264#hardware=cuda
pool_area: rtsp://admin:Optimus0329@192.168.128.11:80/ch9_0.264#video=h264#hardware=cuda
back_slider: rtsp://admin:Optimus0329@192.168.128.11:80/ch5_0.264#video=h264#hardware=cuda
side_house: rtsp://admin:Optimus0329@192.168.128.11:80/ch11_0.264#video=h264#hardware=cuda
patio_area: rtsp://admin:Optimus0329@192.168.128.11:80/ch14_0.264
engineering: rtsp://admin:Optimus0329@192.168.128.65:1554/live/ch0
front_basement: rtsp://admin:Optimus0329@192.168.128.223:554/live/ch0
back_basement: rtsp://admin:Optimus0329@192.168.128.95:554/live/ch0
back_basement2: rtsp://admin:Optimus0329@192.168.128.183:554/live/ch0
webrtc:
listen: ":8555"
@@ -1,66 +0,0 @@
#!/usr/bin/python3
import argparse
import configparser
import os
import sys
import urllib3
import redfish
from datetime import datetime
def log(message, is_error=False):
"""Print message with timestamp"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if is_error:
print(f"[{timestamp}] {message}", file=sys.stderr)
else:
print(f"[{timestamp}] {message}")
# Suppress SSL warnings for self-signed iLO certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
parser.add_argument("hostname", help="iLO hostname or IP address")
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
help="Path to credentials file (default: /etc/ilo_credentials)")
args = parser.parse_args()
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
# File format:
# [ilo]
# username = root
# password = yourpassword
cred_path = args.credentials
if not os.path.exists(cred_path):
log(f"Error: credentials file not found: {cred_path}", is_error=True)
sys.exit(1)
config = configparser.ConfigParser()
config.read(cred_path)
try:
LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root")
LOGIN_PASSWORD = config.get("ilo_credentials", "password")
except (configparser.NoSectionError, configparser.NoOptionError) as e:
log(f"Error reading credentials file: {e}", is_error=True)
sys.exit(1)
BASE_URL = f"https://{args.hostname}"
REDFISHOBJ = redfish.RedfishClient(
base_url=BASE_URL,
username=LOGIN_ACCOUNT,
password=LOGIN_PASSWORD,
)
REDFISHOBJ.login(auth="session")
# Discover the correct reset action URI from the system resource
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
response = REDFISHOBJ.post(reset_uri, body=body)
log(f"Status: {response.status}")
if response.status >= 400:
log(f"Error: {response.read}", is_error=True)
REDFISHOBJ.logout()
@@ -1,66 +0,0 @@
#!/usr/bin/python3
import argparse
import configparser
import os
import sys
import urllib3
import redfish
from datetime import datetime
def log(message, is_error=False):
"""Print message with timestamp"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if is_error:
print(f"[{timestamp}] {message}", file=sys.stderr)
else:
print(f"[{timestamp}] {message}")
# Suppress SSL warnings for self-signed iLO certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
parser.add_argument("hostname", help="iLO hostname or IP address")
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
help="Path to credentials file (default: /etc/ilo_credentials)")
args = parser.parse_args()
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
# File format:
# [ilo]
# username = root
# password = yourpassword
cred_path = args.credentials
if not os.path.exists(cred_path):
log(f"Error: credentials file not found: {cred_path}", is_error=True)
sys.exit(1)
config = configparser.ConfigParser()
config.read(cred_path)
try:
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
LOGIN_PASSWORD = config.get("ilo", "password")
except (configparser.NoSectionError, configparser.NoOptionError) as e:
log(f"Error reading credentials file: {e}", is_error=True)
sys.exit(1)
BASE_URL = f"https://{args.hostname}"
REDFISHOBJ = redfish.RedfishClient(
base_url=BASE_URL,
username=LOGIN_ACCOUNT,
password=LOGIN_PASSWORD,
)
REDFISHOBJ.login(auth="session")
# Discover the correct reset action URI from the system resource
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
response = REDFISHOBJ.post(reset_uri, body=body)
log(f"Status: {response.status}")
if response.status >= 400:
log(f"Error: {response.read}", is_error=True)
REDFISHOBJ.logout()
@@ -1,159 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
import subprocess
import sys
import datetime
# Examples:
# ./ipmi_multipoweron.py host1 host2 host3
# ./ipmi_multipoweron.py host1 host2 host3 -c /path/to/credentials
# ./ipmi_multipoweron.py -f serverlist.txt
# ./ipmi_multipoweron.py -f serverlist.txt -c /path/to/credentials
def ts_print(msg):
"""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}")
DEFAULT_EK = "0000000000000000000000000000000000000000"
def find_credentials_file(provided_path):
"""Find credentials file in standard locations if provided path doesn't exist."""
# If provided path exists, use it
if os.path.exists(provided_path):
return provided_path
# Check standard locations
standard_paths = [
os.path.expanduser("~/credentials"),
"/etc/credentials",
]
for path in standard_paths:
if os.path.exists(path):
return path
# Return the provided path (will fail with proper error message later)
return provided_path
def load_credentials(path):
cred_path = find_credentials_file(path)
if not os.path.exists(cred_path):
ts_print(f"Error: credentials file not found: {cred_path}")
sys.exit(1)
config = configparser.ConfigParser()
config.read(cred_path)
try:
username = config.get("credentials", "username", fallback="root")
password = config.get("credentials", "password")
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
ts_print(f"Error reading credentials file: {e}")
sys.exit(1)
return username, password, encryption_key
def power_on_host(host, ipmi_user, ipmi_pw, ipmi_ek):
"""Power on a single host via IPMI. Returns True if successful."""
cmd = [
"ipmitool",
"-I", "lanplus",
"-H", host,
"-U", ipmi_user,
"-P", ipmi_pw,
"-y", ipmi_ek,
"power", "on",
]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if proc.returncode != 0:
ts_print(f"ERROR: Failed to power on {host}: {proc.stderr.strip()}")
return False
else:
ts_print(f"SUCCESS: {host} - {proc.stdout.strip()}")
return True
def load_servers_from_file(filepath):
"""Load server list from a file (one server per line, lines starting with # are ignored)."""
if not os.path.exists(filepath):
ts_print(f"Error: server list file not found: {filepath}")
sys.exit(1)
servers = []
try:
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith('#'):
servers.append(line)
except Exception as e:
ts_print(f"Error reading server list file: {e}")
sys.exit(1)
return servers
def main():
p = argparse.ArgumentParser(description="Send IPMI power on to multiple servers")
# Either provide servers as arguments or a file
group = p.add_mutually_exclusive_group(required=True)
group.add_argument(
"servers",
nargs="*",
help="List of IPMI host addresses (space-separated)"
)
group.add_argument(
"-f", "--file",
help="Path to file containing list of servers (one per line)"
)
p.add_argument(
"-c",
"--credentials",
default="credentials",
help="Path to credentials file (default: credentials; also checks ~/ and /etc/ if not found)",
)
args = p.parse_args()
# Determine server list
if args.file:
servers = load_servers_from_file(args.file)
else:
servers = args.servers
if not servers:
ts_print("Error: No servers specified")
sys.exit(1)
ts_print(f"Starting power on sequence for {len(servers)} server(s)")
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
success_count = 0
failed_count = 0
for host in servers:
if power_on_host(host, ipmi_user, ipmi_pw, ipmi_ek):
success_count += 1
else:
failed_count += 1
ts_print(f"Completed: {success_count} successful, {failed_count} failed")
if failed_count > 0:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -1,70 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
import subprocess
import sys
from datetime import datetime
DEFAULT_EK = "0000000000000000000000000000000000000000"
def log(message, is_error=False):
"""Print message with timestamp"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if is_error:
print(f"[{timestamp}] {message}", file=sys.stderr)
else:
print(f"[{timestamp}] {message}")
def load_credentials(path):
if not os.path.exists(path):
log(f"Error: credentials file not found: {path}", is_error=True)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
try:
username = config.get("credentials", "username", fallback="root")
password = config.get("credentials", "password")
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
log(f"Error reading credentials file: {e}", is_error=True)
sys.exit(1)
return username, password, encryption_key
def main():
p = argparse.ArgumentParser(description="Send IPMI power off")
p.add_argument("-H", "--host", required=True, help="IPMI host address")
p.add_argument(
"-c",
"--credentials",
default="credentials",
help="Path to credentials file (default: credentials)",
)
args = p.parse_args()
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
cmd = [
"ipmitool",
"-I", "lanplus",
"-H", args.host,
"-U", ipmi_user,
"-P", ipmi_pw,
"-y", ipmi_ek,
"power", "off",
]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if proc.returncode != 0:
log("ipmitool failed: " + proc.stderr.strip(), is_error=True)
sys.exit(proc.returncode)
log(proc.stdout.strip())
if __name__ == "__main__":
main()
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
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}")
DEFAULT_EK = "0000000000000000000000000000000000000000"
def load_credentials(path):
if not os.path.exists(path):
print(f"Error: credentials file not found: {path}", file=sys.stderr)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
try:
username = config.get("ilo", "username", fallback="root")
password = config.get("ilo", "password")
encryption_key = config.get("ilo", "encryption_key", fallback=DEFAULT_EK)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Error reading credentials file: {e}", file=sys.stderr)
sys.exit(1)
return username, password, encryption_key
def main():
p = argparse.ArgumentParser(description="Send IPMI power on")
p.add_argument("-H", "--host", required=True, help="IPMI host address")
p.add_argument(
"-c",
"--credentials",
default="credentials",
help="Path to credentials file (default: credentials)",
)
args = p.parse_args()
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
cmd = [
"ipmitool",
"-I", "lanplus",
"-H", args.host,
"-U", ipmi_user,
"-P", ipmi_pw,
"-y", ipmi_ek,
"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,40 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = REPO_ROOT / "personal" / "tools" / "ilo_powercycle.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"ilo_powercycle",
"--collect-submodules",
"redfish",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "ilo_powercycle.exe" if sys.platform.startswith("win") else "ilo_powercycle"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = SCRIPT_DIR / "ilo_status.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"ilo_status",
"--collect-submodules",
"redfish",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "ilo_status.exe" if sys.platform.startswith("win") else "ilo_status"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = SCRIPT_DIR / "power" / "ipmi_poweroff.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"ipmi_poweroff",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "ipmi_poweroff.exe" if sys.platform.startswith("win") else "ipmi_poweroff"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = SCRIPT_DIR / "power" / "ipmi_poweron.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"ipmi_poweron",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "ipmi_poweron.exe" if sys.platform.startswith("win") else "ipmi_poweron"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
@@ -1,40 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = SCRIPT_DIR / "multi_powercycle.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"multi_powercycle",
"--collect-submodules",
"redfish",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "multi_powercycle.exe" if sys.platform.startswith("win") else "multi_powercycle"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
ENTRYPOINT = SCRIPT_DIR / "fancontrol" / "set_fanspeed.py"
DIST_DIR = REPO_ROOT / "dist"
def main():
command = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--clean",
"--onefile",
"--name",
"set_fanspeed",
str(ENTRYPOINT),
]
try:
subprocess.run(command, cwd=REPO_ROOT, check=True)
except subprocess.CalledProcessError as exc:
raise SystemExit(exc.returncode) from exc
output_name = "set_fanspeed.exe" if sys.platform.startswith("win") else "set_fanspeed"
output_path = DIST_DIR / output_name
print(f"Built executable: {output_path}")
print("Build once on each target OS to get a native executable for that platform.")
if __name__ == "__main__":
main()
-32
View File
@@ -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,282 +0,0 @@
#!/usr/bin/env python3
import subprocess
import re
import sys
import datetime
import argparse
import configparser
import os
try:
from cryptography.fernet import Fernet, InvalidToken
except ImportError:
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
DEFAULT_EK = "0000000000000000000000000000000000000000"
DEFAULT_CREDENTIALS_FILE = "/root/cron_serverlists/credentials.ini"
DEFAULT_KEYFILE_NAME = "secret.key"
# IPMI settings (loaded at runtime from credentials file)
IPMIUSER = None
IPMIPW = None
IPMIEK = DEFAULT_EK
# Fan / temperature settings
FANSPEED = 15 # percent
MAXTEMP = 37 # Celsius
def build_ipmi_base():
base = ["ipmitool", "-I", IPMIINTERFACE, "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK]
if IPMICIPHER is not None:
base += ["-C", str(IPMICIPHER)]
return base
def has_ipmi_session_error(stderr_text):
s = (stderr_text or "").lower()
markers = [
"unable to establish ipmi v2 / rmcp+ session",
"rakp",
"session timeout",
"authentication type",
"invalid user name",
]
return any(marker in s for marker in markers)
def run_ipmi(args):
base = build_ipmi_base()
cmd = base + args
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if p.returncode != 0 or has_ipmi_session_error(p.stderr):
raise RuntimeError(
f"ipmitool failed: {' '.join(cmd)}\n"
f"stdout: {(p.stdout or '').strip() or '<empty>'}\n"
f"stderr: {(p.stderr or '').strip() or '<empty>'}"
)
return p.stdout
def run_ipmi_raw(args):
base = build_ipmi_base()
cmd = base + args
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return cmd, p.returncode, p.stdout, p.stderr
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 is_fernet_token(value):
return isinstance(value, str) and value.startswith("gAAAA")
def load_fernet(keyfile):
if not os.path.exists(keyfile):
raise RuntimeError(f"key file not found: {keyfile}")
with open(keyfile, "rb") as fh:
key = fh.read()
try:
return Fernet(key)
except ValueError as e:
raise RuntimeError(f"invalid key format in key file: {keyfile}") from e
def decrypt_password(value, fernet):
# Support both encrypted and plaintext credentials for smoother migrations.
if not is_fernet_token(value):
return value
if fernet is None:
raise RuntimeError(
"encrypted password detected but key file was not found. "
"Use --keyfile /path/to/secret.key"
)
try:
return fernet.decrypt(value.encode()).decode()
except InvalidToken as e:
raise RuntimeError("failed to decrypt password; key file may not match credentials file") from e
def resolve_credentials_path(cli_path):
if cli_path and os.path.exists(cli_path):
return cli_path
candidates = []
if cli_path:
candidates.append(cli_path)
candidates.append(os.path.join(os.getcwd(), "credentials.ini"))
if DEFAULT_CREDENTIALS_FILE not in candidates:
candidates.append(DEFAULT_CREDENTIALS_FILE)
for candidate in candidates:
if os.path.exists(candidate):
return candidate
raise RuntimeError("credentials file not found. Looked in: " + ", ".join(candidates))
def resolve_keyfile(cli_keyfile, credentials_path):
if cli_keyfile:
return cli_keyfile if os.path.exists(cli_keyfile) else None
candidates = [
os.path.join(os.path.dirname(credentials_path), DEFAULT_KEYFILE_NAME),
os.path.join(os.path.dirname(DEFAULT_CREDENTIALS_FILE), DEFAULT_KEYFILE_NAME),
]
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return None
def load_credentials(path, fernet):
if not os.path.exists(path):
raise RuntimeError(f"credentials file not found: {path}")
config = configparser.ConfigParser()
config.read(path)
try:
section = "ipmi_credentials"
username = config.get(section, "username", fallback="root")
encrypted_password = config.get(section, "password")
password = decrypt_password(encrypted_password, fernet)
encryption_key = config.get(
section,
"ipmiek",
fallback=config.get(section, "encryption_key", fallback=DEFAULT_EK),
)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
raise RuntimeError(f"error reading credentials file: {e}")
return username, password, encryption_key
def get_exhaust_temp():
commands_to_try = [
["sensor", "get", "Exhaust Temp"],
["sdr", "type", "temperature"],
["sensor", "list"],
["sdr", "elist", "full"],
]
debug_sections = []
session_error_seen = False
for args in commands_to_try:
cmd, rc, out, err = run_ipmi_raw(args)
if has_ipmi_session_error(err):
session_error_seen = True
cmd_text = " ".join(cmd)
section = [
f"Command: {cmd_text}",
f"Return code: {rc}",
"STDOUT:",
out.strip() or "<empty>",
"STDERR:",
err.strip() or "<empty>",
]
debug_sections.append("\n".join(section))
# Parse explicit sensor-get output first.
if args[:2] == ["sensor", "get"] and out:
m = re.search(r"Sensor\s+Reading\s*:\s*(-?\d+(?:\.\d+)?)\b.*degrees\s*C", out, re.IGNORECASE)
if m:
return int(float(m.group(1)))
# Parse table outputs that have pipe-delimited sensor rows.
for line in out.splitlines():
parts = [p.strip() for p in line.split("|")]
if len(parts) < 2:
continue
sensor_name = parts[0]
normalized_name = " ".join(sensor_name.lower().split())
if not (normalized_name == "exhaust temp" or normalized_name.startswith("exhaust temp ")):
continue
for part in parts[1:]:
m = re.search(r"(-?\d+(?:\.\d+)?)\s*degrees\s*C", part, re.IGNORECASE)
if m:
return int(float(m.group(1)))
if session_error_seen:
raise RuntimeError(
"Failed to establish an IPMI session. "
"Check host/credentials, then try different options like "
"--interface lan --cipher 3 or --interface lanplus --cipher 17.\n\n"
+ "Debug output from attempted commands:\n\n"
+ "\n\n---\n\n".join(debug_sections)
)
raise RuntimeError(
"Failed to read Exhaust Temp from ipmitool output.\n"
+ "Debug output from attempted commands:\n\n"
+ "\n\n---\n\n".join(debug_sections)
)
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", required=True, help="IPMI host/IP")
parser.add_argument(
"-c",
"--credentials",
default=DEFAULT_CREDENTIALS_FILE,
help="Path to credentials file (default: %(default)s)",
)
parser.add_argument(
"--keyfile",
default=None,
help="Path to Fernet key file for encrypted passwords (default: auto-detect next to credentials)",
)
parser.add_argument("--interface", default="lanplus", choices=["lanplus", "lan"], help="IPMI interface (default: %(default)s)")
parser.add_argument("--cipher", type=int, default=None, help="Optional IPMI cipher suite ID (e.g. 3 or 17)")
args = parser.parse_args()
global IPMIHOST, IPMIINTERFACE, IPMICIPHER, IPMIUSER, IPMIPW, IPMIEK
# set globals so run_ipmi() uses provided connection settings and credentials
IPMIHOST = args.host
IPMIINTERFACE = args.interface
IPMICIPHER = args.cipher
credentials_path = resolve_credentials_path(args.credentials)
keyfile_path = resolve_keyfile(args.keyfile, credentials_path)
fernet = load_fernet(keyfile_path) if keyfile_path else None
IPMIUSER, IPMIPW, IPMIEK = load_credentials(credentials_path, fernet)
ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}")
try:
temp = get_exhaust_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()
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
import sys
import urllib3
import redfish
from datetime import datetime
try:
from cryptography.fernet import Fernet, InvalidToken
except ImportError:
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
DEFAULT_CREDENTIALS_FILE = "/root/cron_serverlists/credentials.ini"
DEFAULT_KEYFILE_NAME = "secret.key"
def log(message, is_error=False):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
stream = sys.stderr if is_error else sys.stdout
print(f"[{timestamp}] {message}", file=stream)
def resolve_credentials_path(cli_path):
"""Resolve credentials path. Prefer explicit CLI path, then search common locations."""
if cli_path and os.path.exists(cli_path):
return cli_path
candidates = []
if cli_path:
candidates.append(cli_path)
candidates.append(os.path.join(os.getcwd(), "credentials.ini"))
if DEFAULT_CREDENTIALS_FILE not in candidates:
candidates.append(DEFAULT_CREDENTIALS_FILE)
for candidate in candidates:
if os.path.exists(candidate):
return candidate
log(
"Error: credentials file not found. Looked in: " + ", ".join(candidates),
is_error=True,
)
sys.exit(1)
def resolve_keyfile(cli_keyfile, credentials_path):
"""Resolve keyfile path. Prefer explicit --keyfile, else search next to credentials file."""
if cli_keyfile:
return cli_keyfile if os.path.exists(cli_keyfile) else None
candidate = os.path.join(os.path.dirname(credentials_path), DEFAULT_KEYFILE_NAME)
if os.path.exists(candidate):
return candidate
return None
def load_fernet(keyfile):
"""Load Fernet instance from the key file created by set_password.py."""
if not os.path.exists(keyfile):
log(f"Error: key file not found: {keyfile}", is_error=True)
sys.exit(1)
with open(keyfile, "rb") as fh:
key = fh.read()
try:
return Fernet(key)
except ValueError:
log(f"Error: invalid key format in key file: {keyfile}", is_error=True)
sys.exit(1)
def is_fernet_token(value):
return isinstance(value, str) and value.startswith("gAAAA")
def decrypt_password(encrypted_password, fernet):
"""Decrypt password encrypted by set_password.py, or pass through plaintext."""
if not is_fernet_token(encrypted_password):
return encrypted_password
if fernet is None:
log(
"Error: encrypted password detected but key file was not found. "
"Use --keyfile /path/to/secret.key.",
is_error=True,
)
sys.exit(1)
try:
return fernet.decrypt(encrypted_password.encode()).decode()
except InvalidToken:
log("Error: failed to decrypt password. Check that --keyfile matches the credentials file.", is_error=True)
sys.exit(1)
def load_ilo_credentials(path, fernet):
config = configparser.ConfigParser()
config.read(path)
section = "ilo_credentials"
if not config.has_section(section):
log(f"Error: missing [{section}] section in {path}", is_error=True)
sys.exit(1)
username = config.get(section, "username", fallback="root")
encrypted_password = config.get(section, "password", fallback=None)
if not encrypted_password:
log(f"Error: missing password in [{section}] section of {path}", is_error=True)
sys.exit(1)
password = decrypt_password(encrypted_password, fernet)
return username, password
# Suppress SSL warnings for self-signed iLO certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(description="Get the power status of an HPE server via iLO Redfish")
parser.add_argument("hostname", help="iLO hostname or IP address")
parser.add_argument("-c", "--credentials", default=None,
help="Path to credentials file (default: auto-detected credentials.ini)")
parser.add_argument("--keyfile", default=None,
help="Path to Fernet key file for decrypting passwords (default: auto-detected secret.key)")
args = parser.parse_args()
cred_path = resolve_credentials_path(args.credentials)
keyfile = resolve_keyfile(args.keyfile, cred_path)
fernet = load_fernet(keyfile) if keyfile else None
LOGIN_ACCOUNT, LOGIN_PASSWORD = load_ilo_credentials(cred_path, fernet)
BASE_URL = f"https://{args.hostname}"
REDFISHOBJ = redfish.RedfishClient(
base_url=BASE_URL,
username=LOGIN_ACCOUNT,
password=LOGIN_PASSWORD,
)
REDFISHOBJ.login(auth="session")
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
power_state = sys_response.dict.get("PowerState", "Unknown")
log(f"Power State: {power_state}")
REDFISHOBJ.logout()
-358
View File
@@ -1,358 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
import subprocess
import sys
from datetime import datetime
import redfish
import urllib3
try:
from cryptography.fernet import Fernet, InvalidToken
except ImportError:
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
DEFAULT_CREDENTIALS_FILE = "/root/cron_serverlists/credentials.ini"
DEFAULT_KEYFILE_NAME = "/root/cron_serverlists/secret.key"
DEFAULT_SERVER_LIST_FILE = "/root/cron_serverlists/serverlist.ini"
# Examples:
# ./multi_powercycle.py on
# ./multi_powercycle.py on --keyfile /root/cron_serverlists/secret.key
# ./multi_powercycle.py off -f /root/cron_serverlists/serverlist.ini --keyfile /root/cron_serverlists/secret.key
# ./multi_powercycle.py on
# ./multi_powercycle.py off
# ./multi_powercycle.py on -f server_groups.txt
#
# File format:
# [ipmi]
# 192.168.129.240
# 192.168.129.241
#
# [ilo]
# 192.168.129.242
DEFAULT_EK = "0000000000000000000000000000000000000000"
def load_fernet(keyfile: str):
"""Load Fernet instance from the key file created by set_password.py."""
if not os.path.exists(keyfile):
log(f"Error: key file not found: {keyfile}", is_error=True)
sys.exit(1)
with open(keyfile, "rb") as fh:
key = fh.read()
try:
return Fernet(key)
except ValueError:
log(f"Error: invalid key format in key file: {keyfile}", is_error=True)
sys.exit(1)
def resolve_keyfile(cli_keyfile, ipmi_credentials_path, ilo_credentials_path):
"""Resolve keyfile path. Prefer explicit --keyfile, else search next to credentials files."""
if cli_keyfile:
return cli_keyfile if os.path.exists(cli_keyfile) else None
candidates = []
for cred_path in (ipmi_credentials_path, ilo_credentials_path):
key_candidate = os.path.join(os.path.dirname(cred_path), DEFAULT_KEYFILE_NAME)
if key_candidate not in candidates:
candidates.append(key_candidate)
# Backward-compatible fallback
legacy_default = os.path.join(os.path.dirname(DEFAULT_CREDENTIALS_FILE), DEFAULT_KEYFILE_NAME)
if legacy_default not in candidates:
candidates.append(legacy_default)
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return None
def resolve_credentials_path(cli_path, server_file_path):
"""Resolve credentials path. Prefer explicit CLI path, then search common locations."""
if cli_path and os.path.exists(cli_path):
return cli_path
candidates = []
if cli_path:
candidates.append(cli_path)
# Credentials next to server list file is a common deployment pattern
candidates.append(os.path.join(os.path.dirname(server_file_path), "credentials.ini"))
# Current working directory fallback
candidates.append(os.path.join(os.getcwd(), "credentials.ini"))
# Legacy default fallback
if DEFAULT_CREDENTIALS_FILE not in candidates:
candidates.append(DEFAULT_CREDENTIALS_FILE)
for candidate in candidates:
if os.path.exists(candidate):
return candidate
log(
"Error: credentials file not found. Looked in: " + ", ".join(candidates),
is_error=True,
)
sys.exit(1)
def is_fernet_token(value: str) -> bool:
"""Best-effort check for Fernet token format from set_password.py."""
return isinstance(value, str) and value.startswith("gAAAA")
def decrypt_password(encrypted_password: str, fernet):
"""Decrypt password encrypted by set_password.py, or pass through plaintext."""
if not is_fernet_token(encrypted_password):
return encrypted_password
if fernet is None:
log(
"Error: encrypted password detected but key file was not found. "
"Use --keyfile /path/to/secret.key.",
is_error=True,
)
sys.exit(1)
try:
return fernet.decrypt(encrypted_password.encode()).decode()
except InvalidToken:
log("Error: failed to decrypt password. Check that --keyfile matches the credentials file.", is_error=True)
sys.exit(1)
def log(message, is_error=False):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
stream = sys.stderr if is_error else sys.stdout
print(f"[{timestamp}] {message}", file=stream)
def parse_server_groups(path):
if not os.path.exists(path):
log(f"Error: server list file not found: {path}", is_error=True)
sys.exit(1)
groups = {"ipmi": [], "ilo": []}
current_group = None
aliases = {
"ipmi": "ipmi",
"dell": "ipmi",
"ilo": "ilo",
}
try:
with open(path, "r", encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
section_name = line[1:-1].strip().lower()
current_group = aliases.get(section_name)
if current_group is None:
log(f"Error: unsupported server group [{section_name}] in {path}", is_error=True)
sys.exit(1)
continue
if current_group is None:
log(f"Error: host entry found before any section header in {path}: {line}", is_error=True)
sys.exit(1)
groups[current_group].append(line)
except OSError as exc:
log(f"Error reading server list file: {exc}", is_error=True)
sys.exit(1)
return groups
def load_ipmi_credentials(path, fernet):
if not os.path.exists(path):
log(f"Error: IPMI credentials file not found: {path}", is_error=True)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
section = "ipmi_credentials"
if not config.has_section(section):
log(f"Error reading IPMI credentials file: missing [{section}] section in {path}", is_error=True)
sys.exit(1)
username = config.get(section, "username", fallback="root")
encrypted_password = config.get(section, "password", fallback=None)
encryption_key = config.get(
section,
"ipmiek",
fallback=config.get(section, "encryption_key", fallback=DEFAULT_EK),
)
if not encrypted_password:
log(f"Error reading IPMI credentials file: missing password in [{section}] section of {path}", is_error=True)
sys.exit(1)
password = decrypt_password(encrypted_password, fernet)
return username, password, encryption_key
def load_ilo_credentials(path, fernet):
if not os.path.exists(path):
log(f"Error: iLO credentials file not found: {path}", is_error=True)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
section = "ilo_credentials"
if not config.has_section(section):
log(f"Error reading iLO credentials file: missing [{section}] section in {path}", is_error=True)
sys.exit(1)
username = config.get(section, "username", fallback="root")
encrypted_password = config.get(section, "password", fallback=None)
if not encrypted_password:
log(f"Error reading iLO credentials file: missing password in [{section}] section of {path}", is_error=True)
sys.exit(1)
password = decrypt_password(encrypted_password, fernet)
return username, password
def ipmi_power(host, action, username, password, encryption_key):
cmd = [
"ipmitool",
"-I",
"lanplus",
"-H",
host,
"-U",
username,
"-P",
password,
"-y",
encryption_key,
"power",
action,
]
log(f"Sending IPMI power {action} to {host}")
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if proc.returncode != 0:
detail = proc.stderr.strip() or proc.stdout.strip() or "unknown error"
log(f"IPMI {action} failed for {host}: {detail}", is_error=True)
return False
detail = proc.stdout.strip() or f"power {action} request accepted"
log(f"IPMI {action} succeeded for {host}: {detail}")
return True
def ilo_powercycle(host, username, password):
base_url = f"https://{host}"
client = redfish.RedfishClient(base_url=base_url, username=username, password=password)
try:
client.login(auth="session")
sys_response = client.get("/redfish/v1/Systems/1/")
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
response = client.post(reset_uri, body={"ResetType": "PushPowerButton"})
log(f"iLO powercycle status for {host}: {response.status}")
if response.status >= 400:
log(f"iLO powercycle failed for {host}: {response.read}", is_error=True)
return False
return True
except Exception as exc:
log(f"iLO powercycle failed for {host}: {exc}", is_error=True)
return False
finally:
try:
client.logout()
except Exception:
pass
def main():
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(
description="Dispatch power operations to IPMI hosts and iLO hosts from a grouped server list"
)
parser.add_argument("action", choices=("on", "off"), help="Power action for IPMI hosts")
parser.add_argument(
"-f",
"--file",
dest="server_file",
default=DEFAULT_SERVER_LIST_FILE,
help=f"Path to server list file (default: {DEFAULT_SERVER_LIST_FILE})",
)
parser.add_argument(
"--ipmi-credentials",
default=DEFAULT_CREDENTIALS_FILE,
help=f"Path to IPMI credentials file (default: {DEFAULT_CREDENTIALS_FILE})",
)
parser.add_argument(
"--ilo-credentials",
default=DEFAULT_CREDENTIALS_FILE,
help=f"Path to iLO credentials file (default: {DEFAULT_CREDENTIALS_FILE})",
)
parser.add_argument(
"--keyfile",
default=None,
help="Path to Fernet key file created by set_password.py (default: auto-detect next to credentials file)",
)
args = parser.parse_args()
args.ipmi_credentials = resolve_credentials_path(args.ipmi_credentials, args.server_file)
args.ilo_credentials = resolve_credentials_path(args.ilo_credentials, args.server_file)
keyfile = resolve_keyfile(args.keyfile, args.ipmi_credentials, args.ilo_credentials)
fernet = load_fernet(keyfile) if keyfile else None
groups = parse_server_groups(args.server_file)
total_hosts = len(groups["ipmi"]) + len(groups["ilo"])
if total_hosts == 0:
log(f"Error: no servers found in {args.server_file}", is_error=True)
sys.exit(1)
log(
f"Starting run for {total_hosts} host(s): {len(groups['ipmi'])} IPMI host(s), {len(groups['ilo'])} iLO host(s)"
)
ipmi_creds = None
ilo_creds = None
failures = 0
if groups["ipmi"]:
ipmi_creds = load_ipmi_credentials(args.ipmi_credentials, fernet)
for host in groups["ipmi"]:
if not ipmi_power(host, args.action, *ipmi_creds):
failures += 1
if groups["ilo"]:
ilo_creds = load_ilo_credentials(args.ilo_credentials, fernet)
for host in groups["ilo"]:
if not ilo_powercycle(host, *ilo_creds):
failures += 1
successes = total_hosts - failures
log(f"Completed run: {successes} successful, {failures} failed")
if failures:
sys.exit(1)
if __name__ == "__main__":
main()
-148
View File
@@ -1,148 +0,0 @@
#!/bin/bash
# Configuration
# If started by root (e.g., root crontab), re-run as postgres.
if [ "$(id -u)" -eq 0 ] && [ "$(id -un)" != "postgres" ]; then
if command -v runuser >/dev/null 2>&1; then
exec runuser -u postgres -- "$0" "$@"
fi
# Fallback for systems without runuser.
exec su - postgres -s /bin/bash -c "$(printf '%q ' "$0" "$@")"
fi
# Avoid permission warnings when root launches this script and postgres inherits /root as cwd.
cd / || exit 1
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
: "${WAL_CLEANUP_ENABLED:=1}"
WAL_ARCHIVE_DIR="/var/lib/postgresql_archive/18/main/pg_wal"
WAL_MIN_AGE_DAYS=7
clean_walfiles() {
local newest_backup=""
local newest_backup_ts=""
local newest_backup_dt=""
local age_cutoff_ts=""
local age_cutoff_dt=""
local backup_ts=""
local file_ts=""
local deleted=0
local backup_file=""
local file=""
local -a backup_files=()
local -a files_to_delete=()
if [[ ! -d "$WAL_ARCHIVE_DIR" ]]; then
log "Archive directory does not exist: $WAL_ARCHIVE_DIR" >&2
return 1
fi
if ! [[ "$WAL_MIN_AGE_DAYS" =~ ^[0-9]+$ ]]; then
log "WAL_MIN_AGE_DAYS must be a non-negative integer" >&2
return 1
fi
mapfile -d '' backup_files < <(find "$WAL_ARCHIVE_DIR" -maxdepth 1 -type f -name '*.backup' -print0)
if [[ ${#backup_files[@]} -eq 0 ]]; then
log "No .backup files found in $WAL_ARCHIVE_DIR; skipping WAL cleanup."
return 0
fi
for backup_file in "${backup_files[@]}"; do
backup_ts=$(stat -c %Y "$backup_file")
if [[ -z "$newest_backup_ts" || "$backup_ts" -gt "$newest_backup_ts" ]]; then
newest_backup_ts="$backup_ts"
newest_backup="$backup_file"
fi
done
age_cutoff_ts=$(date -d "$WAL_MIN_AGE_DAYS days ago" +%s)
newest_backup_dt=$(date -d "@$newest_backup_ts" '+%Y-%m-%d %H:%M:%S')
age_cutoff_dt=$(date -d "@$age_cutoff_ts" '+%Y-%m-%d %H:%M:%S')
log "WAL archive dir : $WAL_ARCHIVE_DIR"
log "Newest .backup file : $newest_backup"
log "Newest .backup mtime : $newest_backup_dt"
log "WAL age cutoff : $age_cutoff_dt"
while IFS= read -r -d '' file; do
file_ts=$(stat -c %Y "$file")
# Delete only when both constraints are true.
if [[ "$file_ts" -lt "$newest_backup_ts" && "$file_ts" -lt "$age_cutoff_ts" ]]; then
files_to_delete+=("$file")
fi
done < <(find "$WAL_ARCHIVE_DIR" -maxdepth 1 -type f ! -name '*.backup' -print0)
# Also delete old .backup files (except the newest one) that exceed retention period
for backup_file in "${backup_files[@]}"; do
if [[ "$backup_file" != "$newest_backup" ]]; then
backup_ts=$(stat -c %Y "$backup_file")
if [[ "$backup_ts" -lt "$age_cutoff_ts" ]]; then
files_to_delete+=("$backup_file")
fi
fi
done
if [[ ${#files_to_delete[@]} -eq 0 ]]; then
log "No WAL files matched cleanup criteria."
return 0
fi
echo ""
log "WAL files matching cleanup criteria (${#files_to_delete[@]}):"
for file in "${files_to_delete[@]}"; do
log " $file"
done
for file in "${files_to_delete[@]}"; do
rm -f -- "$file"
((deleted += 1))
done
echo ""
log "Deleted $deleted WAL file(s)."
}
log "Beginning base backup."
BASEBACKUP_ROOT="/var/lib/postgresql_archive/pgbackups"
BACKUP_DIR="/var/lib/postgresql_archive/pgbackups/basebackup"
mkdir -p "$BACKUP_DIR"
# Perform base backup with WAL streaming
pg_basebackup -h localhost -U wjones -D "$BACKUP_DIR" -Fp -Xs -P
# Optional: Compress
BASEBACKUP_ARCHIVE="${BACKUP_DIR}_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "$BASEBACKUP_ARCHIVE" -C "$(dirname "$BACKUP_DIR")" "$(basename "$BACKUP_DIR")"
rm -rf "$BACKUP_DIR"
# Retention: Delete backups older than 7 days
find "$BASEBACKUP_ROOT" -type f -name "*.tar.gz" -mtime +7 -delete
log "Base backup completed.Now backing up individual databases."
BACKUP_DIR="/var/lib/postgresql_archive/pg_dumpfiles"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
# Ensure directory exists
mkdir -p "$BACKUP_DIR"
# Get all non-system databases and back each one up.
psql -U postgres -d postgres -Atc "SELECT datname FROM pg_database WHERE datistemplate = false AND datname <> 'postgres' ORDER BY datname" | while IFS= read -r DB_NAME; do
pg_dump -U postgres -d "$DB_NAME" | gzip > "$BACKUP_DIR/${DB_NAME}_$TIMESTAMP.sql.gz"
log "Backup completed: ${DB_NAME}_$TIMESTAMP.sql.gz"
done
# Delete backups older than retention period
find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -delete
log "Individual database backups completed and old backups cleaned up."
if [[ "$WAL_CLEANUP_ENABLED" -eq 1 ]]; then
log "Starting WAL archive cleanup."
clean_walfiles
else
log "WAL archive cleanup disabled; skipping final cleanup."
fi
log "Backup workflow completed."
@@ -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.")
+41
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power off
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power on
+14
View File
@@ -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): def client_shutdown(client_type, clientid):
lxc_filepath = "/usr/bin/lxc-stop" lxc_filepath = "/usr/bin/lxc-stop"
qemu_filepath = "/usr/sbin/qm" qemu_filepath = f"/usr/sbin/qm shutdown {clientid}"
try: try:
print(f'Stopping {clientid}') print(f'Stopping {clientid}')
if client_type == 'vm': 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': elif client_type == 'lxc':
process_id = os.spawnv(os.P_NOWAIT, lxc_filepath, [client_type, clientid]) process_id = os.spawnv(os.P_NOWAIT, lxc_filepath, [client_type, clientid])
print(process_id) print(process_id)
@@ -37,6 +37,7 @@ def main():
print(f'Start time: {timestamp}') print(f'Start time: {timestamp}')
for clientid in client_list: for clientid in client_list:
client_shutdown(client_type, clientid) client_shutdown(client_type, clientid)
sleep(1)
else: else:
print(f'Successfully stopped client list.') print(f'Successfully stopped client list.')
-276
View File
@@ -1,276 +0,0 @@
#!/bin/bash
set -euo pipefail
# Airsonic Advanced + PostgreSQL diagnostics
# Focus: slow media scans, index efficiency, write pressure, and cache tuning signals.
PGHOST="${PGHOST:-localhost}"
PGPORT="${PGPORT:-5432}"
PGUSER="${PGUSER:-postgres}"
PGDATABASE="${PGDATABASE:-airsonic}"
OUT_FILE="${1:-airsonic_pg_diagnostics_$(date +%Y%m%d_%H%M%S).txt}"
PSQL=(psql -X -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE")
run_section() {
local title="$1"
local sql="$2"
{
echo
echo "================================================================"
echo "$title"
echo "================================================================"
} >> "$OUT_FILE"
if ! "${PSQL[@]}" -c "$sql" >> "$OUT_FILE" 2>&1; then
echo "Query failed for section: $title" >> "$OUT_FILE"
fi
}
query_single_value() {
local sql="$1"
"${PSQL[@]}" -Atc "$sql" 2>/dev/null || true
}
bytes_to_human() {
local bytes="$1"
if [[ -z "$bytes" || ! "$bytes" =~ ^[0-9]+$ ]]; then
echo "unknown"
return
fi
local kib=$((1024))
local mib=$((1024 * 1024))
local gib=$((1024 * 1024 * 1024))
if (( bytes >= gib )); then
awk -v b="$bytes" -v g="$gib" 'BEGIN { printf "%.2f GiB", b/g }'
elif (( bytes >= mib )); then
awk -v b="$bytes" -v m="$mib" 'BEGIN { printf "%.2f MiB", b/m }'
elif (( bytes >= kib )); then
awk -v b="$bytes" -v k="$kib" 'BEGIN { printf "%.2f KiB", b/k }'
else
echo "${bytes} B"
fi
}
{
echo "Airsonic PostgreSQL Diagnostics"
echo "Generated at: $(date -Iseconds)"
echo "Target: host=$PGHOST port=$PGPORT db=$PGDATABASE user=$PGUSER"
} > "$OUT_FILE"
run_section "1) Server identity and uptime" "
SELECT version();
SELECT now() AS current_time, pg_postmaster_start_time() AS postmaster_start_time;
"
run_section "2) Key PostgreSQL tuning parameters" "
SELECT name, setting, unit, short_desc
FROM pg_settings
WHERE name IN (
'shared_buffers',
'work_mem',
'maintenance_work_mem',
'effective_cache_size',
'max_connections',
'effective_io_concurrency',
'random_page_cost',
'seq_page_cost',
'checkpoint_timeout',
'checkpoint_completion_target',
'autovacuum',
'autovacuum_max_workers',
'autovacuum_naptime'
)
ORDER BY name;
"
run_section "3) Database cache hit ratio and temp usage" "
SELECT
datname,
blks_read,
blks_hit,
ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2) AS cache_hit_pct,
temp_files,
pg_size_pretty(temp_bytes) AS temp_bytes,
deadlocks,
stats_reset
FROM pg_stat_database
WHERE datname = current_database();
"
run_section "4) Most read tables and their index-vs-seq scan profile" "
SELECT
relname,
seq_scan,
idx_scan,
n_live_tup,
n_dead_tup,
ROUND((100.0 * idx_scan / NULLIF(idx_scan + seq_scan, 0))::numeric, 2) AS index_scan_pct,
ROUND((100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0))::numeric, 2) AS table_cache_hit_pct
FROM pg_stat_user_tables t
JOIN pg_statio_user_tables io USING (relid)
ORDER BY (seq_scan + idx_scan) DESC NULLS LAST
LIMIT 25;
"
run_section "5) Potentially unused indexes (can slow writes during scans/indexing)" "
SELECT
s.schemaname,
s.relname AS table_name,
s.indexrelname AS index_name,
s.idx_scan,
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
t.n_tup_ins + t.n_tup_upd + t.n_tup_del AS table_writes_since_reset
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
JOIN pg_stat_user_tables t ON t.relid = s.relid
WHERE s.idx_scan = 0
AND NOT i.indisunique
AND NOT i.indisprimary
ORDER BY pg_relation_size(s.indexrelid) DESC
LIMIT 50;
"
run_section "6) Write-heavy tables (hot spot during media metadata indexing)" "
SELECT
relname,
n_tup_ins,
n_tup_upd,
n_tup_del,
n_tup_hot_upd,
n_dead_tup,
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count,
COALESCE(last_autovacuum::text, 'never') AS last_autovacuum,
COALESCE(last_autoanalyze::text, 'never') AS last_autoanalyze
FROM pg_stat_user_tables
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC
LIMIT 25;
"
run_section "7) Largest relations (tables and indexes)" "
SELECT
n.nspname AS schema_name,
c.relname,
c.relkind,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND c.relkind IN ('r', 'i')
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 30;
"
if [[ "$(query_single_value "SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements';")" == "1" ]]; then
run_section "8) Slowest normalized SQL from pg_stat_statements" "
SELECT
calls,
ROUND(total_exec_time::numeric, 2) AS total_exec_ms,
ROUND((total_exec_time / NULLIF(calls, 0))::numeric, 2) AS mean_exec_ms,
ROUND(rows::numeric / NULLIF(calls, 0), 2) AS avg_rows,
shared_blks_read,
shared_blks_hit,
temp_blks_read,
temp_blks_written,
LEFT(query, 300) AS query_sample
FROM pg_stat_statements
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY mean_exec_ms DESC
LIMIT 20;
"
else
{
echo
echo "================================================================"
echo "8) Slowest normalized SQL from pg_stat_statements"
echo "================================================================"
echo "Extension pg_stat_statements is not installed."
echo "Install with: CREATE EXTENSION pg_stat_statements;"
} >> "$OUT_FILE"
fi
# Heuristic recommendation summary
CACHE_HIT_PCT="$(query_single_value "
SELECT ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2)
FROM pg_stat_database
WHERE datname = current_database();")"
UNUSED_INDEX_COUNT="$(query_single_value "
SELECT COUNT(*)
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
AND NOT i.indisunique
AND NOT i.indisprimary;")"
SHARED_BUFFERS_BYTES="$(query_single_value "
SELECT pg_size_bytes(setting || unit)
FROM pg_settings
WHERE name = 'shared_buffers';")"
TOTAL_RAM_KB=""
if [[ -r /proc/meminfo ]]; then
TOTAL_RAM_KB="$(awk '/MemTotal:/ {print $2}' /proc/meminfo)"
fi
{
echo
echo "================================================================"
echo "9) Recommendations summary"
echo "================================================================"
if [[ -n "$CACHE_HIT_PCT" ]]; then
echo "Cache hit ratio: ${CACHE_HIT_PCT}%"
awk -v hit="$CACHE_HIT_PCT" 'BEGIN {
if (hit < 98.0) {
print "- Action: cache hit ratio is low for media workloads. Increase shared_buffers/effective_cache_size and review slow seq scans."
} else if (hit < 99.0) {
print "- Action: cache hit ratio is acceptable but can likely improve for large libraries."
} else {
print "- Action: cache hit ratio looks healthy."
}
}'
else
echo "Cache hit ratio: unavailable"
fi
if [[ -n "$UNUSED_INDEX_COUNT" ]]; then
echo "Potentially unused non-unique indexes: $UNUSED_INDEX_COUNT"
if [[ "$UNUSED_INDEX_COUNT" =~ ^[0-9]+$ ]] && (( UNUSED_INDEX_COUNT > 0 )); then
echo "- Action: validate and drop truly unused indexes to reduce write overhead during scans/indexing."
else
echo "- Action: no obvious unused non-unique indexes from current stats."
fi
fi
if [[ -n "$SHARED_BUFFERS_BYTES" ]]; then
echo "shared_buffers: $(bytes_to_human "$SHARED_BUFFERS_BYTES")"
if [[ -n "$TOTAL_RAM_KB" && "$TOTAL_RAM_KB" =~ ^[0-9]+$ ]]; then
TOTAL_RAM_BYTES=$((TOTAL_RAM_KB * 1024))
SHARED_BUFFER_PCT=$(awk -v s="$SHARED_BUFFERS_BYTES" -v t="$TOTAL_RAM_BYTES" 'BEGIN { printf "%.2f", (s/t)*100 }')
echo "Estimated shared_buffers/system RAM: ${SHARED_BUFFER_PCT}%"
awk -v p="$SHARED_BUFFER_PCT" 'BEGIN {
if (p < 10.0) {
print "- Action: shared_buffers appears low; typical starting point is ~15-25% of system RAM."
} else if (p > 40.0) {
print "- Action: shared_buffers appears high; ensure OS cache remains adequate."
} else {
print "- Action: shared_buffers proportion looks reasonable."
}
}'
else
echo "- Action: compare shared_buffers to system RAM manually (target often 15-25% for dedicated DB hosts)."
fi
fi
echo "- Action: for media scans, verify high-read tables use selective indexes and that autovacuum keeps dead tuples controlled."
echo "- Action: rerun this script before and during a scan to compare write pressure and cache behavior over time."
} >> "$OUT_FILE"
echo "Diagnostics complete. Report written to: $OUT_FILE"
-536
View File
@@ -1,536 +0,0 @@
#Requires -Version 7.0
# =============================================================================
# batch_rsync.ps1 — Parallel rsync launcher for Windows PowerShell
# =============================================================================
#
# KEY IMPLEMENTATION NOTES
# ------------------------
#
# Parallelism
# Uses RunspacePool + [PowerShell]::Create() so multiple rsync processes
# run concurrently without blocking the main thread. All runspace script
# blocks are self-contained (no closures over outer variables).
#
# As-completed result collection
# WaitHandle.WaitAny() polls the async handles so results are printed as
# soon as each task finishes, matching the behaviour of Python's as_completed().
#
# --files-from (recursive-files mode)
# rsync is fed a NUL-delimited temp file via --from0 --files-from=<tmpfile>
# rather than stdin, because stdin is not reliably available inside a
# runspace. The temp file is always cleaned up in a finally block.
#
# Path handling
# All source paths are converted from Windows backslashes to forward slashes
# before being passed to rsync, which is required for MSYS2/Cygwin rsync
# to interpret them correctly (e.g. C:/data/... not C:\data\...).
#
# Argument passing to rsync
# Uses ProcessStartInfo.ArgumentList (System.Collections.Generic.List<string>)
# rather than a single argument string. This avoids quoting/escaping issues
# with paths that contain spaces or special characters.
# Requires .NET 5+ — hence the #Requires -Version 7.0 constraint.
#
# Auto-tune worker selection
# Workers are derived from average file size and destination type (local vs
# remote). Smaller files favour higher concurrency; large files and remote
# destinations are throttled to avoid saturating the link or dest I/O.
# The -Workers value is always treated as an upper bound.
#
# Size-balanced chunk partitioning (recursive-files mode)
# Files are sorted largest-first and distributed across worker buckets with
# a greedy least-loaded algorithm to minimise the slowest-worker stall.
#
# Remote destination detection
# Heuristics check for rsync://, :: (daemon), Windows drive letters, UNC
# paths, and user@host:/path patterns to decide whether the destination is
# local or remote without requiring an actual connection.
#
# Verbose output
# Uses PowerShell's built-in -Verbose switch (CmdletBinding) so there is no
# custom flag. Pass -Verbose on the command line to enable detailed output.
#
# Requirements
# - PowerShell 7.0+ (for ProcessStartInfo.ArgumentList via .NET 5+)
# - rsync on PATH (MSYS2, Cygwin, or WSL interop on Windows)
#
# =============================================================================
<#
.SYNOPSIS
Parallel rsync launcher for Windows PowerShell.
.DESCRIPTION
Runs multiple rsync invocations in parallel using either top-level entries
or balanced recursive file chunks. Requires rsync on PATH (e.g. via MSYS2
or Cygwin on Windows).
Paths are converted to forward-slash notation for rsync compatibility.
MSYS2 rsync accepts Windows absolute paths using forward slashes (C:/path/...).
.EXAMPLE
# Basic sync (recursive-files mode, auto worker count):
.\batch_rsync.ps1 C:\data user@host:/dest/
.EXAMPLE
# Auto-tune workers based on file size and local vs remote destination:
.\batch_rsync.ps1 C:\data user@host:/dest/ -AutoTune
.EXAMPLE
# Limit to a specific number of parallel workers:
.\batch_rsync.ps1 C:\data D:\backup -Workers 8
.EXAMPLE
# Use top-level mode (one rsync process per immediate source entry):
.\batch_rsync.ps1 C:\data D:\backup -Mode top-level
.EXAMPLE
# Dry run (show what would be transferred without doing it):
.\batch_rsync.ps1 C:\data user@host:/dest/ -DryRun
.EXAMPLE
# Verbose output (command templates, task previews, elapsed time):
.\batch_rsync.ps1 C:\data D:\backup -Verbose
.EXAMPLE
# Show rsync output for all tasks:
.\batch_rsync.ps1 C:\data D:\backup -ShowRsyncOutput
.EXAMPLE
# Show rsync output only when a task fails:
.\batch_rsync.ps1 C:\data user@host:/dest/ -ShowRsyncOutputOnFailure
.EXAMPLE
# Full-featured (auto-tune, verbose, show on failure):
.\batch_rsync.ps1 C:\data user@host:/dest/ -AutoTune -Verbose -ShowRsyncOutputOnFailure
.EXAMPLE
# Override rsync flags entirely:
.\batch_rsync.ps1 C:\data D:\backup -RsyncArgs '-az --checksum'
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0)]
[string]$Src,
[Parameter(Mandatory, Position = 1)]
[string]$Dest,
[ValidateRange(1, [int]::MaxValue)]
[int]$Workers = [Math]::Min(8, [Environment]::ProcessorCount),
[ValidateSet('recursive-files', 'top-level')]
[string]$Mode = 'recursive-files',
[string]$RsyncArgs = '-a --partial --info=stats2',
[switch]$AutoTune,
[switch]$ShowRsyncOutput,
[switch]$ShowRsyncOutputOnFailure,
[switch]$DryRun
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
#region Helper functions
function Format-Bytes {
param([double]$Bytes)
$units = @('B', 'KB', 'MB', 'GB', 'TB')
$value = $Bytes
foreach ($unit in $units) {
if ($value -lt 1024 -or $unit -eq 'TB') {
return '{0:F1}{1}' -f $value, $unit
}
$value /= 1024
}
return '{0:F1}B' -f $Bytes
}
function Test-RemoteDestination {
param([string]$Destination)
if ($Destination -match '^rsync://') { return $true }
if ($Destination -match '::') { return $true }
# Windows drive letters and UNC paths are local
if ($Destination -match '^[a-zA-Z]:[/\\]') { return $false }
if ($Destination -match '^\\\\') { return $false }
# user@host:/path or host:/path patterns are remote
return $Destination -match '^[^:/\\]+@?[^:/\\]*:[^/]'
}
function Get-AutoTunedWorkers {
param(
[int] $TotalUnits,
[double]$AvgSizeBytes,
[bool] $IsRemote,
[int] $CpuCount,
[int] $MaxWorkers
)
$cpu = [Math]::Max(1, $CpuCount)
$MB1 = 1 * 1024 * 1024
$MB32 = 32 * 1024 * 1024
$MB256 = 256 * 1024 * 1024
if ($AvgSizeBytes -lt $MB1) {
$base = [Math]::Min(32, $cpu * 4)
} elseif ($AvgSizeBytes -lt $MB32) {
$base = [Math]::Min(16, $cpu * 2)
} elseif ($AvgSizeBytes -lt $MB256) {
$base = [Math]::Min(8, $cpu)
} else {
$base = [Math]::Min(4, [Math]::Max(2, [int]($cpu / 2)))
}
if ($IsRemote) {
$base = [Math]::Max(2, [Math]::Min($base, 8))
} else {
$base = [Math]::Min($base + 2, 24)
}
$tuned = [Math]::Max(1, [Math]::Min($base, $TotalUnits))
return [Math]::Min($tuned, $MaxWorkers)
}
function Get-RecursiveFileList {
param([string]$SrcDir)
$root = $SrcDir.TrimEnd('\', '/')
Get-ChildItem -LiteralPath $root -Recurse -File |
ForEach-Object { $_.FullName.Substring($root.Length).TrimStart('\', '/') }
}
function Get-SizedFiles {
param([string]$SrcDir, [string[]]$RelFiles)
foreach ($rel in $RelFiles) {
$abs = Join-Path $SrcDir $rel
try { $size = (Get-Item -LiteralPath $abs -ErrorAction Stop).Length }
catch { $size = 0 }
[PSCustomObject]@{ Size = [long]$size; RelPath = $rel }
}
}
function Get-BalancedChunks {
param($SizedFiles, [int]$Workers)
$buckets = @(1..$Workers | ForEach-Object { [System.Collections.Generic.List[string]]::new() })
$bucketSizes = [long[]]::new($Workers)
foreach ($entry in ($SizedFiles | Sort-Object -Property Size -Descending)) {
$minIdx = 0
for ($i = 1; $i -lt $Workers; $i++) {
if ($bucketSizes[$i] -lt $bucketSizes[$minIdx]) { $minIdx = $i }
}
$buckets[$minIdx].Add($entry.RelPath)
$bucketSizes[$minIdx] += $entry.Size
}
$buckets | Where-Object { $_.Count -gt 0 }
}
#endregion
#region Runspace script blocks
# These blocks run inside isolated runspaces — all needed logic must be self-contained.
$rsyncChunkScriptBlock = {
param(
[string] $SrcDir,
[string[]]$RelFiles,
[string] $Dest,
[string[]]$RsyncArgsList,
[bool] $IsDryRun
)
$filesFromTmp = [System.IO.Path]::GetTempFileName()
try {
# Build a NUL-delimited file list using forward slashes for rsync
$lines = $RelFiles | ForEach-Object { $_ -replace '\\', '/' }
$allBytes = [System.Collections.Generic.List[byte]]::new()
foreach ($line in $lines) {
$allBytes.AddRange([System.Text.Encoding]::UTF8.GetBytes($line))
$allBytes.Add([byte]0)
}
[System.IO.File]::WriteAllBytes($filesFromTmp, $allBytes.ToArray())
$srcRoot = ($SrcDir.TrimEnd('\', '/') -replace '\\', '/') + '/'
$cmdArgs = [System.Collections.Generic.List[string]]::new($RsyncArgsList)
if ($IsDryRun) { $cmdArgs.Insert(0, '--dry-run') }
$cmdArgs.AddRange([string[]]@('--from0', "--files-from=$filesFromTmp", $srcRoot, $Dest))
$psi = [System.Diagnostics.ProcessStartInfo]::new('rsync')
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
foreach ($arg in $cmdArgs) { $psi.ArgumentList.Add($arg) }
try {
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
$exitCode = $proc.ExitCode
} catch {
$stdout = ''
$stderr = "rsync not found on PATH: $($_.Exception.Message)"
$exitCode = 127
}
return [PSCustomObject]@{
Label = "chunk($($RelFiles.Count) files)"
ExitCode = $exitCode
Stdout = $stdout
Stderr = $stderr
}
} finally {
Remove-Item -LiteralPath $filesFromTmp -Force -ErrorAction SilentlyContinue
}
}
$rsyncItemScriptBlock = {
param(
[string] $Item,
[string] $Dest,
[string[]]$RsyncArgsList,
[bool] $IsDryRun
)
$srcPath = $Item -replace '\\', '/'
$cmdArgs = [System.Collections.Generic.List[string]]::new($RsyncArgsList)
if ($IsDryRun) { $cmdArgs.Insert(0, '--dry-run') }
$cmdArgs.Add($srcPath)
$cmdArgs.Add($Dest)
$psi = [System.Diagnostics.ProcessStartInfo]::new('rsync')
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
foreach ($arg in $cmdArgs) { $psi.ArgumentList.Add($arg) }
try {
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
$exitCode = $proc.ExitCode
} catch {
$stdout = ''
$stderr = "rsync not found on PATH: $($_.Exception.Message)"
$exitCode = 127
}
return [PSCustomObject]@{
Label = $Item
ExitCode = $exitCode
Stdout = $stdout
Stderr = $stderr
}
}
#endregion
#region Parallel job runner (as-completed using WaitHandle.WaitAny)
function Invoke-ParallelJobs {
param(
[object[]] $Jobs, # PSCustomObjects with .PS and .Handle
[bool] $ShowOutput,
[bool] $ShowOnFailOnly
)
$results = [System.Collections.Generic.List[object]]::new()
$pending = [System.Collections.Generic.List[object]]::new()
foreach ($j in $Jobs) { $pending.Add($j) }
while ($pending.Count -gt 0) {
$handles = @($pending | ForEach-Object { $_.Handle.AsyncWaitHandle })
$idx = [System.Threading.WaitHandle]::WaitAny($handles)
$job = $pending[$idx]
$pending.RemoveAt($idx)
$r = $job.PS.EndInvoke($job.Handle)[0]
$job.PS.Dispose()
$results.Add($r)
$status = if ($r.ExitCode -eq 0) { 'OK' } else { "ERR($($r.ExitCode))" }
Write-Host "[$status] $($r.Label)"
$printOutput = $ShowOutput -and (-not $ShowOnFailOnly -or $r.ExitCode -ne 0)
if ($printOutput) {
if ($r.Stdout.Trim()) {
Write-Host "--- stdout: $($r.Label) ---"
Write-Host $r.Stdout.TrimEnd()
}
if ($r.Stderr.Trim()) {
Write-Warning "--- stderr: $($r.Label) ---`n$($r.Stderr.TrimEnd())"
}
}
}
return $results
}
#endregion
#region Main
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$rsyncArgsList = $RsyncArgs -split '\s+' | Where-Object { $_ -ne '' }
$srcPath = $Src.TrimEnd('\', '/')
$isRemote = Test-RemoteDestination -Destination $Dest
$cpuCount = [Environment]::ProcessorCount
$showOutput = $ShowRsyncOutput -or $ShowRsyncOutputOnFailure
$showOnFail = $ShowRsyncOutputOnFailure -and -not $ShowRsyncOutput
if ($ShowRsyncOutput -and $ShowRsyncOutputOnFailure) {
Write-Warning 'Both -ShowRsyncOutput and -ShowRsyncOutputOnFailure set; showing output for all tasks.'
}
$allResults = $null
if ($Mode -eq 'recursive-files' -and (Test-Path -LiteralPath $srcPath -PathType Container)) {
$relFiles = @(Get-RecursiveFileList -SrcDir $srcPath)
if (-not $relFiles -or $relFiles.Count -eq 0) {
Write-Error "No files found for: $Src"
exit 2
}
$sizedFiles = @(Get-SizedFiles -SrcDir $srcPath -RelFiles $relFiles)
$totalBytes = ($sizedFiles | Measure-Object -Property Size -Sum).Sum
$avgSize = if ($sizedFiles.Count -gt 0) { $totalBytes / $sizedFiles.Count } else { 0 }
if ($AutoTune) {
$effectiveWorkers = Get-AutoTunedWorkers -TotalUnits $sizedFiles.Count -AvgSizeBytes $avgSize `
-IsRemote $isRemote -CpuCount $cpuCount -MaxWorkers $Workers
$destKind = if ($isRemote) { 'remote' } else { 'local' }
Write-Host "Auto-tuned workers=$effectiveWorkers ($destKind dest, avg file $(Format-Bytes $avgSize))"
} else {
$effectiveWorkers = [Math]::Min($Workers, $sizedFiles.Count)
}
$chunks = @(Get-BalancedChunks -SizedFiles $sizedFiles -Workers $effectiveWorkers)
Write-Host "Starting $($chunks.Count) rsync chunk task(s) with $effectiveWorkers worker(s)..."
if ($VerbosePreference -ne 'SilentlyContinue') {
$srcRoot = ($srcPath -replace '\\', '/') + '/'
$previewArgs = (@('rsync') + $rsyncArgsList + @('--from0', '--files-from=<tmpfile>', $srcRoot, $Dest))
if ($DryRun) { $previewArgs = @('rsync', '--dry-run') + $previewArgs[1..($previewArgs.Count-1)] }
Write-Verbose "Command template: $($previewArgs -join ' ')"
}
$pool = [RunspaceFactory]::CreateRunspacePool(1, $effectiveWorkers)
$pool.Open()
$jobs = foreach ($chunk in $chunks) {
$ps = [PowerShell]::Create()
$ps.RunspacePool = $pool
[void]$ps.AddScript($rsyncChunkScriptBlock)
[void]$ps.AddParameter('SrcDir', $srcPath)
[void]$ps.AddParameter('RelFiles', [string[]]$chunk)
[void]$ps.AddParameter('Dest', $Dest)
[void]$ps.AddParameter('RsyncArgsList', [string[]]$rsyncArgsList)
[void]$ps.AddParameter('IsDryRun', $DryRun.IsPresent)
[PSCustomObject]@{ PS = $ps; Handle = $ps.BeginInvoke() }
}
$allResults = Invoke-ParallelJobs -Jobs $jobs -ShowOutput $showOutput -ShowOnFailOnly $showOnFail
$pool.Close()
$pool.Dispose()
} else {
if (Test-Path -LiteralPath $srcPath -PathType Container) {
$items = @(Get-ChildItem -LiteralPath $srcPath | ForEach-Object { $_.FullName })
} elseif ($srcPath -match '[*?]') {
$items = @(Resolve-Path -Path $srcPath | ForEach-Object { $_.ProviderPath })
} else {
$items = @($srcPath)
}
if (-not $items -or $items.Count -eq 0) {
Write-Error "No files found for: $Src"
exit 2
}
$sizeSamples = foreach ($item in $items) {
try {
$fi = Get-Item -LiteralPath $item -ErrorAction Stop
if ($fi.PSIsContainer) { [long](64 * 1024 * 1024) } else { $fi.Length }
} catch { [long]0 }
}
$totalBytes = ($sizeSamples | Measure-Object -Sum).Sum
$avgSize = if ($items.Count -gt 0) { $totalBytes / $items.Count } else { 0 }
if ($AutoTune) {
$effectiveWorkers = Get-AutoTunedWorkers -TotalUnits $items.Count -AvgSizeBytes $avgSize `
-IsRemote $isRemote -CpuCount $cpuCount -MaxWorkers $Workers
$destKind = if ($isRemote) { 'remote' } else { 'local' }
Write-Host "Auto-tuned workers=$effectiveWorkers ($destKind dest, avg unit $(Format-Bytes $avgSize))"
} else {
$effectiveWorkers = [Math]::Min($Workers, $items.Count)
}
Write-Host "Starting $($items.Count) rsync task(s) with $effectiveWorkers worker(s)..."
if ($VerbosePreference -ne 'SilentlyContinue') {
Write-Verbose 'Task list preview:'
$previewItems = $items | Select-Object -First 10
for ($i = 0; $i -lt $previewItems.Count; $i++) {
Write-Verbose (' {0}. {1}' -f ($i + 1), $previewItems[$i])
}
if ($items.Count -gt 10) { Write-Verbose " ... and $($items.Count - 10) more" }
}
$pool = [RunspaceFactory]::CreateRunspacePool(1, $effectiveWorkers)
$pool.Open()
$jobs = foreach ($item in $items) {
if ($VerbosePreference -ne 'SilentlyContinue') {
$previewArgs = (@('rsync') + $rsyncArgsList + @(($item -replace '\\', '/'), $Dest))
if ($DryRun) { $previewArgs = @('rsync', '--dry-run') + $previewArgs[1..($previewArgs.Count-1)] }
Write-Verbose "Scheduling: $($previewArgs -join ' ')"
}
$ps = [PowerShell]::Create()
$ps.RunspacePool = $pool
[void]$ps.AddScript($rsyncItemScriptBlock)
[void]$ps.AddParameter('Item', $item)
[void]$ps.AddParameter('Dest', $Dest)
[void]$ps.AddParameter('RsyncArgsList', [string[]]$rsyncArgsList)
[void]$ps.AddParameter('IsDryRun', $DryRun.IsPresent)
[PSCustomObject]@{ PS = $ps; Handle = $ps.BeginInvoke() }
}
$allResults = Invoke-ParallelJobs -Jobs $jobs -ShowOutput $showOutput -ShowOnFailOnly $showOnFail
$pool.Close()
$pool.Dispose()
}
#endregion
#region Summary
$failed = @($allResults | Where-Object { $_.ExitCode -ne 0 })
$elapsed = [Math]::Round($timer.Elapsed.TotalSeconds, 2)
Write-Host "`nCompleted: $($allResults.Count) tasks, $($failed.Count) failed in ${elapsed}s."
if ($failed.Count -gt 0) {
Write-Host 'Failures detail:'
foreach ($r in $failed) {
Write-Host "--- $($r.Label) (exit $($r.ExitCode)) ---"
if ($r.Stdout.Trim()) { Write-Host $r.Stdout.TrimEnd() }
if ($r.Stderr.Trim()) { Write-Host $r.Stderr.TrimEnd() }
}
exit 1
}
exit 0
#endregion
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env python3
"""Fix the powerball CSV file format."""
import shutil
# Read the original file
with open('powerball_numbers.csv', 'r') as f:
lines = f.readlines()
# Create backup
shutil.copy('powerball_numbers.csv', 'powerball_numbers.csv.backup')
# Open output file
with open('powerball_numbers.csv', 'w') as f:
# Write header
f.write('Draw Date,Winning Numbers,Multiplier\n')
# Process each line
for line in lines:
parts = line.strip().split(',')
# Extract components
month = parts[0]
day_year = parts[1].split('/')
day = day_year[0]
year = day_year[1]
# Format date as MM/DD/YYYY
date = f"{int(month):02d}/{int(day):02d}/{year}"
# Get the 6 winning numbers (5 white balls + 1 powerball)
numbers = ' '.join(parts[2:8])
# Get the multiplier
multiplier = parts[8]
# Write the formatted line
f.write(f'{date},{numbers},{multiplier}\n')
print("✓ File fixed! Created backup at powerball_numbers.csv.backup")
print("\nFirst few lines of fixed file:")
with open('powerball_numbers.csv', 'r') as f:
for i, line in enumerate(f):
if i < 5:
print(line.strip())
else:
break
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/python3
import argparse
import configparser
import os
import sys
import urllib3
import redfish
# Suppress SSL warnings for self-signed iLO certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
parser.add_argument("hostname", help="iLO hostname or IP address")
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
help="Path to credentials file (default: /etc/ilo_credentials)")
args = parser.parse_args()
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
# File format:
# [ilo]
# username = root
# password = yourpassword
cred_path = args.credentials
if not os.path.exists(cred_path):
print(f"Error: credentials file not found: {cred_path}", file=sys.stderr)
sys.exit(1)
config = configparser.ConfigParser()
config.read(cred_path)
try:
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
LOGIN_PASSWORD = config.get("ilo", "password")
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Error reading credentials file: {e}", file=sys.stderr)
sys.exit(1)
BASE_URL = f"https://{args.hostname}"
REDFISHOBJ = redfish.RedfishClient(
base_url=BASE_URL,
username=LOGIN_ACCOUNT,
password=LOGIN_PASSWORD,
)
REDFISHOBJ.login(auth="session")
# Discover the correct reset action URI from the system resource
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
response = REDFISHOBJ.post(reset_uri, body=body)
print(f"Status: {response.status}")
if response.status >= 400:
print(f"Error: {response.read}")
REDFISHOBJ.logout()
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
import subprocess
import sys
DEFAULT_EK = "0000000000000000000000000000000000000000"
def load_credentials(path):
if not os.path.exists(path):
print(f"Error: credentials file not found: {path}", file=sys.stderr)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
try:
username = config.get("credentials", "username", fallback="root")
password = config.get("credentials", "password")
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Error reading credentials file: {e}", file=sys.stderr)
sys.exit(1)
return username, password, encryption_key
def main():
p = argparse.ArgumentParser(description="Send IPMI power off")
p.add_argument("-H", "--host", required=True, help="IPMI host address")
p.add_argument(
"-c",
"--credentials",
default="credentials",
help="Path to credentials file (default: credentials)",
)
args = p.parse_args()
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
cmd = [
"ipmitool",
"-I", "lanplus",
"-H", args.host,
"-U", ipmi_user,
"-P", ipmi_pw,
"-y", ipmi_ek,
"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()
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
import argparse
import configparser
import os
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}")
DEFAULT_EK = "0000000000000000000000000000000000000000"
def load_credentials(path):
if not os.path.exists(path):
print(f"Error: credentials file not found: {path}", file=sys.stderr)
sys.exit(1)
config = configparser.ConfigParser()
config.read(path)
try:
username = config.get("ilo", "username", fallback="root")
password = config.get("ilo", "password")
encryption_key = config.get("ilo", "encryption_key", fallback=DEFAULT_EK)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Error reading credentials file: {e}", file=sys.stderr)
sys.exit(1)
return username, password, encryption_key
def main():
p = argparse.ArgumentParser(description="Send IPMI power on")
p.add_argument("-H", "--host", required=True, help="IPMI host address")
p.add_argument(
"-c",
"--credentials",
default="credentials",
help="Path to credentials file (default: credentials)",
)
args = p.parse_args()
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
cmd = [
"ipmitool",
"-I", "lanplus",
"-H", args.host,
"-U", ipmi_user,
"-P", ipmi_pw,
"-y", ipmi_ek,
"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()
-268
View File
@@ -1,268 +0,0 @@
param(
[string]$PgHost = "localhost",
[int]$PgPort = 5432,
[string]$PgUser = "postgres",
[string]$BenchDb = "pgbench",
[int]$Scale = 50,
[int]$Duration = 60,
[string]$Clients = "1 4 8 16 32",
[int]$Jobs = 4,
[int]$WarmupSeconds = 15,
[bool]$RunReadOnly = $true,
[bool]$RunCustom = $false,
[string]$CustomSqlFile = "pgbench_custom_workload.sql",
[string]$CustomModeName = "custom_sql",
[ValidateRange(1, 99)][int]$CustomReadPct = 80,
[ValidateRange(1, 100)][int]$CustomHotPct = 90,
[ValidateRange(1, 4)][int]$CustomTxnSize = 2,
[ValidateRange(100, 100000000)][int]$CustomHotAccounts = 10000,
[bool]$Reinit = $false
)
$ErrorActionPreference = "Stop"
function Require-Command {
param([Parameter(Mandatory = $true)][string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "Missing required command: $Name"
}
}
function Invoke-PsqlScalar {
param(
[Parameter(Mandatory = $true)][string]$Database,
[Parameter(Mandatory = $true)][string]$Sql
)
$output = & psql -X -v ON_ERROR_STOP=1 -h $PgHost -p $PgPort -U $PgUser -d $Database -Atc $Sql
if ($LASTEXITCODE -ne 0) {
throw "psql failed running SQL: $Sql"
}
return ($output | Select-Object -First 1)
}
function Invoke-PsqlNonQuery {
param(
[Parameter(Mandatory = $true)][string]$Database,
[Parameter(Mandatory = $true)][string]$Sql
)
& psql -X -v ON_ERROR_STOP=1 -h $PgHost -p $PgPort -U $PgUser -d $Database -c $Sql | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "psql failed running SQL: $Sql"
}
}
function Ensure-BenchmarkDatabase {
$exists = Invoke-PsqlScalar -Database "postgres" -Sql "SELECT 1 FROM pg_database WHERE datname = '$BenchDb';"
if ($exists -ne "1") {
Write-Host "Creating benchmark database: $BenchDb"
Invoke-PsqlNonQuery -Database "postgres" -Sql "CREATE DATABASE $BenchDb;"
}
}
function Needs-Init {
$hasTable = Invoke-PsqlScalar -Database $BenchDb -Sql "SELECT to_regclass('public.pgbench_accounts') IS NOT NULL;"
return $hasTable -ne "t"
}
function Init-OrReinitData {
if ($Reinit -or (Needs-Init)) {
Write-Host "Initializing pgbench schema/data (scale=$Scale)"
& pgbench -h $PgHost -p $PgPort -U $PgUser -i -s $Scale $BenchDb | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "pgbench initialization failed"
}
}
else {
Write-Host "Using existing pgbench data in $BenchDb"
}
}
function Parse-Tps {
param([Parameter(Mandatory = $true)][string]$Text)
$matches = [regex]::Matches($Text, "(?m)^tps\s*=\s*([0-9]+(?:\.[0-9]+)?)")
if ($matches.Count -gt 0) {
return $matches[$matches.Count - 1].Groups[1].Value
}
return ""
}
function Parse-AvgLatencyMs {
param([Parameter(Mandatory = $true)][string]$Text)
$matches = [regex]::Matches($Text, "(?m)^latency average\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*ms")
if ($matches.Count -gt 0) {
return $matches[$matches.Count - 1].Groups[1].Value
}
return ""
}
function Run-Case {
param(
[Parameter(Mandatory = $true)][string]$Mode,
[Parameter(Mandatory = $true)][int]$ClientCount,
[AllowEmptyCollection()][string[]]$ExtraArgs = @(),
[Parameter(Mandatory = $true)][string]$ResultDir,
[Parameter(Mandatory = $true)][string]$SummaryCsv
)
$logFile = Join-Path $ResultDir ("{0}_c{1}.txt" -f $Mode, $ClientCount)
$txLogPrefix = Join-Path $ResultDir ("{0}_c{1}_txlog" -f $Mode, $ClientCount)
Write-Host "Running mode=$Mode, clients=$ClientCount, duration=${Duration}s"
# Warm cache and avoid measuring first-run effects.
& pgbench -h $PgHost -p $PgPort -U $PgUser -n -M prepared -j $Jobs -c $ClientCount -T $WarmupSeconds @ExtraArgs $BenchDb | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Warm-up run failed for mode=$Mode clients=$ClientCount"
}
$runOutput = & pgbench -h $PgHost -p $PgPort -U $PgUser -n -M prepared -j $Jobs -c $ClientCount -T $Duration -r -l --log-prefix $txLogPrefix @ExtraArgs $BenchDb 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Measured run failed for mode=$Mode clients=$ClientCount"
}
$runOutput | Set-Content -Path $logFile -Encoding UTF8
$text = ($runOutput -join [Environment]::NewLine)
$tps = Parse-Tps -Text $text
$latencyMs = Parse-AvgLatencyMs -Text $text
$percentiles = Get-PercentileLatency -LogPrefix $txLogPrefix
"$Mode,$ClientCount,$Duration,$Jobs,$tps,$latencyMs,$($percentiles.P95Ms),$($percentiles.P99Ms),$logFile" | Add-Content -Path $SummaryCsv
}
function Get-PercentileLatency {
param([Parameter(Mandatory = $true)][string]$LogPrefix)
$logFiles = Get-ChildItem -Path ($LogPrefix + "*") -File -ErrorAction SilentlyContinue
if (-not $logFiles -or $logFiles.Count -eq 0) {
return @{ P95Ms = ""; P99Ms = "" }
}
# Keep memory bounded even for very large transaction logs.
$maxSampleSize = 200000
$reservoir = New-Object System.Collections.Generic.List[double]
$reservoir.Capacity = $maxSampleSize
$random = [System.Random]::new()
$seen = 0
foreach ($file in $logFiles) {
$reader = [System.IO.File]::OpenText($file.FullName)
try {
while (($line = $reader.ReadLine()) -ne $null) {
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) {
continue
}
$tokens = $line -split "[\s,]+" | Where-Object { $_ -ne "" }
if ($tokens.Count -ge 3 -and $tokens[2] -match "^[0-9]+(?:\.[0-9]+)?$") {
$latUs = [double]$tokens[2]
$seen++
if ($reservoir.Count -lt $maxSampleSize) {
$reservoir.Add($latUs)
}
else {
$idx = $random.Next(0, $seen)
if ($idx -lt $maxSampleSize) {
$reservoir[$idx] = $latUs
}
}
}
}
}
finally {
$reader.Dispose()
}
}
if ($reservoir.Count -eq 0) {
return @{ P95Ms = ""; P99Ms = "" }
}
$sorted = $reservoir | Sort-Object
$p95Us = Select-PercentileValue -SortedValues $sorted -Percentile 95
$p99Us = Select-PercentileValue -SortedValues $sorted -Percentile 99
return @{
P95Ms = [math]::Round(($p95Us / 1000.0), 3)
P99Ms = [math]::Round(($p99Us / 1000.0), 3)
}
}
function Select-PercentileValue {
param(
[Parameter(Mandatory = $true)][double[]]$SortedValues,
[Parameter(Mandatory = $true)][ValidateRange(1, 99)][int]$Percentile
)
$n = $SortedValues.Count
$rank = [math]::Ceiling(($Percentile / 100.0) * $n)
if ($rank -lt 1) { $rank = 1 }
if ($rank -gt $n) { $rank = $n }
return $SortedValues[$rank - 1]
}
function Resolve-CustomSqlPath {
if ([System.IO.Path]::IsPathRooted($CustomSqlFile)) {
return $CustomSqlFile
}
return (Join-Path (Get-Location) $CustomSqlFile)
}
Require-Command -Name "psql"
Require-Command -Name "pgbench"
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$resultDir = Join-Path (Get-Location) ("pgbench_results_{0}" -f $timestamp)
$summaryCsv = Join-Path $resultDir "summary.csv"
New-Item -Path $resultDir -ItemType Directory -Force | Out-Null
"mode,clients,duration_seconds,jobs,tps,latency_ms,p95_ms,p99_ms,details_file" | Set-Content -Path $summaryCsv -Encoding UTF8
Ensure-BenchmarkDatabase
Init-OrReinitData
$customSqlPath = ""
if ($RunCustom) {
$customSqlPath = Resolve-CustomSqlPath
if (-not (Test-Path -Path $customSqlPath -PathType Leaf)) {
throw "Custom SQL file not found: $customSqlPath"
}
}
$clientValues = $Clients -split "\s+" | Where-Object { $_ -match "^[0-9]+$" }
if ($clientValues.Count -eq 0) {
throw "No valid client values found. Example: -Clients '1 4 8 16 32'"
}
foreach ($c in $clientValues) {
$clientInt = [int]$c
Run-Case -Mode "read_write" -ClientCount $clientInt -ExtraArgs @() -ResultDir $resultDir -SummaryCsv $summaryCsv
if ($RunReadOnly) {
Run-Case -Mode "read_only" -ClientCount $clientInt -ExtraArgs @("-S") -ResultDir $resultDir -SummaryCsv $summaryCsv
}
if ($RunCustom) {
Run-Case -Mode $CustomModeName -ClientCount $clientInt -ExtraArgs @(
"-D", "read_pct=$CustomReadPct",
"-D", "hot_pct=$CustomHotPct",
"-D", "txn_size=$CustomTxnSize",
"-D", "hot_accounts=$CustomHotAccounts",
"-f", $customSqlPath
) -ResultDir $resultDir -SummaryCsv $summaryCsv
}
}
Write-Host ""
Write-Host "Benchmark completed."
Write-Host "Summary: $summaryCsv"
Write-Host "Raw outputs: $resultDir"
-188
View File
@@ -1,188 +0,0 @@
#!/bin/bash
set -euo pipefail
# Repeatable pgbench benchmark harness.
# Usage example:
# PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=secret \
# ./pgbench_benchmark.sh
#
# Optional environment variables:
# BENCH_DB=pgbench
# SCALE=50
# DURATION=60
# CLIENTS="1 4 8 16 32"
# JOBS=4
# WARMUP_SECONDS=15
# RUN_READ_ONLY=1
# RUN_CUSTOM=0
# CUSTOM_SQL_FILE=pgbench_custom_workload.sql
# CUSTOM_MODE_NAME=custom_sql
# CUSTOM_READ_PCT=80
# CUSTOM_HOT_PCT=90
# CUSTOM_TXN_SIZE=2
# CUSTOM_HOT_ACCOUNTS=10000
# REINIT=0
PGHOST="${PGHOST:-localhost}"
PGPORT="${PGPORT:-5432}"
PGUSER="${PGUSER:-postgres}"
BENCH_DB="${BENCH_DB:-pgbench}"
SCALE="${SCALE:-50}"
DURATION="${DURATION:-60}"
CLIENTS="${CLIENTS:-1 4 8 16 32}"
JOBS="${JOBS:-4}"
WARMUP_SECONDS="${WARMUP_SECONDS:-15}"
RUN_READ_ONLY="${RUN_READ_ONLY:-1}"
RUN_CUSTOM="${RUN_CUSTOM:-0}"
CUSTOM_SQL_FILE="${CUSTOM_SQL_FILE:-pgbench_custom_workload.sql}"
CUSTOM_MODE_NAME="${CUSTOM_MODE_NAME:-custom_sql}"
CUSTOM_READ_PCT="${CUSTOM_READ_PCT:-80}"
CUSTOM_HOT_PCT="${CUSTOM_HOT_PCT:-90}"
CUSTOM_TXN_SIZE="${CUSTOM_TXN_SIZE:-2}"
CUSTOM_HOT_ACCOUNTS="${CUSTOM_HOT_ACCOUNTS:-10000}"
REINIT="${REINIT:-0}"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
RESULT_DIR="pgbench_results_${TIMESTAMP}"
SUMMARY_CSV="${RESULT_DIR}/summary.csv"
PSQL=(psql -X -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER")
PGBENCH=(pgbench -h "$PGHOST" -p "$PGPORT" -U "$PGUSER")
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Missing required command: $cmd" >&2
exit 1
fi
}
create_database_if_missing() {
local exists
exists="$("${PSQL[@]}" -d postgres -Atc "SELECT 1 FROM pg_database WHERE datname = '${BENCH_DB}';" || true)"
if [[ "$exists" != "1" ]]; then
echo "Creating benchmark database: ${BENCH_DB}"
"${PSQL[@]}" -d postgres -c "CREATE DATABASE ${BENCH_DB};"
fi
}
needs_init() {
local rel
rel="$("${PSQL[@]}" -d "$BENCH_DB" -Atc "SELECT to_regclass('public.pgbench_accounts') IS NOT NULL;")"
[[ "$rel" != "t" ]]
}
init_or_reinit_data() {
if [[ "$REINIT" == "1" ]] || needs_init; then
echo "Initializing pgbench schema/data (scale=${SCALE})"
"${PGBENCH[@]}" -i -s "$SCALE" "$BENCH_DB"
else
echo "Using existing pgbench data in ${BENCH_DB}"
fi
}
extract_tps() {
local output_file="$1"
awk -F'= ' '/tps =/{print $2}' "$output_file" | awk '{print $1}' | tail -n 1
}
extract_avg_latency() {
local output_file="$1"
awk -F'= ' '/latency average =/{print $2}' "$output_file" | awk '{print $1}' | tail -n 1
}
select_percentile_ms_from_prefix() {
local log_prefix="$1"
local percentile="$2"
local -a log_files
local -a values_us
local count rank value_us
shopt -s nullglob
log_files=("${log_prefix}"*)
shopt -u nullglob
if (( ${#log_files[@]} == 0 )); then
echo ""
return
fi
mapfile -t values_us < <(awk 'NF >= 3 && $3 ~ /^[0-9]+(\.[0-9]+)?$/ {print $3}' "${log_files[@]}" | sort -n)
count="${#values_us[@]}"
if (( count == 0 )); then
echo ""
return
fi
rank=$(( (percentile * count + 99) / 100 ))
if (( rank < 1 )); then rank=1; fi
if (( rank > count )); then rank=count; fi
value_us="${values_us[$((rank - 1))]}"
awk -v us="$value_us" 'BEGIN { printf "%.3f", us/1000.0 }'
}
run_case() {
local mode="$1"
local clients="$2"
shift 2
local -a extra_args=("$@")
local log_file="${RESULT_DIR}/${mode}_c${clients}.txt"
local tx_log_prefix="${RESULT_DIR}/${mode}_c${clients}_txlog"
echo "Running mode=${mode}, clients=${clients}, duration=${DURATION}s"
# Warm cache and avoid measuring first-run effects.
"${PGBENCH[@]}" -n -M prepared -j "$JOBS" -c "$clients" -T "$WARMUP_SECONDS" "${extra_args[@]}" "$BENCH_DB" > /dev/null
"${PGBENCH[@]}" -n -M prepared -j "$JOBS" -c "$clients" -T "$DURATION" -r -l --log-prefix="$tx_log_prefix" "${extra_args[@]}" "$BENCH_DB" > "$log_file"
local tps latency p95 p99
tps="$(extract_tps "$log_file")"
latency="$(extract_avg_latency "$log_file")"
p95="$(select_percentile_ms_from_prefix "$tx_log_prefix" 95)"
p99="$(select_percentile_ms_from_prefix "$tx_log_prefix" 99)"
echo "${mode},${clients},${DURATION},${JOBS},${tps},${latency},${p95},${p99},${log_file}" >> "$SUMMARY_CSV"
}
main() {
require_command psql
require_command pgbench
mkdir -p "$RESULT_DIR"
echo "mode,clients,duration_seconds,jobs,tps,latency_ms,p95_ms,p99_ms,details_file" > "$SUMMARY_CSV"
create_database_if_missing
init_or_reinit_data
if [[ "$RUN_CUSTOM" == "1" ]]; then
if [[ ! -f "$CUSTOM_SQL_FILE" ]]; then
echo "Custom SQL file not found: $CUSTOM_SQL_FILE" >&2
exit 1
fi
fi
for c in $CLIENTS; do
run_case "read_write" "$c"
if [[ "$RUN_READ_ONLY" == "1" ]]; then
run_case "read_only" "$c" "-S"
fi
if [[ "$RUN_CUSTOM" == "1" ]]; then
run_case "$CUSTOM_MODE_NAME" "$c" \
"-D" "read_pct=${CUSTOM_READ_PCT}" \
"-D" "hot_pct=${CUSTOM_HOT_PCT}" \
"-D" "txn_size=${CUSTOM_TXN_SIZE}" \
"-D" "hot_accounts=${CUSTOM_HOT_ACCOUNTS}" \
"-f" "$CUSTOM_SQL_FILE"
fi
done
echo
echo "Benchmark completed."
echo "Summary: $SUMMARY_CSV"
echo "Raw outputs: $RESULT_DIR"
}
main "$@"
-306
View File
@@ -1,306 +0,0 @@
#!/usr/bin/env python3
"""Export pgbench summary.csv into an HTML report with TPS and latency charts."""
import argparse
import csv
import json
from pathlib import Path
from typing import Dict, List, Optional
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Create an HTML chart report from pgbench summary.csv")
parser.add_argument("summary_csv", help="Path to summary.csv generated by pgbench benchmark script")
parser.add_argument(
"-o",
"--output",
default=None,
help="Output HTML path (default: <summary_dir>/pgbench_report.html)",
)
return parser.parse_args()
def load_rows(path: Path) -> List[Dict[str, str]]:
with path.open("r", encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
expected = {"mode", "clients", "tps", "latency_ms"}
if not expected.issubset(set(reader.fieldnames or [])):
missing = sorted(expected - set(reader.fieldnames or []))
raise ValueError(f"summary.csv missing required columns: {', '.join(missing)}")
return list(reader)
def parse_optional_float(raw: str) -> Optional[float]:
value = (raw or "").strip()
if not value:
return None
try:
return float(value)
except ValueError:
return None
def build_series(rows: List[Dict[str, str]]) -> Dict[str, List[Dict[str, float]]]:
series: Dict[str, List[Dict[str, float]]] = {}
for row in rows:
mode = row["mode"].strip()
try:
clients = int(row["clients"])
tps = float(row["tps"])
latency = float(row["latency_ms"])
except ValueError:
continue
point: Dict[str, float] = {
"clients": clients,
"tps": tps,
"latency_ms": latency,
}
p95 = parse_optional_float(row.get("p95_ms", ""))
p99 = parse_optional_float(row.get("p99_ms", ""))
if p95 is not None:
point["p95_ms"] = p95
if p99 is not None:
point["p99_ms"] = p99
series.setdefault(mode, []).append(point)
for mode in series:
series[mode].sort(key=lambda item: item["clients"])
return series
def html_report(title: str, data: Dict[str, List[Dict[str, float]]]) -> str:
payload = json.dumps(data)
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>{title}</title>
<style>
:root {{
--bg: #f7f7f2;
--fg: #182020;
--panel: #ffffff;
--muted: #4f5b5b;
--grid: #dde4e4;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: radial-gradient(circle at 8% 15%, #e7f6f2, var(--bg));
color: var(--fg);
}}
.container {{
max-width: 1100px;
margin: 0 auto;
padding: 24px;
}}
.card {{
background: var(--panel);
border: 1px solid #e2ebeb;
border-radius: 14px;
padding: 16px;
margin-bottom: 18px;
box-shadow: 0 10px 26px rgba(20, 30, 30, 0.06);
}}
h1 {{ margin: 0 0 8px 0; font-size: 1.6rem; }}
p {{ margin: 0; color: var(--muted); }}
canvas {{ width: 100%; height: 360px; display: block; }}
.legend {{ display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }}
.legend-item {{ display: inline-flex; align-items: center; gap: 8px; font-size: 0.9rem; color: var(--muted); }}
.swatch {{ width: 14px; height: 14px; border-radius: 3px; display: inline-block; }}
</style>
</head>
<body>
<div class=\"container\">
<div class=\"card\">
<h1>{title}</h1>
<p>Generated from summary.csv. X-axis = clients. Lines are grouped by mode.</p>
</div>
<div class=\"card\">
<h2>TPS by Clients</h2>
<canvas id=\"tpsChart\" width=\"1040\" height=\"360\"></canvas>
<div id=\"legend\" class=\"legend\"></div>
</div>
<div class=\"card\">
<h2>Latency (ms) by Clients</h2>
<canvas id=\"latencyChart\" width=\"1040\" height=\"360\"></canvas>
</div>
<div class=\"card\">
<h2>p95 Latency (ms) by Clients</h2>
<canvas id=\"p95Chart\" width=\"1040\" height=\"360\"></canvas>
</div>
<div class=\"card\">
<h2>p99 Latency (ms) by Clients</h2>
<canvas id=\"p99Chart\" width=\"1040\" height=\"360\"></canvas>
</div>
</div>
<script>
const series = {payload};
const palette = ["#0b6e4f", "#f26419", "#33658a", "#2f4858", "#5f0f40", "#9a031e", "#386641", "#6a4c93"];
function flattenValues(metric) {{
const out = [];
Object.values(series).forEach(items => items.forEach(i => {{
const v = i[metric];
if (typeof v === 'number' && Number.isFinite(v)) out.push(v);
}}));
return out;
}}
function flattenClients() {{
const out = [];
Object.values(series).forEach(items => items.forEach(i => out.push(i.clients)));
return out;
}}
function drawChart(canvasId, metric, yLabel) {{
const c = document.getElementById(canvasId);
const ctx = c.getContext('2d');
const W = c.width;
const H = c.height;
const m = {{ top: 22, right: 20, bottom: 44, left: 60 }};
const x0 = m.left, y0 = H - m.bottom, x1 = W - m.right, y1 = m.top;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#182020';
ctx.font = '13px Segoe UI, sans-serif';
ctx.fillText(yLabel, 8, 16);
const clientsAll = flattenClients();
const valuesAll = flattenValues(metric);
if (!clientsAll.length || !valuesAll.length) {{
ctx.fillText('No data in summary.csv for this metric', x0 + 10, y0 - 10);
return;
}}
const minX = Math.min(...clientsAll);
const maxX = Math.max(...clientsAll);
const minY = 0;
const maxY = Math.max(...valuesAll) * 1.1;
const x = v => x0 + ((v - minX) / Math.max(1, maxX - minX)) * (x1 - x0);
const y = v => y0 - ((v - minY) / Math.max(1e-9, maxY - minY)) * (y0 - y1);
ctx.strokeStyle = '#dde4e4';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {{
const yy = y0 - ((y0 - y1) * i / 5);
ctx.beginPath();
ctx.moveTo(x0, yy);
ctx.lineTo(x1, yy);
ctx.stroke();
const val = (maxY * i / 5).toFixed(1);
ctx.fillStyle = '#4f5b5b';
ctx.fillText(val, 16, yy + 4);
}}
const xTicks = [...new Set(clientsAll)].sort((a, b) => a - b);
xTicks.forEach(v => {{
const xx = x(v);
ctx.beginPath();
ctx.moveTo(xx, y0);
ctx.lineTo(xx, y0 + 5);
ctx.strokeStyle = '#9da9a9';
ctx.stroke();
ctx.fillStyle = '#4f5b5b';
ctx.fillText(String(v), xx - 6, y0 + 18);
}});
ctx.strokeStyle = '#182020';
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y0);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x0, y1);
ctx.stroke();
const legend = document.getElementById('legend');
if (metric === 'tps') legend.innerHTML = '';
Object.entries(series).forEach(([mode, items], idx) => {{
const color = palette[idx % palette.length];
const sorted = [...items]
.filter(p => typeof p[metric] === 'number' && Number.isFinite(p[metric]))
.sort((a, b) => a.clients - b.clients);
if (!sorted.length) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
sorted.forEach((p, i) => {{
const px = x(p.clients);
const py = y(p[metric]);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}});
ctx.stroke();
sorted.forEach(p => {{
const px = x(p.clients);
const py = y(p[metric]);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fill();
}});
if (metric === 'tps') {{
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `<span class=\"swatch\" style=\"background:${{color}}\"></span>${{mode}}`;
legend.appendChild(item);
}}
}});
}}
drawChart('tpsChart', 'tps', 'TPS');
drawChart('latencyChart', 'latency_ms', 'Latency (ms)');
drawChart('p95Chart', 'p95_ms', 'p95 (ms)');
drawChart('p99Chart', 'p99_ms', 'p99 (ms)');
</script>
</body>
</html>
"""
def main() -> int:
args = parse_args()
summary_path = Path(args.summary_csv).resolve()
if not summary_path.is_file():
raise FileNotFoundError(f"summary.csv not found: {summary_path}")
rows = load_rows(summary_path)
if not rows:
raise ValueError("summary.csv has no data rows")
series = build_series(rows)
if not series:
raise ValueError("No valid numeric rows found in summary.csv")
output_path = (
Path(args.output).resolve()
if args.output
else summary_path.parent / "pgbench_report.html"
)
title = f"pgbench report: {summary_path.parent.name}"
output_path.write_text(html_report(title=title, data=series), encoding="utf-8")
print(f"Report written: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-102
View File
@@ -1,102 +0,0 @@
# pgbench benchmark quickstart
This workspace now includes repeatable benchmark harnesses:
- `pgbench_benchmark.ps1` (Windows PowerShell)
- `pgbench_benchmark.sh` (Bash)
- `pgbench_custom_workload.sql` (sample custom SQL transaction mix)
- `pgbench_export_report.py` (exports TPS/latency chart report from `summary.csv`)
## 1) Prerequisites
- PostgreSQL client tools installed (`psql`, `pgbench`)
- Network access to your PostgreSQL server
- Credentials available via environment variables (`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`)
## 2) Run the benchmark (Windows PowerShell)
From this folder:
```powershell
$env:PGPASSWORD = "your_password"
.\pgbench_benchmark.ps1 -PgHost localhost -PgPort 5432 -PgUser postgres
```
You can override defaults:
```powershell
$env:PGPASSWORD = "your_password"
.\pgbench_benchmark.ps1 -BenchDb pgbench -Scale 100 -Duration 120 -Clients "1 8 16 32 64" -Jobs 8 -Reinit $true
```
Run with custom SQL workload mode enabled:
```powershell
$env:PGPASSWORD = "your_password"
.\pgbench_benchmark.ps1 -RunCustom $true -CustomSqlFile .\pgbench_custom_workload.sql -CustomModeName app_like -CustomReadPct 85 -CustomHotPct 92 -CustomTxnSize 3 -CustomHotAccounts 12000
```
## 3) Optional Bash usage (WSL/Git Bash)
```bash
chmod +x pgbench_benchmark.sh
PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=your_password ./pgbench_benchmark.sh
```
Run with custom SQL workload mode enabled:
```bash
PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=your_password RUN_CUSTOM=1 CUSTOM_SQL_FILE=./pgbench_custom_workload.sql CUSTOM_MODE_NAME=app_like CUSTOM_READ_PCT=85 CUSTOM_HOT_PCT=92 CUSTOM_TXN_SIZE=3 CUSTOM_HOT_ACCOUNTS=12000 ./pgbench_benchmark.sh
```
## 4) Optional tuning inputs (Bash script)
```bash
BENCH_DB=pgbench SCALE=100 DURATION=120 CLIENTS="1 8 16 32 64" JOBS=8 REINIT=1 ./pgbench_benchmark.sh
```
Environment variables:
- `BENCH_DB`: database name for benchmark data (default: `pgbench`)
- `SCALE`: data size scale factor (default: `50`)
- `DURATION`: measured run time per test case in seconds (default: `60`)
- `CLIENTS`: space-separated client counts (default: `"1 4 8 16 32"`)
- `JOBS`: worker threads for pgbench (default: `4`)
- `WARMUP_SECONDS`: unmeasured warm-up before each test (default: `15`)
- `RUN_READ_ONLY`: set to `0` to skip `-S` read-only tests (default: `1`)
- `REINIT`: set to `1` to reinitialize data each run (default: `0`)
- `RUN_CUSTOM`: set to `1` to run custom SQL workload mode (default: `0`)
- `CUSTOM_SQL_FILE`: path to custom SQL file for `pgbench -f` (default: `pgbench_custom_workload.sql`)
- `CUSTOM_MODE_NAME`: mode label written to `summary.csv` for custom runs (default: `custom_sql`)
- `CUSTOM_READ_PCT` / `-CustomReadPct`: read share percentage for custom mode; writes happen in `100-read_pct` (default: `80`)
- `CUSTOM_HOT_PCT` / `-CustomHotPct`: chance to pick from hot account set in custom mode (default: `90`)
- `CUSTOM_TXN_SIZE` / `-CustomTxnSize`: custom transaction size tier from `1` to `4` (default: `2`)
- `CUSTOM_HOT_ACCOUNTS` / `-CustomHotAccounts`: number of hot accounts near start of keyspace (default: `10000`)
## 5) Output
Each run creates:
- `pgbench_results_YYYYMMDD_HHMMSS/summary.csv`
- Per-test raw output files in the same results directory
Use `summary.csv` to compare TPS and latency across runs.
`summary.csv` now includes percentile latency columns:
- `p95_ms`
- `p99_ms`
## 6) Export chart report from summary.csv
Generate an HTML report with TPS and latency charts:
```powershell
python .\pgbench_export_report.py .\pgbench_results_YYYYMMDD_HHMMSS\summary.csv
```
Optional output path:
```powershell
python .\pgbench_export_report.py .\pgbench_results_YYYYMMDD_HHMMSS\summary.csv -o .\pgbench_results_YYYYMMDD_HHMMSS\my_report.html
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-946
View File
@@ -1,946 +0,0 @@
#!/usr/bin/env python3
"""
Script to read and parse Powerball numbers from a CSV file with advanced pattern detection.
"""
import csv
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Tuple, Optional, Set
from collections import Counter, defaultdict
from itertools import combinations
def read_powerball_csv(filepath: str) -> List[Dict]:
"""
Read a CSV file containing Powerball numbers.
Expected CSV format:
Draw Date,Winning Numbers,Multiplier
09/26/2020,11 21 27 36 62 24,3
Args:
filepath: Path to the CSV file
Returns:
List of dictionaries containing parsed powerball data
"""
powerball_data = []
try:
with open(filepath, 'r', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row_num, row in enumerate(reader, start=2): # start=2 because row 1 is header
try:
# Skip empty rows
if not any(row.values()):
continue
# Parse the row
draw_date = datetime.strptime(row['Draw Date'].strip(), '%m/%d/%Y')
# Parse winning numbers - filter out empty strings
numbers_str = row['Winning Numbers'].strip()
winning_numbers = [int(n) for n in numbers_str.split() if n.strip()]
multiplier = int(row['Multiplier'].strip())
powerball_data.append({
'draw_date': draw_date,
'date_str': draw_date.strftime('%Y-%m-%d'),
'winning_numbers': winning_numbers,
'multiplier': multiplier
})
except ValueError as e:
print(f"Warning: Skipping row {row_num} due to error: {e}")
print(f" Row data: {row}")
continue
except FileNotFoundError:
print(f"Error: File '{filepath}' not found.")
return []
except Exception as e:
print(f"Error: Failed to read CSV file - {e}")
return []
return powerball_data
def display_powerball_data(data: List[Dict]) -> None:
"""
Display powerball data in a formatted table.
Args:
data: List of powerball data dictionaries
"""
if not data:
print("No data to display.")
return
print(f"{'Draw Date':<12} {'Winning Numbers':<30} {'Multiplier':<10}")
print("-" * 52)
for entry in data:
numbers_str = ' '.join(map(str, entry['winning_numbers']))
print(f"{entry['date_str']:<12} {numbers_str:<30} {entry['multiplier']:<10}")
def _get_unweighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
"""Return top 5 white balls and top Powerball from a draw list."""
if not draws:
return [], None
white_counter = Counter()
powerball_counter = Counter()
for entry in draws:
numbers = entry['winning_numbers']
for number in numbers[:5]:
white_counter[number] += 1
if len(numbers) >= 6:
powerball_counter[numbers[5]] += 1
top_white = [number for number, _ in white_counter.most_common(5)]
top_powerball = powerball_counter.most_common(1)[0][0] if powerball_counter else None
return top_white, top_powerball
def _get_recency_weighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
"""Return top numbers weighted so newer draws contribute more."""
if not draws:
return [], None
sorted_draws = sorted(draws, key=lambda x: x['draw_date'])
total_draws = len(sorted_draws)
white_scores: Dict[int, float] = defaultdict(float)
powerball_scores: Dict[int, float] = defaultdict(float)
for index, entry in enumerate(sorted_draws, start=1):
weight = index / total_draws
numbers = entry['winning_numbers']
for number in numbers[:5]:
white_scores[number] += weight
if len(numbers) >= 6:
powerball_scores[numbers[5]] += weight
top_white = [number for number, _ in sorted(white_scores.items(), key=lambda x: x[1], reverse=True)[:5]]
top_powerball = max(powerball_scores.items(), key=lambda x: x[1])[0] if powerball_scores else None
return top_white, top_powerball
def _get_aggressive_recency_weighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
"""Return top numbers with an aggressive recency curve (cubic weighting)."""
if not draws:
return [], None
sorted_draws = sorted(draws, key=lambda x: x['draw_date'])
total_draws = len(sorted_draws)
white_scores: Dict[int, float] = defaultdict(float)
powerball_scores: Dict[int, float] = defaultdict(float)
for index, entry in enumerate(sorted_draws, start=1):
normalized_position = index / total_draws
weight = normalized_position ** 3
numbers = entry['winning_numbers']
for number in numbers[:5]:
white_scores[number] += weight
if len(numbers) >= 6:
powerball_scores[numbers[5]] += weight
top_white = [number for number, _ in sorted(white_scores.items(), key=lambda x: x[1], reverse=True)[:5]]
top_powerball = max(powerball_scores.items(), key=lambda x: x[1])[0] if powerball_scores else None
return top_white, top_powerball
def analyze_most_frequent_numbers(data: List[Dict]) -> Dict:
"""
Analyze and display the most frequent number in each slot position.
Args:
data: List of powerball data dictionaries
"""
if not data:
print("No data to analyze.")
return {}
# Initialize lists to hold numbers for each position
num_positions = len(data[0]['winning_numbers'])
position_numbers = [[] for _ in range(num_positions)]
# Collect all numbers for each position
for entry in data:
for position, number in enumerate(entry['winning_numbers']):
position_numbers[position].append(number)
# Find most common number in each position
print("\n" + "=" * 60)
print("Most Frequent Numbers by Slot Position")
print("=" * 60)
most_frequent = []
for position, numbers in enumerate(position_numbers, start=1):
counter = Counter(numbers)
most_common_num, frequency = counter.most_common(1)[0]
most_frequent.append(most_common_num)
percentage = (frequency / len(numbers)) * 100
print(f"Position {position}: {most_common_num:<3} (appears {frequency} times, {percentage:.1f}%)")
print("-" * 60)
print(f"Most frequent numbers combined: {' '.join(map(str, most_frequent))}")
# Analyze most frequent number across first 5 positions combined
print("\n" + "=" * 60)
print("Additional Analysis")
print("=" * 60)
# Combine all numbers from positions 1-5
first_five_combined = []
for i in range(min(5, num_positions)):
first_five_combined.extend(position_numbers[i])
top_five_numbers = []
top_ten_numbers = []
if first_five_combined:
counter_first_five = Counter(first_five_combined)
top_five_numbers = counter_first_five.most_common(5)
top_ten_numbers = counter_first_five.most_common(10)
print("Top 5 most drawn numbers in first 5 positions combined:")
for rank, (number, frequency) in enumerate(top_five_numbers, start=1):
percentage = (frequency / len(first_five_combined)) * 100
print(f" {rank}. {number:<3} (appears {frequency} times, {percentage:.1f}%)")
# Analyze the sixth position (Powerball)
powerball_ranked = []
most_common_sixth = None
if num_positions >= 6:
sixth_position_numbers = position_numbers[5]
counter_sixth = Counter(sixth_position_numbers)
powerball_ranked = counter_sixth.most_common(5)
most_common_sixth, freq_sixth = counter_sixth.most_common(1)[0]
percentage_sixth = (freq_sixth / len(sixth_position_numbers)) * 100
print(f"\nMost drawn number in position 6 (Powerball): {most_common_sixth} (appears {freq_sixth} times, {percentage_sixth:.1f}%)")
# Third strategy: most frequent numbers from the most recent 104 draws
sorted_by_date = sorted(data, key=lambda x: x['draw_date'])
recent_104_draws = sorted_by_date[-104:]
recent_104_top_five, recent_104_powerball = _get_unweighted_top_numbers(recent_104_draws)
# Fourth strategy: weighted-by-recency across all draws
weighted_top_five, weighted_powerball = _get_recency_weighted_top_numbers(data)
# Fifth strategy: aggressive weighted-by-recency across all draws
aggressive_weighted_top_five, aggressive_weighted_powerball = _get_aggressive_recency_weighted_top_numbers(data)
# Return statistics for prediction
return {
'top_five_combined': [num for num, freq in top_five_numbers] if first_five_combined else [],
'top_ten_combined': [num for num, freq in top_ten_numbers],
'most_common_powerball': most_common_sixth,
'powerball_ranked': [num for num, freq in powerball_ranked],
'recent_104_top_five': recent_104_top_five,
'recent_104_powerball': recent_104_powerball,
'weighted_top_five': weighted_top_five,
'weighted_powerball': weighted_powerball,
'aggressive_weighted_top_five': aggressive_weighted_top_five,
'aggressive_weighted_powerball': aggressive_weighted_powerball
}
def generate_suggested_combination(stats: dict) -> None:
"""
Generate a suggested combination based on statistical analysis.
Args:
stats: Dictionary containing statistical analysis results
"""
print("\n" + "=" * 60)
print("Suggested Combination Based on Statistical Analysis")
print("=" * 60)
print("\n*** DISCLAIMER: This is purely statistical analysis.")
print("Past results do NOT predict future outcomes.")
print("Each draw is random and independent.\n")
top_five = stats['top_five_combined']
top_ten = stats.get('top_ten_combined', [])
powerball = stats['most_common_powerball']
ranked_powerballs = stats.get('powerball_ranked', [])
recent_104_top_five = stats.get('recent_104_top_five', [])
recent_104_powerball = stats.get('recent_104_powerball')
weighted_top_five = stats.get('weighted_top_five', [])
weighted_powerball = stats.get('weighted_powerball')
aggressive_weighted_top_five = stats.get('aggressive_weighted_top_five', [])
aggressive_weighted_powerball = stats.get('aggressive_weighted_powerball')
if len(top_five) >= 5 and powerball:
# Use the top 5 most frequent numbers from positions 1-5
primary_numbers = sorted(top_five[:5])
primary_powerball = powerball
# Build a secondary set from the next-most-frequent historical numbers.
secondary_pool = top_ten[5:10]
if len(secondary_pool) < 5:
secondary_pool = top_ten[:5]
secondary_numbers = sorted(secondary_pool[:5])
secondary_powerball = ranked_powerballs[1] if len(ranked_powerballs) > 1 else primary_powerball
third_numbers = sorted(recent_104_top_five[:5]) if len(recent_104_top_five) >= 5 else []
third_powerball = recent_104_powerball
fourth_numbers = sorted(weighted_top_five[:5]) if len(weighted_top_five) >= 5 else []
fourth_powerball = weighted_powerball
fifth_numbers = sorted(aggressive_weighted_top_five[:5]) if len(aggressive_weighted_top_five) >= 5 else []
fifth_powerball = aggressive_weighted_powerball
print("Primary set (most frequent):")
print(f" White balls: {' '.join(map(str, primary_numbers))}")
print(f" Powerball: {primary_powerball}")
print("\nSecondary set (next-most frequent):")
print(f" White balls: {' '.join(map(str, secondary_numbers))}")
print(f" Powerball: {secondary_powerball}")
if third_numbers and third_powerball:
print("\nThird set (most frequent in recent 104 draws):")
print(f" White balls: {' '.join(map(str, third_numbers))}")
print(f" Powerball: {third_powerball}")
if fourth_numbers and fourth_powerball:
print("\nFourth set (weighted by recency):")
print(f" White balls: {' '.join(map(str, fourth_numbers))}")
print(f" Powerball: {fourth_powerball}")
if fifth_numbers and fifth_powerball:
print("\nFifth set (aggressive recency weighting):")
print(f" White balls: {' '.join(map(str, fifth_numbers))}")
print(f" Powerball: {fifth_powerball}")
print()
print(f"Primary combination: {' '.join(map(str, primary_numbers))} + {primary_powerball}")
print(f"Secondary combination: {' '.join(map(str, secondary_numbers))} + {secondary_powerball}")
if third_numbers and third_powerball:
print(f"Third combination: {' '.join(map(str, third_numbers))} + {third_powerball}")
if fourth_numbers and fourth_powerball:
print(f"Fourth combination: {' '.join(map(str, fourth_numbers))} + {fourth_powerball}")
if fifth_numbers and fifth_powerball:
print(f"Fifth combination: {' '.join(map(str, fifth_numbers))} + {fifth_powerball}")
print()
print("These combinations are based on:")
print(" • The 5 most frequently drawn numbers across all white ball positions")
print(f" • The most frequently drawn Powerball number ({primary_powerball})")
print(" • A secondary set from the next-most-frequent white ball and Powerball values")
print(" • Recent-window frequency (last 104 draws)")
print(" • Recency-weighted scoring where newer draws count more")
print(" • Aggressive recency weighting using a cubic curve")
else:
print("Insufficient data to generate a suggestion.")
def detect_consecutive_patterns(data: List[Dict]) -> Dict:
"""
Analyze frequency of consecutive numbers in draws.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with consecutive number statistics
"""
consecutive_counts = Counter()
total_draws = len(data)
draws_with_consecutives = 0
consecutive_pairs = []
for entry in data:
white_balls = sorted(entry['winning_numbers'][:5])
has_consecutive = False
for i in range(len(white_balls) - 1):
if white_balls[i+1] - white_balls[i] == 1:
consecutive_counts[f"{white_balls[i]}-{white_balls[i+1]}"] += 1
consecutive_pairs.append((white_balls[i], white_balls[i+1]))
has_consecutive = True
if has_consecutive:
draws_with_consecutives += 1
print("\n" + "=" * 60)
print("PATTERN: Consecutive Numbers Analysis")
print("=" * 60)
percentage = (draws_with_consecutives / total_draws) * 100 if total_draws > 0 else 0
print(f"Draws with consecutive numbers: {draws_with_consecutives}/{total_draws} ({percentage:.1f}%)")
if consecutive_counts:
print("\nMost common consecutive pairs:")
for pair, count in consecutive_counts.most_common(10):
pair_pct = (count / total_draws) * 100
print(f" {pair}: {count} times ({pair_pct:.1f}%)")
return {
'draws_with_consecutives': draws_with_consecutives,
'percentage': percentage,
'common_pairs': dict(consecutive_counts.most_common(10))
}
def detect_odd_even_patterns(data: List[Dict]) -> Dict:
"""
Analyze odd/even distribution patterns.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with odd/even distribution statistics
"""
distribution = Counter()
for entry in data:
white_balls = entry['winning_numbers'][:5]
odd_count = sum(1 for num in white_balls if num % 2 == 1)
even_count = 5 - odd_count
distribution[f"{odd_count}odd-{even_count}even"] += 1
print("\n" + "=" * 60)
print("PATTERN: Odd/Even Distribution")
print("=" * 60)
print("Distribution of odd vs even numbers:")
total = sum(distribution.values())
for pattern, count in sorted(distribution.items(), key=lambda x: x[1], reverse=True):
percentage = (count / total) * 100
print(f" {pattern}: {count} times ({percentage:.1f}%)")
return {'distribution': dict(distribution)}
def detect_high_low_patterns(data: List[Dict]) -> Dict:
"""
Analyze high/low number distribution (1-35 = low, 36-69 = high).
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with high/low distribution statistics
"""
distribution = Counter()
SPLIT_POINT = 35
for entry in data:
white_balls = entry['winning_numbers'][:5]
low_count = sum(1 for num in white_balls if num <= SPLIT_POINT)
high_count = 5 - low_count
distribution[f"{low_count}low-{high_count}high"] += 1
print("\n" + "=" * 60)
print(f"PATTERN: High/Low Distribution (Low: 1-{SPLIT_POINT}, High: {SPLIT_POINT+1}-69)")
print("=" * 60)
print("Distribution of low vs high numbers:")
total = sum(distribution.values())
for pattern, count in sorted(distribution.items(), key=lambda x: x[1], reverse=True):
percentage = (count / total) * 100
print(f" {pattern}: {count} times ({percentage:.1f}%)")
return {'distribution': dict(distribution)}
def detect_sum_patterns(data: List[Dict]) -> Dict:
"""
Analyze the sum of winning numbers.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with sum statistics
"""
sums = []
for entry in data:
white_balls = entry['winning_numbers'][:5]
total = sum(white_balls)
sums.append(total)
print("\n" + "=" * 60)
print("PATTERN: Sum of Winning Numbers")
print("=" * 60)
avg_sum = sum(sums) / len(sums) if sums else 0
min_sum = min(sums) if sums else 0
max_sum = max(sums) if sums else 0
print(f"Average sum: {avg_sum:.1f}")
print(f"Range: {min_sum} - {max_sum}")
# Create buckets for sum ranges
buckets = Counter()
for s in sums:
bucket = (s // 25) * 25 # Group into 25-number ranges
buckets[f"{bucket}-{bucket+24}"] += 1
print("\nSum distribution by range:")
for range_str, count in sorted(buckets.items(), key=lambda x: int(x[0].split('-')[0])):
percentage = (count / len(sums)) * 100
print(f" {range_str}: {count} times ({percentage:.1f}%)")
return {
'average': avg_sum,
'min': min_sum,
'max': max_sum,
'buckets': dict(buckets)
}
def detect_gap_patterns(data: List[Dict]) -> Dict:
"""
Analyze gaps (spacing) between consecutive numbers when sorted.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with gap statistics
"""
all_gaps = []
for entry in data:
white_balls = sorted(entry['winning_numbers'][:5])
gaps = [white_balls[i+1] - white_balls[i] for i in range(len(white_balls) - 1)]
all_gaps.extend(gaps)
print("\n" + "=" * 60)
print("PATTERN: Gap Analysis (spacing between sorted numbers)")
print("=" * 60)
avg_gap = sum(all_gaps) / len(all_gaps) if all_gaps else 0
gap_counter = Counter(all_gaps)
print(f"Average gap: {avg_gap:.1f}")
print("\nMost common gap sizes:")
for gap, count in gap_counter.most_common(10):
percentage = (count / len(all_gaps)) * 100
print(f" Gap of {gap}: {count} times ({percentage:.1f}%)")
return {
'average_gap': avg_gap,
'gap_distribution': dict(gap_counter.most_common(15))
}
def detect_hot_cold_numbers(data: List[Dict], recent_draws: int = 20) -> Dict:
"""
Identify hot (frequently appearing) and cold (rarely appearing) numbers in recent draws.
Args:
data: List of powerball data dictionaries
recent_draws: Number of recent draws to analyze
Returns:
Dictionary with hot/cold number analysis
"""
sorted_data = sorted(data, key=lambda x: x['draw_date'])
recent = sorted_data[-recent_draws:] if len(sorted_data) >= recent_draws else sorted_data
white_counter = Counter()
powerball_counter = Counter()
for entry in recent:
for num in entry['winning_numbers'][:5]:
white_counter[num] += 1
if len(entry['winning_numbers']) >= 6:
powerball_counter[entry['winning_numbers'][5]] += 1
print("\n" + "=" * 60)
print(f"PATTERN: Hot & Cold Numbers (Last {len(recent)} draws)")
print("=" * 60)
print(f"\nHottest white balls (appeared most in last {len(recent)} draws):")
for num, count in white_counter.most_common(10):
print(f" {num}: {count} times")
print(f"\nHottest Powerballs:")
for num, count in powerball_counter.most_common(5):
print(f" {num}: {count} times")
# Cold numbers - all possible white ball numbers (1-69) that appeared least
all_white_numbers = set(range(1, 70))
appeared = set(white_counter.keys())
not_appeared = all_white_numbers - appeared
print(f"\nColdest white balls (appeared least or not at all):")
cold_numbers = sorted(
[(num, white_counter.get(num, 0)) for num in all_white_numbers],
key=lambda x: (x[1], x[0])
)[:10]
for num, count in cold_numbers:
if count == 0:
print(f" {num}: 0 times (not appeared)")
else:
print(f" {num}: {count} times")
return {
'hot_white': dict(white_counter.most_common(10)),
'hot_powerball': dict(powerball_counter.most_common(5)),
'cold_white': dict(cold_numbers[:10])
}
def detect_overdue_numbers(data: List[Dict]) -> Dict:
"""
Identify numbers that haven't appeared in the longest time.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with overdue number analysis
"""
sorted_data = sorted(data, key=lambda x: x['draw_date'])
# Track last appearance of each number
last_seen_white = {}
last_seen_powerball = {}
for entry in sorted_data:
draw_date = entry['draw_date']
for num in entry['winning_numbers'][:5]:
last_seen_white[num] = draw_date
if len(entry['winning_numbers']) >= 6:
last_seen_powerball[entry['winning_numbers'][5]] = draw_date
most_recent_draw = sorted_data[-1]['draw_date']
# Calculate days since last appearance
overdue_white = {}
for num in range(1, 70): # White balls are 1-69
if num in last_seen_white:
days_overdue = (most_recent_draw - last_seen_white[num]).days
overdue_white[num] = days_overdue
else:
overdue_white[num] = float('inf') # Never appeared
overdue_powerball = {}
for num in range(1, 27): # Powerballs are 1-26
if num in last_seen_powerball:
days_overdue = (most_recent_draw - last_seen_powerball[num]).days
overdue_powerball[num] = days_overdue
else:
overdue_powerball[num] = float('inf')
print("\n" + "=" * 60)
print("PATTERN: Overdue Numbers (longest time since last appearance)")
print("=" * 60)
print("\nMost overdue white balls:")
sorted_overdue_white = sorted(overdue_white.items(), key=lambda x: x[1], reverse=True)[:10]
for num, days in sorted_overdue_white:
if days == float('inf'):
print(f" {num}: Never appeared")
else:
print(f" {num}: {days} days ago")
print("\nMost overdue Powerballs:")
sorted_overdue_powerball = sorted(overdue_powerball.items(), key=lambda x: x[1], reverse=True)[:5]
for num, days in sorted_overdue_powerball:
if days == float('inf'):
print(f" {num}: Never appeared")
else:
print(f" {num}: {days} days ago")
return {
'overdue_white': dict(sorted_overdue_white),
'overdue_powerball': dict(sorted_overdue_powerball)
}
def detect_number_pairs(data: List[Dict], top_n: int = 10) -> Dict:
"""
Identify which number pairs appear together most frequently.
Args:
data: List of powerball data dictionaries
top_n: Number of top pairs to display
Returns:
Dictionary with pair frequency analysis
"""
pair_counter = Counter()
for entry in data:
white_balls = entry['winning_numbers'][:5]
# Get all pairs from this draw
for pair in combinations(sorted(white_balls), 2):
pair_counter[pair] += 1
print("\n" + "=" * 60)
print("PATTERN: Number Pair Analysis")
print("=" * 60)
print(f"\nTop {top_n} most common number pairs:")
for (num1, num2), count in pair_counter.most_common(top_n):
percentage = (count / len(data)) * 100
print(f" ({num1}, {num2}): appeared together {count} times ({percentage:.1f}%)")
return {
'top_pairs': dict(pair_counter.most_common(top_n))
}
def detect_decade_distribution(data: List[Dict]) -> Dict:
"""
Analyze how numbers are distributed across decades (1-9, 10-19, 20-29, etc.).
Args:
data: List of powerball data dictionaries
Returns:
Dictionary with decade distribution statistics
"""
decade_counts = Counter()
for entry in data:
white_balls = entry['winning_numbers'][:5]
for num in white_balls:
decade = (num // 10) * 10
decade_label = f"{decade:02d}-{decade+9:02d}" if decade < 60 else "60-69"
decade_counts[decade_label] += 1
print("\n" + "=" * 60)
print("PATTERN: Decade Distribution")
print("=" * 60)
print("How often numbers from each range appear:")
total = sum(decade_counts.values())
for decade in ["00-09", "10-19", "20-29", "30-39", "40-49", "50-59", "60-69"]:
count = decade_counts.get(decade, 0)
percentage = (count / total) * 100 if total > 0 else 0
print(f" {decade}: {count} times ({percentage:.1f}%)")
return {'decade_distribution': dict(decade_counts)}
def run_all_pattern_analyses(data: List[Dict]) -> Dict:
"""
Run all pattern detection analyses.
Args:
data: List of powerball data dictionaries
Returns:
Dictionary containing all pattern analysis results
"""
print("\n" + "#" * 60)
print("# ADVANCED PATTERN DETECTION ANALYSIS")
print("#" * 60)
results = {}
results['consecutive'] = detect_consecutive_patterns(data)
results['odd_even'] = detect_odd_even_patterns(data)
results['high_low'] = detect_high_low_patterns(data)
results['sum'] = detect_sum_patterns(data)
results['gaps'] = detect_gap_patterns(data)
results['hot_cold'] = detect_hot_cold_numbers(data, recent_draws=20)
results['overdue'] = detect_overdue_numbers(data)
results['pairs'] = detect_number_pairs(data, top_n=15)
results['decades'] = detect_decade_distribution(data)
return results
def generate_pattern_based_combinations(data: List[Dict], pattern_results: Dict, num_combinations: int = 5) -> List[Dict]:
"""
Generate number combinations based on pattern analysis.
Args:
data: List of powerball data dictionaries
pattern_results: Results from run_all_pattern_analyses
num_combinations: Number of combinations to generate
Returns:
List of dictionaries containing combination details
"""
import random
print("\n" + "=" * 60)
print("PATTERN-BASED NUMBER COMBINATIONS")
print("=" * 60)
print("\n*** DISCLAIMER: These combinations are based on pattern analysis.")
print("Lottery draws are random. Past patterns do NOT predict future results.")
print("Play responsibly.\n")
combinations = []
# Get pattern insights
hot_white = list(pattern_results['hot_cold']['hot_white'].keys())[:15]
cold_white = [num for num, _ in pattern_results['hot_cold']['cold_white'].items() if _ < 3][:10]
overdue_white = [num for num, days in pattern_results['overdue']['overdue_white'].items() if days != float('inf')][:15]
hot_powerball = list(pattern_results['hot_cold']['hot_powerball'].keys())[:3]
overdue_powerball = [num for num, days in pattern_results['overdue']['overdue_powerball'].items() if days != float('inf')][:5]
# Get most common distributions
odd_even_dist = pattern_results['odd_even']['distribution']
most_common_oe = max(odd_even_dist.items(), key=lambda x: x[1])[0]
target_odds = int(most_common_oe.split('odd')[0])
high_low_dist = pattern_results['high_low']['distribution']
most_common_hl = max(high_low_dist.items(), key=lambda x: x[1])[0]
target_lows = int(most_common_hl.split('low')[0])
# Get average sum
avg_sum = pattern_results['sum']['average']
sum_tolerance = 30
# Get top pairs
top_pairs = list(pattern_results['pairs']['top_pairs'].keys())[:20]
# Strategy descriptions
strategies = [
("Hot Numbers Focus", hot_white, "Uses frequently drawn recent numbers"),
("Balanced Hot + Overdue", hot_white[:8] + overdue_white[:12], "Mixes hot numbers with overdue picks"),
("Overdue Numbers Focus", overdue_white, "Focuses on numbers that haven't appeared recently"),
("Cold Numbers Gamble", cold_white + hot_white[:10], "Includes rarely drawn numbers"),
("Pair-Based Selection", [n for pair in top_pairs for n in pair], "Uses numbers from common pairs")
]
for strategy_idx in range(min(num_combinations, len(strategies))):
strategy_name, number_pool, strategy_desc = strategies[strategy_idx]
# Ensure we have enough numbers in pool
if len(number_pool) < 20:
number_pool = list(set(number_pool + hot_white + list(range(1, 70))))
# Try to generate a combination that fits the patterns
attempts = 0
max_attempts = 1000
selected = [] # Initialize to avoid unbound variable
while attempts < max_attempts:
# Select 5 random numbers from pool
selected = random.sample(number_pool[:30], min(5, len(number_pool[:30])))
selected = sorted(selected)
# Check odd/even distribution
odds_count = sum(1 for n in selected if n % 2 == 1)
if abs(odds_count - target_odds) > 1:
attempts += 1
continue
# Check high/low distribution
lows_count = sum(1 for n in selected if n <= 35)
if abs(lows_count - target_lows) > 1:
attempts += 1
continue
# Check sum is reasonable
total = sum(selected)
if abs(total - avg_sum) > sum_tolerance:
attempts += 1
continue
# Check gaps are reasonable (not too clustered or too spread)
gaps = [selected[i+1] - selected[i] for i in range(4)]
if min(gaps) < 2 or max(gaps) > 25:
attempts += 1
continue
# Good combination found
break
# If we didn't find a perfect match, selected still has the last attempt
if not selected:
selected = sorted(random.sample(range(1, 70), 5))
# Select powerball
if strategy_idx % 2 == 0 and hot_powerball:
powerball = random.choice(hot_powerball)
elif overdue_powerball:
powerball = random.choice(overdue_powerball[:5])
else:
powerball = random.randint(1, 26)
# Calculate combination stats
odds = sum(1 for n in selected if n % 2 == 1)
evens = 5 - odds
lows = sum(1 for n in selected if n <= 35)
highs = 5 - lows
total = sum(selected)
combinations.append({
'strategy': strategy_name,
'description': strategy_desc,
'numbers': selected,
'powerball': powerball,
'odds': odds,
'evens': evens,
'lows': lows,
'highs': highs,
'sum': total,
'attempts': attempts
})
# Display combinations
for i, combo in enumerate(combinations, 1):
print(f"\nCombination #{i}: {combo['strategy']}")
print(f" Strategy: {combo['description']}")
print(f" Numbers: {' '.join(map(str, combo['numbers']))} + Powerball: {combo['powerball']}")
print(f" Stats: {combo['odds']}odd-{combo['evens']}even, {combo['lows']}low-{combo['highs']}high, Sum: {combo['sum']}")
print("\n" + "=" * 60)
print("Quick Pick Lines:")
print("=" * 60)
for i, combo in enumerate(combinations, 1):
numbers_str = ' - '.join(f"{n:2d}" for n in combo['numbers'])
print(f" Line {i}: {numbers_str} | PB: {combo['powerball']:2d}")
return combinations
def main():
"""Main function."""
# Default to 'powerball_numbers.csv' in the same directory as the script
script_dir = Path(__file__).parent
csv_file = script_dir / 'powerball_numbers.csv'
# You can also specify a different file path here
# csv_file = Path('path/to/your/file.csv')
print(f"Reading Powerball numbers from: {csv_file}\n")
powerball_data = read_powerball_csv(str(csv_file))
if powerball_data:
print(f"Successfully loaded {len(powerball_data)} records.\n")
display_powerball_data(powerball_data)
stats = analyze_most_frequent_numbers(powerball_data)
generate_suggested_combination(stats)
# Run advanced pattern detection
pattern_results = run_all_pattern_analyses(powerball_data)
# Generate pattern-based combinations
pattern_combos = generate_pattern_based_combinations(powerball_data, pattern_results, num_combinations=5)
else:
print("Failed to load powerball data.")
if __name__ == '__main__':
main()
-382
View File
@@ -1,382 +0,0 @@
#!/bin/python3
"""
Parallel rsync launcher.
Requires rsync on PATH (e.g. via MSYS/Cygwin on Windows).
Runs multiple rsync invocations in parallel using either top-level entries
or balanced recursive file chunks.
"""
# Usage examples:
#
# Basic sync (recursive-files mode, auto worker count):
# python pyrsync.py /src/dir user@host:/dest/
#
# Auto-tune workers based on file size and local vs remote destination:
# python pyrsync.py /src/dir user@host:/dest/ --auto-tune
#
# Limit to a specific number of parallel workers:
# python pyrsync.py /src/dir /local/dest/ -w 8
#
# Use top-level mode (one rsync process per immediate source entry):
# python pyrsync.py /src/dir /local/dest/ --mode top-level
#
# Dry run (show what would be transferred without doing it):
# python pyrsync.py /src/dir user@host:/dest/ --dry-run
#
# Verbose output (shows command templates, task list preview, and elapsed time):
# python pyrsync.py /src/dir /local/dest/ -v
#
# Show rsync output for all tasks:
# python pyrsync.py /src/dir /local/dest/ --show-rsync-output
#
# Show rsync output only when a task fails:
# python pyrsync.py /src/dir user@host:/dest/ --show-rsync-output-on-failure
#
# Full-featured example (auto-tune, verbose, show failures):
# python pyrsync.py /src/dir user@host:/dest/ --auto-tune -v --show-rsync-output-on-failure
#
# Override rsync flags entirely:
# python pyrsync.py /src/dir /local/dest/ -r "-az --checksum"
import argparse
import os
import re
import shlex
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import List, Sequence, Tuple
# DEFAULT_RSYNC_ARGS = ["-av", "--partial", "--compress", "--info=progress2"]
DEFAULT_RSYNC_ARGS = ["-a", "--partial", "--info=stats2"]
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 build_recursive_file_list(src_dir: Path) -> List[Path]:
"""Return all files under src_dir as relative paths for --files-from."""
return [p.relative_to(src_dir) for p in src_dir.rglob("*") if p.is_file()]
def get_sized_files(src_dir: Path, rel_files: Sequence[Path]) -> List[Tuple[int, Path]]:
"""Return (size_bytes, rel_path) tuples with best-effort size lookup."""
sized_files: List[Tuple[int, Path]] = []
for rel_path in rel_files:
abs_path = src_dir / rel_path
try:
size = abs_path.stat().st_size
except OSError:
size = 0
sized_files.append((size, rel_path))
return sized_files
def partition_by_size(sized_files: Sequence[Tuple[int, Path]], workers: int) -> List[List[Path]]:
"""Greedy size-balanced partitioning to reduce worker skew."""
buckets: List[List[Path]] = [[] for _ in range(workers)]
bucket_sizes = [0] * workers
# Largest files first gives better load balancing across workers.
sorted_files = sorted(sized_files, key=lambda x: x[0], reverse=True)
for size, rel_path in sorted_files:
idx = min(range(workers), key=lambda i: bucket_sizes[i])
buckets[idx].append(rel_path)
bucket_sizes[idx] += size
return [b for b in buckets if b]
def detect_remote_destination(dest: str) -> bool:
"""Heuristic detection of rsync remote destinations."""
if dest.startswith("rsync://"):
return True
if "::" in dest:
return True
if re.match(r"^[a-zA-Z]:[\\/]", dest):
return False
if re.match(r"^\\\\", dest):
return False
return bool(re.match(r"^[^:/\\]+@?[^:/\\]*:[^/].*", dest))
def format_bytes(num_bytes: float) -> str:
units = ["B", "KB", "MB", "GB", "TB"]
value = float(num_bytes)
for unit in units:
if value < 1024.0 or unit == units[-1]:
return f"{value:.1f}{unit}"
value /= 1024.0
return f"{num_bytes:.1f}B"
def auto_tune_workers(
total_units: int,
avg_size_bytes: float,
is_remote: bool,
cpu_count: int,
configured_workers: int,
) -> int:
"""Pick worker count from workload size profile and destination type."""
cpu = max(1, cpu_count)
if avg_size_bytes < 1 * 1024 * 1024:
base = min(32, cpu * 4)
elif avg_size_bytes < 32 * 1024 * 1024:
base = min(16, cpu * 2)
elif avg_size_bytes < 256 * 1024 * 1024:
base = min(8, cpu)
else:
base = min(4, max(2, cpu // 2))
if is_remote:
base = max(2, min(base, 8))
else:
base = min(base + 2, 24)
tuned = max(1, min(base, total_units))
return min(tuned, configured_workers)
def maybe_print_task_output(label: str, out: str, err: str, show_output: bool, show_on_fail_only: bool, code: int) -> None:
if not show_output:
return
if show_on_fail_only and code == 0:
return
if out.strip():
print(f"--- stdout: {label} ---")
print(out.rstrip())
if err.strip():
print(f"--- stderr: {label} ---", file=sys.stderr)
print(err.rstrip(), file=sys.stderr)
def run_rsync(item: Path, dest: str, rsync_args: List[str], dry_run: bool) -> Tuple[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 run_rsync_files_from(
src_dir: Path,
rel_files: Sequence[Path],
dest: str,
rsync_args: List[str],
dry_run: bool,
) -> Tuple[str, int, str, str]:
cmd = ["rsync", *rsync_args]
if dry_run:
cmd.append("--dry-run")
cmd += ["--from0", "--files-from=-", f"{src_dir}{os.sep}", dest]
payload = "\0".join(str(p) for p in rel_files) + "\0"
try:
proc = subprocess.run(cmd, input=payload, capture_output=True, text=True)
label = f"chunk({len(rel_files)} files)"
return label, proc.returncode, proc.stdout, proc.stderr
except FileNotFoundError:
return "chunk(0 files)", 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=min(8, (os.cpu_count() or 4)),
help="Parallel rsync workers (default: min(8, CPU count))",
)
ap.add_argument(
"--mode",
choices=["top-level", "recursive-files"],
default="recursive-files",
help=(
"top-level: one rsync per immediate src entry; "
"recursive-files: split full file tree into balanced rsync chunks"
),
)
ap.add_argument("-r", "--rsync-args", default=" ".join(DEFAULT_RSYNC_ARGS),
help="Additional rsync args (quoted). Default: -a --partial --info=stats2")
ap.add_argument(
"--auto-tune",
action="store_true",
help=(
"Auto-tune worker count from average file size and destination type. "
"Uses --workers as an upper bound."
),
)
ap.add_argument(
"-v",
"--verbose",
action="store_true",
help="Show detailed progress, including task start/completion and rsync command lines.",
)
ap.add_argument(
"--show-rsync-output",
action="store_true",
help="Print rsync stdout/stderr for each task (can be noisy).",
)
ap.add_argument(
"--show-rsync-output-on-failure",
action="store_true",
help="Print rsync stdout/stderr only for failed tasks.",
)
ap.add_argument("--dry-run", action="store_true", help="Don't transfer, just show what would happen")
args = ap.parse_args()
if args.workers < 1:
print("--workers must be >= 1", file=sys.stderr)
sys.exit(2)
rsync_args = shlex.split(args.rsync_args)
results = []
started_at = time.perf_counter()
if args.show_rsync_output and args.show_rsync_output_on_failure:
print(
"Both --show-rsync-output and --show-rsync-output-on-failure set; "
"showing output for all tasks.",
file=sys.stderr,
)
show_output = args.show_rsync_output or args.show_rsync_output_on_failure
show_on_fail_only = args.show_rsync_output_on_failure and not args.show_rsync_output
src_path = Path(args.src)
is_remote_dest = detect_remote_destination(args.dest)
cpu_count = os.cpu_count() or 4
if args.mode == "recursive-files" and src_path.is_dir():
rel_files = build_recursive_file_list(src_path)
if not rel_files:
print("No files found for:", args.src, file=sys.stderr)
sys.exit(2)
sized_files = get_sized_files(src_path, rel_files)
avg_size = sum(size for size, _ in sized_files) / max(1, len(sized_files))
if args.auto_tune:
workers = auto_tune_workers(len(rel_files), avg_size, is_remote_dest, cpu_count, args.workers)
dest_kind = "remote" if is_remote_dest else "local"
print(
f"Auto-tuned workers={workers} ({dest_kind} dest, avg file {format_bytes(avg_size)})"
)
else:
workers = min(args.workers, len(rel_files))
chunks = partition_by_size(sized_files, workers)
print(f"Starting {len(chunks)} rsync chunk task(s) with {workers} worker(s)...")
if args.verbose:
cmd_preview = ["rsync", *rsync_args]
if args.dry_run:
cmd_preview.append("--dry-run")
cmd_preview += ["--from0", "--files-from=-", f"{src_path}{os.sep}", args.dest]
print("Command template:", " ".join(shlex.quote(part) for part in cmd_preview))
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {
ex.submit(run_rsync_files_from, src_path, chunk, args.dest, rsync_args, args.dry_run): chunk
for chunk in chunks
}
for fut in as_completed(futures):
label, code, out, err = fut.result()
results.append((label, code, out, err))
status = "OK" if code == 0 else f"ERR({code})"
print(f"[{status}] {label}")
maybe_print_task_output(label, out, err, show_output, show_on_fail_only, code)
else:
items = build_file_list(args.src)
if not items:
print("No files found for:", args.src, file=sys.stderr)
sys.exit(2)
size_samples = []
for item in items:
try:
if item.is_file():
size_samples.append(item.stat().st_size)
else:
size_samples.append(64 * 1024 * 1024)
except OSError:
size_samples.append(0)
avg_size = sum(size_samples) / max(1, len(size_samples))
if args.auto_tune:
workers = auto_tune_workers(len(items), avg_size, is_remote_dest, cpu_count, args.workers)
dest_kind = "remote" if is_remote_dest else "local"
print(
f"Auto-tuned workers={workers} ({dest_kind} dest, avg unit {format_bytes(avg_size)})"
)
else:
workers = min(args.workers, len(items))
print(f"Starting {len(items)} rsync task(s) with {workers} worker(s)...")
if args.verbose:
print("Task list preview:")
preview_limit = 10
for idx, item in enumerate(items[:preview_limit], start=1):
print(f" {idx}. {item}")
if len(items) > preview_limit:
print(f" ... and {len(items) - preview_limit} more")
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {}
for item in items:
if args.verbose:
cmd_preview = ["rsync", *rsync_args]
if args.dry_run:
cmd_preview.append("--dry-run")
cmd_preview += [str(item), args.dest]
print("Scheduling:", " ".join(shlex.quote(part) for part in cmd_preview))
futures[ex.submit(run_rsync, item, args.dest, rsync_args, args.dry_run)] = item
for fut in as_completed(futures):
item, code, out, err = fut.result()
results.append((str(item), code, out, err))
status = "OK" if code == 0 else f"ERR({code})"
print(f"[{status}] {item}")
maybe_print_task_output(str(item), out, err, show_output, show_on_fail_only, code)
# summary
failed = [r for r in results if r[1] != 0]
elapsed = time.perf_counter() - started_at
print(f"\nCompleted: {len(results)} tasks, {len(failed)} failed in {elapsed:.2f}s.")
if failed:
print("Failures detail:")
for label, code, out, err in failed:
print(f"--- {label} (exit {code}) ---")
if out:
print(out.strip())
if err:
print(err.strip())
sys.exit(1 if failed else 0)
if __name__ == "__main__":
main()
-40
View File
@@ -1,40 +0,0 @@
import csv
from pathlib import Path
def collect_first_tokens_from_full_table_files(directory: Path) -> list[str]:
first_tokens: list[str] = []
for file_path in directory.iterdir():
# Match file names like "Full_Table_List_..." regardless of case.
if file_path.is_file() and file_path.name.lower().startswith("full_table_list"):
with file_path.open("r", encoding="utf-8", errors="ignore") as f:
for line in f:
parts = line.strip().split()
if parts:
first_tokens.append(parts[0])
return first_tokens
def main() -> None:
current_directory = Path.cwd()
tokens = collect_first_tokens_from_full_table_files(current_directory)
tokens.sort()
for token in tokens:
print(token)
output_file = current_directory / "full_table_list_tokens.csv"
with output_file.open("w", newline="", encoding="utf-8") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(["value"])
for token in tokens:
writer.writerow([token])
print(f"\nTotal extracted values: {len(tokens)}")
print(f"Saved CSV file: {output_file}")
if __name__ == "__main__":
main()
-105
View File
@@ -1,105 +0,0 @@
#Requires -Version 5.1
# Use -ShutdownRegardless to force shutdown even if one or more sync jobs fail.
param(
[switch]$ShutdownRegardless
)
$FfsExe = 'C:\Program Files\FreeFileSync\FreeFileSync.exe'
$ScriptDir = $PSScriptRoot
$LogDir = Join-Path $ScriptDir 'logs'
$ReportDir = Join-Path $ScriptDir 'reports'
$SyncJobs = @(
'C:\sync_jobs\shuttlebay_SyncSettings.ffs_batch'
'C:\sync_jobs\main_bridge_SyncSettings.ffs_batch'
'C:\sync_jobs\engineering_SyncSettings.ffs_batch'
)
# ── Setup ──────────────────────────────────────────────────────────────────────
foreach ($dir in $LogDir, $ReportDir) {
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
}
$RunTs = Get-Date -Format 'yyyyMMdd_HHmmss'
$LogFile = Join-Path $LogDir "sync_history_$RunTs.log"
function Write-Log {
param([string]$Message)
$line = "[$(Get-Date -Format 'ddd MM/dd/yyyy HH:mm:ss.ff')] $Message"
Write-Host $line
Add-Content -Path $LogFile -Value $line
}
Write-Log 'Sync run started'
Write-Host "Log file: $LogFile"
# ── Run sync jobs ──────────────────────────────────────────────────────────────
$HadFatalError = $false
$FatalExitCodes = @()
foreach ($job in $SyncJobs) {
$jobName = Split-Path $job -Leaf
Write-Host ''
Write-Log "Running synchronization: $jobName"
$tmpOut = [IO.Path]::GetTempFileName()
$proc = Start-Process -FilePath $FfsExe -ArgumentList $job `
-RedirectStandardOutput $tmpOut -NoNewWindow -PassThru -Wait
$rc = $proc.ExitCode
$output = Get-Content -Path $tmpOut -Raw
Remove-Item $tmpOut -Force
if ($output) { Add-Content -Path $LogFile -Value $output }
switch ($rc) {
0 { Write-Log 'Synchronization completed successfully.' }
1 { Write-Log 'Synchronization completed with warnings.' }
default {
Write-Log "Synchronization failed or was aborted. Exit code: $rc"
$HadFatalError = $true
$FatalExitCodes += $rc
continue
}
}
}
Write-Host ''
Write-Log 'All synchronization jobs completed.'
# ── Copy HTML reports ──────────────────────────────────────────────────────────
$logPaths = Select-String -Path $LogFile -Pattern '"logFile"\s*:\s*"([^"]+)"' |
ForEach-Object { $_.Matches[0].Groups[1].Value -replace '\\\\', '\' } |
Sort-Object -Unique
$copied = 0
foreach ($path in $logPaths) {
if ([IO.Path]::GetExtension($path) -ieq '.html') {
if (Test-Path -LiteralPath $path) {
Copy-Item -LiteralPath $path -Destination $ReportDir -Force
Write-Log "Copied report: $(Split-Path $path -Leaf)"
$copied++
} else {
Write-Log "Report not found: $path"
}
}
}
Write-Log "Total reports copied: $copied"
# ── Shutdown ───────────────────────────────────────────────────────────────────
if ($HadFatalError) {
$codes = ($FatalExitCodes | Sort-Object -Unique) -join ', '
Write-Log "One or more synchronization jobs failed. Fatal exit code(s): $codes"
if (-not $ShutdownRegardless) {
Write-Log 'Skipping shutdown because not all synchronization jobs completed successfully.'
Write-Log 'Use -ShutdownRegardless to force shutdown even when jobs fail.'
exit ($FatalExitCodes | Select-Object -Last 1)
}
Write-Log 'Forced shutdown enabled. Continuing to shutdown despite synchronization failures.'
}
Write-Log 'Shutting down server now.'
Invoke-CimMethod -ClassName Win32_OperatingSystem -MethodName Win32Shutdown -Arguments @{ Flags = 5 }