Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ celerybeat.pid
.venv
env/
venv/

# Written by install.sh into the install directory
.instance-name
ENV/
env.bak/
venv.bak/
Expand Down
36 changes: 36 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
curl -fsSL https://raw.githubusercontent.com/smswithoutborders/RelaySMS-Publisher/main/install.sh | sudo bash
```

Installs to `/opt/relaysms/relaysms-publisher` by default. Pass `--install-dir PATH` for a different location, or run without it and you'll be prompted.

### Running Multiple Instances

To run a second, independent copy of Publisher on the same host, give it its own install directory and `--instance-name`:

```bash
sudo ./install.sh --install-dir /srv/relaysms-acme --instance-name acme
```

This namespaces the systemd units (`relaysms-publisher-acme.target`, `relaysms-publisher-acme-rest.service`, ...) so they don't collide with the default instance's, and `manage.sh` inside each install directory manages only its own instance. You still need to give each instance distinct `PORT`/`GRPC_PORT` values in its `.env`: two instances can't share a port on the same host, and `install.sh` doesn't assign this for you.

Re-running `install.sh` against an existing named instance doesn't require repeating `--instance-name`; it's remembered automatically. Passing a *different* `--instance-name` than the instance was set up with is rejected.

Run `install.sh --help` for the full flag list, including `--force-deps` to reinstall system dependencies even if already marked done.

## Manual Installation

### System Dependencies
Expand Down Expand Up @@ -253,6 +269,16 @@ sudo ./scripts/setup-postgres.sh --db-name relaysms --db-user relaysms

Add `--db-password PASS` to set a specific password instead of a generated one. Both scripts write the resulting `DATABASE_DIALECT` and connection details into `.env` for you; the sections below are for manual configuration instead (e.g. pointing at a database server on another host).

**Already have a database?** Add `--db-existing` (plus `--db-host`, `--db-port` if not local) to connect to it instead of installing/provisioning a new one:

```bash
sudo ./install.sh --setup-db postgres --db-existing \
--db-host db.example.com --db-port 5432 \
--db-name relaysms --db-user relaysms --db-password 'your-existing-password'
```

This only validates the connection and writes it to `.env` — it never creates, alters, or drops anything on that server. If the connection or credentials are wrong, the error message tells you to re-run without `--db-existing` to create a new local database instead. Run interactively (no `--setup-db`) and you'll be prompted for "existing" or "new" instead of needing the flags.

**SQLite (default):**

```bash
Expand Down Expand Up @@ -318,6 +344,16 @@ sudo ./scripts/setup-rabbitmq.sh --broker-vhost relaysms --broker-user relaysms

Add `--broker-password PASS` to set a specific password instead of a generated one. The script writes the resulting `CELERY_BROKER_TYPE` and `CELERY_RABBITMQ_URL` into `.env` for you; the block below is for manual configuration instead (e.g. pointing at a broker on another host).

**Already have a broker?** Add `--broker-existing` (plus `--broker-host` if not local) to connect to it instead:

```bash
sudo ./install.sh --setup-broker rabbitmq --broker-existing \
--broker-host mq.example.com \
--broker-vhost relaysms --broker-user relaysms --broker-password 'your-existing-password'
```

This validates the credentials against the broker's management API (default port `15672`, override with `--broker-mgmt-port`) rather than `rabbitmqctl`, since that only talks to a local node. It never creates or modifies anything on the broker. A failed check tells you to re-run without `--broker-existing` to create a new local broker instead.

```bash
# Broker/backend type: sqlite | redis | rabbitmq
CELERY_BROKER_TYPE=sqlite
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Add `--setup-observability` to also stand up self-hosted tracing/metrics/uptime

Run with no flags at all and the installer walks you through each of these choices interactively instead.

Add `--install-dir PATH` to install somewhere other than `/opt/relaysms/relaysms-publisher`, and `--instance-name NAME` to run a second, independent copy on the same host. See [Running Multiple Instances](INSTALL.md#running-multiple-instances).

Manage services:

```bash
Expand Down
1 change: 0 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

set -Ee

# Catches failures not already wrapped in error(), with line context.
on_err() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: aborted at line $1 (last command: $2)" >&2; }
trap 'on_err "$LINENO" "$BASH_COMMAND"' ERR

Expand Down
72 changes: 4 additions & 68 deletions gateway-clients.sh
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-only
#
# Wrapper around `python3 -m gateway_clients.cli` that removes the guesswork
# of running the gateway clients CLI correctly: it resolves the install
# directory, loads .env, runs as the correct service user (so file
# ownership never drifts), and uses the project venv automatically.

set -Eeuo pipefail

DEFAULT_INSTALL_DIR="/opt/relaysms/relaysms-publisher"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/scripts/lib.sh"

log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; }
error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
exit 1
}
# Catches failures not already wrapped in error(), with line context.
on_err() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: aborted at line $1 (last command: $2)" >&2; }
trap 'on_err "$LINENO" "$BASH_COMMAND"' ERR

# Resolve INSTALL_DIR: prefer the production install path if it exists and
# looks like a real install, otherwise fall back to this script's own
# directory (development checkout).
if [ -f "$DEFAULT_INSTALL_DIR/gateway_clients/cli.py" ]; then
INSTALL_DIR="$DEFAULT_INSTALL_DIR"
else
INSTALL_DIR="$SCRIPT_DIR"
fi
INSTALL_DIR="$SCRIPT_DIR"

[ -f "$INSTALL_DIR/gateway_clients/cli.py" ] ||
error "gateway_clients/cli.py not found under $INSTALL_DIR. Is RelaySMS Publisher installed there?"
Expand All @@ -39,20 +18,7 @@ VENV_DIR="$INSTALL_DIR/venv"
[ -x "$VENV_DIR/bin/python3" ] ||
error "Virtualenv not found at $VENV_DIR. Run install.sh or 'make build-setup' first."

# Resolve the service user: prefer the User= set in the installed systemd
# unit (source of truth after install.sh), fall back to the .env owner,
# then to whoever is running this script.
detect_service_user() {
local unit="/etc/systemd/system/relaysms-publisher-rest.service"
if [ -f "$unit" ]; then
grep -E "^User=" "$unit" | head -1 | cut -d= -f2 && return
fi
if [ -f "$ENV_FILE" ]; then
stat -c '%U' "$ENV_FILE" 2>/dev/null && return
fi
id -un
}

INSTANCE_NAME="$(read_instance_name)"
SERVICE_USER="$(detect_service_user)"
CURRENT_USER="$(id -un)"

Expand Down Expand Up @@ -100,44 +66,14 @@ Examples:
EOF
}

read_env_var() {
local key="$1"
grep -E "^${key}[[:space:]]*=" "$ENV_FILE" 2>/dev/null | tail -1 |
sed 's/^[^=]*=//;s/^[[:space:]]*//;s/[[:space:]]*$//'
}

cmd_env() {
echo "Install dir : $INSTALL_DIR"
echo "Env file : $ENV_FILE"
echo "Service user : $SERVICE_USER"
echo "Current user : $CURRENT_USER"
echo "Venv : $VENV_DIR"
echo
echo "GATEWAY_CLIENTS_REGISTRY_FILE = $(read_env_var GATEWAY_CLIENTS_REGISTRY_FILE)"
}

# Runs a command line as SERVICE_USER, in INSTALL_DIR, with .env loaded and
# the venv on PATH. Works whether this script is invoked as root, via sudo,
# or directly as the service user (no unnecessary sudo prompt in that case).
run_as_service_user() {
local inner_cmd="$1"
local run_cmd="
set -a
# shellcheck disable=SC1090
. '$ENV_FILE'
set +a
cd '$INSTALL_DIR'
export PATH=\"$VENV_DIR/bin:$PATH\"
$inner_cmd
"

if [ "$CURRENT_USER" = "$SERVICE_USER" ]; then
bash -c "$run_cmd"
elif [ "$EUID" -eq 0 ]; then
sudo -u "$SERVICE_USER" bash -c "$run_cmd"
else
error "Must run as '$SERVICE_USER' or with sudo (current user: $CURRENT_USER)."
fi
echo "GATEWAY_CLIENTS_REGISTRY_FILE = $(read_env_var GATEWAY_CLIENTS_REGISTRY_FILE "$ENV_FILE")"
}

main() {
Expand Down
6 changes: 5 additions & 1 deletion grpc_services/v3/exchange_oauth2_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ def ExchangeOAuth2CodeAndStore(self, request, context):
)

if pipe.get("error"):
logger.error(
"Adapter error for platform %r: %s", request.platform, pipe["error"]
)
return self.handle_create_grpc_error_response(
context,
response,
pipe["error"],
grpc.StatusCode.INVALID_ARGUMENT,
grpc.StatusCode.INTERNAL,
user_msg="Oops! Something went wrong. Please try again later.",
error_type="UNKNOWN",
)

Expand Down
6 changes: 5 additions & 1 deletion grpc_services/v3/exchange_pnba_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,15 @@ def ExchangePNBACodeAndStore(self, request, context):
)

if pipe.get("error"):
logger.error(
"Adapter error for platform %r: %s", request.platform, pipe["error"]
)
return self.handle_create_grpc_error_response(
context,
response,
pipe["error"],
grpc.StatusCode.INVALID_ARGUMENT,
grpc.StatusCode.INTERNAL,
user_msg="Oops! Something went wrong. Please try again later.",
error_type="UNKNOWN",
)

Expand Down
6 changes: 5 additions & 1 deletion grpc_services/v3/get_oauth2_auth_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ def GetOAuth2AuthorizationUrl(self, request, context):
)

if pipe.get("error"):
logger.error(
"Adapter error for platform %r: %s", request.platform, pipe["error"]
)
return self.handle_create_grpc_error_response(
context,
response,
pipe["error"],
grpc.StatusCode.INVALID_ARGUMENT,
grpc.StatusCode.INTERNAL,
user_msg="Oops! Something went wrong. Please try again later.",
error_type="UNKNOWN",
)

Expand Down
6 changes: 5 additions & 1 deletion grpc_services/v3/get_pnba_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ def GetPNBACode(self, request, context):
)

if pipe.get("error"):
logger.error(
"Adapter error for platform %r: %s", request.platform, pipe["error"]
)
return self.handle_create_grpc_error_response(
context,
response,
pipe["error"],
grpc.StatusCode.INVALID_ARGUMENT,
grpc.StatusCode.INTERNAL,
user_msg="Oops! Something went wrong. Please try again later.",
error_type="UNKNOWN",
)

Expand Down
Loading