2019-01-16 03:17:33 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
|
|
import os
|
2019-04-13 01:48:34 +02:00
|
|
|
import re
|
2019-01-16 03:17:33 +01:00
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import tempfile
|
2019-07-23 23:58:11 +02:00
|
|
|
from typing import IO
|
2019-04-15 22:06:02 +02:00
|
|
|
|
2019-01-16 03:17:33 +01:00
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
sys.path.append(BASE_DIR)
|
2020-02-29 16:29:16 +01:00
|
|
|
from scripts.lib.zulip_tools import get_postgres_pwent, run, su_to_zulip
|
2019-01-16 03:17:33 +01:00
|
|
|
|
2020-02-29 16:29:16 +01:00
|
|
|
POSTGRES_PWENT = get_postgres_pwent()
|
2019-01-16 03:17:33 +01:00
|
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("tarball", help="Filename of input tarball")
|
|
|
|
|
2019-04-15 22:06:02 +02:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def restore_backup(tarball_file: IO[bytes]) -> None:
|
2019-04-15 22:06:02 +02:00
|
|
|
|
2019-01-16 03:17:33 +01:00
|
|
|
su_to_zulip(save_suid=True)
|
|
|
|
|
2020-01-08 00:06:39 +01:00
|
|
|
from scripts.lib.setup_path import setup_path
|
|
|
|
|
|
|
|
setup_path()
|
2019-01-16 03:17:33 +01:00
|
|
|
|
|
|
|
# First, we unpack the /etc/zulip configuration, so we know how
|
|
|
|
# this server is supposed to be configured (and can import
|
2019-08-28 06:04:21 +02:00
|
|
|
# /etc/zulip/settings.py via `from django.conf import settings`,
|
2019-01-16 03:17:33 +01:00
|
|
|
# next). Ignore errors if zulip-backup/settings is not present
|
|
|
|
# (E.g. because this is a development backup).
|
2019-04-15 22:06:02 +02:00
|
|
|
tarball_file.seek(0, 0)
|
2019-01-16 03:17:33 +01:00
|
|
|
subprocess.call(
|
2021-02-12 08:19:30 +01:00
|
|
|
["tar", "--directory=/etc/zulip", "--strip-components=2", "-xz", "zulip-backup/settings"],
|
2019-04-15 22:06:02 +02:00
|
|
|
stdin=tarball_file,
|
2019-01-16 03:17:33 +01:00
|
|
|
)
|
|
|
|
|
2019-08-28 06:04:21 +02:00
|
|
|
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"
|
|
|
|
from django.conf import settings
|
2019-01-16 03:17:33 +01:00
|
|
|
|
|
|
|
paths = [
|
|
|
|
("settings", "/etc/zulip"),
|
|
|
|
# zproject will only be present for development environment backups.
|
|
|
|
("zproject", os.path.join(settings.DEPLOY_ROOT, "zproject")),
|
|
|
|
]
|
2019-06-05 02:56:47 +02:00
|
|
|
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)))
|
2019-01-16 03:17:33 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-04-13 01:48:34 +02:00
|
|
|
assert not any("|" in name or "|" in path for name, path in paths)
|
|
|
|
transform_args = [
|
|
|
|
r"--transform=s|^zulip-backup/{}(/.*)?$|{}\1|x".format(
|
2021-02-12 08:19:30 +01:00
|
|
|
re.escape(name),
|
|
|
|
path.replace("\\", r"\\"),
|
2019-04-13 01:48:34 +02:00
|
|
|
)
|
|
|
|
for name, path in paths
|
|
|
|
]
|
|
|
|
|
2019-01-16 03:17:33 +01:00
|
|
|
os.mkdir(os.path.join(tmp, "zulip-backup"))
|
2019-04-15 22:06:02 +02:00
|
|
|
tarball_file.seek(0, 0)
|
2020-09-02 06:59:07 +02:00
|
|
|
run(["tar", "-C", tmp, *transform_args, "-xPz"], stdin=tarball_file)
|
2019-01-16 03:17:33 +01:00
|
|
|
|
2019-05-21 00:02:19 +02:00
|
|
|
# Now, extract the the database backup, destroy the old
|
|
|
|
# database, and create a new, empty database.
|
2020-02-29 16:29:16 +01:00
|
|
|
db = settings.DATABASES["default"]
|
|
|
|
assert isinstance(db["NAME"], str)
|
2019-01-16 03:17:33 +01:00
|
|
|
db_dir = os.path.join(tmp, "zulip-backup", "database")
|
|
|
|
os.setresuid(0, 0, 0)
|
2020-02-29 16:29:16 +01:00
|
|
|
run(["chown", "-R", POSTGRES_PWENT.pw_name, "--", tmp])
|
|
|
|
os.setresuid(POSTGRES_PWENT.pw_uid, POSTGRES_PWENT.pw_uid, 0)
|
|
|
|
|
2020-10-27 23:32:39 +01:00
|
|
|
postgresql_env = dict(os.environ)
|
2020-02-29 16:29:16 +01:00
|
|
|
if db["HOST"] not in ["", "localhost"]:
|
2020-10-27 23:32:39 +01:00
|
|
|
postgresql_env["PGHOST"] = db["HOST"]
|
2020-02-29 16:29:16 +01:00
|
|
|
if "PORT" in db:
|
2020-10-27 23:32:39 +01:00
|
|
|
postgresql_env["PGPORT"] = db["PORT"]
|
|
|
|
postgresql_env["PGUSER"] = db["USER"]
|
2020-02-29 16:29:16 +01:00
|
|
|
if "PASSWORD" in db:
|
2020-10-27 23:32:39 +01:00
|
|
|
postgresql_env["PGPASSWORD"] = db["PASSWORD"]
|
2020-02-29 16:29:16 +01:00
|
|
|
|
2019-01-16 03:17:33 +01:00
|
|
|
run(
|
|
|
|
[
|
2021-02-12 08:19:30 +01:00
|
|
|
os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "terminate-psql-sessions"),
|
|
|
|
db["NAME"],
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
],
|
2020-10-27 23:32:39 +01:00
|
|
|
env=postgresql_env,
|
2019-01-16 03:17:33 +01:00
|
|
|
)
|
2020-10-27 23:32:39 +01:00
|
|
|
run(["dropdb", "--if-exists", "--", db["NAME"]], cwd="/", env=postgresql_env)
|
2021-02-12 08:19:30 +01:00
|
|
|
run(
|
|
|
|
["createdb", "--owner=zulip", "--template=template0", "--", db["NAME"]],
|
|
|
|
cwd="/",
|
|
|
|
env=postgresql_env,
|
|
|
|
)
|
2020-02-29 16:29:16 +01:00
|
|
|
os.setresuid(0, 0, 0)
|
2019-01-16 03:17:33 +01:00
|
|
|
|
|
|
|
if settings.PRODUCTION:
|
2020-02-19 03:57:14 +01:00
|
|
|
# In case we are restoring a backup from an older Zulip
|
|
|
|
# version, there may be new secrets to generate.
|
2021-02-12 08:19:30 +01:00
|
|
|
subprocess.check_call(
|
|
|
|
[
|
|
|
|
os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "generate_secrets.py"),
|
|
|
|
"--production",
|
|
|
|
]
|
|
|
|
)
|
2020-02-19 03:57:14 +01:00
|
|
|
|
2020-10-23 02:43:28 +02:00
|
|
|
# If there is a local RabbitMQ, we need to reconfigure it
|
|
|
|
# to ensure the RabbitMQ password matches the value in the
|
2019-06-07 01:35:52 +02:00
|
|
|
# restored zulip-secrets.conf. We need to be careful to
|
2020-10-23 02:43:28 +02:00
|
|
|
# only do this if RabbitMQ is configured to run locally on
|
2019-06-07 01:35:52 +02:00
|
|
|
# the system.
|
|
|
|
rabbitmq_host = subprocess.check_output(
|
2021-02-12 08:19:30 +01:00
|
|
|
[
|
|
|
|
os.path.join(settings.DEPLOY_ROOT, "scripts", "get-django-setting"),
|
|
|
|
"RABBITMQ_HOST",
|
|
|
|
],
|
|
|
|
universal_newlines=True,
|
|
|
|
).strip()
|
2019-06-07 01:35:52 +02:00
|
|
|
if rabbitmq_host in ["127.0.0.1", "::1", "localhost", "localhost6"]:
|
2021-02-12 08:19:30 +01:00
|
|
|
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "configure-rabbitmq")])
|
2019-06-07 01:35:52 +02:00
|
|
|
|
|
|
|
# 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.
|
2021-02-12 08:19:30 +01:00
|
|
|
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "zulip-puppet-apply"), "-f"])
|
2019-05-21 00:02:19 +02:00
|
|
|
|
|
|
|
# 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.
|
2020-02-29 16:29:16 +01:00
|
|
|
os.setresuid(POSTGRES_PWENT.pw_uid, POSTGRES_PWENT.pw_uid, 0)
|
2020-10-27 23:32:39 +01:00
|
|
|
run(["pg_restore", "--dbname=" + db["NAME"], "--", db_dir], cwd="/", env=postgresql_env)
|
2020-02-29 16:29:16 +01:00
|
|
|
os.setresuid(0, 0, 0)
|
2019-05-21 00:02:19 +02:00
|
|
|
run(["chown", "-R", str(uid), "--", tmp])
|
|
|
|
os.setresuid(uid, uid, 0)
|
|
|
|
|
|
|
|
if settings.PRODUCTION:
|
2019-01-16 03:17:33 +01:00
|
|
|
run(["supervisorctl", "restart", "all"])
|
|
|
|
|
|
|
|
run([os.path.join(settings.DEPLOY_ROOT, "scripts", "setup", "flush-memcached")])
|
2019-04-15 22:06:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
with open(args.tarball, "rb") as tarball_file:
|
|
|
|
restore_backup(tarball_file)
|