diff --git a/Network/time_machine_traveler.py b/Network/time_machine_traveler.py new file mode 100755 index 000000000..47de6569f --- /dev/null +++ b/Network/time_machine_traveler.py @@ -0,0 +1,243 @@ +#!/opt/homebrew/bin/python3 +# -*- coding: utf-8 -*- +# Time Machine Traveler Helper +# v1.0 +# Pavel Zhovner +# zhovner +# https://user-images.githubusercontent.com/774290/132701329-36b01255-50f4-4902-8ea3-0088edb38b2b.jpg +# Helps run remote Time Machine. Test network speed to SMB server and start backup only if SMB server is speed enough. +# python3,iperf3,osascript +# +#string(SMB_SHARE_ADDRESS=""): Your SMB share address +#string(WORKGROUP_NAME=""): Your SMB share address +#string(SMB_MOUNT_PATH=""): Your SMB share address +#string(SMB_USER=""): Your SMB share address +#string(SMB_SHARE_PATH=""): Your SMB share address +#string(SPEED_TEST_SERVER=""): Your SMB share address +#string(SPEED_TEST_DURATION=""): Your SMB share address +#string(SPEED_TEST_TIMEOUT=""): Your SMB share address +#string(MIN_SPEED=""): Your SMB share address +#string(MAX_LOAD_AVERAGE=""): Your SMB share address + + +### Requirements +# brew install python3 iperf3 +# pip3 install osascript + +### How to use: +# 1. Mount remote SMB share and save credentials in system keychain +# 2. Configure Time Machine to SMB share in System Preferences. Make sure that first backup is created correctly +# 3. Disable automatic backup in Time Machine +# 4. Setup this script + +import subprocess +import json +import glob +import os +import time +import sys +import datetime + +# If you still have this error after install osascript, try to set right python3 shell bang +try: + import osascript +except ImportError: + generate_output('''Can't import osascript module +Run: pip3 install osascript''', status='FATAL_ERROR') + + +def generate_output(message, status): + + if status == 'FATAL_ERROR': + print("⚠️") + print("---") + print("Time Machine Travel Helper | font=LucidaGrande-Bold") + print("---") + print("Fatal error:") + print(message) + + if status == 'IDLE': + print("🕒") + print("---") + print("Time Machine Travel Helper | font=LucidaGrande-Bold") + # TODO: Show latest backup date + print("Last Run: " + datetime.datetime.now().strftime("%d %b %Y - %H:%M") + " | size=10") + print("---") + print(message) + print("Skipping backup...") + + if status == 'RUN': + print("🌀") + print("---") + print("Time Machine Travel Helper | font=LucidaGrande-Bold") + print("---") + # TODO: Show backup status and progress + print("Backuping...") + + # Manually run button + print("---") + print("Run now | refresh=true") + + # Trying to print some logs if we have some + try: + # Output log in dropdown menu + print("Logs") + + # Current time machine path settings + print("-- Time Machine Settings:") + for l in TMUTIL_SETTINGS.splitlines(): + print("-- " + l) + + # SMB status + + # Display upload speed + if UPLOAD_SPEED: + print("-- ") + print("-- Speed Test to: " + SPEED_TEST_SERVER) + print("-- ====================================================") + print("-- Upload Speed: " + str(UPLOAD_SPEED) + " Mbits/sec") + + # List Time Machine bundles files + print("-- ") + print("-- Found " + str(len(TIME_MACHINE_BACKUPS)) + " Time Machine files:") + print("-- ====================================================") + for l in TIME_MACHINE_BACKUPS: + print("-- " + str(l)) + except: + pass + + # Stop script after print logs + # we need to always exit with 0 because of xbar + exit(0) + + + +# Settings VARIABLES +# --------------------------------------------------------------- + +SMB_SHARE_ADDRESS = os.environ["SMB_SHARE_ADDRESS"] +WORKGROUP_NAME = os.environ["WORKGROUP_NAME"] +SMB_MOUNT_PATH = os.environ["SMB_MOUNT_PATH"] +SMB_USER = os.environ["SMB_USER"] +SMB_SHARE_PATH = os.environ["SMB_SHARE_PATH"] + +SPEED_TEST_SERVER = os.environ["SPEED_TEST_SERVER"] +SPEED_TEST_DURATION = os.environ["SPEED_TEST_DURATION"] +SPEED_TEST_TIMEOUT = os.environ["SPEED_TEST_TIMEOUT"] + +MIN_SPEED = os.environ["SPEED_TEST_TIMEOUT"] +MAX_LOAD_AVERAGE = int(os.environ["SPEED_TEST_TIMEOUT"]) + +# --------------------------------------------------------------- + + +#### Script exit status to draw correct icon +STATUS = "" + +#### Print Time Mahcine settings +TMUTIL_SETTINGS_SUB = subprocess.run(["tmutil", "destinationinfo"], capture_output=True) +TMUTIL_SETTINGS = (str(TMUTIL_SETTINGS_SUB.stdout, 'utf-8')) +# Stop if Time Machine not configured +if "No destinations configured" in TMUTIL_SETTINGS: + generate_output("Time Machine not configured",status='FATAL_ERROR') + + +#### Check Load Average. Exit if system busy +LA = round(os.getloadavg()[0]) +if LA > MAX_LOAD_AVERAGE: + generate_output("System load is to high.",status='IDLE') + + +#### Check if Time Machine running +# exit if backup running +# +# TODO: Display running progress +# +# Use python tmutil function if need more info from running time machine process +# https://gist.github.com/andrewbenson/cc5fd79ff6999f0524b8979fe17937a3 +# +TMUTIL_PHASE_SUB = subprocess.run(["tmutil", "currentphase"], capture_output=True) +TMUTIL_PHASE = str(TMUTIL_PHASE_SUB.stdout, 'utf-8').strip() +if TMUTIL_PHASE != "BackupNotRunning": + generate_output("Backup is running...",status='RUN') + + +#### Check if SMB share availible +SMBUTIL_SUB = subprocess.run(["smbutil", "status", SMB_SHARE_ADDRESS], capture_output=True) +SMB_CHECK_RESULT = str(SMBUTIL_SUB.stdout, 'utf-8').strip() + +# Check if SMB share connect have workgoup name +# Oterwise stop because SMB share not reacheble +if WORKGROUP_NAME not in SMB_CHECK_RESULT: + generate_output("SMB " + SMB_SHARE_ADDRESS + " not availible.",status='IDLE') + + +#### Run Network Speed Test +# --------------------------------------------------------------- +try: + IPERF_SUB = subprocess.run( + ["/opt/homebrew/bin/iperf3", + "--connect-timeout", SPEED_TEST_TIMEOUT, # Drop connection in laggy network + "--time", SPEED_TEST_DURATION, # Run speed test shorter then default 10 seconds + "--json", # Output in JSON format + "--client", SPEED_TEST_SERVER], # Connects to Speed Test server in client mode + capture_output=True) +except: + generate_output("iperf3 failed to run",status='FATAL_ERROR') + +# Convert iperf3 output to valid JSON +IPERF_RESULT = str(IPERF_SUB.stdout, 'utf-8') # Convert bytes to string +IPERF_JSON = json.loads(IPERF_RESULT) +# Exit if error key found +if "error" in IPERF_JSON: + generate_output("iperf3: " + IPERF_JSON["error"],status='FATAL_ERROR') + +# Else calculate the upload speed and decide if it's enough to start Time Machine +else: + USPEED_FLOAT = IPERF_JSON["end"]["sum_sent"]["bits_per_second"] + UPLOAD_SPEED = round(USPEED_FLOAT) // 1000000 + if int(UPLOAD_SPEED) < int(MIN_SPEED): + generate_output("Internet is too slow: " + str(UPLOAD_SPEED) + " Mbits/sec\n" + "Minimum is: " + str(MIN_SPEED) + " Mbits/sec",status='IDLE') + + +#### Mounting SMB share if not mounted. Using osascript (hardest part) +if not os.path.isdir(SMB_MOUNT_PATH): + #print(SMB_MOUNT_PATH + " not founded. Trying to mount...") + OSA_ARGS = 'tell application "Finder" to mount volume ' + '"smb://' + SMB_USER + '@' + SMB_SHARE_ADDRESS + '/' + SMB_SHARE_PATH + '"' + osacode,osaout,osaerr = osascript.run(OSA_ARGS) + #time.sleep(1) + +# Looking for a SMB Path again after mount +if not os.path.isdir(SMB_MOUNT_PATH): + generate_output("Mount " + SMB_MOUNT_PATH + " Failed",status='FATAL_ERROR') + #print("Command used to mount via osascript: " + str(OSA_ARGS)) + + +#### Looking for *.sparsebundle files on SMB share +# exit if not founded +# Create Time Machine backup manually before using this script +TIME_MACHINE_BACKUPS = glob.glob(SMB_MOUNT_PATH + "/*.sparsebundle") +if not TIME_MACHINE_BACKUPS: + print_error("Time Machine files not found on SMB share.\n Run Time Machine first time manually before using this script") + +#### Run Time Machine backup +TIME_MACHINE_SUB = subprocess.run( + ["tmutil", + "startbackup"], + capture_output=True) + +if TIME_MACHINE_SUB.returncode != 0: + #print("ERROR: Time Machine Starting Failed!") + #osacode,osaout,osaerr = osascript.run('display notification "⚠️ ERROR: Time Machine Starting Failed!" with Title "Time Machine Helper"') + print_error('Time Machine start failed\n run: "tmutil startbackup" in terminal') + +### Check if Time Machine really starts backup +TMUTIL_PHASE_SUB = subprocess.run(["tmutil", "currentphase"], capture_output=True) +TMUTIL_PHASE = str(TMUTIL_PHASE_SUB.stdout, 'utf-8').strip() +if 'BackupNotRunning' in TMUTIL_PHASE: + generate_output('Time Machine not started\n Run manually: \"tmutil startbackup\" in terminal',status='FATAL_ERROR') + +# Generate output +# --------------------------------------------------------------- + +generate_output("",status='RUN') \ No newline at end of file