zulip/scripts/setup/restore-backup

176 lines
6.5 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python3
import argparse
import os
import re
import subprocess
import sys
import tempfile
from typing import IO
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(BASE_DIR)
from scripts.lib.zulip_tools import assert_running_as_root, get_postgres_pwent, run, su_to_zulip
POSTGRES_PWENT = get_postgres_pwent()
parser = argparse.ArgumentParser()
parser.add_argument("tarball", help="Filename of input tarball")
parser.add_argument(
"--keep-settings", help="Do not overwrite local /etc/zulip/settings.py", action="store_true"
)
parser.add_argument(
"--keep-zulipconf", help="Do not overwrite local /etc/zulip/zulip.conf", action="store_true"
)
def restore_backup(tarball_file: IO[bytes], keep_settings: bool, keep_zulipconf: bool) -> None:
assert_running_as_root()
su_to_zulip(save_suid=True)
from scripts.lib.setup_path import setup_path
setup_path()
# First, we unpack the /etc/zulip configuration, so we know how
# this server is supposed to be configured (and can import
# /etc/zulip/settings.py via `from django.conf import settings`,
# next). Ignore errors if zulip-backup/settings is not present
# (E.g. because this is a development backup).
tarball_file.seek(0, 0)
excludes = []
if keep_settings:
excludes += ["--exclude=settings.py"]
if keep_zulipconf:
excludes += ["--exclude=zulip.conf"]
run(
[
"tar",
"--directory=/etc/zulip",
*excludes,
"--strip-components=2",
"-xz",
"zulip-backup/settings",
],
stdin=tarball_file,
)
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"
from django.conf import settings
paths = [
# zproject will only be present for development environment backups.
("zproject", os.path.join(settings.DEPLOY_ROOT, "zproject")),
]
if settings.LOCAL_UPLOADS_DIR is not None:
# We only need to restore LOCAL_UPLOADS_DIR if the system is
# configured to locally host uploads.
paths.append(("uploads", os.path.join(settings.DEPLOY_ROOT, settings.LOCAL_UPLOADS_DIR)))
with tempfile.TemporaryDirectory(prefix="zulip-restore-backup-") as tmp:
uid = os.getuid()
gid = os.getgid()
os.setresuid(0, 0, 0)
for name, path in paths:
os.makedirs(path, exist_ok=True)
os.chown(path, uid, gid)
os.setresuid(uid, uid, 0)
assert not any("|" in name or "|" in path for name, path in paths)
transform_args = [
r"--transform=s|^zulip-backup/{}(/.*)?$|{}\1|x".format(
re.escape(name),
path.replace("\\", r"\\"),
)
for name, path in paths
]
os.mkdir(os.path.join(tmp, "zulip-backup"))
tarball_file.seek(0, 0)
run(["tar", "-C", tmp, *transform_args, "-xPz"], stdin=tarball_file)
# Now, extract the the database backup, destroy the old
# database, and create a new, empty database.
db = settings.DATABASES["default"]
assert isinstance(db["NAME"], str)
db_dir = os.path.join(tmp, "zulip-backup", "database")
os.setresuid(0, 0, 0)
run(["chown", "-R", POSTGRES_PWENT.pw_name, "--", tmp])
os.setresuid(POSTGRES_PWENT.pw_uid, POSTGRES_PWENT.pw_uid, 0)
postgresql_env = dict(os.environ)
if db["HOST"] not in ["", "localhost"]:
postgresql_env["PGHOST"] = db["HOST"]
if "PORT" in db:
postgresql_env["PGPORT"] = db["PORT"]
postgresql_env["PGUSER"] = db["USER"]
if "PASSWORD" in db:
postgresql_env["PGPASSWORD"] = db["PASSWORD"]
run(
[
os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "terminate-psql-sessions"),
db["NAME"],
],
env=postgresql_env,
)
run(["dropdb", "--if-exists", "--", db["NAME"]], cwd="/", env=postgresql_env)
run(
["createdb", "--owner=zulip", "--template=template0", "--", db["NAME"]],
cwd="/",
env=postgresql_env,
)
os.setresuid(0, 0, 0)
if settings.PRODUCTION:
# In case we are restoring a backup from an older Zulip
# version, there may be new secrets to generate.
run(
[
os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "generate_secrets.py"),
"--production",
]
)
# If there is a local RabbitMQ, we need to reconfigure it
# to ensure the RabbitMQ password matches the value in the
# restored zulip-secrets.conf. We need to be careful to
# only do this if RabbitMQ is configured to run locally on
# the system.
rabbitmq_host = subprocess.check_output(
[
os.path.join(settings.DEPLOY_ROOT, "scripts", "get-django-setting"),
"RABBITMQ_HOST",
],
text=True,
).strip()
if rabbitmq_host in ["127.0.0.1", "::1", "localhost", "localhost6"]:
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "configure-rabbitmq")])
# In production, we also need to do a `zulip-puppet-apply`
# in order to apply any configuration from
# /etc/zulip/zulip.conf to this system, since it was
# originally installed without the restored copy of that
# file.
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "zulip-puppet-apply"), "-f"])
# Now, restore the the database backup using pg_restore. This
# needs to run after zulip-puppet-apply to ensure full-text
# search extensions are available and installed.
os.setresuid(POSTGRES_PWENT.pw_uid, POSTGRES_PWENT.pw_uid, 0)
run(["pg_restore", "--dbname=" + db["NAME"], "--", db_dir], cwd="/", env=postgresql_env)
os.setresuid(0, 0, 0)
run(["chown", "-R", str(uid), "--", tmp])
os.setresuid(uid, uid, 0)
if settings.PRODUCTION:
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "restart-server")])
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "flush-memcached")])
if __name__ == "__main__":
args = parser.parse_args()
with open(args.tarball, "rb") as tarball_file:
restore_backup(tarball_file, args.keep_settings, args.keep_zulipconf)