From f2dd88223fafa1b9017af6561da33f9526e08218 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 22 Jun 2022 23:39:01 +0530 Subject: [PATCH] feat: add chatwoot ctl(cwctl) cli tool (#4836) * chore: Add log messages for stages skipped during installation * feat: allow chatwoot user to start/stop/restart chatwoot service * feat: init options support * feat: add option support to linux install script [c|h|i|l|s|u|wi] Install/Update/Configure/Manage your Chatwoot installation Example: cwctl -i master Example: cwctl -l web Example: cwctl --logs worker Example: cwctl --upgrade Example: cwctl -c Installation/Upgrade: -i, --install install Chatwoot with the git branch specified -u, --upgrade upgrade Chatwoot to latest version -s, --ssl fetch and install ssl certificates using LetsEncrypt -w, --webserver install and configure Nginx webserver Management: -c, --console open ruby console -l, --logs tail logs from Chatwoot. Supported values include web/worker. Miscellaneous: -h, --help display this help text and exit * feat: add cwctl to PATH * feat: add -v to cwctl * chore: switch db migration to db:chatwoot_prepare * fix: reload systemd files after update * fix: improve -s -w cwctl options * chore: throw error if run without options Signed-off-by: Vishnu Narayanan * feat: add -d/--debug option to cwctl * fix: remove hardcoded ruby version in cwctl --upgrade * chore: improve cwctl -v function * fix: disable cwctl selfupdate * chore: cleanup * feat: allow chatwoot user to run cwctl * chore: cwctl improve formatting for log messages * fix: variable expansion inside heredoc * feat: save pg_pass to file to support idempotency One of the things preventing idempotency was the postgres password generated at run-time to setup postgres initally. This commit saves the password to the file if postgres setup function is executed and reloads on future re-runs if needed. * chore: formatting * chore: add cwctl promotion message at the end of installation * feat: add comments * chore: add chatwoot and cwctl version files * feat: add func to get latest chatwoot version * chore: formatting * feat: add --restart option to cwctl * chore: update --help with restart option details * chore: minor improvements to --restart --- VERSION_CW | 1 + VERSION_CWCTL | 1 + deployment/chatwoot | 6 + deployment/setup_20.04.sh | 705 +++++++++++++++++++++++++++++++++----- 4 files changed, 631 insertions(+), 82 deletions(-) create mode 100644 VERSION_CW create mode 100644 VERSION_CWCTL create mode 100644 deployment/chatwoot diff --git a/VERSION_CW b/VERSION_CW new file mode 100644 index 000000000..e70b4523a --- /dev/null +++ b/VERSION_CW @@ -0,0 +1 @@ +2.6.0 diff --git a/VERSION_CWCTL b/VERSION_CWCTL new file mode 100644 index 000000000..f1547e6d1 --- /dev/null +++ b/VERSION_CWCTL @@ -0,0 +1 @@ +2.0.7 diff --git a/deployment/chatwoot b/deployment/chatwoot new file mode 100644 index 000000000..942674b24 --- /dev/null +++ b/deployment/chatwoot @@ -0,0 +1,6 @@ +# give chatwoot user permission to start/stop/restart chatwoot systemd service + +%chatwoot ALL=NOPASSWD: /bin/systemctl start chatwoot.target +%chatwoot ALL=NOPASSWD: /bin/systemctl stop chatwoot.target +%chatwoot ALL=NOPASSWD: /bin/systemctl restart chatwoot.target +%chatwoot ALL=NOPASSWD: /usr/local/bin/cwctl diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 15f538846..0c884f445 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -1,14 +1,127 @@ #!/usr/bin/env bash -# Description: Chatwoot installation script +# Description: Install and manage a Chatwoot installation. # OS: Ubuntu 20.04 LTS -# Script Version: 1.0 +# Script Version: 2.0.7 # Run this script as root -set -eu -o pipefail -trap exit_handler EXIT +set -eu -o errexit -o pipefail -o noclobber -o nounset + +# -allow a command to fail with !’s side effect on errexit +# -use return value from ${PIPESTATUS[0]}, because ! hosed $? +! getopt --test > /dev/null +if [[ ${PIPESTATUS[0]} -ne 4 ]]; then + echo '`getopt --test` failed in this environment.' + exit 1 +fi + +# Global variables +# option --output/-o requires 1 argument +LONGOPTS=console,debug,help,install:,logs:,restart,ssl,upgrade,webserver,version +OPTIONS=cdhi:l:rsuwv +CWCTL_VERSION="2.0.7" pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '') - + +# if user does not specify an option +if [ -z "$1" ]; then + echo "No options specified. Use --help to learn more." + exit 1 +fi + +# -regarding ! and PIPESTATUS see above +# -temporarily store output to be able to check for errors +# -activate quoting/enhanced mode (e.g. by writing out “--options”) +# -pass arguments only via -- "$@" to separate them correctly +! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") +if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + # e.g. return value is 1 + # then getopt has complained about wrong arguments to stdout + exit 2 +fi +# read getopt’s output this way to handle the quoting right: +eval set -- "$PARSED" + +c=n d=n h=n i=n l=n r=n s=n u=n w=n v=n BRANCH=master SERVICE=web +# Iterate options in order and nicely split until we see -- +while true; do + case "$1" in + -c|--console) + c=y + break + ;; + -d|--debug) + d=y + break + ;; + -h|--help) + h=y + break + ;; + -i|--install) + i=y + BRANCH="$2" + break + ;; + -l|--logs) + l=y + SERVICE="$2" + break + ;; + -r|--restart) + r=y + break + ;; + -s|--ssl) + s=y + shift + ;; + -u|--upgrade) + u=y + break + ;; + -w|--webserver) + w=y + shift + ;; + -v|--version) + v=y + shift + ;; + --) + shift + break + ;; + *) + echo "Invalid option(s) specified. Use help(-h) to learn more." + exit 3 + ;; + esac +done + +# log if debug flag set +if [ "$d" == "y" ]; then + echo "console: $c, debug: $d, help: $h, install: $i, BRANCH: $BRANCH, \ + logs: $l, SERVICE: $SERVICE, ssl: $s, upgrade: $u, webserver: $w" +fi + +# exit if script is not run as root +if [ "$(id -u)" -ne 0 ]; then + echo 'This needs to be run as root.' >&2 + exit 1 +fi + +trap exit_handler EXIT + +############################################################################## +# Invoked upon EXIT signal from bash +# Upon non-zero exit, notifies the user to check log file. +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function exit_handler() { if [ "$?" -ne 0 ]; then echo -en "\nSome error has occured. Check '/var/log/chatwoot-setup.log' for details.\n" @@ -16,33 +129,41 @@ function exit_handler() { fi } -if [ "$(id -u)" -ne 0 ]; then - echo 'This script should be run as root.' >&2 - exit 1 -fi - -if [[ -z "$1" ]]; then - BRANCH="master" -else - BRANCH="$1" -fi - +############################################################################## +# Read user input related to domain setup +# Globals: +# domain_name +# le_email +# Arguments: +# None +# Outputs: +# None +############################################################################## function get_domain_info() { - read -rp 'Enter the domain/subdomain for Chatwoot (e.g., chatwoot.domain.com) :' domain_name - read -rp 'Enter an email address for LetsEncrypt to send reminders when your SSL certificate is up for renewal :' le_email + read -rp 'Enter the domain/subdomain for Chatwoot (e.g., chatwoot.domain.com): ' domain_name + read -rp 'Enter an email address for LetsEncrypt to send reminders when your SSL certificate is up for renewal: ' le_email cat << EOF -This script will generate SSL certificates via LetsEncrypt and serve Chatwoot at -https://$domain_name. Proceed further once you have pointed your DNS to the IP of the instance. +This script will generate SSL certificates via LetsEncrypt and +serve Chatwoot at https://$domain_name. +Proceed further once you have pointed your DNS to the IP of the instance. EOF read -rp 'Do you wish to proceed? (yes or no): ' exit_true - if [ "$exit_true" == "no" ] - then + if [ "$exit_true" == "no" ]; then exit 1 fi } +############################################################################## +# Install common dependencies +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function install_dependencies() { apt update && apt upgrade -y apt install -y curl @@ -60,16 +181,58 @@ function install_dependencies() { libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev sudo } +############################################################################## +# Install postgres and redis +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function install_databases() { apt install -y postgresql postgresql-contrib redis-server } +############################################################################## +# Install nginx and cerbot for LetsEncrypt +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function install_webserver() { apt install -y nginx nginx-full certbot python3-certbot-nginx } +############################################################################## +# Create chatwoot linux user +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## +function create_cw_user() { + if ! id -u "chatwoot"; then + adduser --disabled-login --gecos "" chatwoot + fi +} + +############################################################################## +# Install rvm(ruby version manager) +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function configure_rvm() { - adduser --disabled-login --gecos "" chatwoot + create_cw_user gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB @@ -77,28 +240,85 @@ function configure_rvm() { adduser chatwoot rvm } +############################################################################## +# Save the pgpass used to setup postgres +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## +function save_pgpass() { + mkdir -p /opt/chatwoot/config + file="/opt/chatwoot/config/.pg_pass" + if ! test -f "$file"; then + echo $pg_pass > /opt/chatwoot/config/.pg_pass + fi +} + +############################################################################## +# Get the pgpass used to setup postgres if installation fails midway +# and needs to be re-run +# Globals: +# pg_pass +# Arguments: +# None +# Outputs: +# None +############################################################################## +function get_pgpass() { + file="/opt/chatwoot/config/.pg_pass" + if test -f "$file"; then + pg_pass=$(cat $file) + fi + +} + +############################################################################## +# Configure postgres to create chatwoot db user. +# Enable postgres and redis systemd services. +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function configure_db() { - sudo -i -u postgres psql << EOF - \set pass `echo $pg_pass` - CREATE USER chatwoot CREATEDB; - ALTER USER chatwoot PASSWORD :'pass'; - ALTER ROLE chatwoot SUPERUSER; - UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1'; - DROP DATABASE template1; - CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE'; - UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1'; - \c template1 - VACUUM FREEZE; + save_pgpass + get_pgpass + sudo -i -u postgres psql << EOF + \set pass `echo $pg_pass` + CREATE USER chatwoot CREATEDB; + ALTER USER chatwoot PASSWORD :'pass'; + ALTER ROLE chatwoot SUPERUSER; + UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1'; + DROP DATABASE template1; + CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE'; + UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1'; + \c template1 + VACUUM FREEZE; EOF systemctl enable redis-server.service systemctl enable postgresql - } +############################################################################## +# Install Chatwoot +# This includes setting up ruby, cloning repo and installing dependencies. +# Globals: +# pg_pass +# Arguments: +# None +# Outputs: +# None +############################################################################## function setup_chatwoot() { - secret=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo '') - RAILS_ENV=production + local secret=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo '') + local RAILS_ENV=production + get_pgpass sudo -i -u chatwoot << EOF rvm --version @@ -123,34 +343,64 @@ function setup_chatwoot() { rake assets:precompile RAILS_ENV=production EOF - } - +############################################################################## +# Run database migrations. +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function run_db_migrations(){ sudo -i -u chatwoot << EOF cd chatwoot - RAILS_ENV=production bundle exec rake db:create - RAILS_ENV=production bundle exec rake db:reset + RAILS_ENV=production bundle exec rails db:chatwoot_prepare EOF - } - +############################################################################## +# Setup Chatwoot systemd services and cwctl CLI +# Globals: +# None +# Arguments: +# None +# Outputs: +# None +############################################################################## function configure_systemd_services() { cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target + cp /home/chatwoot/chatwoot/deployment/chatwoot /etc/sudoers.d/chatwoot + cp /home/chatwoot/chatwoot/deployment/setup_20.04.sh /usr/local/bin/cwctl + chmod +x /usr/local/bin/cwctl + systemctl enable chatwoot.target systemctl start chatwoot.target } - +############################################################################## +# Fetch and install SSL certificates from LetsEncrypt +# Modify the nginx config and restart nginx. +# Also modifies FRONTEND_URL in .env file. +# Globals: +# None +# Arguments: +# domain_name +# le_email +# Outputs: +# None +############################################################################## function setup_ssl() { - echo "debug: setting up ssl" - echo "debug: domain: $domain_name" - echo "debug: letsencrypt email: $le_email" + if [ "$d" == "y" ]; then + echo "debug: setting up ssl" + echo "debug: domain: $domain_name" + echo "debug: letsencrypt email: $le_email" + fi curl https://ssl-config.mozilla.org/ffdhe4096.txt >> /etc/ssl/dhparam wget https://raw.githubusercontent.com/chatwoot/chatwoot/develop/deployment/nginx_chatwoot.conf cp nginx_chatwoot.conf /etc/nginx/sites-available/nginx_chatwoot.conf @@ -165,75 +415,123 @@ EOF systemctl restart chatwoot.target } +############################################################################## +# Setup logging +# Globals: +# LOG_FILE +# Arguments: +# None +# Outputs: +# None +############################################################################## function setup_logging() { touch /var/log/chatwoot-setup.log LOG_FILE="/var/log/chatwoot-setup.log" } -function main() { +function ssl_success_message() { + cat << EOF - setup_logging +*************************************************************************** +Woot! Woot!! Chatwoot server installation is complete. +The server will be accessible at https://$domain_name + +Join the community at https://chatwoot.com/community +*************************************************************************** + +EOF +} + +function cwctl_message() { + echo $'\U0001F680 Try out the all new Chatwoot CLI tool to manage your installation.' + echo $'\U0001F680 Type "cwctl --help" to learn more.' +} + + +############################################################################## +# This function handles the installation(-i/--install) +# Globals: +# CW_VERSION +# Arguments: +# None +# Outputs: +# None +############################################################################## +function get_cw_version() { + CW_VERSION=$(curl -s https://app.chatwoot.com/api | python3 -c 'import sys,json;data=json.loads(sys.stdin.read()); print(data["version"])') +} + +############################################################################## +# This function handles the installation(-i/--install) +# Globals: +# configure_webserver +# install_pg_redis +# Arguments: +# None +# Outputs: +# None +############################################################################## +function install() { + get_cw_version cat << EOF *************************************************************************** - Chatwoot Installation (latest) + Chatwoot Installation (v$CW_VERSION) *************************************************************************** For more verbose logs, open up a second terminal and follow along using, -'tail -f /var/log/chatwoot'. +'tail -f /var/log/chatwoot-setup.log'. EOF sleep 3 read -rp 'Would you like to configure a domain and SSL for Chatwoot?(yes or no): ' configure_webserver - if [ "$configure_webserver" == "yes" ] - then + if [ "$configure_webserver" == "yes" ]; then get_domain_info fi echo -en "\n" read -rp 'Would you like to install Postgres and Redis? (Answer no if you plan to use external services): ' install_pg_redis - if [ "$install_pg_redis" == "no" ] - then - echo "***** Skipping Postgres and Redis installation. ****" - fi - echo -en "\n➥ 1/9 Installing dependencies. This takes a while.\n" install_dependencies &>> "${LOG_FILE}" - if [ "$install_pg_redis" != "no" ] - then - echo "➥ 2/9 Installing databases" + if [ "$install_pg_redis" != "no" ]; then + echo "➥ 2/9 Installing databases." install_databases &>> "${LOG_FILE}" + else + echo "➥ 2/9 Skipping Postgres and Redis installation." fi - if [ "$configure_webserver" == "yes" ] - then - echo "➥ 3/9 Installing webserver" + if [ "$configure_webserver" == "yes" ]; then + echo "➥ 3/9 Installing webserver." install_webserver &>> "${LOG_FILE}" + else + echo "➥ 3/9 Skipping webserver installation." fi echo "➥ 4/9 Setting up Ruby" configure_rvm &>> "${LOG_FILE}" - if [ "$install_pg_redis" != "no" ] - then - echo "➥ 5/9 Setting up the database" + if [ "$install_pg_redis" != "no" ]; then + echo "➥ 5/9 Setting up the database." configure_db &>> "${LOG_FILE}" + else + echo "➥ 5/9 Skipping database setup." fi - echo "➥ 6/9 Installing Chatwoot. This takes a while." + echo "➥ 6/9 Installing Chatwoot. This takes a long while." setup_chatwoot &>> "${LOG_FILE}" - if [ "$install_pg_redis" != "no" ] - then - echo "➥ 7/9 Running migrations" + if [ "$install_pg_redis" != "no" ]; then + echo "➥ 7/9 Running database migrations." run_db_migrations &>> "${LOG_FILE}" + else + echo "➥ 7/9 Skipping database migrations." fi - echo "➥ 8/9 Setting up systemd services" + echo "➥ 8/9 Setting up systemd services." configure_systemd_services &>> "${LOG_FILE}" public_ip=$(curl http://checkip.amazonaws.com -s) @@ -241,6 +539,7 @@ EOF if [ "$configure_webserver" != "yes" ] then cat << EOF +➥ 9/9 Skipping SSL/TLS setup. *************************************************************************** Woot! Woot!! Chatwoot server installation is complete. @@ -251,24 +550,20 @@ https://www.chatwoot.com/docs/deployment/deploy-chatwoot-in-linux-vm Join the community at https://chatwoot.com/community *************************************************************************** + EOF + cwctl_message else - echo "➥ 9/9 Setting up SSL/TLS" + echo "➥ 9/9 Setting up SSL/TLS." setup_ssl &>> "${LOG_FILE}" - cat << EOF - -*************************************************************************** -Woot! Woot!! Chatwoot server installation is complete. -The server will be accessible at https://$domain_name - -Join the community at https://chatwoot.com/community -*************************************************************************** -EOF + ssl_success_message + cwctl_message fi if [ "$install_pg_redis" == "no" ] then cat <