Skip to content

Instantly share code, notes, and snippets.

@zhovner
Last active September 5, 2021 22:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zhovner/3f2496da87528c76cac0ba312b4b20d3 to your computer and use it in GitHub Desktop.
Save zhovner/3f2496da87528c76cac0ba312b4b20d3 to your computer and use it in GitHub Desktop.
Time Machine helper for travelers
#!/opt/homebrew/bin/python3
###################################################
# Remote Time Machine helper for remote SMB share #
# Run backup only if SMB server speed is high #
###################################################
### Requirements:
# brew install iperf3 python3
# pip install osascript
### How to use:
# 1. Configure Time Machine to smb share in normal mode. SMB credential should be saved in system keychain.
# 2. Disable automatic backup in Time Machine
# 3. Run this script in background via Launchd N times a day
### EDIT YOUR VARIABLES
# ---------------------------------------------------------------------
SMB_SHARE_ADDRESS = 'share.lan.example.com'
WORKGROUP_NAME = 'WORKGROUP'
SMB_MOUNT_PATH = '/Volumes/ivanfolder'
SMB_USER = 'ivan'
SMB_SHARE_PATH = 'ivanfolder'
SPEED_TEST_SERVER = "iperf.lan.example.com"
SPEED_TEST_DURATION = "3" # seconds
SPEED_TEST_TIMEOUT = "60" # milliseconds
MIN_SPEED = "60" # Min upload speed in Mbit/s to activate Time Machine
MAX_LOAD_AVERAGE = 5 # Max load average when starting backup
# ---------------------------------------------------------------------
import subprocess
import json
import glob
import os
import osascript
import time
import sys
import datetime
print('''
##############################
# Remote Time Machine helper #
##############################
''')
print("Started: " + datetime.datetime.now().strftime("%d %b %Y %H:%M"))
# Print Time Mahcine settings
TMUTIL_SETTINGS_SUB = subprocess.run(["tmutil", "destinationinfo"], capture_output=True)
print("Time Machine Settings:")
print(str(TMUTIL_SETTINGS_SUB.stdout, 'utf-8'))
# Check if Time Machine running
# exit if backup running
#
# tmutil function if need more info from running time machine
# 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":
print("Backup process is running. Exiting...")
exit()
# Check Load Average. Exit if system busy
LA = round(os.getloadavg()[0])
if LA > MAX_LOAD_AVERAGE:
print("Current Load Average is to high: " + str(LA))
print("Exiting...")
osacode,osaout,osaerr = osascript.run('display notification "🥵CPU usage is to high! Skipping backup." with Title "Time Machine Helper"')
exit()
# Check if SMB share availible
SMBUTIL_SUB = subprocess.run(["smbutil", "status", SMB_SHARE_ADDRESS], capture_output=True)
SMBUTIL_RESULT = str(SMBUTIL_SUB.stdout, 'utf-8')
# Check if smbutil respone have WORKGROUP_NAME
if WORKGROUP_NAME in SMBUTIL_RESULT:
print("SMB " + SMB_SHARE_ADDRESS + " Available\n")
print(SMBUTIL_RESULT)
else:
print("Error: SMB " + SMB_SHARE_ADDRESS + " not availible")
exit()
# Run Network Speed Test
print("Running 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:
print("Iperf3 failed. Exiting")
exit()
# 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:
print("Iperf3 failed:")
print(IPERF_JSON["error"])
exit()
# 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
print("Upload Speed: " + str(UPLOAD_SPEED) + " Mbits/sec")
# If Upload Speed slow then exit
if int(UPLOAD_SPEED) < int(MIN_SPEED):
print("Internet is too slow. Minimum speed is: " + str(MIN_SPEED) + " Mbits/sec")
OSA_ARGS = 'display notification "🐌 Internet is to slow for backup: ' + str(int(UPLOAD_SPEED)) + ' Mbit/s "' + 'with Title "Time Machine Helper"'
osacode,osaout,osaerr = osascript.run(OSA_ARGS)
exit()
# Check if SMB share mounted and Time Machine backup existed on it
print("Looking for Time Machine backups on " + SMB_MOUNT_PATH)
# 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):
print("Mount " + SMB_MOUNT_PATH + " Failed")
print("Command used to mount vis osascript: " + str(OSA_ARGS))
exit()
# 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 TIME_MACHINE_BACKUPS:
print("Founded " + str(len(TIME_MACHINE_BACKUPS)) + " Time Machine files:")
print(*TIME_MACHINE_BACKUPS, sep='\n')
else:
print("Error: no Time Machine files found. Create it manually before using this script")
osacode,osaout,osaerr = osascript.run('display notification "⚠️ No Time Machine Files Found on Share" with Title "Time Machine Helper"')
print("Exiting...")
exit()
# Everything looks good
# Lets start backup
print("Staring Time Machine backup...")
TIME_MACHINE_SUB = subprocess.run(
["tmutil",
"startbackup"],
capture_output=True)
if TIME_MACHINE_SUB.returncode == 0:
print("Time Machine Started successfully!")
osacode,osaout,osaerr = osascript.run('display notification "💾 Backup started..." with Title "Time Machine Helper"')
else:
print("ERROR: Time Machine Starting Failed!")
osacode,osaout,osaerr = osascript.run('display notification "⚠️ ERROR: Time Machine Starting Failed!" with Title "Time Machine Helper"')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment