Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions benchmate/api/actions/backup_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import os
import subprocess
import time

import frappe

from benchmate.api.utils import get_benchmate_settings


def update_backup_log_status(docname, new_text=None, status=None):
"""
Update the BM Log record for a given docname.
Appends log text and/or updates the status field, committing immediately.
"""
try:
log_doc = frappe.get_doc("BM Log", docname)

# Append new log text if provided
if new_text:
updated_log = (log_doc.log or "") + new_text
log_doc.db_set("log", updated_log, update_modified=False)

# Update status if provided
if status:
log_doc.db_set("status", status, update_modified=False)

frappe.db.commit()
log_doc.reload()
except Exception as e:
frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteBackupLogs")


def backup_site_background(bench_name: str, bench_path: str, site_name: str, sudo_password: str):
"""
Background task to take a backup of a Frappe site inside a given bench.
Captures real-time logs into BM Log doctype,
and cleans up temporary log files after completion.
"""
bench_path = os.path.abspath(bench_path)
log_file = os.path.join(bench_path, f"bench_backup_site_{site_name}.log")

# Create a unique BM Log record for tracking
log_timestamp = int(time.time())
log_name = f"Backup Site-{log_timestamp}"
if not frappe.db.exists("BM Log", log_name):
frappe.get_doc(
{
"doctype": "BM Log",
"title": f"Backup Site - {site_name}",
"log": "",
"log_timestamp": log_timestamp,
"status": "In Process",
"action": "Backup Site",
}
).insert(ignore_permissions=True)
frappe.db.commit()

# Command to run backup with files
cmd = [
"sudo",
"-S",
"bench",
"--site",
site_name,
"backup",
"--with-files",
]

try:
# Launch subprocess and redirect stdout/stderr into a log file
with open(log_file, "w") as f:
proc = subprocess.Popen(
cmd,
cwd=bench_path,
stdin=subprocess.PIPE,
stdout=f,
stderr=subprocess.STDOUT,
text=True,
)
# Send sudo password
proc.stdin.write(sudo_password + "\n")
proc.stdin.flush()
proc.stdin.close()

# Wait for process completion with timeout (15 mins)
try:
proc.wait(timeout=900)
except subprocess.TimeoutExpired:
proc.kill()
update_backup_log_status(
log_name,
new_text="\nTimed out while taking backup!\n",
status="Error",
)
frappe.msgprint(
msg=f"Timeout expired while backing up site {site_name}.",
title="Site Backup Timeout",
alert=True,
indicator="red",
)
return

# Tail the log file and update BM Log in real-time
with open(log_file) as f:
f.seek(0, os.SEEK_SET)
for line in f:
update_backup_log_status(log_name, new_text=line)

# Update status based on exit code
if proc.returncode == 0:
update_backup_log_status(log_name, status="Success")
else:
update_backup_log_status(log_name, status="Error")

except Exception as e:
frappe.msgprint(
msg=f"Error while taking backup of site {site_name} in bench {bench_name}",
title="Site Backup Error",
realtime=True,
alert=True,
indicator="red",
)
frappe.log_error(f"Error running bench backup: {e}", "BenchMate SiteBackupLogs")
update_backup_log_status(log_name, status="Error")

else:
frappe.msgprint(
msg=f"Backup for site {site_name} completed successfully in bench {bench_name}",
title="Site Backup Success",
realtime=True,
alert=True,
indicator="green",
)

finally:
# Always clean up the temporary log file
try:
if os.path.exists(log_file):
os.remove(log_file)
except Exception as cleanup_error:
frappe.log_error(
f"Failed to remove temp log file {log_file}: {cleanup_error}",
"BenchMate SiteBackupLogs",
)


@frappe.whitelist()
def execute(bench_name: str, bench_path: str, site_name: str):
"""
Public API method (whitelisted) to enqueue site backup.
Validates input and enqueues the background site backup task.
"""
if not bench_path or not site_name:
frappe.throw("bench_path and site_name are required", frappe.ValidationError)

# Fetch global BenchMate settings (sudo password)
settings = get_benchmate_settings()
sudo_password = settings.get("sudo_password")

if not sudo_password:
frappe.throw("Sudo password not configured", frappe.ValidationError)

try:
frappe.enqueue(
backup_site_background,
queue="long",
timeout=3600,
bench_name=bench_name,
bench_path=bench_path,
site_name=site_name,
sudo_password=sudo_password,
)
except Exception as e:
frappe.throw(f"Failed to enqueue site backup: {e!s}")

return {
"success": True,
"message": (
f"Backing up site <b>{site_name}</b> in the background. Check the <b>BM Log</b> for more details."
),
"data": None,
}
25 changes: 13 additions & 12 deletions benchmate/api/actions/create_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

def update_log_status(docname, new_text=None, status=None):
"""
? Update the BM Site Creation Logs record for a given docname.
? Update the BM Log record for a given docname.
? Appends log text and/or updates the status field, committing immediately.
"""
try:
log_doc = frappe.get_doc("BM Site Creation Logs", docname)
log_doc = frappe.get_doc("BM Log", docname)

# ? Append new log text if provided
if new_text:
Expand All @@ -27,31 +27,32 @@ def update_log_status(docname, new_text=None, status=None):
frappe.db.commit()
log_doc.reload()
except Exception as e:
frappe.log_error(f"Error updating BM Site Creation Logs: {e}", "BenchMate SiteCreationLogs")
frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteCreationLogs")


def create_site_background(
bench_name: str, bench_path: str, site_name: str, sudo_password: str, mysql_root_password: str
):
"""
Background task to create a new Frappe site inside a given bench.
Captures real-time logs into BM Site Creation Logs doctype,
Captures real-time logs into BM Log doctype,
and cleans up temporary log files after completion.
"""
bench_path = os.path.abspath(bench_path)
log_file = os.path.join(bench_path, f"bench_new_site_{site_name}.log")

# ? Create a unique BM Site Creation Logs record for tracking
# ? Create a unique BM Log record for tracking
log_timestamp = int(time.time())
log_name = f"{site_name}-{log_timestamp}"
if not frappe.db.exists("BM Site Creation Logs", log_name):
log_name = f"Create Site-{log_timestamp}"
if not frappe.db.exists("BM Log", log_name):
frappe.get_doc(
{
"doctype": "BM Site Creation Logs",
"site_name": site_name,
"doctype": "BM Log",
"title": f"Create Site - {site_name}",
"log": "",
"log_timestamp": log_timestamp,
"status": "In Process",
"action": "Create Site",
}
).insert(ignore_permissions=True)
frappe.db.commit()
Expand Down Expand Up @@ -85,7 +86,7 @@ def create_site_background(
proc.stdin.flush()
proc.stdin.close()

# ? Tail the log file and update BM Site Creation Logs in real-time
# ? Tail the log file and update BM Log in real-time
try:
with open(log_file) as f:
f.seek(0, os.SEEK_END) # ? Move to end for live tailing
Expand Down Expand Up @@ -233,6 +234,6 @@ def execute(bench_name: str, bench_path: str, site_name: str):

return {
"success": True,
"message": f"Creating <b>{site_name}</b> in background Check <b>BM Site Creation Logs</b> for more details.",
"data": {"bench_path": bench_path},
"message": f"Creating <b>{site_name}</b> in background Check <b>BM Log</b> for more details.",
"data": None,
}
25 changes: 13 additions & 12 deletions benchmate/api/actions/drop_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

def update_deletion_log_status(docname, new_text=None, status=None):
"""
? Update the BM Site Deletion Logs record for a given docname.
? Update the BM Log record for a given docname.
? Appends log text and/or updates the status field, committing immediately.
"""
try:
log_doc = frappe.get_doc("BM Site Deletion Logs", docname)
log_doc = frappe.get_doc("BM Log", docname)

# ? Append new log text if provided
if new_text:
Expand All @@ -27,31 +27,32 @@ def update_deletion_log_status(docname, new_text=None, status=None):
frappe.db.commit()
log_doc.reload()
except Exception as e:
frappe.log_error(f"Error updating BM Site Deletion Logs: {e}", "BenchMate SiteDeletionLogs")
frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteDeletionLogs")


def drop_site_background(
bench_name: str, bench_path: str, site_name: str, sudo_password: str, mysql_root_password: str
):
"""
Background task to drop (delete) a Frappe site inside a given bench.
Captures real-time logs into BM Site Deletion Logs doctype,
Captures real-time logs into BM Log doctype,
and cleans up temporary log files after completion.
"""
bench_path = os.path.abspath(bench_path)
log_file = os.path.join(bench_path, f"bench_drop_site_{site_name}.log")

# ? Create a unique BM Site Deletion Logs record for tracking
# ? Create a unique BM Log record for tracking
log_timestamp = int(time.time())
log_name = f"{site_name}-{log_timestamp}"
if not frappe.db.exists("BM Site Deletion Logs", log_name):
log_name = f"Drop Site-{log_timestamp}"
if not frappe.db.exists("BM Log", log_name):
frappe.get_doc(
{
"doctype": "BM Site Deletion Logs",
"site_name": site_name,
"doctype": "BM Log",
"title": f"Drop Site - {site_name}",
"log": "",
"log_timestamp": log_timestamp,
"status": "In Process",
"action": "Drop Site",
}
).insert(ignore_permissions=True)
frappe.db.commit()
Expand Down Expand Up @@ -103,7 +104,7 @@ def drop_site_background(
)
return

# ? Tail the log file and update BM Site Deletion Logs in real-time
# ? Tail the log file and update BM Log in real-time
with open(log_file) as f:
f.seek(0, os.SEEK_SET)
# ? Stream entire log file content to document
Expand Down Expand Up @@ -200,7 +201,7 @@ def execute(bench_name: str, bench_path: str, site_name: str):
"success": True,
"message": (
f"Deleting site <b>{site_name}</b> in the background. "
f"Check the <b>BM Site Deletion Logs</b> for more details."
f"Check the <b>BM Log</b> for more details."
),
"data": {"bench_path": bench_path},
"data": None,
}
Loading