#!/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 isn’t 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. --postgres-version=12 Sets the version of Postgres that will be installed. --postgres-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:,postgres-version:,postgres-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 ;; --postgres-version) POSTGRES_VERSION="$2" shift shift ;; --postgres-missing-dictionaries) POSTGRES_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. default is # zulip::profile::voyager for an all-in-one system or # zulip::profile::dockervoyager for Docker. Use # e.g. zulip::profile::app_frontend for a Zulip frontend server. PUPPET_CLASSES="${PUPPET_CLASSES:-zulip::profile::voyager}" VIRTUALENV_NEEDED="${VIRTUALENV_NEEDED:-yes}" POSTGRES_VERSION="${POSTGRES_VERSION:-12}" 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 "$POSTGRES_MISSING_DICTIONARIES" ] && [ -n "$NO_OVERWRITE_SETTINGS" ]; then set +x echo "error: --postgres-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 # 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. localedef -i en_US -f UTF-8 en_US.UTF-8 export LC_ALL="en_US.UTF-8" export LANG="en_US.UTF-8" export LANGUAGE="en_US.UTF-8" # 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 debian10 | ubuntu18.04 | ubuntu20.04) ;; *) set +x cat <&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::voyager" ] && [ -z "$USE_CERTBOT""$SELF_SIGNED_CERT" ] && { ! [ -e "/etc/ssl/private/zulip.key" ] || ! [ -e "/etc/ssl/certs/zulip.combined-chain.crt" ]; }; then set +x cat <&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 \ puppet git curl wget jq \ python3 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 if [ -n "$USE_CERTBOT" ]; then "$ZULIP_PATH"/scripts/setup/setup-certbot \ --no-zulip-conf --method=standalone \ "$EXTERNAL_HOST" --email "$ZULIP_ADMINISTRATOR" elif [ -n "$SELF_SIGNED_CERT" ]; then "$ZULIP_PATH"/scripts/setup/generate-self-signed-cert \ --exists-ok "${EXTERNAL_HOST:-$(hostname)}" fi # Create and activate a virtualenv if [ "$VIRTUALENV_NEEDED" = "yes" ]; then "$ZULIP_PATH"/scripts/lib/create-production-venv "$ZULIP_PATH" "$ZULIP_PATH"/scripts/lib/create-thumbor-venv "$ZULIP_PATH" fi "$ZULIP_PATH"/scripts/lib/install-node # 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 \ >/dev/null else # Write out more than we need, and remove sections that are not # applicable to the classes that are actually necessary. cat </etc/zulip/zulip.conf [machine] puppet_classes = $PUPPET_CLASSES deploy_type = production [postgresql] version = $POSTGRES_VERSION EOF if [ -n "$USE_CERTBOT" ]; then crudini --set /etc/zulip/zulip.conf certbot auto_renew yes fi if [ -n "$POSTGRES_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 \ >/dev/null # We only need the Postgres 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::postgres_common" || [ "$package_system" != apt ]; then crudini --del /etc/zulip/zulip.conf postgresql fi # Note: there are four dpkg-query outputs to consider: # # root@host# dpkg-query --showformat '${Status}\n' -W rabbitmq-server 2>/dev/null # root@host# apt install rabbitmq-server # root@host# dpkg-query --showformat '${Status}\n' -W rabbitmq-server 2>/dev/null # install ok installed # root@host# apt remove rabbitmq-server # root@host# dpkg-query --showformat '${Status}\n' -W rabbitmq-server 2>/dev/null # deinstall ok config-files # root@host# apt purge rabbitmq-server # root@host# dpkg-query --showformat '${Status}\n' -W rabbitmq-server 2>/dev/null # unknown ok not-installed # # (There are more possibilities in the case of dpkg errors.) Here # we are checking for either empty or not-installed. if ! dpkg-query --showformat '${Status}\n' -W rabbitmq-server 2>/dev/null | grep -vq ' not-installed$'; then cat <>/etc/zulip/zulip.conf [rabbitmq] nodename = zulip@localhost EOF 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 upgrade elif [ "$package_system" = yum ]; then # No action is required because `yum update` already does upgrade. : fi if has_class "zulip::nginx" && ! has_class "zulip::profile::dockervoyager"; 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 </dev/null; then set +x cat <