Files
zulip/scripts/setup/restore-backup
Alex Vandiver a4d0f03319 scripts: Switch to stop-server/restart-server.
stop-server and restart-server address all services which talk to the
database, and are thus more correct than restarting or stopping
everything in supervisor.

This is possible now that the previous commit ensures that the zulip
user can read the zulip installation directory during
`create-database`; previously, that directory was still owned by root
when `create-database` was run, whereas now it is in
`~zulip/deployments/`.
2022-03-21 16:33:28 -07:00

159 lines
5.9 KiB
Python
Executable File

#!/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 get_postgres_pwent, run, su_to_zulip
POSTGRES_PWENT = get_postgres_pwent()
parser = argparse.ArgumentParser()
parser.add_argument("tarball", help="Filename of input tarball")
def restore_backup(tarball_file: IO[bytes]) -> None:
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)
subprocess.call(
["tar", "--directory=/etc/zulip", "--strip-components=2", "-xz", "zulip-backup/settings"],
stdin=tarball_file,
)
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"
from django.conf import settings
paths = [
("settings", "/etc/zulip"),
# 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.
subprocess.check_call(
[
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)