Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- import threading
- import time
- import os
- import subprocess
- import shutil
- import signal
- import sys
- lock = threading.Lock()
- DEVICE_DIR = "/mnt/data/devices"
- BASE_MNT = "/mnt"
- DRIVE_DIR_BACK = "/mnt/data/.drive"
- DRIVE_DIR = "/mnt/data/drive"
- SLOTS_DIR_BACK = f"{DRIVE_DIR_BACK}/slots"
- ARRAY_DIR_BACK = f"{DRIVE_DIR_BACK}/array"
- ALL_DIR_BACK = f"{DRIVE_DIR_BACK}/array/all"
- BY_SLOT = f"{DRIVE_DIR}/by-slot"
- BY_LABEL = f"{DRIVE_DIR}/by-label"
- BY_ARRAY = f"{DRIVE_DIR}/by-array"
- MERGEFS_OPTIONS = "defaults,allow_other,use_ino,moveonenospc=true,category.create=ff,category.search=ff"
- MERGEFS_TARGET = f"{BY_ARRAY}/all"
- SLEEP_DELAY = 2
- CONFIG = {
- "array": [
- "ata-SSD_480GB_YS202010481034HP",
- "uuid-2",
- "uuid-3"
- ],
- "parity": [
- "uuid-5p"
- ],
- "paths": [
- "pci-0000:00:17.0-ata-1",
- "pci-0000:00:17.0-ata-3",
- "pci-0000:07:00.0-sas-phy3-lun-0",
- "pci-0000:07:00.0-sas-phy2-lun-0",
- "pci-0000:07:00.0-sas-phy1-lun-0",
- "pci-0000:07:00.0-sas-phy0-lun-0",
- "pci-0000:00:17.0-ata-2",
- "pci-0000:00:17.0-ata-4",
- "pci-0000:07:00.0-sas-phy7-lun-0",
- "pci-0000:07:00.0-sas-phy6-lun-0",
- "pci-0000:07:00.0-sas-phy6-lun-0",
- "pci-0000:07:00.0-sas-phy4-lun-0"
- ],
- "sleeptime": [
- ("wwn-0x50014ee2b8b089be", 60)
- ]
- }
- mounted_disks = {}
- mounted_label = {}
- mounted_array = {}
- mounted_sleep = {}
- hwmon_number = {}
- def set_sleep_time():
- cmd = ["hd-idle", "-i", "0"]
- for disk_id, seconds in CONFIG.get("sleeptime", []):
- device_path = f"/dev/disk/by-id/{disk_id}"
- idle_value = str(seconds)
- cmd.extend(["-a", device_path, "-i", idle_value])
- print(f"Set sleep for {device_path} -> {seconds}Sec (hd-idle -i {idle_value})")
- cmd.append("-s")
- cmd.append("1")
- screen_cmd = ["screen", "-dmS", "hdidle"] + cmd
- print(f"Launching in screen: {' '.join(screen_cmd)}")
- subprocess.call(screen_cmd)
- def check_and_exit(command):
- result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- if result.returncode != 0:
- print(f"Error executing command: {command}", file=sys.stderr)
- print(result.stderr.decode(), file=sys.stderr)
- exit(1)
- def check_module_loaded(module):
- result = subprocess.run(f"grep -qw {module} /proc/modules", shell=True)
- return result.returncode == 0
- def mount_if_needed(target, source, options=None):
- if not os.path.ismount(target):
- print(f"Mounting {source} to {target}")
- if options:
- subprocess.run(f"mount -o {options} {source} {target}", shell=True)
- else:
- subprocess.run(f"mount --bind {source} {target}", shell=True)
- subprocess.run(f"mount -o remount,ro,bind {target}", shell=True)
- def is_mountpoint(path):
- result = subprocess.run(["mountpoint", "-q", path])
- return result.returncode == 0
- def unmount_if_needed(target):
- print(f"Unmounting if needed {target}")
- if is_mountpoint(target):
- print(f"Unmounting {target}")
- subprocess.run(["umount", target], check=True)
- def cleanup_by_label_dir():
- print(f"[*] Cleaning {BY_LABEL} (unmounted directories)...")
- for dirpath in os.listdir(BY_LABEL):
- dirpath = os.path.join(BY_LABEL, dirpath)
- if os.path.isdir(dirpath) and not os.path.ismount(dirpath):
- print(f"[-] Removing unused directory: {dirpath}")
- os.rmdir(dirpath)
- def cleanup_array_dir():
- print(f"[*] Cleaning {ARRAY_DIR_BACK} (unmounted directories)...")
- for dirpath in os.listdir(ARRAY_DIR_BACK):
- dirpath = os.path.join(ARRAY_DIR_BACK, dirpath)
- if os.path.isdir(dirpath) and not os.path.ismount(dirpath):
- print(f"[-] Removing unused directory: {dirpath}")
- os.rmdir(dirpath)
- def init_umount():
- subprocess.call(["pkill", "-f", "hd-idle"])
- unmount_if_needed(MERGEFS_TARGET)
- if os.path.isdir(BY_LABEL):
- for path in os.listdir(BY_LABEL):
- path = os.path.join(BY_LABEL, path)
- if os.path.isdir(path):
- unmount_if_needed(path)
- cleanup_by_label_dir()
- if os.path.isdir(BY_ARRAY):
- for path in os.listdir(BY_ARRAY):
- path = os.path.join(BY_ARRAY, path)
- if os.path.isdir(path):
- unmount_if_needed(path)
- cleanup_array_dir()
- for i in range(1, len(CONFIG["paths"]) + 1):
- path = os.path.join(BY_SLOT, f"hdd{i}")
- if os.path.exists(path) and os.path.ismount(path):
- print(f"Umount {path}")
- subprocess.run(f"umount {path}", shell=True)
- unmount_if_needed(BY_SLOT)
- unmount_if_needed(BY_ARRAY)
- if os.path.exists(DEVICE_DIR):
- shutil.rmtree(DEVICE_DIR)
- os.makedirs(DEVICE_DIR)
- def signal_handler(sig, frame):
- print("\n[INFO] Interrupt detected (Ctrl+C), unmounting...")
- init_umount()
- sys.exit(0)
- def init_structure():
- print("[*] Checking required module and programs")
- if not check_module_loaded("drivetemp"):
- print("Error: Kernel module 'drivetemp' is not loaded.", file=sys.stderr)
- exit(1)
- if shutil.which("screen") is None:
- print("Error: 'screen' is not installed.", file=sys.stderr)
- print("Install it with: sudo apt install screen", file=sys.stderr)
- exit(1)
- if shutil.which("hd-idle") is None:
- print("Error: 'hd-idle' is not installed.", file=sys.stderr)
- print("Follow the instructions at: https://github.com/adelolmo/hd-idle", file=sys.stderr)
- exit(1)
- print("[*] Unmounting")
- init_umount()
- print("[*] Initializing structure...")
- for idx, path in enumerate(CONFIG["paths"], start=1):
- slot = f"hdd{idx}"
- src = f"/dev/disk/by-path/{path}"
- dst = os.path.join(DEVICE_DIR, slot)
- try:
- os.symlink(src, dst)
- print(f"[OK] Link: {dst} -> {src}")
- except FileNotFoundError:
- print(f"[WARN] Source not found: {src}")
- except FileExistsError:
- print(f"[SKIP] Link already exists: {dst}")
- except Exception as e:
- print(f"[ERROR] Failed to link {dst}: {e}")
- os.makedirs(DEVICE_DIR, exist_ok=True)
- os.makedirs(DRIVE_DIR_BACK, exist_ok=True)
- os.makedirs(DRIVE_DIR, exist_ok=True)
- os.makedirs(SLOTS_DIR_BACK, exist_ok=True)
- os.makedirs(ARRAY_DIR_BACK, exist_ok=True)
- os.makedirs(BY_SLOT, exist_ok=True)
- os.makedirs(BY_LABEL, exist_ok=True)
- os.makedirs(BY_ARRAY, exist_ok=True)
- for i in range(1, len(CONFIG["paths"]) + 1):
- os.makedirs(os.path.join(SLOTS_DIR_BACK, f"hdd{i}"), exist_ok=True)
- create_configured_array_dirs()
- os.makedirs(ALL_DIR_BACK, exist_ok=True)
- mount_if_needed(BY_SLOT, SLOTS_DIR_BACK)
- mount_if_needed(BY_ARRAY, ARRAY_DIR_BACK)
- branches = ""
- for driveid in CONFIG["array"]:
- branch = os.path.join(BY_ARRAY, driveid)
- print(f"Branch = {branch}")
- branches += branch + ":"
- branches = branches[:-1]
- subprocess.run(f"mergerfs -o {MERGEFS_OPTIONS} {branches} {MERGEFS_TARGET}", shell=True)
- set_sleep_time()
- def create_configured_array_dirs():
- for driveid in CONFIG["array"]:
- print(f"driveid={driveid}")
- os.makedirs(os.path.join(ARRAY_DIR_BACK, driveid), exist_ok=True)
- for driveid in CONFIG["parity"]:
- print(f"driveid={driveid}")
- os.makedirs(os.path.join(ARRAY_DIR_BACK, driveid), exist_ok=True)
- def mount_disk(name):
- device = os.path.realpath(f"{DEVICE_DIR}/{name}")
- disk = os.path.basename(device)
- disk_id = get_disk_id(disk)
- largest_partition = get_largest_partition(disk)
- target = f"{BY_SLOT}/{name}"
- if os.path.exists(f"/dev/{largest_partition}") and name not in mounted_disks:
- uuid = subprocess.getoutput(f"/usr/sbin/blkid -s UUID -o value /dev/{largest_partition}")
- label = subprocess.getoutput(f"/usr/sbin/blkid -s LABEL -o value /dev/{largest_partition}")
- if not label:
- label = "NoName"
- newlabel = find_unique_label_name(label)
- print(f"[+] Mounting /dev/{largest_partition} (UUID: {uuid}, Label: {label}, Disk: {disk_id}) on {target}")
- os.makedirs(target, exist_ok=True)
- if subprocess.call(["mount", f"/dev/{largest_partition}", target]) == 0:
- mounted_disks[name] = f"/dev/{largest_partition}"
- os.makedirs(f"{BY_LABEL}/{newlabel}", exist_ok=True)
- if subprocess.call(["mount", "--bind", target, f"{BY_LABEL}/{newlabel}"]) == 0:
- mounted_label[name] = f"{BY_LABEL}/{newlabel}"
- print(f"mount --bind {target} {BY_LABEL}/{newlabel}")
- dirarray = f"{BY_ARRAY}/{disk_id}"
- if os.path.isdir(dirarray):
- subprocess.call(["mount", "--bind", target, dirarray])
- print(f"mount --bind {target} {dirarray}")
- mounted_array[name] = dirarray
- mounted_sleep[name] = False
- hwmonnum = get_hwmon_number(disk)
- print(f"hwmon number: {hwmonnum}")
- hwmon_number[name] = hwmonnum
- def unmount_disk(name):
- target = f"{BY_SLOT}/{name}"
- if name in mounted_disks:
- print(f"[-] Unmounting {target}")
- subprocess.call(["umount", "-f", target])
- del mounted_disks[name]
- mounted_sleep.pop(name, None)
- hwmon_number.pop(name, None)
- if name in mounted_label:
- print(f"[-] Unmounting {mounted_label[name]}")
- subprocess.call(["umount", "-f", mounted_label[name]])
- del mounted_label[name]
- if name in mounted_array:
- print(f"[-] Unmounting {mounted_array[name]}")
- subprocess.call(["umount", "-f", mounted_array[name]])
- del mounted_array[name]
- def get_largest_partition(disk):
- try:
- output = subprocess.check_output(["lsblk", "-o", "NAME,SIZE", "-r", f"/dev/{disk}"], text=True)
- lines = output.strip().split("\n")
- partitions = [line.split()[0] for line in lines if line.startswith(f"{disk}") and line != disk]
- sizes = {line.split()[0]: line.split()[1] for line in lines if line.startswith(f"{disk}") and line != disk}
- partitions.sort(key=lambda p: parse_size(sizes[p]))
- return partitions[-1] if partitions else None
- except Exception:
- return None
- def parse_size(size_str):
- unit = size_str[-1].upper()
- try:
- value = float(size_str[:-1])
- except ValueError:
- return 0
- if unit == 'G':
- return value * 1024
- elif unit == 'M':
- return value
- elif unit == 'K':
- return value / 1024
- elif unit == 'T':
- return value * 1024 * 1024
- return value
- def get_disk_id(disk):
- try:
- output = subprocess.check_output("ls -l /dev/disk/by-id/", shell=True, text=True)
- lines = output.strip().split("\n")
- candidates = [line.split()[-1] for line in lines if line.endswith(f"../../{disk}")]
- ids = [line.split()[8] for line in lines if line.endswith(f"../../{disk}")]
- for entry in ids:
- if entry.startswith("wwn-"):
- return entry
- if ids:
- return ids[0]
- except Exception:
- pass
- return None
- def find_unique_label_name(base):
- label = base
- count = 2
- while os.path.exists(f"/mnt/disks/by-label/{label}"):
- label = f"{base}-{count}"
- count += 1
- return label
- def get_hwmon_number(dev_path):
- try:
- info = subprocess.check_output(["udevadm", "info", "--query=all", "--name", f"/dev/{dev_path}"], text=True)
- path_line = next((line for line in info.splitlines() if line.startswith("P:")), None)
- if path_line:
- sys_path = path_line.split(" ")[1].split("/block/")[0]
- output = subprocess.check_output("find /sys/class/hwmon -type l -exec readlink {} \\;", shell=True, text=True)
- for line in output.splitlines():
- if sys_path in line:
- import re
- match = re.search(r"hwmon(\d+)", line)
- if match:
- return match.group(1)
- except Exception:
- pass
- return "-1"
- def get_drivetemp(hwmon_num):
- try:
- hwmon_num = int(hwmon_num)
- except ValueError:
- return "0"
- if hwmon_num < 0:
- return "0"
- temp_file = f"/sys/class/hwmon/hwmon{hwmon_num}/temp1_input"
- if os.path.isfile(temp_file):
- try:
- with open(temp_file, "r") as f:
- temp_raw = f.read().strip()
- if temp_raw.isdigit():
- return str(int(temp_raw) // 1000)
- else:
- return "0"
- except Exception:
- return "0"
- else:
- return "0"
- def monitor_sleep_and_temp():
- while True:
- with lock:
- for name in list(mounted_disks.keys()):
- device_link = os.path.join(DEVICE_DIR, name)
- try:
- device_path = os.path.realpath(device_link)
- disk_sdx = os.path.basename(device_path)
- base_device = f"/dev/{disk_sdx}"
- if os.path.exists(base_device) and os.path.exists(device_path):
- try:
- output = subprocess.check_output(["hdparm", "-C", base_device], stderr=subprocess.DEVNULL, text=True)
- state_line = next((line for line in output.splitlines() if "state" in line), "")
- state = state_line.strip().split()[-1] if state_line else ""
- except subprocess.CalledProcessError:
- state = ""
- current_sleep = state == "standby"
- print(f"state = {state}")
- previous_sleep = mounted_sleep.get(name, None)
- if previous_sleep != current_sleep:
- mounted_sleep[name] = current_sleep
- if current_sleep:
- print(f"[i] {name} is now in standby.")
- else:
- print(f"[i] {name} is now active.")
- if not current_sleep:
- hwmon_num = hwmon_number.get(name, -1)
- temp = get_drivetemp(hwmon_num)
- print(f"Temperature for {name}: {temp}°C")
- except FileNotFoundError:
- continue
- time.sleep(SLEEP_DELAY)
- signal.signal(signal.SIGINT, signal_handler)
- if __name__ == "__main__":
- init_structure()
- print(f"[*] Watching {DEVICE_DIR} for auto-mount...")
- monitor_thread = threading.Thread(target=monitor_sleep_and_temp, daemon=True)
- monitor_thread.start()
- while True:
- with lock:
- for link in os.listdir(DEVICE_DIR):
- full_path = os.path.join(DEVICE_DIR, link)
- name = os.path.basename(link)
- if os.path.exists(full_path):
- mount_disk(name)
- else:
- unmount_disk(name)
- for name in list(mounted_disks.keys()):
- link = os.path.join(DEVICE_DIR, name)
- try:
- device = os.path.realpath(link)
- if not os.path.exists(device) or not os.path.exists(f"/sys/class/block/{os.path.basename(device)}"):
- unmount_disk(name)
- except Exception:
- unmount_disk(name)
- time.sleep(SLEEP_DELAY)
Add Comment
Please, Sign In to add comment