Guest User

Untitled

a guest
Apr 16th, 2025
28
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 15.59 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import threading
  4. import time
  5. import os
  6. import subprocess
  7. import shutil
  8. import signal
  9. import sys
  10.  
  11. lock = threading.Lock()
  12.  
  13. DEVICE_DIR = "/mnt/data/devices"
  14.  
  15. BASE_MNT = "/mnt"
  16. DRIVE_DIR_BACK = "/mnt/data/.drive"
  17. DRIVE_DIR = "/mnt/data/drive"
  18.  
  19. SLOTS_DIR_BACK = f"{DRIVE_DIR_BACK}/slots"
  20. ARRAY_DIR_BACK = f"{DRIVE_DIR_BACK}/array"
  21. ALL_DIR_BACK = f"{DRIVE_DIR_BACK}/array/all"
  22.  
  23. BY_SLOT = f"{DRIVE_DIR}/by-slot"
  24. BY_LABEL = f"{DRIVE_DIR}/by-label"
  25. BY_ARRAY = f"{DRIVE_DIR}/by-array"
  26.  
  27. MERGEFS_OPTIONS = "defaults,allow_other,use_ino,moveonenospc=true,category.create=ff,category.search=ff"
  28. MERGEFS_TARGET = f"{BY_ARRAY}/all"
  29.  
  30. SLEEP_DELAY = 2
  31.  
  32. CONFIG = {
  33.     "array": [
  34.         "ata-SSD_480GB_YS202010481034HP",
  35.         "uuid-2",
  36.         "uuid-3"
  37.     ],
  38.     "parity": [
  39.         "uuid-5p"
  40.     ],
  41.     "paths": [
  42.         "pci-0000:00:17.0-ata-1",
  43.         "pci-0000:00:17.0-ata-3",
  44.         "pci-0000:07:00.0-sas-phy3-lun-0",
  45.         "pci-0000:07:00.0-sas-phy2-lun-0",
  46.         "pci-0000:07:00.0-sas-phy1-lun-0",
  47.         "pci-0000:07:00.0-sas-phy0-lun-0",
  48.         "pci-0000:00:17.0-ata-2",
  49.         "pci-0000:00:17.0-ata-4",
  50.         "pci-0000:07:00.0-sas-phy7-lun-0",
  51.         "pci-0000:07:00.0-sas-phy6-lun-0",
  52.         "pci-0000:07:00.0-sas-phy6-lun-0",
  53.         "pci-0000:07:00.0-sas-phy4-lun-0"
  54.     ],
  55.     "sleeptime": [
  56.         ("wwn-0x50014ee2b8b089be", 60)
  57.     ]
  58. }
  59.  
  60. mounted_disks = {}
  61. mounted_label = {}
  62. mounted_array = {}
  63. mounted_sleep = {}
  64. hwmon_number = {}
  65.  
  66. def set_sleep_time():
  67.     cmd = ["hd-idle", "-i", "0"]
  68.     for disk_id, seconds in CONFIG.get("sleeptime", []):
  69.         device_path = f"/dev/disk/by-id/{disk_id}"
  70.         idle_value = str(seconds)
  71.         cmd.extend(["-a", device_path, "-i", idle_value])
  72.         print(f"Set sleep for {device_path} -> {seconds}Sec (hd-idle -i {idle_value})")
  73.     cmd.append("-s")
  74.     cmd.append("1")
  75.     screen_cmd = ["screen", "-dmS", "hdidle"] + cmd
  76.     print(f"Launching in screen: {' '.join(screen_cmd)}")
  77.     subprocess.call(screen_cmd)
  78.  
  79. def check_and_exit(command):
  80.     result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  81.     if result.returncode != 0:
  82.         print(f"Error executing command: {command}", file=sys.stderr)
  83.         print(result.stderr.decode(), file=sys.stderr)
  84.         exit(1)
  85.  
  86. def check_module_loaded(module):
  87.     result = subprocess.run(f"grep -qw {module} /proc/modules", shell=True)
  88.     return result.returncode == 0
  89.  
  90. def mount_if_needed(target, source, options=None):
  91.     if not os.path.ismount(target):
  92.         print(f"Mounting {source} to {target}")
  93.         if options:
  94.             subprocess.run(f"mount -o {options} {source} {target}", shell=True)
  95.         else:
  96.             subprocess.run(f"mount --bind {source} {target}", shell=True)
  97.         subprocess.run(f"mount -o remount,ro,bind {target}", shell=True)
  98.  
  99. def is_mountpoint(path):
  100.     result = subprocess.run(["mountpoint", "-q", path])
  101.     return result.returncode == 0
  102.  
  103. def unmount_if_needed(target):
  104.     print(f"Unmounting if needed {target}")
  105.     if is_mountpoint(target):
  106.         print(f"Unmounting {target}")
  107.         subprocess.run(["umount", target], check=True)
  108.  
  109. def cleanup_by_label_dir():
  110.     print(f"[*] Cleaning {BY_LABEL} (unmounted directories)...")
  111.     for dirpath in os.listdir(BY_LABEL):
  112.         dirpath = os.path.join(BY_LABEL, dirpath)
  113.         if os.path.isdir(dirpath) and not os.path.ismount(dirpath):
  114.             print(f"[-] Removing unused directory: {dirpath}")
  115.             os.rmdir(dirpath)
  116.  
  117. def cleanup_array_dir():
  118.     print(f"[*] Cleaning {ARRAY_DIR_BACK} (unmounted directories)...")
  119.     for dirpath in os.listdir(ARRAY_DIR_BACK):
  120.         dirpath = os.path.join(ARRAY_DIR_BACK, dirpath)
  121.         if os.path.isdir(dirpath) and not os.path.ismount(dirpath):
  122.             print(f"[-] Removing unused directory: {dirpath}")
  123.             os.rmdir(dirpath)
  124.  
  125. def init_umount():
  126.     subprocess.call(["pkill", "-f", "hd-idle"])
  127.     unmount_if_needed(MERGEFS_TARGET)
  128.     if os.path.isdir(BY_LABEL):
  129.         for path in os.listdir(BY_LABEL):
  130.             path = os.path.join(BY_LABEL, path)
  131.             if os.path.isdir(path):
  132.                 unmount_if_needed(path)
  133.     cleanup_by_label_dir()
  134.     if os.path.isdir(BY_ARRAY):
  135.         for path in os.listdir(BY_ARRAY):
  136.             path = os.path.join(BY_ARRAY, path)
  137.             if os.path.isdir(path):
  138.                 unmount_if_needed(path)
  139.     cleanup_array_dir()
  140.     for i in range(1, len(CONFIG["paths"]) + 1):
  141.         path = os.path.join(BY_SLOT, f"hdd{i}")
  142.         if os.path.exists(path) and os.path.ismount(path):
  143.             print(f"Umount {path}")
  144.             subprocess.run(f"umount {path}", shell=True)
  145.     unmount_if_needed(BY_SLOT)
  146.     unmount_if_needed(BY_ARRAY)
  147.     if os.path.exists(DEVICE_DIR):
  148.         shutil.rmtree(DEVICE_DIR)
  149.     os.makedirs(DEVICE_DIR)
  150.  
  151. def signal_handler(sig, frame):
  152.     print("\n[INFO] Interrupt detected (Ctrl+C), unmounting...")
  153.     init_umount()
  154.     sys.exit(0)
  155.  
  156. def init_structure():
  157.     print("[*] Checking required module and programs")
  158.     if not check_module_loaded("drivetemp"):
  159.         print("Error: Kernel module 'drivetemp' is not loaded.", file=sys.stderr)
  160.         exit(1)
  161.     if shutil.which("screen") is None:
  162.         print("Error: 'screen' is not installed.", file=sys.stderr)
  163.         print("Install it with: sudo apt install screen", file=sys.stderr)
  164.         exit(1)
  165.     if shutil.which("hd-idle") is None:
  166.         print("Error: 'hd-idle' is not installed.", file=sys.stderr)
  167.         print("Follow the instructions at: https://github.com/adelolmo/hd-idle", file=sys.stderr)
  168.         exit(1)
  169.     print("[*] Unmounting")
  170.     init_umount()
  171.     print("[*] Initializing structure...")
  172.     for idx, path in enumerate(CONFIG["paths"], start=1):
  173.         slot = f"hdd{idx}"
  174.         src = f"/dev/disk/by-path/{path}"
  175.         dst = os.path.join(DEVICE_DIR, slot)
  176.         try:
  177.             os.symlink(src, dst)
  178.             print(f"[OK] Link: {dst} -> {src}")
  179.         except FileNotFoundError:
  180.             print(f"[WARN] Source not found: {src}")
  181.         except FileExistsError:
  182.             print(f"[SKIP] Link already exists: {dst}")
  183.         except Exception as e:
  184.             print(f"[ERROR] Failed to link {dst}: {e}")
  185.     os.makedirs(DEVICE_DIR, exist_ok=True)
  186.     os.makedirs(DRIVE_DIR_BACK, exist_ok=True)
  187.     os.makedirs(DRIVE_DIR, exist_ok=True)
  188.     os.makedirs(SLOTS_DIR_BACK, exist_ok=True)
  189.     os.makedirs(ARRAY_DIR_BACK, exist_ok=True)
  190.     os.makedirs(BY_SLOT, exist_ok=True)
  191.     os.makedirs(BY_LABEL, exist_ok=True)
  192.     os.makedirs(BY_ARRAY, exist_ok=True)
  193.     for i in range(1, len(CONFIG["paths"]) + 1):
  194.         os.makedirs(os.path.join(SLOTS_DIR_BACK, f"hdd{i}"), exist_ok=True)
  195.     create_configured_array_dirs()
  196.     os.makedirs(ALL_DIR_BACK, exist_ok=True)
  197.     mount_if_needed(BY_SLOT, SLOTS_DIR_BACK)
  198.     mount_if_needed(BY_ARRAY, ARRAY_DIR_BACK)
  199.     branches = ""
  200.     for driveid in CONFIG["array"]:
  201.         branch = os.path.join(BY_ARRAY, driveid)
  202.         print(f"Branch = {branch}")
  203.         branches += branch + ":"
  204.     branches = branches[:-1]
  205.     subprocess.run(f"mergerfs -o {MERGEFS_OPTIONS} {branches} {MERGEFS_TARGET}", shell=True)
  206.     set_sleep_time()
  207.  
  208. def create_configured_array_dirs():
  209.     for driveid in CONFIG["array"]:
  210.         print(f"driveid={driveid}")
  211.         os.makedirs(os.path.join(ARRAY_DIR_BACK, driveid), exist_ok=True)
  212.     for driveid in CONFIG["parity"]:
  213.         print(f"driveid={driveid}")
  214.         os.makedirs(os.path.join(ARRAY_DIR_BACK, driveid), exist_ok=True)
  215.  
  216. def mount_disk(name):
  217.     device = os.path.realpath(f"{DEVICE_DIR}/{name}")
  218.     disk = os.path.basename(device)
  219.     disk_id = get_disk_id(disk)
  220.     largest_partition = get_largest_partition(disk)
  221.     target = f"{BY_SLOT}/{name}"
  222.     if os.path.exists(f"/dev/{largest_partition}") and name not in mounted_disks:
  223.         uuid = subprocess.getoutput(f"/usr/sbin/blkid -s UUID -o value /dev/{largest_partition}")
  224.         label = subprocess.getoutput(f"/usr/sbin/blkid -s LABEL -o value /dev/{largest_partition}")
  225.         if not label:
  226.             label = "NoName"
  227.         newlabel = find_unique_label_name(label)
  228.         print(f"[+] Mounting /dev/{largest_partition} (UUID: {uuid}, Label: {label}, Disk: {disk_id}) on {target}")
  229.         os.makedirs(target, exist_ok=True)
  230.         if subprocess.call(["mount", f"/dev/{largest_partition}", target]) == 0:
  231.             mounted_disks[name] = f"/dev/{largest_partition}"
  232.         os.makedirs(f"{BY_LABEL}/{newlabel}", exist_ok=True)
  233.         if subprocess.call(["mount", "--bind", target, f"{BY_LABEL}/{newlabel}"]) == 0:
  234.             mounted_label[name] = f"{BY_LABEL}/{newlabel}"
  235.             print(f"mount --bind {target} {BY_LABEL}/{newlabel}")
  236.         dirarray = f"{BY_ARRAY}/{disk_id}"
  237.         if os.path.isdir(dirarray):
  238.             subprocess.call(["mount", "--bind", target, dirarray])
  239.             print(f"mount --bind {target} {dirarray}")
  240.             mounted_array[name] = dirarray
  241.         mounted_sleep[name] = False
  242.         hwmonnum = get_hwmon_number(disk)
  243.         print(f"hwmon number: {hwmonnum}")
  244.         hwmon_number[name] = hwmonnum
  245.  
  246. def unmount_disk(name):
  247.     target = f"{BY_SLOT}/{name}"
  248.     if name in mounted_disks:
  249.         print(f"[-] Unmounting {target}")
  250.         subprocess.call(["umount", "-f", target])
  251.         del mounted_disks[name]
  252.         mounted_sleep.pop(name, None)
  253.         hwmon_number.pop(name, None)
  254.     if name in mounted_label:
  255.         print(f"[-] Unmounting {mounted_label[name]}")
  256.         subprocess.call(["umount", "-f", mounted_label[name]])
  257.         del mounted_label[name]
  258.     if name in mounted_array:
  259.         print(f"[-] Unmounting {mounted_array[name]}")
  260.         subprocess.call(["umount", "-f", mounted_array[name]])
  261.         del mounted_array[name]
  262.  
  263. def get_largest_partition(disk):
  264.     try:
  265.         output = subprocess.check_output(["lsblk", "-o", "NAME,SIZE", "-r", f"/dev/{disk}"], text=True)
  266.         lines = output.strip().split("\n")
  267.         partitions = [line.split()[0] for line in lines if line.startswith(f"{disk}") and line != disk]
  268.         sizes = {line.split()[0]: line.split()[1] for line in lines if line.startswith(f"{disk}") and line != disk}
  269.         partitions.sort(key=lambda p: parse_size(sizes[p]))
  270.         return partitions[-1] if partitions else None
  271.     except Exception:
  272.         return None
  273.  
  274. def parse_size(size_str):
  275.     unit = size_str[-1].upper()
  276.     try:
  277.         value = float(size_str[:-1])
  278.     except ValueError:
  279.         return 0
  280.     if unit == 'G':
  281.         return value * 1024
  282.     elif unit == 'M':
  283.         return value
  284.     elif unit == 'K':
  285.         return value / 1024
  286.     elif unit == 'T':
  287.         return value * 1024 * 1024
  288.     return value
  289.  
  290. def get_disk_id(disk):
  291.     try:
  292.         output = subprocess.check_output("ls -l /dev/disk/by-id/", shell=True, text=True)
  293.         lines = output.strip().split("\n")
  294.         candidates = [line.split()[-1] for line in lines if line.endswith(f"../../{disk}")]
  295.         ids = [line.split()[8] for line in lines if line.endswith(f"../../{disk}")]
  296.         for entry in ids:
  297.             if entry.startswith("wwn-"):
  298.                 return entry
  299.         if ids:
  300.             return ids[0]
  301.     except Exception:
  302.         pass
  303.     return None
  304.  
  305. def find_unique_label_name(base):
  306.     label = base
  307.     count = 2
  308.     while os.path.exists(f"/mnt/disks/by-label/{label}"):
  309.         label = f"{base}-{count}"
  310.         count += 1
  311.     return label
  312.  
  313. def get_hwmon_number(dev_path):
  314.     try:
  315.         info = subprocess.check_output(["udevadm", "info", "--query=all", "--name", f"/dev/{dev_path}"], text=True)
  316.         path_line = next((line for line in info.splitlines() if line.startswith("P:")), None)
  317.         if path_line:
  318.             sys_path = path_line.split(" ")[1].split("/block/")[0]
  319.             output = subprocess.check_output("find /sys/class/hwmon -type l -exec readlink {} \\;", shell=True, text=True)
  320.             for line in output.splitlines():
  321.                 if sys_path in line:
  322.                     import re
  323.                     match = re.search(r"hwmon(\d+)", line)
  324.                     if match:
  325.                         return match.group(1)
  326.     except Exception:
  327.         pass
  328.     return "-1"
  329.  
  330. def get_drivetemp(hwmon_num):
  331.     try:
  332.         hwmon_num = int(hwmon_num)
  333.     except ValueError:
  334.         return "0"
  335.     if hwmon_num < 0:
  336.         return "0"
  337.     temp_file = f"/sys/class/hwmon/hwmon{hwmon_num}/temp1_input"
  338.     if os.path.isfile(temp_file):
  339.         try:
  340.             with open(temp_file, "r") as f:
  341.                 temp_raw = f.read().strip()
  342.             if temp_raw.isdigit():
  343.                 return str(int(temp_raw) // 1000)
  344.             else:
  345.                 return "0"
  346.         except Exception:
  347.             return "0"
  348.     else:
  349.         return "0"
  350.  
  351. def monitor_sleep_and_temp():
  352.     while True:
  353.         with lock:
  354.             for name in list(mounted_disks.keys()):
  355.                 device_link = os.path.join(DEVICE_DIR, name)
  356.                 try:
  357.                     device_path = os.path.realpath(device_link)
  358.                     disk_sdx = os.path.basename(device_path)
  359.                     base_device = f"/dev/{disk_sdx}"
  360.                     if os.path.exists(base_device) and os.path.exists(device_path):
  361.                         try:
  362.                             output = subprocess.check_output(["hdparm", "-C", base_device], stderr=subprocess.DEVNULL, text=True)
  363.                             state_line = next((line for line in output.splitlines() if "state" in line), "")
  364.                             state = state_line.strip().split()[-1] if state_line else ""
  365.                         except subprocess.CalledProcessError:
  366.                             state = ""
  367.                         current_sleep = state == "standby"
  368.                         print(f"state = {state}")
  369.                         previous_sleep = mounted_sleep.get(name, None)
  370.                         if previous_sleep != current_sleep:
  371.                             mounted_sleep[name] = current_sleep
  372.                             if current_sleep:
  373.                                 print(f"[i] {name} is now in standby.")
  374.                             else:
  375.                                 print(f"[i] {name} is now active.")
  376.                         if not current_sleep:
  377.                             hwmon_num = hwmon_number.get(name, -1)
  378.                             temp = get_drivetemp(hwmon_num)
  379.                             print(f"Temperature for {name}: {temp}°C")
  380.                 except FileNotFoundError:
  381.                     continue
  382.         time.sleep(SLEEP_DELAY)
  383.  
  384. signal.signal(signal.SIGINT, signal_handler)
  385.  
  386. if __name__ == "__main__":
  387.     init_structure()
  388.     print(f"[*] Watching {DEVICE_DIR} for auto-mount...")
  389.     monitor_thread = threading.Thread(target=monitor_sleep_and_temp, daemon=True)
  390.     monitor_thread.start()
  391.     while True:
  392.         with lock:
  393.             for link in os.listdir(DEVICE_DIR):
  394.                 full_path = os.path.join(DEVICE_DIR, link)
  395.                 name = os.path.basename(link)
  396.                 if os.path.exists(full_path):
  397.                     mount_disk(name)
  398.                 else:
  399.                     unmount_disk(name)
  400.             for name in list(mounted_disks.keys()):
  401.                 link = os.path.join(DEVICE_DIR, name)
  402.                 try:
  403.                     device = os.path.realpath(link)
  404.                     if not os.path.exists(device) or not os.path.exists(f"/sys/class/block/{os.path.basename(device)}"):
  405.                         unmount_disk(name)
  406.                 except Exception:
  407.                     unmount_disk(name)
  408.         time.sleep(SLEEP_DELAY)
  409.  
Add Comment
Please, Sign In to add comment