From 4f34e687fc665ce36a5a2d175b0bd4f6b5a2d04c Mon Sep 17 00:00:00 2001 From: karan-sanskar Date: Mon, 8 Sep 2025 00:29:38 +0530 Subject: [PATCH 1/3] [Feat]: Single logs doctype BM Log with all the actions --- benchmate/api/actions/create_site.py | 23 ++-- benchmate/api/actions/drop_site.py | 23 ++-- benchmate/api/sync.py | 13 ++- .../__init__.py | 0 .../bm_sync_log.js => bm_log/bm_log.js} | 2 +- .../bm_log.json} | 39 ++++--- .../bm_sync_log.py => bm_log/bm_log.py} | 2 +- .../test_bm_log.py} | 4 +- .../bm_site_creation_logs.js | 8 -- .../bm_site_creation_logs.json | 102 ------------------ .../bm_site_creation_logs.py | 9 -- .../test_bm_site_creation_logs.py | 20 ---- .../doctype/bm_site_deletion_logs/__init__.py | 0 .../bm_site_deletion_logs.js | 8 -- .../bm_site_deletion_logs.py | 9 -- .../test_bm_site_deletion_logs.py | 20 ---- .../benchmate/doctype/bm_sync_log/__init__.py | 0 .../doctype/bm_sync_log/bm_sync_log.json | 90 ---------------- .../workspace/benchmate/benchmate.json | 25 +---- 19 files changed, 65 insertions(+), 332 deletions(-) rename benchmate/benchmate/doctype/{bm_site_creation_logs => bm_log}/__init__.py (100%) rename benchmate/benchmate/doctype/{bm_sync_log/bm_sync_log.js => bm_log/bm_log.js} (78%) rename benchmate/benchmate/doctype/{bm_site_deletion_logs/bm_site_deletion_logs.json => bm_log/bm_log.json} (77%) rename benchmate/benchmate/doctype/{bm_sync_log/bm_sync_log.py => bm_log/bm_log.py} (86%) rename benchmate/benchmate/doctype/{bm_sync_log/test_bm_sync_log.py => bm_log/test_bm_log.py} (85%) delete mode 100644 benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js delete mode 100644 benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json delete mode 100644 benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py delete mode 100644 benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py delete mode 100644 benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py delete mode 100644 benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js delete mode 100644 benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py delete mode 100644 benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py delete mode 100644 benchmate/benchmate/doctype/bm_sync_log/__init__.py delete mode 100644 benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json diff --git a/benchmate/api/actions/create_site.py b/benchmate/api/actions/create_site.py index e3f3ccf..92a2430 100644 --- a/benchmate/api/actions/create_site.py +++ b/benchmate/api/actions/create_site.py @@ -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: @@ -27,7 +27,7 @@ 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( @@ -35,23 +35,24 @@ def create_site_background( ): """ 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() @@ -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 @@ -233,6 +234,6 @@ def execute(bench_name: str, bench_path: str, site_name: str): return { "success": True, - "message": f"Creating {site_name} in background Check BM Site Creation Logs for more details.", + "message": f"Creating {site_name} in background Check BM Log for more details.", "data": {"bench_path": bench_path}, } diff --git a/benchmate/api/actions/drop_site.py b/benchmate/api/actions/drop_site.py index 75bd5a5..445ab79 100644 --- a/benchmate/api/actions/drop_site.py +++ b/benchmate/api/actions/drop_site.py @@ -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: @@ -27,7 +27,7 @@ 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( @@ -35,23 +35,24 @@ def drop_site_background( ): """ 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() @@ -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 @@ -200,7 +201,7 @@ def execute(bench_name: str, bench_path: str, site_name: str): "success": True, "message": ( f"Deleting site {site_name} in the background. " - f"Check the BM Site Deletion Logs for more details." + f"Check the BM Log for more details." ), "data": {"bench_path": bench_path}, } diff --git a/benchmate/api/sync.py b/benchmate/api/sync.py index 18fde8e..7b1b0a3 100644 --- a/benchmate/api/sync.py +++ b/benchmate/api/sync.py @@ -3,6 +3,7 @@ import json import os import subprocess +import time from pathlib import Path import frappe @@ -153,15 +154,17 @@ def sync_bench_details(): # ? Log sync error frappe.get_doc( { - "doctype": "BM Sync Log", + "doctype": "BM Log", "title": message, "status": "Error", "log": json.dumps(data, indent=4), + "log_timestamp": int(time.time()), + "action": "Sync", } ).insert() frappe.log_error(message, str(e)) - # ? Commit the BM Sync Log + # ? Commit the BM Log frappe.db.commit() return { @@ -172,7 +175,7 @@ def sync_bench_details(): else: # ? Prepare success log - message = "Benches synced successfully." + message = "Benches synced successfully" data = { "updated_benches": updated_benches, "updated_apps": updated_apps, @@ -182,10 +185,12 @@ def sync_bench_details(): # ? Log sync success frappe.get_doc( { - "doctype": "BM Sync Log", + "doctype": "BM Log", "title": message, "status": "Success", "log": json.dumps(data, indent=4), + "log_timestamp": int(time.time()), + "action": "Sync", } ).insert() diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/__init__.py b/benchmate/benchmate/doctype/bm_log/__init__.py similarity index 100% rename from benchmate/benchmate/doctype/bm_site_creation_logs/__init__.py rename to benchmate/benchmate/doctype/bm_log/__init__.py diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js b/benchmate/benchmate/doctype/bm_log/bm_log.js similarity index 78% rename from benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js rename to benchmate/benchmate/doctype/bm_log/bm_log.js index 6262056..07e8da0 100644 --- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js +++ b/benchmate/benchmate/doctype/bm_log/bm_log.js @@ -1,7 +1,7 @@ // Copyright (c) 2025, Karan Mistry and contributors // For license information, please see license.txt -// frappe.ui.form.on("BM Sync Log", { +// frappe.ui.form.on("BM Log", { // refresh(frm) { // }, diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json b/benchmate/benchmate/doctype/bm_log/bm_log.json similarity index 77% rename from benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json rename to benchmate/benchmate/doctype/bm_log/bm_log.json index 5302bd1..c694a72 100644 --- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json +++ b/benchmate/benchmate/doctype/bm_log/bm_log.json @@ -1,12 +1,13 @@ { "actions": [], "allow_rename": 1, - "autoname": "format:{site_name}-{log_timestamp}", - "creation": "2025-09-07 23:41:44.865581", + "autoname": "format:{action}-{log_timestamp}", + "creation": "2025-09-08 00:09:25.552471", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "site_name", + "title", + "action", "column_break_bkvd", "status", "log_timestamp", @@ -14,14 +15,6 @@ "log" ], "fields": [ - { - "fieldname": "site_name", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Site Name", - "read_only": 1 - }, { "fieldname": "column_break_bkvd", "fieldtype": "Column Break" @@ -40,7 +33,6 @@ "fieldname": "log_timestamp", "fieldtype": "Data", "in_list_view": 1, - "in_standard_filter": 1, "label": "Log Timestamp", "read_only": 1, "unique": 1 @@ -55,15 +47,32 @@ "in_list_view": 1, "label": "Log", "read_only": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Title", + "read_only": 1 + }, + { + "fieldname": "action", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Action", + "options": "Other\nSync\nCreate Site\nDrop Site\nBackup Site\nRestore Site\nStart Bench\nStop Bench", + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-09-07 23:41:44.865581", + "modified": "2025-09-08 00:20:53.335826", "modified_by": "Administrator", "module": "BenchMate", - "name": "BM Site Deletion Logs", + "name": "BM Log", "naming_rule": "Expression", "owner": "Administrator", "permissions": [ @@ -97,6 +106,6 @@ "title": "Error" } ], - "title_field": "site_name", + "title_field": "title", "track_seen": 1 } diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py b/benchmate/benchmate/doctype/bm_log/bm_log.py similarity index 86% rename from benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py rename to benchmate/benchmate/doctype/bm_log/bm_log.py index 7d0f3ed..5999550 100644 --- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py +++ b/benchmate/benchmate/doctype/bm_log/bm_log.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class BMSyncLog(Document): +class BMLog(Document): pass diff --git a/benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py b/benchmate/benchmate/doctype/bm_log/test_bm_log.py similarity index 85% rename from benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py rename to benchmate/benchmate/doctype/bm_log/test_bm_log.py index 3e49b0e..19380e1 100644 --- a/benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py +++ b/benchmate/benchmate/doctype/bm_log/test_bm_log.py @@ -11,9 +11,9 @@ IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -class IntegrationTestBMSyncLog(IntegrationTestCase): +class IntegrationTestBMLog(IntegrationTestCase): """ - Integration tests for BMSyncLog. + Integration tests for BMLog. Use this class for testing interactions between multiple components. """ diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js deleted file mode 100644 index 57d60fc..0000000 --- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Karan Mistry and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("BM Site Creation Logs", { -// refresh(frm) { - -// }, -// }); diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json deleted file mode 100644 index e8c4c71..0000000 --- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "format:{site_name}-{log_timestamp}", - "creation": "2025-08-27 02:56:40.293176", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "site_name", - "column_break_bkvd", - "status", - "log_timestamp", - "section_break_jlrx", - "log" - ], - "fields": [ - { - "fieldname": "column_break_bkvd", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_jlrx", - "fieldtype": "Section Break" - }, - { - "fieldname": "log", - "fieldtype": "Long Text", - "in_list_view": 1, - "label": "Log", - "read_only": 1 - }, - { - "default": "In Process", - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "options": "\nIn Process\nSuccess\nError", - "read_only": 1 - }, - { - "fieldname": "site_name", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Site Name", - "read_only": 1 - }, - { - "fieldname": "log_timestamp", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Log Timestamp", - "read_only": 1, - "unique": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-08-27 03:39:40.158232", - "modified_by": "Administrator", - "module": "BenchMate", - "name": "BM Site Creation Logs", - "naming_rule": "Expression", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [ - { - "color": "Blue", - "title": "In Process" - }, - { - "color": "Green", - "title": "Success" - }, - { - "color": "Red", - "title": "Error" - } - ], - "title_field": "site_name", - "track_seen": 1 -} diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py deleted file mode 100644 index 1639f9c..0000000 --- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2025, Karan Mistry and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class BMSiteCreationLogs(Document): - pass diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py b/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py deleted file mode 100644 index 8169f2c..0000000 --- a/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025, Karan Mistry and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class IntegrationTestBMSiteCreationLogs(IntegrationTestCase): - """ - Integration tests for BMSiteCreationLogs. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js deleted file mode 100644 index 95cd396..0000000 --- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Karan Mistry and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("BM Site Deletion Logs", { -// refresh(frm) { - -// }, -// }); diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py deleted file mode 100644 index 0d96f97..0000000 --- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2025, Karan Mistry and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class BMSiteDeletionLogs(Document): - pass diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py deleted file mode 100644 index c239de8..0000000 --- a/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025, Karan Mistry and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class IntegrationTestBMSiteDeletionLogs(IntegrationTestCase): - """ - Integration tests for BMSiteDeletionLogs. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/benchmate/benchmate/doctype/bm_sync_log/__init__.py b/benchmate/benchmate/doctype/bm_sync_log/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json b/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json deleted file mode 100644 index 2b3bbdf..0000000 --- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "format:BM-SL-{#}", - "creation": "2025-08-18 01:20:36.254906", - "doctype": "DocType", - "engine": "MyISAM", - "field_order": [ - "title", - "column_break_nerx", - "status", - "section_break_djza", - "log" - ], - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "read_only": 1 - }, - { - "fieldname": "log", - "fieldtype": "Long Text", - "in_list_view": 1, - "label": "Log", - "read_only": 1 - }, - { - "default": "Error", - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Status", - "options": "\nError\nSuccess\nLog", - "read_only": 1 - }, - { - "fieldname": "column_break_nerx", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_djza", - "fieldtype": "Section Break" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-08-27 03:00:11.397603", - "modified_by": "Administrator", - "module": "BenchMate", - "name": "BM Sync Log", - "naming_rule": "Expression", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [ - { - "color": "Red", - "title": "Error" - }, - { - "color": "Green", - "title": "Success" - }, - { - "color": "Blue", - "title": "Log" - } - ], - "title_field": "title", - "track_seen": 1 -} diff --git a/benchmate/benchmate/workspace/benchmate/benchmate.json b/benchmate/benchmate/workspace/benchmate/benchmate.json index ad73c54..b387095 100644 --- a/benchmate/benchmate/workspace/benchmate/benchmate.json +++ b/benchmate/benchmate/workspace/benchmate/benchmate.json @@ -1,7 +1,7 @@ { "app": "benchmate", "charts": [], - "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"OdX7vGpNVa\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Sync Log\",\"col\":3}},{\"id\":\"CZYPa4MOMK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Creation Logs\",\"col\":3}},{\"id\":\"5GjUZsGiN_\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Deletion Logs\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]", + "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"IKOaT4BzS3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Log\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]", "creation": "2025-08-18 03:30:43.609066", "custom_blocks": [], "docstatus": 0, @@ -15,7 +15,7 @@ "label": "BenchMate", "link_type": "DocType", "links": [], - "modified": "2025-09-07 23:42:31.333942", + "modified": "2025-09-08 00:28:19.448700", "modified_by": "Administrator", "module": "BenchMate", "name": "BenchMate", @@ -27,14 +27,6 @@ "roles": [], "sequence_id": 23.0, "shortcuts": [ - { - "color": "Grey", - "doc_view": "List", - "label": "Bm Site Creation Logs", - "link_to": "BM Site Creation Logs", - "stats_filter": "[]", - "type": "DocType" - }, { "color": "Grey", "doc_view": "List", @@ -47,8 +39,8 @@ { "color": "Grey", "doc_view": "List", - "label": "Bm Site Deletion Logs", - "link_to": "BM Site Deletion Logs", + "label": "BM Log", + "link_to": "BM Log", "stats_filter": "[]", "type": "DocType" }, @@ -70,15 +62,6 @@ "stats_filter": "[]", "type": "DocType" }, - { - "color": "Grey", - "doc_view": "List", - "format": "{}", - "label": "BM Sync Log", - "link_to": "BM Sync Log", - "stats_filter": "[]", - "type": "DocType" - }, { "color": "Grey", "doc_view": "List", From 424a5a5a74aae5a6c559172e328b429bc5dbf483 Mon Sep 17 00:00:00 2001 From: karan-sanskar Date: Mon, 8 Sep 2025 00:53:01 +0530 Subject: [PATCH 2/3] [Feat]: Backup Site new action added... --- benchmate/api/actions/backup_site.py | 182 ++++++++++++++++++ benchmate/api/actions/create_site.py | 2 +- benchmate/api/actions/drop_site.py | 2 +- .../benchmate/doctype/bm_bench/bm_bench.js | 117 ++++++++++- 4 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 benchmate/api/actions/backup_site.py diff --git a/benchmate/api/actions/backup_site.py b/benchmate/api/actions/backup_site.py new file mode 100644 index 0000000..6a0c683 --- /dev/null +++ b/benchmate/api/actions/backup_site.py @@ -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 {site_name} in the background. Check the BM Log for more details." + ), + "data": None, + } diff --git a/benchmate/api/actions/create_site.py b/benchmate/api/actions/create_site.py index 92a2430..76014c2 100644 --- a/benchmate/api/actions/create_site.py +++ b/benchmate/api/actions/create_site.py @@ -235,5 +235,5 @@ def execute(bench_name: str, bench_path: str, site_name: str): return { "success": True, "message": f"Creating {site_name} in background Check BM Log for more details.", - "data": {"bench_path": bench_path}, + "data": None, } diff --git a/benchmate/api/actions/drop_site.py b/benchmate/api/actions/drop_site.py index 445ab79..639665a 100644 --- a/benchmate/api/actions/drop_site.py +++ b/benchmate/api/actions/drop_site.py @@ -203,5 +203,5 @@ def execute(bench_name: str, bench_path: str, site_name: str): f"Deleting site {site_name} in the background. " f"Check the BM Log for more details." ), - "data": {"bench_path": bench_path}, + "data": None, } diff --git a/benchmate/benchmate/doctype/bm_bench/bm_bench.js b/benchmate/benchmate/doctype/bm_bench/bm_bench.js index 4c6eb72..4efcc80 100644 --- a/benchmate/benchmate/doctype/bm_bench/bm_bench.js +++ b/benchmate/benchmate/doctype/bm_bench/bm_bench.js @@ -28,6 +28,15 @@ function addBenchActions(frm) { __("Actions") ); + // ? Add "Backup Site" button and pair it with handler + frm.add_custom_button( + __("Backup Site"), + function () { + backupSite(frm); + }, + __("Actions") + ); + // ? Add "Start Bench" button and pair it with handler frm.add_custom_button( __("Start Bench"), @@ -47,6 +56,7 @@ function addBenchActions(frm) { ); } +// ? Function to handle Cerate Site action function createSite(frm) { let dialog = new frappe.ui.Dialog({ title: __("Create New Site"), @@ -180,7 +190,7 @@ function dropSite(frm) { // ? Fields inside the dialog fields: [ { - label: __("Site"), // ? BM Site link field + label: __("Site"), fieldname: "site", fieldtype: "Link", options: "BM Site", @@ -213,7 +223,7 @@ function dropSite(frm) { }, }, { - label: __("Site Name"), // ? Read-only field to confirm exact site_name + label: __("Site Name"), fieldname: "site_name", fieldtype: "Data", read_only: 1, @@ -231,9 +241,9 @@ function dropSite(frm) { frappe.call({ method: "benchmate.api.actions.drop_site.execute", args: { - bench_name: frm.doc.name, // ? Current bench name - bench_path: frm.doc.path, // ? Bench path - site_name: values.site_name, // ? Site selected to drop + bench_name: frm.doc.name, + bench_path: frm.doc.path, + site_name: values.site_name, }, freeze: true, freeze_message: __(`Dropping Site ${values.site_name}...`), @@ -267,3 +277,100 @@ function dropSite(frm) { // ? Display the dialog to the user dialog.show(); } + +// ? Function to handle the Backup Site action from BM Bench form +function backupSite(frm) { + // ? Create a dialog box for site selection and backup confirmation + let dialog = new frappe.ui.Dialog({ + title: __("Backup A Site"), + + // ? Fields inside the dialog + fields: [ + { + label: __("Site"), + fieldname: "site", + fieldtype: "Link", + options: "BM Site", + reqd: 1, + get_query: function () { + // ? Restrict sites only for the current bench + return { + filters: [["bench_name", "=", frm.doc.name]], + }; + }, + onchange: function () { + // ? On site selection, fetch the actual site_name + let site = dialog.get_value("site"); + if (site) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "BM Site", + filters: { name: site }, + fieldname: ["site_name"], + }, + freeze: true, + freeze_message: __(`Fetching Site Name...`), + callback: function (response) { + // ? Auto-populate the readonly Site Name field + dialog.set_value("site_name", response.message.site_name); + }, + }); + } + }, + }, + { + label: __("Site Name"), + fieldname: "site_name", + fieldtype: "Data", + read_only: 1, + reqd: 1, + }, + ], + + // ? Primary action button: Backup Site + primary_action_label: __("Backup"), + primary_action(values) { + // ? Hide dialog after confirmation + dialog.hide(); + + // ? Call server-side method to backup the site + frappe.call({ + method: "benchmate.api.actions.backup_site.execute", + args: { + bench_name: frm.doc.name, + bench_path: frm.doc.path, + site_name: values.site_name, + }, + freeze: true, + freeze_message: __(`Taking Backup Of Site ${values.site_name}...`), + + // ? Handle callback after server execution + callback: function (r) { + if (r.message.success) { + // ? Show success message on site drop + frappe.show_alert( + { + message: __(r.message.message), + indicator: "green", + }, + 5 + ); + } else { + // ? Show error message if failed + frappe.show_alert( + { + message: __(r.message.message), + indicator: "red", + }, + 5 + ); + } + }, + }); + }, + }); + + // ? Display the dialog to the user + dialog.show(); +} From 6a2f7486528050f0eb586026087f864eeb85e955 Mon Sep 17 00:00:00 2001 From: karan-sanskar Date: Mon, 8 Sep 2025 01:40:22 +0530 Subject: [PATCH 3/3] [Feat]: Restore Site Job --- benchmate/api/actions/restore_site.py | 216 ++++++++++++++++++ .../benchmate/doctype/bm_bench/bm_bench.js | 136 +++++++++++ 2 files changed, 352 insertions(+) create mode 100644 benchmate/api/actions/restore_site.py diff --git a/benchmate/api/actions/restore_site.py b/benchmate/api/actions/restore_site.py new file mode 100644 index 0000000..01a0aed --- /dev/null +++ b/benchmate/api/actions/restore_site.py @@ -0,0 +1,216 @@ +import os +import subprocess +import time + +import frappe + +from benchmate.api.utils import get_benchmate_settings + + +def update_restore_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) + + if new_text: + updated_log = (log_doc.log or "") + new_text + log_doc.db_set("log", updated_log, update_modified=False) + + 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 SiteRestoreLogs") + + +def restore_site_background( + bench_name: str, + bench_path: str, + site_name: str, + db_files_path: str, + public_files_path: str, + private_files_path: str, + sudo_password: str, + mysql_root_password: str, +): + """ + Background task to restore a Frappe site from backup files. + 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_restore_site_{site_name}.log") + + # Create BM Log record + log_timestamp = int(time.time()) + log_name = f"Restore Site-{log_timestamp}" + if not frappe.db.exists("BM Log", log_name): + frappe.get_doc( + { + "doctype": "BM Log", + "title": f"Restore Site - {site_name}", + "log": "", + "log_timestamp": log_timestamp, + "status": "In Process", + "action": "Restore Site", + } + ).insert(ignore_permissions=True) + frappe.db.commit() + + # Build restore command with MySQL root password + cmd = [ + "sudo", + "-S", + "bench", + "--site", + site_name, + "--force", + "restore", + db_files_path, + "--with-public-files", + public_files_path, + "--with-private-files", + private_files_path, + "--mariadb-root-password", + mysql_root_password, # ✅ Pass MySQL root password + ] + + try: + 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 with timeout (20 mins) + try: + proc.wait(timeout=1200) + except subprocess.TimeoutExpired: + proc.kill() + update_restore_log_status( + log_name, + new_text="\nTimed out while restoring site!\n", + status="Error", + ) + frappe.msgprint( + msg=f"Timeout expired while restoring site {site_name}.", + title="Site Restore Timeout", + alert=True, + indicator="red", + ) + return + + # Stream logs into BM Log + with open(log_file) as f: + f.seek(0, os.SEEK_SET) + for line in f: + update_restore_log_status(log_name, new_text=line) + + # Success or error status + if proc.returncode == 0: + update_restore_log_status(log_name, status="Success") + else: + update_restore_log_status(log_name, status="Error") + + except Exception as e: + frappe.msgprint( + msg=f"Error while restoring site {site_name} in bench {bench_name}", + title="Site Restore Error", + realtime=True, + alert=True, + indicator="red", + ) + frappe.log_error(f"Error running bench restore: {e}", "BenchMate SiteRestoreLogs") + update_restore_log_status(log_name, status="Error") + + else: + frappe.msgprint( + msg=f"Site {site_name} restored successfully in bench {bench_name}", + title="Site Restore Success", + realtime=True, + alert=True, + indicator="green", + ) + + finally: + 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 SiteRestoreLogs", + ) + + +@frappe.whitelist() +def execute( + bench_name: str, + bench_path: str, + site_name: str, + db_files_path: str, + public_files_path: str, + private_files_path: str, +): + """ + Public API method (whitelisted) to enqueue site restore. + Validates input and enqueues the background site restore task. + """ + if not bench_path or not site_name or not db_files_path: + frappe.throw("bench_path, site_name and db_files_path are required", frappe.ValidationError) + + # ? Update the paths with full paths as per the current bench and site + absolute_site_path = os.path.abspath(frappe.get_site_path()) + db_files_path = os.path.join(absolute_site_path, db_files_path.lstrip("/")) + public_files_path = os.path.join(absolute_site_path, public_files_path.lstrip("/")) + private_files_path = os.path.join(absolute_site_path, private_files_path.lstrip("/")) + + settings = get_benchmate_settings() + sudo_password = settings.get("sudo_password") + mysql_root_password = settings.get("db_password") + + if not sudo_password: + frappe.throw("Sudo password not configured", frappe.ValidationError) + + if not mysql_root_password: + frappe.throw("MySQL root password not configured", frappe.ValidationError) + + try: + frappe.enqueue( + restore_site_background, + queue="long", + timeout=7200, + bench_name=bench_name, + bench_path=bench_path, + site_name=site_name, + db_files_path=db_files_path, + public_files_path=public_files_path, + private_files_path=private_files_path, + sudo_password=sudo_password, + mysql_root_password=mysql_root_password, + ) + + except Exception as e: + frappe.throw(f"Failed to enqueue site restore: {e!s}") + + return { + "success": True, + "message": ( + f"Restoring site {site_name} in the background. Check the BM Log for more details." + ), + "data": None, + } diff --git a/benchmate/benchmate/doctype/bm_bench/bm_bench.js b/benchmate/benchmate/doctype/bm_bench/bm_bench.js index 4efcc80..75ae9cc 100644 --- a/benchmate/benchmate/doctype/bm_bench/bm_bench.js +++ b/benchmate/benchmate/doctype/bm_bench/bm_bench.js @@ -37,6 +37,15 @@ function addBenchActions(frm) { __("Actions") ); + // ? Add "Restore Site" button and pair it with handler + frm.add_custom_button( + __("Restore Site"), + function () { + restoreSite(frm); + }, + __("Actions") + ); + // ? Add "Start Bench" button and pair it with handler frm.add_custom_button( __("Start Bench"), @@ -374,3 +383,130 @@ function backupSite(frm) { // ? Display the dialog to the user dialog.show(); } + +// ? Function to handle the Restore Site action from BM Bench form +function restoreSite(frm) { + // ? Create a dialog box for site selection and restore confirmation + let dialog = new frappe.ui.Dialog({ + title: __("Restore A Site"), + + // ? Fields inside the dialog + fields: [ + { + label: __("Site"), + fieldname: "site", + fieldtype: "Link", + options: "BM Site", + reqd: 1, + get_query: function () { + // ? Restrict sites only for the current bench + return { + filters: [["bench_name", "=", frm.doc.name]], + }; + }, + onchange: function () { + // ? On site selection, fetch the actual site_name + let site = dialog.get_value("site"); + if (site) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "BM Site", + filters: { name: site }, + fieldname: ["site_name"], + }, + freeze: true, + freeze_message: __(`Fetching Site Name...`), + callback: function (response) { + // ? Auto-populate the readonly Site Name field + dialog.set_value("site_name", response.message.site_name); + }, + }); + } + }, + }, + { + label: __("Site Name"), + fieldname: "site_name", + fieldtype: "Data", + read_only: 1, + reqd: 1, + }, + { + label: __("Database Files"), + fieldname: "db_files_path", + fieldtype: "Attach", + description: + "Upload the database backup file. Usually file name ends in .sql.gz or .sql", + reqd: 1, + options: { restrictions: { allowed_file_types: [".sql", ".sql.gz"] } }, + }, + { + label: __("Public Files"), + fieldname: "public_files_path", + fieldtype: "Attach", + description: + "Upload the public files backup. Usually file name ends in -files.tar", + reqd: 1, + options: { restrictions: { allowed_file_types: [".tar"] } }, + }, + { + label: __("Private Files"), + fieldname: "private_files_path", + fieldtype: "Attach", + description: + "Upload the private files backup. Usually file name ends in -private-files.tar", + reqd: 1, + options: { restrictions: { allowed_file_types: [".tar"] } }, + }, + ], + + // ? Primary action button: Restore Site + primary_action_label: __("Restore"), + primary_action(values) { + // ? Hide dialog after confirmation + dialog.hide(); + + // ? Call server-side method to restore the site + frappe.call({ + method: "benchmate.api.actions.restore_site.execute", + args: { + bench_name: frm.doc.name, + bench_path: frm.doc.path, + site_name: values.site_name, + db_files_path: values.db_files_path, + public_files_path: values.public_files_path, + private_files_path: values.private_files_path, + }, + freeze: true, + freeze_message: __(`Restoring Site ${values.site_name}...`), + + // ? Handle callback after server execution + callback: function (r) { + if (r.message.success) { + // ? Show success message on site drop + frappe.show_alert( + { + message: __(r.message.message), + indicator: "green", + }, + 5 + ); + } else { + // ? Show error message if failed + frappe.show_alert( + { + message: __(r.message.message), + indicator: "red", + }, + 5 + ); + } + }, + }); + }, + }); + + // ? Display the dialog to the user + dialog.show(); +}