export: Add migration status file to export tarball.

This commit updates the export process to write the migration status of
the realm as a JSON file to be included in the export tarball.

This is a preparatory step for adding an assertion to ensure that the
importing and exporting realms have a compatible set of applied
migrations.
This commit is contained in:
PieterCK 2024-10-29 16:00:57 +07:00 committed by Tim Abbott
parent 70d559cafa
commit 40bcb4b42b
3 changed files with 131 additions and 1 deletions

View File

@ -31,6 +31,7 @@ from django.utils.timezone import now as timezone_now
import zerver.lib.upload import zerver.lib.upload
from analytics.models import RealmCount, StreamCount, UserCount from analytics.models import RealmCount, StreamCount, UserCount
from scripts.lib.zulip_tools import overwrite_symlink from scripts.lib.zulip_tools import overwrite_symlink
from version import ZULIP_VERSION
from zerver.lib.avatar_hash import user_avatar_base_path_from_ids from zerver.lib.avatar_hash import user_avatar_base_path_from_ids
from zerver.lib.pysa import mark_sanitized from zerver.lib.pysa import mark_sanitized
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
@ -97,6 +98,8 @@ SourceFilter: TypeAlias = Callable[[Record], bool]
CustomFetch: TypeAlias = Callable[[TableData, Context], None] CustomFetch: TypeAlias = Callable[[TableData, Context], None]
AppMigrations: TypeAlias = dict[str, list[str]]
class MessagePartial(TypedDict): class MessagePartial(TypedDict):
zerver_message: list[Record] zerver_message: list[Record]
@ -104,6 +107,11 @@ class MessagePartial(TypedDict):
realm_id: int realm_id: int
class MigrationStatusJson(TypedDict):
migrations_by_app: AppMigrations
zulip_version: str
MESSAGE_BATCH_CHUNK_SIZE = 1000 MESSAGE_BATCH_CHUNK_SIZE = 1000
ALL_ZULIP_TABLES = { ALL_ZULIP_TABLES = {
@ -2093,6 +2101,8 @@ def do_export_realm(
export_full_with_consent=export_type == RealmExport.EXPORT_FULL_WITH_CONSENT, export_full_with_consent=export_type == RealmExport.EXPORT_FULL_WITH_CONSENT,
) )
do_common_export_processes(output_dir)
logging.info("Finished exporting %s", realm.string_id) logging.info("Finished exporting %s", realm.string_id)
create_soft_link(source=output_dir, in_progress=False) create_soft_link(source=output_dir, in_progress=False)
@ -2594,3 +2604,33 @@ def get_realm_exports_serialized(realm: Realm) -> list[dict[str, Any]]:
export_type=export.type, export_type=export.type,
) )
return sorted(exports_dict.values(), key=lambda export_dict: export_dict["id"]) return sorted(exports_dict.values(), key=lambda export_dict: export_dict["id"])
def get_migrations_by_app() -> AppMigrations:
from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.recorder import MigrationRecorder
recorder = MigrationRecorder(connections[DEFAULT_DB_ALIAS])
applied = recorder.applied_migrations()
migrations_by_app: AppMigrations = {}
for app_name, migration_name in applied:
migrations_by_app.setdefault(app_name, []).append(migration_name)
return migrations_by_app
def export_migration_status(output_dir: str) -> None:
migration_status_json = MigrationStatusJson(
migrations_by_app=get_migrations_by_app(), zulip_version=ZULIP_VERSION
)
output_file = os.path.join(output_dir, "migration_status.json")
with open(output_file, "wb") as f:
f.write(orjson.dumps(migration_status_json, option=orjson.OPT_INDENT_2))
def do_common_export_processes(output_dir: str) -> None:
# Performs common task(s) necessary for preparing Zulip data exports.
# This function is typically shared with migration tools in the
# `zerver/data_import` directory.
logging.info("Exporting migration status")
export_migration_status(output_dir)

View File

@ -0,0 +1,68 @@
{
"migrations_by_app": {
"contenttypes": ["0001_initial", "0002_remove_content_type_name"],
"auth": [
"0001_initial",
"0002_alter_permission_name_max_length",
"0003_alter_user_email_max_length",
"0004_alter_user_username_opts"
],
"zerver": [
"0001_initial",
"0029_realm_subdomain",
"0030_realm_org_type",
"0031_remove_system_avatar_source"
],
"analytics": [
"0001_initial",
"0002_remove_huddlecount",
"0003_fillstate"
],
"confirmation": [
"0001_initial",
"0002_realmcreationkey",
"0003_emailchangeconfirmation"
],
"zilencer": [
"0001_initial",
"0002_remote_zulip_server",
"0003_add_default_for_remotezulipserver_last_updated_field"
],
"corporate": [
"0001_initial",
"0002_customer_default_discount",
"0003_customerplan"
],
"otp_static": ["0001_initial", "0002_throttling", "0003_add_timestamps"],
"otp_totp": ["0001_initial", "0002_auto_20190420_0723", "0003_add_timestamps"],
"pgroonga": ["0001_enable", "0002_html_escape_subject", "0003_v2_api_upgrade"],
"phonenumber": ["0001_initial", "0001_squashed_0001_initial"],
"two_factor": [
"0001_initial",
"0002_auto_20150110_0810",
"0003_auto_20150817_1733",
"0004_auto_20160205_1827"
],
"sessions": ["0001_initial"],
"default": [
"0001_initial",
"0002_add_related_name",
"0003_alter_email_max_length",
"0004_auto_20160423_0400"
],
"social_auth": [
"0001_initial",
"0002_add_related_name",
"0003_alter_email_max_length",
"0004_auto_20160423_0400",
"0005_auto_20160727_2333"
],
"social_django": [
"0006_partial",
"0007_code_timestamp",
"0008_partial_timestamp",
"0009_auto_20191118_0520"
]
},
"zulip_version": "10.0-dev+git"
}

View File

@ -42,7 +42,13 @@ from zerver.lib import upload
from zerver.lib.avatar_hash import user_avatar_path from zerver.lib.avatar_hash import user_avatar_path
from zerver.lib.bot_config import set_bot_config from zerver.lib.bot_config import set_bot_config
from zerver.lib.bot_lib import StateHandler from zerver.lib.bot_lib import StateHandler
from zerver.lib.export import Record, do_export_realm, do_export_user, export_usermessages_batch from zerver.lib.export import (
MigrationStatusJson,
Record,
do_export_realm,
do_export_user,
export_usermessages_batch,
)
from zerver.lib.import_realm import do_import_realm, get_incoming_message_ids from zerver.lib.import_realm import do_import_realm, get_incoming_message_ids
from zerver.lib.streams import create_stream_if_needed from zerver.lib.streams import create_stream_if_needed
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
@ -334,6 +340,20 @@ class ExportFile(ZulipTestCase):
db_paths = {user_avatar_path(user) + ".original"} db_paths = {user_avatar_path(user) + ".original"}
self.assertEqual(exported_paths, db_paths) self.assertEqual(exported_paths, db_paths)
def verify_migration_status_json(self) -> None:
# This function asserts that the generated migration_status.json
# is structurally familiar for it to be used for assertion at
# import_realm.py. Hence, it doesn't really matter if the individual
# apps' migrations in migration_status.json fixture are outdated.
exported: MigrationStatusJson = read_json("migration_status.json")
fixture: MigrationStatusJson = orjson.loads(
self.fixture_data("migration_status.json", "import_fixtures")
)
for app, migrations in fixture["migrations_by_app"].items():
self.assertTrue(
set(migrations).issubset(set(exported["migrations_by_app"].get(app, []))),
)
class RealmImportExportTest(ExportFile): class RealmImportExportTest(ExportFile):
def create_user_and_login(self, email: str, realm: Realm) -> None: def create_user_and_login(self, email: str, realm: Realm) -> None:
@ -392,6 +412,7 @@ class RealmImportExportTest(ExportFile):
self.verify_avatars(user) self.verify_avatars(user)
self.verify_emojis(user, is_s3=False) self.verify_emojis(user, is_s3=False)
self.verify_realm_logo_and_icon() self.verify_realm_logo_and_icon()
self.verify_migration_status_json()
def test_public_only_export_files_private_uploads_not_included(self) -> None: def test_public_only_export_files_private_uploads_not_included(self) -> None:
""" """
@ -439,6 +460,7 @@ class RealmImportExportTest(ExportFile):
self.verify_avatars(user) self.verify_avatars(user)
self.verify_emojis(user, is_s3=True) self.verify_emojis(user, is_s3=True)
self.verify_realm_logo_and_icon() self.verify_realm_logo_and_icon()
self.verify_migration_status_json()
def test_zulip_realm(self) -> None: def test_zulip_realm(self) -> None:
realm = Realm.objects.get(string_id="zulip") realm = Realm.objects.get(string_id="zulip")