174 lines
5.9 KiB
Perl
174 lines
5.9 KiB
Perl
#!/usr/bin/env perl
|
|||
|
|
# Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
|
|
# SPDX-License-Identifier: MIT
|
||
|
|
#
|
||
|
|
# install.pl: install nuntius as a systemd service.
|
||
|
|
#
|
||
|
|
# nuntius ships as a single static binary. Build it on your workstation, upload
|
||
|
|
# the binary, the systemd unit, and the .env template to the server, then run
|
||
|
|
# this script there as root from that directory:
|
||
|
|
#
|
||
|
|
# scp bin/nuntius nuntius.service .env.example user@host:/tmp/nuntius/
|
||
|
|
# ssh user@host
|
||
|
|
# cd /tmp/nuntius && sudo perl install.pl
|
||
|
|
#
|
||
|
|
# It expects ./nuntius and ./nuntius.service (and optionally ./.env.example) in
|
||
|
|
# the current working directory. Idempotent: re-running is safe. It skips the
|
||
|
|
# user, config, and .env when they already exist, and never starts the service.
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use warnings;
|
||
|
|
|
||
|
|
my $nuntius_user = 'nuntius';
|
||
|
|
my $bin_dest = '/usr/local/bin/nuntius';
|
||
|
|
my $config_dir = '/etc/nuntius';
|
||
|
|
my $config_file = '/etc/nuntius/config.toml';
|
||
|
|
my $env_file = '/etc/nuntius/.env';
|
||
|
|
my $data_dir = '/var/lib/nuntius';
|
||
|
|
my $service_dest = '/etc/systemd/system/nuntius.service';
|
||
|
|
|
||
|
|
sub note { print qq{==> $_[0]\n} }
|
||
|
|
sub warn_ { print qq{WARN: $_[0]\n} }
|
||
|
|
sub fail { print qq{ERROR: $_[0]\n}; exit 1 }
|
||
|
|
|
||
|
|
# run executes one child in list form, so no argument is ever word-split or
|
||
|
|
# globbed, and dies naming the attempt when the child fails.
|
||
|
|
sub run {
|
||
|
|
my (@cmd) = @_;
|
||
|
|
system(@cmd) == 0
|
||
|
|
or die qq{ERROR: could not run '$cmd[0]': exit code } . ($? >> 8) . qq{\n};
|
||
|
|
}
|
||
|
|
|
||
|
|
# silenced runs a block with STDOUT and STDERR pointed at /dev/null, restored
|
||
|
|
# afterwards, so the caller sees only what it decides to print.
|
||
|
|
sub silenced(&) {
|
||
|
|
open(my $save_out, '>&', \*STDOUT) or die qq{ERROR: could not save STDOUT: $!\n};
|
||
|
|
open(my $save_err, '>&', \*STDERR) or die qq{ERROR: could not save STDERR: $!\n};
|
||
|
|
open(STDOUT, '>', '/dev/null') or die qq{ERROR: could not silence STDOUT: $!\n};
|
||
|
|
open(STDERR, '>&', \*STDOUT) or die qq{ERROR: could not silence STDERR: $!\n};
|
||
|
|
my $result = $_[0]->();
|
||
|
|
open(STDOUT, '>&', $save_out) or die qq{ERROR: could not restore STDOUT: $!\n};
|
||
|
|
open(STDERR, '>&', $save_err) or die qq{ERROR: could not restore STDERR: $!\n};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub require_root {
|
||
|
|
$> == 0 or fail('Run as root: sudo perl install.pl');
|
||
|
|
}
|
||
|
|
|
||
|
|
sub require_files {
|
||
|
|
-f './nuntius'
|
||
|
|
or fail('./nuntius binary not found: copy it here first (e.g. rsync bin/nuntius).');
|
||
|
|
-f './nuntius.service'
|
||
|
|
or fail('./nuntius.service not found: copy it from the repository root.');
|
||
|
|
}
|
||
|
|
|
||
|
|
sub create_user {
|
||
|
|
if (silenced { system('id', $nuntius_user) == 0 }) {
|
||
|
|
note("User $nuntius_user already exists.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
note("Creating system user $nuntius_user...");
|
||
|
|
run('useradd', '--system', '--shell', '/usr/sbin/nologin',
|
||
|
|
'--home-dir', $data_dir, '--user-group', $nuntius_user);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub create_data_dir {
|
||
|
|
note("Setting up $data_dir...");
|
||
|
|
unless (-d $data_dir) {
|
||
|
|
mkdir($data_dir, 0750) or die qq{ERROR: could not create $data_dir: $!\n};
|
||
|
|
}
|
||
|
|
my $uid = getpwnam($nuntius_user)
|
||
|
|
or die qq{ERROR: could not look up user $nuntius_user\n};
|
||
|
|
my $gid = getgrnam($nuntius_user)
|
||
|
|
or die qq{ERROR: could not look up group $nuntius_user\n};
|
||
|
|
chown($uid, $gid, $data_dir) or die qq{ERROR: could not chown $data_dir: $!\n};
|
||
|
|
chmod(0750, $data_dir) or die qq{ERROR: could not chmod $data_dir: $!\n};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub install_binary {
|
||
|
|
note("Installing binary to $bin_dest...");
|
||
|
|
run('install', '-m', '0755', './nuntius', $bin_dest);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub install_service {
|
||
|
|
note("Installing systemd unit $service_dest...");
|
||
|
|
run('install', '-m', '0644', './nuntius.service', $service_dest);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub generate_config {
|
||
|
|
if (-f $config_file) {
|
||
|
|
note("Config $config_file already exists; leaving it untouched.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
note("Generating $config_file...");
|
||
|
|
unless (-d $config_dir) {
|
||
|
|
mkdir($config_dir, 0755) or die qq{ERROR: could not create $config_dir: $!\n};
|
||
|
|
}
|
||
|
|
|
||
|
|
# nuntius writes the template config on first start; run it briefly, then
|
||
|
|
# let `timeout` send SIGTERM (graceful shutdown). The write happens at
|
||
|
|
# startup, before the timeout elapses. timeout exits 124 on SIGTERM and
|
||
|
|
# 137 on SIGKILL, so its status is deliberately not checked here.
|
||
|
|
silenced { system('timeout', '-k', '5s', '4s', $bin_dest) };
|
||
|
|
|
||
|
|
if (!-f $config_file) {
|
||
|
|
warn_("nuntius did not create $config_file; check the uploaded binary.");
|
||
|
|
}
|
||
|
|
|
||
|
|
# Fix ownership in case nuntius wrote files as root.
|
||
|
|
run('chown', '-R', "$nuntius_user:$nuntius_user", $data_dir);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub install_env {
|
||
|
|
if (-f $env_file) {
|
||
|
|
note("$env_file already exists; leaving it untouched.");
|
||
|
|
}
|
||
|
|
elsif (-f './.env.example') {
|
||
|
|
note("Creating $env_file from .env.example (edit it!)...");
|
||
|
|
run('install', '-m', '0600', './.env.example', $env_file);
|
||
|
|
run('chown', "root:$nuntius_user", $env_file);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
warn_(".env.example not found; skipping $env_file. Create it before starting.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sub enable_service {
|
||
|
|
note('Reloading systemd and enabling nuntius...');
|
||
|
|
run('systemctl', 'daemon-reload');
|
||
|
|
run('systemctl', 'enable', 'nuntius.service');
|
||
|
|
}
|
||
|
|
|
||
|
|
sub final_notes {
|
||
|
|
print <<"END_NOTES";
|
||
|
|
|
||
|
|
============================================================
|
||
|
|
nuntius installed and enabled, but NOT started.
|
||
|
|
|
||
|
|
Next manual steps:
|
||
|
|
1. Set the SMTP password:
|
||
|
|
sudo \$EDITOR $env_file
|
||
|
|
2. Edit the auto-generated config (smtp.host, smtp.user, allowed_origins):
|
||
|
|
sudo \$EDITOR $config_file
|
||
|
|
3. Start the service:
|
||
|
|
sudo systemctl start nuntius
|
||
|
|
sudo journalctl -u nuntius -f
|
||
|
|
4. Add the Caddy reverse_proxy directive (see README.md), then:
|
||
|
|
sudo caddy validate && sudo systemctl reload caddy
|
||
|
|
============================================================
|
||
|
|
END_NOTES
|
||
|
|
}
|
||
|
|
|
||
|
|
require_root();
|
||
|
|
require_files();
|
||
|
|
create_user();
|
||
|
|
create_data_dir();
|
||
|
|
install_binary();
|
||
|
|
install_service();
|
||
|
|
generate_config();
|
||
|
|
install_env();
|
||
|
|
enable_service();
|
||
|
|
final_notes();
|