2
0
mirror of https://github.com/frappe/frappe.git synced 2024-06-12 21:32:26 +00:00
frappe/frappe/migrate.py
2024-05-24 15:48:00 +02:00

208 lines
6.1 KiB
Python

# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import contextlib
import functools
import json
import os
from textwrap import dedent
import frappe
import frappe.model.sync
import frappe.modules.patch_handler
import frappe.translate
from frappe.cache_manager import clear_global_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.database.schema import add_column
from frappe.deferred_insert import save_to_db as flush_deferred_inserts
from frappe.desk.notifications import clear_notifications
from frappe.modules.patch_handler import PatchType
from frappe.modules.utils import sync_customizations
from frappe.search.website_search import build_index_for_all_routes
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.utils.fixtures import sync_fixtures
from frappe.website.utils import clear_website_cache
BENCH_START_MESSAGE = dedent(
"""
Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:
$ bench start
Otherwise, check the server logs and ensure that all the required services are running.
"""
)
def atomic(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
try:
ret = method(*args, **kwargs)
frappe.db.commit()
return ret
except Exception as e:
# database itself can be gone while attempting rollback.
# We should preserve original exception in this case.
with contextlib.suppress(Exception):
frappe.db.rollback()
raise e
return wrapper
class SiteMigration:
"""Migrate all apps to the current version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)
- sync dashboards
- sync jobs
- sync fixtures
- sync customizations
- sync languages
- sync web pages (from /www)
- run after migrate hooks
"""
def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None:
self.skip_failing = skip_failing
self.skip_search_index = skip_search_index
def setUp(self):
"""Complete setup required for site migration"""
frappe.flags.touched_tables = set()
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
frappe.clear_cache()
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
clear_global_cache()
if os.path.exists(self.touched_tables_file):
os.remove(self.touched_tables_file)
frappe.flags.in_migrate = True
def tearDown(self):
"""Run operations that should be run post schema updation processes
This should be executed irrespective of outcome
"""
frappe.translate.clear_cache()
clear_website_cache()
clear_notifications()
with open(self.touched_tables_file, "w") as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
if not self.skip_search_index:
print(f"Queued rebuilding of search index for {frappe.local.site}")
frappe.enqueue(build_index_for_all_routes, queue="long")
frappe.publish_realtime("version-update")
frappe.flags.touched_tables.clear()
frappe.flags.in_migrate = False
@atomic
def pre_schema_updates(self):
"""Executes `before_migrate` hooks"""
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks("before_migrate", app_name=app):
frappe.get_attr(fn)()
@atomic
def run_schema_updates(self):
"""Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files"""
frappe.modules.patch_handler.run_all(
skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync
)
frappe.model.sync.sync_all()
frappe.modules.patch_handler.run_all(
skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync
)
@atomic
def post_schema_updates(self):
"""Execute pending migration tasks post patches execution & schema sync
This includes:
* Sync `Scheduled Job Type` and scheduler events defined in hooks
* Sync fixtures & custom scripts
* Sync in-Desk Module Dashboards
* Sync customizations: Custom Fields, Property Setters, Custom Permissions
* Sync Frappe's internal language master
* Flush deferred inserts made during maintenance mode.
* Sync Portal Menu Items
* Sync Installed Applications Version History
* Execute `after_migrate` hooks
"""
print("Syncing jobs...")
sync_jobs()
print("Syncing fixtures...")
sync_fixtures()
print("Syncing dashboards...")
sync_dashboards()
print("Syncing customizations...")
sync_customizations()
print("Syncing languages...")
sync_languages()
print("Flushing deferred inserts...")
flush_deferred_inserts()
print("Removing orphan doctypes...")
frappe.model.sync.remove_orphan_doctypes()
print("Syncing portal menu...")
frappe.get_single("Portal Settings").sync_menu()
print("Updating installed applications...")
frappe.get_single("Installed Applications").update_versions()
print("Executing `after_migrate` hooks...")
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks("after_migrate", app_name=app):
frappe.get_attr(fn)()
def required_services_running(self) -> bool:
"""Return True if all required services are running. Return False and print
instructions to stdout when required services are not available.
"""
service_status = check_connection(redis_services=["redis_cache"])
are_services_running = all(service_status.values())
if not are_services_running:
for service in service_status:
if not service_status.get(service, True):
print(f"Service {service} is not running.")
print(BENCH_START_MESSAGE)
return are_services_running
def run(self, site: str):
"""Run Migrate operation on site specified. This method initializes
and destroys connections to the site database.
"""
from frappe.utils.synchronization import filelock
if site:
frappe.init(site=site)
frappe.connect()
if not self.required_services_running():
raise SystemExit(1)
with filelock("bench_migrate", timeout=1):
self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
self.post_schema_updates()
finally:
self.tearDown()
frappe.destroy()