zulip/scripts/lib/install

569 lines
18 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env bash
set -e
usage() {
# A subset of this documentation also appears in docs/production/install.md
cat <<'EOF'
Usage:
install --hostname=zulip.example.com --email=zulip-admin@example.com [options...]
install --help
Options:
--hostname=zulip.example.com
The user-accessible domain name for this Zulip server, i.e., what users will type
in their web browser. Required, unless --no-init-db is set and --certbot is not.
--email=zulip-admin@example.com
The email address of the person or team who should get support and error emails
from this Zulip server. Required, unless --no-init-db is set and --certbot is
not.
--certbot
Obtains a free SSL certificate for the server using Certbot,
https://certbot.eff.org/ Recommended. Conflicts with --self-signed-cert.
--self-signed-cert
Generate a self-signed SSL certificate for the server. This isnt suitable for
production use, but may be convenient for testing. Conflicts with --certbot.
--cacert=/path/to/ca.pem
Set the CA which used to establish TLS to all public internet sites during the
install process; used when this command is run once in a highly-controlled
environment to produce an image which is used elsewhere. Uncommon.
--postgresql-database-name=zulip
Sets the PostgreSQL database name.
--postgresql-database-user=zulip
Sets the PostgreSQL database user.
--postgresql-version=14
Sets the version of PostgreSQL that will be installed.
--postgresql-missing-dictionaries
Set postgresql.missing_dictionaries, which alters the initial database. Use with
cloud managed databases like RDS. Conflicts with --no-overwrite-settings.
--no-init-db
Does not do any database initialization; use when you already have a Zulip
database.
--no-overwrite-settings
Preserve existing `/etc/zulip` configuration files.
--no-dist-upgrade
Skip the initial `apt-get dist-upgrade`.
EOF
}
# Shell option parsing. Over time, we'll want to move some of the
# environment variables below into this self-documenting system.
args="$(getopt -o '' --long help,hostname:,email:,certbot,self-signed-cert,cacert:,postgresql-database-name:,postgresql-database-user:,postgresql-version:,postgresql-missing-dictionaries,no-init-db,no-overwrite-settings,no-dist-upgrade -n "$0" -- "$@")"
eval "set -- $args"
while true; do
case "$1" in
--help)
usage
exit 0
;;
--hostname)
EXTERNAL_HOST="$2"
shift
shift
;;
--email)
ZULIP_ADMINISTRATOR="$2"
shift
shift
;;
--certbot)
USE_CERTBOT=1
shift
;;
--cacert)
export CUSTOM_CA_CERTIFICATES="$2"
shift
shift
;;
--self-signed-cert)
SELF_SIGNED_CERT=1
shift
;;
--postgresql-database-name)
POSTGRESQL_DATABASE_NAME="$2"
shift
shift
;;
--postgresql-database-user)
POSTGRESQL_DATABASE_USER="$2"
shift
shift
;;
--postgresql-version)
POSTGRESQL_VERSION="$2"
shift
shift
;;
--postgresql-missing-dictionaries)
POSTGRESQL_MISSING_DICTIONARIES=1
shift
;;
--no-init-db)
NO_INIT_DB=1
shift
;;
--no-overwrite-settings)
NO_OVERWRITE_SETTINGS=1
shift
;;
--no-dist-upgrade)
NO_DIST_UPGRADE=1
shift
;;
--)
shift
break
;;
esac
done
if [ "$#" -gt 0 ]; then
usage >&2
exit 1
fi
## Options from environment variables.
#
# Specify options for apt.
read -r -a APT_OPTIONS <<<"${APT_OPTIONS:-}"
# Install additional packages.
read -r -a ADDITIONAL_PACKAGES <<<"${ADDITIONAL_PACKAGES:-}"
# Comma-separated list of Puppet manifests to install. The default is
# zulip::profile::standalone for an all-in-one system or
# zulip::profile::docker for Docker. Use
# e.g. zulip::profile::app_frontend for a Zulip frontend server.
PUPPET_CLASSES="${PUPPET_CLASSES:-zulip::profile::standalone}"
VIRTUALENV_NEEDED="${VIRTUALENV_NEEDED:-yes}"
POSTGRESQL_VERSION="${POSTGRESQL_VERSION:-14}"
if [ -n "$SELF_SIGNED_CERT" ] && [ -n "$USE_CERTBOT" ]; then
set +x
echo "error: --self-signed-cert and --certbot are incompatible" >&2
echo >&2
usage >&2
exit 1
fi
if [ -n "$POSTGRESQL_MISSING_DICTIONARIES" ] && [ -n "$NO_OVERWRITE_SETTINGS" ]; then
set +x
echo "error: --postgresql-missing-dictionaries and --no-overwrite-settings are incompatible" >&2
echo >&2
usage >&2
exit 1
fi
if [ -z "$EXTERNAL_HOST" ] || [ -z "$ZULIP_ADMINISTRATOR" ]; then
if [ -n "$USE_CERTBOT" ] || [ -z "$NO_INIT_DB" ]; then
usage >&2
exit 1
fi
fi
if [ "$EXTERNAL_HOST" = zulip.example.com ] \
|| [ "$ZULIP_ADMINISTRATOR" = zulip-admin@example.com ]; then
# These example values are specifically checked for and would fail
# later; see check_config in zerver/lib/management.py.
echo 'error: The example hostname and email must be replaced with real values.' >&2
echo >&2
usage >&2
exit 1
fi
case "$POSTGRESQL_VERSION" in
[0-9] | [0-9].* | 10 | 10.*)
echo "error: PostgreSQL 11 or newer is required." >&2
exit 1
;;
esac
# Do set -x after option parsing is complete
set -x
ZULIP_PATH="$(readlink -f "$(dirname "$0")"/../..)"
# Force a known locale. Some packages on PyPI fail to install in some locales.
export LC_ALL="C.UTF-8"
export LANG="C.UTF-8"
export LANGUAGE="C.UTF-8"
# Force a known path; this fixes problems on Debian where `su` from
# non-root may not adjust `$PATH` to root's.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Check for a supported OS release.
if [ -f /etc/os-release ]; then
os_info="$(
. /etc/os-release
printf '%s\n' "$ID" "$ID_LIKE" "$VERSION_ID" "$VERSION_CODENAME"
)"
{
read -r os_id
read -r os_id_like
read -r os_version_id
read -r os_version_codename || true
} <<<"$os_info"
case " $os_id $os_id_like " in
*' debian '*)
package_system="apt"
;;
*' rhel '*)
package_system="yum"
;;
esac
fi
case "$os_id $os_version_id" in
'debian 11' | 'ubuntu 20.04' | 'ubuntu 22.04') ;;
*)
set +x
cat <<EOF
Unsupported OS release: $os_id $os_version_id
Zulip in production is supported only on:
- Debian 11
- Ubuntu 20.04 LTS
- Ubuntu 22.04 LTS
For more information, see:
https://zulip.readthedocs.io/en/latest/production/requirements.html
EOF
exit 1
;;
esac
has_universe() {
apt-cache policy \
| grep -q "^ release v=$os_version_id,o=Ubuntu,a=$os_version_codename,n=$os_version_codename,l=Ubuntu,c=universe"
}
if [ "$os_id" = ubuntu ] && ! has_universe && ! { apt-get update && has_universe; }; then
set +x
cat <<'EOF'
You must enable the Ubuntu Universe repository before installing
Zulip. You can do this with:
sudo add-apt-repository universe
sudo apt update
For more information, see:
https://zulip.readthedocs.io/en/latest/production/requirements.html
EOF
exit 1
fi
case ",$PUPPET_CLASSES," in
*,zulip::profile::standalone,* | *,zulip::profile::postgresql,*)
if [ "$package_system" = apt ]; then
# We're going to install PostgreSQL from the PostgreSQL apt
# repository; this may conflict with the existing PostgreSQL.
OTHER_PG="$(dpkg --get-selections \
| grep -E '^postgresql-[0-9]+\s+install$' \
| grep -v "^postgresql-$POSTGRESQL_VERSION\b" \
| cut -f 1)" || true
if [ -n "$OTHER_PG" ]; then
INDENTED="${OTHER_PG//$'\n'/$'\n' }"
SPACED="${OTHER_PG//$'\n'/ }"
cat <<EOF
The following PostgreSQL servers were found to already be installed:
$INDENTED
Zulip needs to install PostgreSQL $POSTGRESQL_VERSION, but does not wish
to uninstall existing databases in order to do so. Remove all other
PostgreSQL servers manually before running the installer:
sudo apt-get remove $SPACED
EOF
exit 1
fi
fi
;;
esac
# Check for at least ~1.86GB of RAM before starting installation;
# otherwise users will find out about insufficient RAM via weird
# errors like a segfault running `pip install`.
# Additionally, some AWS images that are advertised to be 2 GB
# are actually 1880000B in size.
mem_kb=$(head -n1 /proc/meminfo | awk '{print $2}')
if [ "$mem_kb" -lt 1860000 ]; then
set +x
echo -e '\033[0;31m' >&2
echo "Insufficient RAM. Zulip requires at least 2GB of RAM." >&2
echo >&2
echo -e '\033[0m' >&2
exit 1
fi
# Do package update, e.g. do `apt-get update` on Debian
if [ "$package_system" = apt ]; then
# setup-apt-repo does an `apt-get update`
"$ZULIP_PATH"/scripts/lib/setup-apt-repo
elif [ "$package_system" = yum ]; then
"$ZULIP_PATH"/scripts/lib/setup-yum-repo
fi
# Check early for missing SSL certificates
if [ "$PUPPET_CLASSES" = "zulip::profile::standalone" ] && [ -z "$USE_CERTBOT""$SELF_SIGNED_CERT" ] && { ! [ -e "/etc/ssl/private/zulip.key" ] || ! [ -e "/etc/ssl/certs/zulip.combined-chain.crt" ]; }; then
set +x
cat <<EOF
No SSL certificate found. One or both required files is missing:
/etc/ssl/private/zulip.key
/etc/ssl/certs/zulip.combined-chain.crt
Suggested solutions:
* For most sites, the --certbot option is recommended.
* If you have your own key and cert, see docs linked below
for how to install them.
* For non-production testing, try the --self-signed-cert option.
For help and more details, see our SSL documentation:
https://zulip.readthedocs.io/en/latest/production/ssl-certificates.html
Once fixed, just rerun scripts/setup/install; it'll pick up from here!
EOF
2020-06-17 23:45:20 +02:00
exit 1
fi
# don't run dist-upgrade in one click apps to make the
# installation process more seamless.
if [ -z "$NO_DIST_UPGRADE" ]; then
if [ "$package_system" = apt ]; then
apt-get -y dist-upgrade "${APT_OPTIONS[@]}"
elif [ "$package_system" = yum ]; then
# On CentOS, there is no need to do `yum -y upgrade` because `yum -y
# update` already does the same thing.
:
fi
fi
if [ "$package_system" = apt ]; then
# Note that any additions to these lists must also be added to
# `zulip::profile::base` such that the new dependency is seen by
# upgrades, as well as new installs.
if ! apt-get install -y \
python3 python3-yaml puppet git curl jq crudini \
"${ADDITIONAL_PACKAGES[@]}"; then
set +x
echo -e '\033[0;31m' >&2
echo "Installing packages failed; is network working and (on Ubuntu) the universe repository enabled?" >&2
echo >&2
echo -e '\033[0m' >&2
exit 1
fi
elif [ "$package_system" = yum ]; then
if ! yum install -y \
python3 python3-pyyaml puppet git curl jq crudini \
"${ADDITIONAL_PACKAGES[@]}"; then
set +x
echo -e '\033[0;31m' >&2
echo "Installing packages failed; is network working?" >&2
echo >&2
echo -e '\033[0m' >&2
exit 1
fi
fi
# We generate a self-signed cert even with certbot, so we can use the
# webroot authenticator, which requires nginx be set up with a
# certificate.
if [ -n "$SELF_SIGNED_CERT" ] || [ -n "$USE_CERTBOT" ]; then
"$ZULIP_PATH"/scripts/setup/generate-self-signed-cert \
--exists-ok "${EXTERNAL_HOST:-$(hostname)}"
fi
# Create and activate a virtualenv
2016-07-12 20:46:49 +02:00
if [ "$VIRTUALENV_NEEDED" = "yes" ]; then
"$ZULIP_PATH"/scripts/lib/create-production-venv "$ZULIP_PATH"
2016-07-12 20:46:49 +02:00
fi
"$ZULIP_PATH"/scripts/lib/install-node
"$ZULIP_PATH"/scripts/lib/install-yarn
# Generate /etc/zulip/zulip.conf .
mkdir -p /etc/zulip
has_class() {
grep -qx "$1" /var/lib/puppet/classes.txt
}
# puppet apply --noop fails unless the user that it _would_ chown
# files to exists; https://tickets.puppetlabs.com/browse/PUP-3907
#
# The home directory here should match what's declared in base.pp.
id -u zulip &>/dev/null || useradd -m zulip --home-dir /home/zulip
if [ -n "$NO_OVERWRITE_SETTINGS" ] && [ -e "/etc/zulip/zulip.conf" ]; then
"$ZULIP_PATH"/scripts/zulip-puppet-apply --noop \
--write-catalog-summary \
--classfile=/var/lib/puppet/classes.txt
else
# Write out more than we need, and remove sections that are not
# applicable to the classes that are actually necessary.
cat <<EOF >/etc/zulip/zulip.conf
[machine]
puppet_classes = $PUPPET_CLASSES
deploy_type = production
[postgresql]
version = $POSTGRESQL_VERSION
EOF
if [ -n "$POSTGRESQL_MISSING_DICTIONARIES" ]; then
crudini --set /etc/zulip/zulip.conf postgresql missing_dictionaries true
fi
"$ZULIP_PATH"/scripts/zulip-puppet-apply --noop \
--write-catalog-summary \
--classfile=/var/lib/puppet/classes.txt
# We only need the PostgreSQL version setting on database hosts; but
# we don't know if this is a database host until we have the catalog summary.
if ! has_class "zulip::postgresql_common" || [ "$package_system" != apt ]; then
crudini --del /etc/zulip/zulip.conf postgresql
fi
if [ -n "$POSTGRESQL_DATABASE_NAME" ]; then
crudini --set /etc/zulip/zulip.conf postgresql database_name "$POSTGRESQL_DATABASE_NAME"
fi
if [ -n "$POSTGRESQL_DATABASE_USER" ]; then
crudini --set /etc/zulip/zulip.conf postgresql database_user "$POSTGRESQL_DATABASE_USER"
fi
fi
if has_class "zulip::app_frontend_base"; then
if [ -z "$NO_OVERWRITE_SETTINGS" ] || ! [ -e "/etc/zulip/settings.py" ]; then
cp -a "$ZULIP_PATH"/zproject/prod_settings_template.py /etc/zulip/settings.py
if [ -n "$EXTERNAL_HOST" ]; then
sed -i "s/^EXTERNAL_HOST =.*/EXTERNAL_HOST = '$EXTERNAL_HOST'/" /etc/zulip/settings.py
fi
if [ -n "$ZULIP_ADMINISTRATOR" ]; then
sed -i "s/^ZULIP_ADMINISTRATOR =.*/ZULIP_ADMINISTRATOR = '$ZULIP_ADMINISTRATOR'/" /etc/zulip/settings.py
fi
fi
ln -nsf /etc/zulip/settings.py "$ZULIP_PATH"/zproject/prod_settings.py
"$ZULIP_PATH"/scripts/setup/generate_secrets.py --production
fi
"$ZULIP_PATH"/scripts/zulip-puppet-apply -f
if [ "$package_system" = apt ]; then
apt-get -y --with-new-pkgs upgrade
elif [ "$package_system" = yum ]; then
# No action is required because `yum update` already does upgrade.
:
fi
if [ -n "$USE_CERTBOT" ]; then
"$ZULIP_PATH"/scripts/setup/setup-certbot \
"$EXTERNAL_HOST" --email "$ZULIP_ADMINISTRATOR"
fi
if has_class "zulip::nginx" && ! has_class "zulip::profile::docker"; then
# Check nginx was configured properly now that we've installed it.
# Most common failure mode is certs not having been installed.
if ! nginx -t; then
(
set +x
cat <<EOF
Verifying the Zulip nginx configuration failed!
This is almost always a problem with your SSL certificates. See:
https://zulip.readthedocs.io/en/latest/production/ssl-certificates.html
Once fixed, just rerun scripts/setup/install; it'll pick up from here!
EOF
exit 1
)
fi
fi
if has_class "zulip::profile::rabbitmq"; then
if ! rabbitmqctl status >/dev/null; then
set +x
cat <<EOF
RabbitMQ seems to not have started properly after the installation process.
Often this is caused by misconfigured /etc/hosts in virtualized environments.
For more information, see:
https://github.com/zulip/zulip/issues/53#issuecomment-143805121
EOF
exit 1
fi
fi
# Set up a basic .gitconfig for the 'zulip' user
if [ -n "$ZULIP_ADMINISTRATOR" ]; then
(
cd / # Make sure the current working directory is readable by zulip
su zulip -c "git config --global user.email $ZULIP_ADMINISTRATOR"
su zulip -c "git config --global user.name 'Zulip Server ($EXTERNAL_HOST)'"
)
fi
if ! has_class "zulip::app_frontend_base"; then
set +x
cat <<EOF
Success!
Not configuring PostgreSQL, or /home/zulip/deployments, because this
is not a front-end install.
EOF
exit 0
fi
# Frontend deploys use /home/zulip/deployments; without this, the
# install directory is also only readable by root.
deploy_path=$("$ZULIP_PATH"/scripts/lib/zulip_tools.py make_deploy_path)
mv "$ZULIP_PATH" "$deploy_path"
ln -nsf /home/zulip/deployments/next "$ZULIP_PATH"
ln -nsf "$deploy_path" /home/zulip/deployments/next
ln -nsf "$deploy_path" /home/zulip/deployments/current
ln -nsf /etc/zulip/settings.py "$deploy_path"/zproject/prod_settings.py
mkdir -p "$deploy_path"/prod-static/serve
cp -rT "$deploy_path"/prod-static/serve /home/zulip/prod-static
chown -R zulip:zulip /home/zulip /var/log/zulip /etc/zulip/settings.py
if ! [ -e "/home/zulip/prod-static/generated" ]; then
# If we're installing from a Git checkout, we need to run
# `tools/update-prod-static` in order to build the static
# assets.
su zulip -c '/home/zulip/deployments/current/tools/update-prod-static'
fi
if [ -n "$NO_INIT_DB" ]; then
set +x
cat <<EOF
Success!
Stopping because --no-init-db was passed. To complete the
installation, configure PostgreSQL by creating the database and
database user, and then run:
su zulip -c '/home/zulip/deployments/current/scripts/setup/initialize-database'
su zulip -c '/home/zulip/deployments/current/manage.py generate_realm_creation_link'
EOF
exit 0
else
/home/zulip/deployments/current/scripts/setup/create-database
su zulip -c '/home/zulip/deployments/current/scripts/setup/initialize-database --quiet'
su zulip -c '/home/zulip/deployments/current/manage.py generate_realm_creation_link'
fi