Add Clone Tool

This commit is contained in:
Taylor Helsper 2021-02-03 19:11:19 -06:00
parent 1f94d3f074
commit c0dd28d222
13 changed files with 603 additions and 30 deletions

View File

@ -0,0 +1,272 @@
#!/usr/bin/python3
import time
import os
import subprocess
import signal
import logging
from systemd import journal
from threading import Thread
log = logging.getLogger('mynode')
log.addHandler(journal.JournaldLogHandler())
log.setLevel(logging.INFO)
def print_and_log(msg):
global log
print(msg)
log.info(msg)
def set_clone_state(state):
print_and_log("Clone State: {}".format(state))
try:
with open("/tmp/.clone_state", "w") as f:
f.write(state)
os.system("sync")
return True
except:
return False
return False
def reset_clone_error():
os.system("rm /tmp/.clone_error")
def reset_clone_confirm():
os.system("rm /tmp/.clone_confirm")
def set_clone_error(error_msg):
print_and_log("Clone Error: {}".format(error_msg))
try:
with open("/tmp/.clone_error", "w") as f:
f.write(error_msg)
os.system("sync")
return True
except:
return False
return False
def check_pid(pid):
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def send_usr1_sig(process_id):
while check_pid(process_id):
os.kill(process_id, signal.SIGUSR1)
time.sleep(3)
def get_drive_size(drive):
size = -1
try:
lsblk_output = subprocess.check_output(f"lsblk -b /dev/{drive} | grep disk", shell=True).decode("utf-8")
parts = lsblk_output.split()
size = int(parts[3])
except:
pass
print_and_log(f"Drive {drive} size: {size}")
return size
def check_partition_for_mynode(partition):
is_mynode = False
try:
subprocess.check_output(f"mount -o ro /dev/{partition} /mnt/hdd", shell=True)
if os.path.isfile("/mnt/hdd/.mynode"):
is_mynode = True
except Exception as e:
# Mount failed, could be target drive
pass
finally:
time.sleep(1)
os.system("umount /mnt/hdd")
return is_mynode
def find_partitions_for_drive(drive):
partitions = []
try:
ls_output = subprocess.check_output(f"ls /sys/block/{drive}/ | grep {drive}", shell=True).decode("utf-8")
partitions = ls_output.split()
except:
pass
return partitions
def find_drives():
drives = []
try:
ls_output = subprocess.check_output("ls /sys/block/ | egrep 'hd.*|vd.*|sd.*|nvme.*'", shell=True).decode("utf-8")
drives = ls_output.split()
except:
pass
return drives
def main():
# Set initial state
set_clone_state("detecting")
reset_clone_error()
reset_clone_confirm()
os.system("umount /mnt/hdd")
os.system("rm /tmp/.clone_target_drive_has_mynode")
# Detect drives
drives = find_drives()
print_and_log(f"Drives: {drives}")
# Check exactly two drives found
drive_count = len(drives)
if drive_count != 2:
print_and_log("Clone tool did not find 2 drives!")
set_clone_state("error")
set_clone_error("Clone tool needs 2 drives! Found {}.".format(drive_count))
return
# Detect Source and Target Drives
mynode_drive = "not_found"
mynode_found = False
target_drive = "not_found"
target_found = False
both_drives_have_mynode = False
for d in drives:
partitions = find_partitions_for_drive(d)
print_and_log(f"Drive {d} paritions: {partitions}")
if len(partitions) == 0:
# No partition found - must be target drive since its empty
if target_found:
set_clone_state("error")
set_clone_error("Two target drives found. Is myNode drive missing?")
return
else:
target_found = True
target_drive = d
else:
for p in partitions:
a = round(time.time() * 1000)
if check_partition_for_mynode(p):
if mynode_found:
# Second drive has myNode partition (failed clone?) - use size to determine target
both_drives_have_mynode = True
drive_1_size = get_drive_size(mynode_drive)
drive_2_size = get_drive_size(d)
if drive_2_size >= drive_1_size:
mynode_drive = mynode_drive
target_drive = d
else:
target_drive = mynode_drive
mynode_drive = d
target_found = True
else:
print_and_log(f"myNode Partition Found: {p}")
mynode_drive = d
mynode_found = True
else:
if target_found:
set_clone_state("error")
set_clone_error("Two target drives found. Is myNode drive missing?")
return
else:
target_found = True
target_drive = d
b = round(time.time() * 1000)
total_time = b - a
print_and_log(f"Checked partition {p} in {total_time}ms")
# Successfully found source and target, wait for confirm
print_and_log(f"Source Drive: {mynode_drive}")
print_and_log(f"Target Drive: {target_drive}")
if both_drives_have_mynode:
os.system("touch /tmp/.clone_target_drive_has_mynode")
os.system(f"echo {mynode_drive} > /tmp/.clone_source")
os.system(f"echo {target_drive} > /tmp/.clone_target")
set_clone_state("need_confirm")
while not os.path.isfile("/tmp/.clone_confirm"):
time.sleep(1)
# Clone drives
set_clone_state("in_progress")
os.system("echo 'Starting clone.' > /tmp/.clone_progress")
try:
cmd = ["dd","bs=64K",f"if=/dev/{mynode_drive}",f"of=/dev/{target_drive}","conv=sync,noerror"]
#cmd = ["dd","bs=512",f"if=/dev/zero",f"of=/dev/null","count=5999999","conv=sync,noerror"]
dd = subprocess.Popen(cmd, stderr=subprocess.PIPE)
print_and_log("DD PID: {}".format(dd.pid))
thread = Thread(target=send_usr1_sig, args=(dd.pid,))
thread.start()
for l in dd.stderr:
l = l.decode("utf-8")
if 'bytes' in l:
try:
out_fd = open('/tmp/.clone_progress','w')
out_fd.write(l)
out_fd.close()
except Exception as e:
print_and_log("Write Exception: " + str(e))
while dd.poll() is None:
time.sleep(5)
print_and_log("Waiting on dd exit...")
print_and_log("DD RET CODE: {}".format(dd.returncode))
if dd.returncode != 0:
# DD had an error - log it
if dd.stderr != None:
for l in dd.stderr:
print_and_log("DD STDERR: "+l.decode("utf-8"))
if dd.stdout != None:
for l in dd.stdout:
print_and_log("DD STDOUT: "+l.decode("utf-8"))
set_clone_state("error")
set_clone_error("DD failed with return code {}".format(dd.returncode))
return
print_and_log("DD IS COMPLETE")
# PAUSE IF DD WAS SUCCESSFUL
# if dd.returncode == 0:
# set_clone_state("error")
# set_clone_error("DD WAS SUCCESSFUL!!!! Remove temp code.")
# while True:
# time.sleep(60)
# Update partitions (removes all + makes new without removing data)
print_and_log("Updating Partitions...")
os.system("echo 'Updating partitions...' > /tmp/.clone_progress")
subprocess.check_output(f"/usr/bin/format_drive.sh {target_drive}", shell=True)
time.sleep(2)
# Resize filesystem to fill up whole drive
print_and_log("Resizing Filesystem...")
os.system("echo 'Resizing filesystem...' > /tmp/.clone_progress")
os.system(f"partprobe /dev/{target_drive}")
time.sleep(2)
subprocess.check_output(f"e2fsck -y -f /dev/{target_drive}1", shell=True)
subprocess.check_output(f"resize2fs /dev/{target_drive}1", shell=True)
except subprocess.CalledProcessError as e:
print_and_log("CalledProcessError")
print_and_log(e.stderr)
print_and_log(e.stdout)
set_clone_state("error")
set_clone_error("Clone failed: {}".format(e))
return
except Exception as e:
set_clone_state("error")
set_clone_error("Clone failed: {}".format(e))
return
# Complete - wait for reboot
set_clone_state("complete")
print_and_log("Clone Complete!")
print_and_log("Waiting for reboot...")
while True:
time.sleep(60)
# This is the main entry point for the program
if __name__ == "__main__":
try:
main()
except Exception as e:
print_and_log("Exception: {}".format(str(e)))
set_clone_error("Exception: {}".format(str(e)))

View File

@ -17,10 +17,10 @@ while [ $? -eq 0 ]; do
#echo "$drive still found..."
# Check drive usage
usage=$(df -h /mnt/hdd | grep /dev | awk '{print $5}' | cut -d'%' -f1)
echo "Usage $usage"
if [ $usage -ge 99 ]; then
mb_available=$(df --block-size=M /mnt/hdd | grep /dev | awk '{print $4}' | cut -d'M' -f1)
if [ $mb_available -le 1000 ]; then
# Usage is 99%+, reboot to get into drive_full state with services stopped
echo "High Drive Usage: $mb_available MB available"
/usr/bin/mynode-reboot
fi

View File

@ -125,11 +125,12 @@ proc runCommand {args} {
proc mountFileSystems {} {
findBlockDevices hardDrives
set drive_count [llength $hardDrives]
puts "Found these $drive_count drives: ${hardDrives}"
puts "Found these harddrives: ${hardDrives}"
findAllPartitionsForBlockDevices $hardDrives partitions
puts "Found these existing harddrive partitions: ${partitions}"
puts "Found these existing drive partitions: ${partitions}"
if {![checkPartitionsForExistingMyNodeFs partitions]} {
puts "No existing drive found. Creating new one."

View File

@ -76,9 +76,23 @@ if [ $IS_RASPI -eq 1 ] || [ $IS_ROCK64 -eq 1 ] || [ $IS_ROCKPRO64 -eq 1 ]; then
fi
umount /mnt/hdd || true
# Check drive
# If multiple drives detected, start clone tool
drive_count=$(ls /sys/block/ | egrep "hd.*|vd.*|sd.*|nvme.*" | wc -l)
if [ "$drive_count" -gt 1 ] || [ -f /home/bitcoin/open_clone_tool ]; then
rm -f /home/bitcoin/open_clone_tool
echo "drive_clone" > $MYNODE_STATUS_FILE
sync
while [ 1 ]; do
python3 /usr/bin/clone_drive.py || true
sleep 60s
done
fi
# Check drive (only if exactly 1 is found)
set +e
if [ $IS_X86 = 0 ]; then
if [ $IS_X86 = 0 ] && [ "$drive_count" -eq 1 ]; then
touch /tmp/repairing_drive
for d in /dev/sd*1 /dev/hd*1 /dev/vd*1 /dev/nvme*p1; do
echo "Repairing drive $d ...";
@ -95,13 +109,13 @@ rm -f /tmp/repairing_drive
set -e
# Mount HDD (format if necessary)
# Mount HDD (normal boot, format if necessary)
while [ ! -f /mnt/hdd/.mynode ]
do
# Clear status
rm -f $MYNODE_STATUS_FILE
# Normal boot - find drive
rm -f $MYNODE_STATUS_FILE # Clear status
mount_drive.tcl || true
sleep 5
sleep 5s
done
@ -117,12 +131,12 @@ fi
# Check drive usage
usage=$(df -h /mnt/hdd | grep /dev | awk '{print $5}' | cut -d'%' -f1)
while [ $usage -ge 98 ]; do
mb_available=$(df --block-size=M /mnt/hdd | grep /dev | awk '{print $4}' | cut -d'M' -f1)
if [ $mb_available -le 1200 ]; then
echo "drive_full" > $MYNODE_STATUS_FILE
sleep 10s
usage=$(df -h /mnt/hdd | grep /dev | awk '{print $5}' | cut -d'%' -f1)
done
mb_available=$(df --block-size=M /mnt/hdd | grep /dev | awk '{print $4}' | cut -d'M' -f1)
fi
# Setup Drive

View File

@ -389,6 +389,7 @@ STATE_DRIVE_MISSING = "drive_missing"
STATE_DRIVE_CONFIRM_FORMAT = "drive_format_confirm"
STATE_DRIVE_FORMATTING = "drive_formatting"
STATE_DRIVE_MOUNTED = "drive_mounted"
STATE_DRIVE_CLONE = "drive_clone"
STATE_DRIVE_FULL = "drive_full"
STATE_GEN_DHPARAM = "gen_dhparam"
STATE_QUICKSYNC_DOWNLOAD = "quicksync_download"
@ -406,16 +407,7 @@ def get_mynode_status():
status_file = "/tmp/.mynode_status"
status = STATE_UNKNOWN
# If its been a while, check for error conditions
uptime_in_sec = get_system_uptime_in_seconds()
if uptime_in_sec > 120:
# Check for read-only sd card
if is_mount_read_only("/"):
return STATE_ROOTFS_READ_ONLY
if is_mount_read_only("/mnt/hdd"):
return STATE_HDD_READ_ONLY
# Get status stored on drive
# Get status
if (os.path.isfile(status_file)):
try:
with open(status_file, "r") as f:
@ -424,10 +416,61 @@ def get_mynode_status():
status = STATE_DRIVE_MISSING
else:
status = STATE_DRIVE_MISSING
# If its been a while, check for error conditions
uptime_in_sec = get_system_uptime_in_seconds()
if uptime_in_sec > 120:
# Check for read-only sd card
if is_mount_read_only("/"):
return STATE_ROOTFS_READ_ONLY
# Check for read-only drive (unless cloning - it purposefully mounts read only)
if is_mount_read_only("/mnt/hdd") and status != STATE_DRIVE_CLONE:
return STATE_HDD_READ_ONLY
except:
status = STATE_UNKNOWN
return status
#==================================
# myNode Clone Tool
#==================================
CLONE_STATE_DETECTING = "detecting"
CLONE_STATE_ERROR = "error"
CLONE_STATE_NEED_CONFIRM = "need_confirm"
CLONE_STATE_IN_PROGRESS = "in_progress"
CLONE_STATE_COMPLETE = "complete"
def get_clone_state():
return get_file_contents("/tmp/.clone_state")
def get_clone_error():
return get_file_contents("/tmp/.clone_error")
def get_clone_progress():
return get_file_contents("/tmp/.clone_progress")
def get_clone_source_drive():
return get_file_contents("/tmp/.clone_source")
def get_clone_target_drive():
return get_file_contents("/tmp/.clone_target")
def get_clone_target_drive_has_mynode():
return os.path.isfile("/tmp/.clone_target_drive_has_mynode")
def get_drive_info(drive):
data = {}
data["name"] = "NOT_FOUND"
try:
lsblk_output = subprocess.check_output("lsblk -io KNAME,TYPE,SIZE,MODEL,VENDOR /dev/{} | grep disk".format(drive), shell=True).decode("utf-8")
parts = lsblk_output.split()
data["name"] = parts[0]
data["size"] = parts[2]
data["model"] = parts[3]
data["vendor"] = parts[4]
except:
pass
return data
#==================================
# Log functions (non-systemd based)
#==================================

View File

@ -217,7 +217,8 @@ def index():
message += "<p style='font-size: 16px; width: 800px; margin: auto;'>"
message += "To prevent corrupting any data, your device has stopped running most apps until more free space is available. "
message += "Please free up some space or attach a larger drive.<br/><br/>"
message += "If enabled, disabling QuickSync can save a large amount of space."
message += "If enabled, disabling <a href='/settings#quicksync'>QuickSync</a> can save a large amount of space.<br/><br/>"
message += "To move to larger drive, try the <a href='/settings#clone_tool'>Clone Tool</a>."
message += "</p>"
templateData = {
"title": "myNode Drive Full",
@ -226,6 +227,74 @@ def index():
"ui_settings": read_ui_settings()
}
return render_template('state.html', **templateData)
elif status == STATE_DRIVE_CLONE:
clone_state = get_clone_state()
if clone_state == CLONE_STATE_DETECTING:
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"subheader_text": Markup("Detecting Drives..."),
"ui_settings": read_ui_settings(),
"refresh_rate": 10
}
return render_template('state.html', **templateData)
elif clone_state == CLONE_STATE_ERROR:
error = get_clone_error()
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"subheader_text": Markup("Clone Error<br/></br>" + error + "<br/><br/><br/><small>Retrying in one minute."),
"ui_settings": read_ui_settings(),
"refresh_rate": 10
}
return render_template('state.html', **templateData)
elif clone_state == CLONE_STATE_NEED_CONFIRM:
# Clone was confirmed
if request.args.get('clone_confirm'):
os.system("touch /tmp/.clone_confirm")
time.sleep(3)
return redirect("/")
source_drive = get_clone_source_drive()
target_drive = get_clone_target_drive()
target_drive_has_mynode = get_clone_target_drive_has_mynode()
source_drive_info = get_drive_info(source_drive)
target_drive_info = get_drive_info(target_drive)
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"target_drive_has_mynode": target_drive_has_mynode,
"source_drive_info": source_drive_info,
"target_drive_info": target_drive_info,
"ui_settings": read_ui_settings(),
}
return render_template('clone_confirm.html', **templateData)
elif clone_state == CLONE_STATE_IN_PROGRESS:
progress = get_clone_progress()
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"subheader_text": Markup("Cloning...<br/><br/>" + progress),
"ui_settings": read_ui_settings(),
"refresh_rate": 5
}
return render_template('state.html', **templateData)
elif clone_state == CLONE_STATE_COMPLETE:
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"subheader_text": Markup("Clone Complete!"),
"ui_settings": read_ui_settings(),
}
return render_template('clone_complete.html', **templateData)
else:
templateData = {
"title": "myNode Clone Tool",
"header_text": "Cloning Tool",
"subheader_text": "Unknown Clone State: " + clone_state,
"ui_settings": read_ui_settings()
}
return render_template('state.html', **templateData)
elif status == STATE_GEN_DHPARAM:
templateData = {
"title": "myNode Generating Data",

View File

@ -483,6 +483,27 @@ def reset_docker_page():
}
return render_template('reboot.html', **templateData)
@mynode_settings.route("/settings/open-clone-tool")
def open_clone_tool_page():
check_logged_in()
check_and_mark_reboot_action("open_clone_tool")
os.system("touch /home/bitcoin/open_clone_tool")
os.system("sync")
# Trigger reboot
t = Timer(1.0, reboot_device)
t.start()
# Display wait page
templateData = {
"title": "myNode Reboot",
"header_text": "Restarting",
"subheader_text": "Restarting to Open Clone Tool....",
"ui_settings": read_ui_settings()
}
return render_template('reboot.html', **templateData)
@mynode_settings.route("/settings/reset-electrs")
def reset_electrs_page():

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,37 @@
<!DOCTYPE html lang="en">
<head>
<title>{{ title }}</title>
{% include 'includes/head.html' %}
{% if refresh_rate is defined and refresh_rate is not none %}
<meta http-equiv="refresh" content="{{ refresh_rate }}">
{% else %}
<meta http-equiv="refresh" content="30">
{% endif %}
<script>
$(document).ready(function() {
$("#reboot-device").on("click", function() {
window.location.href="/settings/reboot-device"
});
});
</script>
</head>
<body>
{% include 'includes/logo_header.html' %}
<div class="state_header">{{ header_text }}</div>
<div class="state_subheader">Clone Complete!</div>
<div class="format_div" style="width: 600px;">
<p>Remove the original drive now and click Reboot.</p>
<br/><br/>
<button id="reboot-device" value="Login" class="ui-button ui-widget ui-corner-all format_button">Reboot</button>
<br/><br/>
<div style="height: 40px;">&nbsp;</div>
{% include 'includes/footer.html' %}
</body>
</html>

View File

@ -0,0 +1,94 @@
<!DOCTYPE html lang="en">
<head>
<title>{{ title }}</title>
{% include 'includes/head.html' %}
{% if refresh_rate is defined and refresh_rate is not none %}
<meta http-equiv="refresh" content="{{ refresh_rate }}">
{% else %}
<meta http-equiv="refresh" content="30">
{% endif %}
<style>
td {
border: none;
}
</style>
<script>
$(document).ready(function() {
$("#format-confirm").on("click", function() {
window.location.href="/?clone_confirm=1"
});
$("#reboot-device").on("click", function() {
window.location.href="/settings/reboot-device"
});
});
</script>
</head>
<body>
{% include 'includes/logo_header.html' %}
<div class="state_header">{{ header_text }}</div>
<div class="format_div" style="width: 600px;">
<table style="margin: auto; width: 600px; text-align: center;">
<tr>
<td><img style="width: 140px; margin: auto;" src="{{ url_for('static', filename="images/drive2.png")}}"/></td>
<td><img style="width: 120px; margin: auto;" src="{{ url_for('static', filename="images/right_arrow.png")}}"/></td>
<td><img style="width: 140px; margin: auto;" src="{{ url_for('static', filename="images/drive2.png")}}"/></td>
</tr>
<tr>
<tr>
<td><b>Source Drive</b></td>
<td></td>
<td><b>Target Drive</b></td>
</tr>
</tr>
<tr>
<td>
{{source_drive_info['size']}}<br/>
{{source_drive_info['model']}}<br/>
{{source_drive_info['vendor']}}<br/>
/dev/{{source_drive_info['name']}}<br/>
</td>
<td></td>
<td>
{{target_drive_info['size']}}<br/>
{{target_drive_info['model']}}<br/>
{{target_drive_info['vendor']}}<br/>
/dev/{{target_drive_info['name']}}<br/>
</td>
</tr>
{% if target_drive_has_mynode %}
<tr>
<td></td>
<td></td>
<td>
<span style="color: red;">myNode Data Detected!</span>
</td>
</tr>
{% endif %}
</table>
<p><b>Warning!</b></p>
{% if target_drive_has_mynode %}
<p>myNode data was detected on the target drive! This can be caused by a previous clone failure. Be sure the target drive is the new drive you want to overwrite!</p>
{% endif %}
<p>All existing data will be lost on the target drive. If this is not OK, remove the additional drive now and click Reboot.</p>
<p>It is highly recommended that both drives be externally powered. Running two drives from USB power on the device can cause clone failures.</p>
<br/>
<button id="format-confirm" value="Login" class="ui-button ui-widget ui-corner-all format_button">Confirm Clone</button>
<br/><br/>
<button id="reboot-device" value="Login" class="ui-button ui-widget ui-corner-all format_button">Reboot</button>
<br/><br/>
</div>
<div style="height: 40px;">&nbsp;</div>
{% include 'includes/footer.html' %}
</body>
</html>

View File

@ -20,7 +20,8 @@
<div class="main_page_error_block">
<center>
<p>Your drive is {{drive_usage}} full and free space is running very low. You may need to upgrade to a larger drive.</p>
<p>If QuickSync is enabled, try disabling it to save some space.</p>
<p>If QuickSync is enabled, try <a href="/settings#quicksync">disabling</a> it to save some space.</p>
<p>To migrate to a larger drive, try the <a href="/settings#clone_tool">Clone Tool</a>.</p>
</center>
</div>
{% endif %}

View File

@ -438,6 +438,7 @@
<br/>
<div class="settings_block">
<a id="mynode"></a>
<div class="settings_block_header">myNode</div>
<div class="settings_block_subheader">Version</div>
@ -509,6 +510,7 @@
<div class="settings_block">
<a id="applications"></a>
<div class="settings_block_header">Applications</div>
<div class="settings_block_subheader">Manage Applications</div>
@ -519,6 +521,7 @@
<div class="settings_block">
<a id="ui"></a>
<div class="settings_block_header">User Interface</div>
<div class="settings_block_subheader">Dark Mode</div>
@ -541,13 +544,11 @@
</label>
<br/><br/>
<button id="https_forced" style="display: none;" class="ui-button ui-widget ui-corner-all settings_button_small">Save</button>
</div>
<div class="settings_block">
<a id="device"></a>
<div class="settings_block_header">Device</div>
<div class="settings_block_subheader">Reboot Device</div>
@ -576,6 +577,7 @@
<div class="settings_block">
<a id="firewall"></a>
<div class="settings_block_header">Firewall</div>
<div class="settings_block_subheader">Rules</div>
@ -595,6 +597,7 @@
<div class="settings_block">
<a id="quicksync"></a>
<div class="settings_block_header">QuickSync</div>
<div class="settings_block_subheader">Toggle QuickSync</div>
@ -645,6 +648,7 @@
<div class="settings_block">
<a id="bitcoin"></a>
<div class="settings_block_header">Bitcoin</div>
<div class="settings_block_subheader">Edit Config</div>
@ -688,6 +692,7 @@
<div class="settings_block">
<a id="lightning"></a>
<div class="settings_block_header">Lightning</div>
<div class="settings_block_subheader">Download Channel Backup</div>
@ -719,6 +724,7 @@
<div class="settings_block">
<a id="electrum"></a>
<div class="settings_block_header">Electrum Server</div>
<div class="settings_block_subheader">Reset Electrum Server</div>
@ -737,6 +743,7 @@
<div class="settings_block">
<a id="tor"></a>
<div class="settings_block_header">Tor</div>
<div class="settings_block_subheader">Use Tor for Bitcoin and Lightning</div>
@ -776,6 +783,7 @@
</div>
<div class="settings_block">
<a id="services"></a>
<div class="settings_block_header">Services</div>
<div class="settings_block_subheader">Netdata</div>
@ -790,6 +798,7 @@
</div>
<div class="settings_block">
<a id="docker"></a>
<div class="settings_block_header">Docker</div>
<div class="settings_block_subheader">Reset Docker</div>
@ -799,6 +808,17 @@
</div>
<div class="settings_block">
<a id="clone_tool"></a>
<div class="settings_block_header">Clone Tool</div>
<div class="settings_block_subheader">Clone Tool</div>
This will reboot your device and open the clone tool. It can be used to migrate to a larger external drive.
<br/>
<a href="/settings/open-clone-tool" class="ui-button ui-widget ui-corner-all settings_button">Open Clone Tool</a>
</div>
<div class="settings_block">
<a id="advanced"></a>
<div class="settings_block_header">Advanced</div>
<div class="settings_block_subheader">Reset HTTPS Certificates</div>
@ -883,6 +903,7 @@
<div class="settings_block">
<a id="developer"></a>
<div class="settings_block_header">Developer</div>
{% if not product_key_skipped %}