feat: add worker install and deploy
Все проверки выполнены успешно
CI / test (push) Successful in 2m24s
Docker / Build and publish worker image (push) Successful in 13m24s
Все проверки выполнены успешно
CI / test (push) Successful in 2m24s
Docker / Build and publish worker image (push) Successful in 13m24s
Этот коммит содержится в:
@@ -1,4 +1,4 @@
|
||||
FROM reg.rsxx.ru/library/golang:1-trixie AS builder
|
||||
FROM golang:1-trixie AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
@@ -14,9 +14,9 @@ RUN CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildDate=${BUILD_DATE}" \
|
||||
-o /out/rsmon-worker ./cmd/rsmon-worker
|
||||
|
||||
FROM reg.rsxx.ru/library/debian:13-slim
|
||||
FROM debian:13-slim
|
||||
|
||||
RUN apt-get update \
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates chromium tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --gid 10001 rsmon-worker \
|
||||
|
||||
1
Makefile
1
Makefile
@@ -14,6 +14,7 @@ test:
|
||||
RSMON_ENV=test CWD=$(CURDIR) go test \
|
||||
./cmd/rsmon-worker \
|
||||
./internal/distworker \
|
||||
./internal/installer \
|
||||
./internal/webapp \
|
||||
./internal/workercluster \
|
||||
./internal/wire \
|
||||
|
||||
74
README.md
74
README.md
@@ -37,6 +37,9 @@ docker compose up -d
|
||||
docker compose logs -f worker
|
||||
```
|
||||
|
||||
Compose pulls `reg.rsxx.ru/rsmon/rsmon-worker:latest` by default. Set
|
||||
`RSMON_WORKER_IMAGE` to use another published tag.
|
||||
|
||||
The operator console is bound to `127.0.0.1:27401` by default. Set
|
||||
`WORKER_BIND_IP` only when a firewall or TLS reverse proxy protects the port.
|
||||
Persistent web and cluster state is stored in the `worker-data` volume.
|
||||
@@ -44,13 +47,13 @@ Persistent web and cluster state is stored in the `worker-data` volume.
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker build -t rsmon-worker:local .
|
||||
docker pull reg.rsxx.ru/rsmon/rsmon-worker:latest
|
||||
docker run --rm \
|
||||
--cap-add NET_RAW \
|
||||
--env-file .env \
|
||||
-p 127.0.0.1:27401:27401 \
|
||||
-v rsmon-worker-data:/var/lib/rsmon-worker \
|
||||
rsmon-worker:local
|
||||
reg.rsxx.ru/rsmon/rsmon-worker:latest
|
||||
```
|
||||
|
||||
Published images use these tags:
|
||||
@@ -73,16 +76,36 @@ sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates chromium libcap2-bin tzdata
|
||||
```
|
||||
|
||||
Build and install:
|
||||
Build and install with a token file so the token does not enter shell history or
|
||||
the process list:
|
||||
|
||||
```bash
|
||||
make build
|
||||
cp packaging/systemd/worker.env.example worker.env
|
||||
# Edit worker.env.
|
||||
sudo ./scripts/install-systemd.sh --env ./worker.env
|
||||
printf '%s\n' 'WORKER_TOKEN' > worker-token
|
||||
chmod 600 worker-token
|
||||
sudo ./bin/rsmon-worker install --token-file worker-token
|
||||
rm worker-token
|
||||
```
|
||||
|
||||
To install an already downloaded release binary:
|
||||
`install` copies the running binary to `/usr/local/bin/rsmon-worker`, writes the
|
||||
mode-0600 configuration at `/etc/rsmon-worker/worker.env`, installs a simple
|
||||
root-run systemd unit, and enables and starts it. Use `--url` to override
|
||||
`https://rsmon.ru`, `--binary` to install another binary, or `--no-start` to
|
||||
configure without starting.
|
||||
|
||||
The Docker alternative pulls the prebuilt image and installs a systemd unit
|
||||
that runs it:
|
||||
|
||||
```bash
|
||||
sudo ./bin/rsmon-worker install --docker --token-file worker-token
|
||||
```
|
||||
|
||||
The image defaults to `reg.rsxx.ru/rsmon/rsmon-worker:latest`; override it with
|
||||
`--image`.
|
||||
|
||||
The token can also be passed as `--token` or `--api-key`, but that can expose it
|
||||
through shell history and process inspection. The legacy repository-based
|
||||
installer remains available:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/install-systemd.sh --binary ./rsmon-worker --env ./worker.env
|
||||
@@ -96,9 +119,40 @@ journalctl -u rsmon-worker -f
|
||||
sudo systemctl restart rsmon-worker
|
||||
```
|
||||
|
||||
The service runs as the dedicated `rsmon-worker` user, stores state below
|
||||
`/var/lib/rsmon-worker`, reads secrets from `/etc/rsmon-worker/worker.env`, and
|
||||
has only `CAP_NET_RAW` for ICMP checks.
|
||||
The service runs as root, reads secrets from
|
||||
`/etc/rsmon-worker/worker.env`, and can execute ICMP checks without additional
|
||||
capability setup.
|
||||
|
||||
## SSH deployment
|
||||
|
||||
`deploy` uploads the selected worker binary and a temporary mode-0600
|
||||
configuration over SSH, then runs the binary's `install` command through root or
|
||||
`sudo`. The remote host needs Linux, systemd, `base64`, and either root SSH or
|
||||
sudo access.
|
||||
|
||||
```bash
|
||||
./bin/rsmon-worker deploy \
|
||||
--host worker.example.com \
|
||||
--user deploy \
|
||||
--identity-file ~/.ssh/id_ed25519 \
|
||||
--token-file worker-token
|
||||
```
|
||||
|
||||
Add `--docker` to install remotely by uploading only the configuration and
|
||||
systemd unit, then running `docker pull` on the target. This mode does not upload
|
||||
or execute the local worker binary, so the local and remote architectures may
|
||||
differ.
|
||||
|
||||
The default SSH port is 22 and the default RSMon URL is `https://rsmon.ru`.
|
||||
Encrypted keys use `--key-passphrase-file`; password authentication uses
|
||||
`--password-file`; password-protected sudo uses `--sudo-password-file`. Direct
|
||||
secret flags are supported for interactive convenience but file options are
|
||||
safer for automation.
|
||||
|
||||
SSH host keys are checked against `~/.ssh/known_hosts` by default. Use
|
||||
`--known-hosts PATH` or pin `--host-key-fingerprint SHA256:...`. The explicit
|
||||
`--insecure-host-key` option disables host authentication and should only be
|
||||
used in a trusted disposable environment.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ The checker looks for CA certificates in the following locations (in order):
|
||||
2. `/etc/ssl/cert.pem` - macOS system certificates
|
||||
3. `/etc/pki/tls/certs/ca-bundle.crt` - RHEL/CentOS system certificates
|
||||
4. `/usr/local/share/ca-certificates/` - Custom certificate directory
|
||||
5. `/data/rsmon/docker/cert-bundles/output/` - Project-specific certificate bundles
|
||||
|
||||
### Mozilla CA Bundle
|
||||
|
||||
@@ -38,7 +37,7 @@ The project includes the Mozilla CA certificate bundle which contains the same C
|
||||
|
||||
```bash
|
||||
# Downloaded from: https://curl.se/ca/cacert.pem
|
||||
# Location: docker/cert-bundles/output/mozilla-ca-bundle.crt
|
||||
# Install additional certificates in the operating system trust store.
|
||||
# Certificate count: 144 CA certificates
|
||||
```
|
||||
|
||||
@@ -125,12 +124,11 @@ To update the CA certificate bundles:
|
||||
|
||||
```bash
|
||||
# Download latest Mozilla CA bundle
|
||||
cd /data/rsmon
|
||||
curl -fsSL -o docker/cert-bundles/output/mozilla-ca-bundle.crt \
|
||||
curl -fsSL -o /usr/local/share/ca-certificates/mozilla-ca-bundle.crt \
|
||||
https://curl.se/ca/cacert.pem
|
||||
|
||||
# Verify
|
||||
grep -c "BEGIN CERTIFICATE" docker/cert-bundles/output/mozilla-ca-bundle.crt
|
||||
grep -c "BEGIN CERTIFICATE" /usr/local/share/ca-certificates/mozilla-ca-bundle.crt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -141,7 +139,7 @@ This error occurs when no CA certificates can be found. Solutions:
|
||||
|
||||
1. Ensure the system has `ca-certificates` package installed
|
||||
2. Place custom CA certificates in `/usr/local/share/ca-certificates/`
|
||||
3. Add certificates to the project bundle at `docker/cert-bundles/output/`
|
||||
3. Add certificates to the operating system trust store
|
||||
|
||||
### Certificate validation failures
|
||||
|
||||
|
||||
@@ -313,7 +313,6 @@ func LoadBrowserCARoots() (chrome, firefox *x509.CertPool, err error) {
|
||||
"/etc/ssl/cert.pem", // macOS
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS
|
||||
"/usr/local/share/ca-certificates/", // Custom certs
|
||||
"/data/rsmon/docker/cert-bundles/output/", // Our custom bundled certs
|
||||
}
|
||||
|
||||
for _, certPath := range certPaths {
|
||||
@@ -389,7 +388,6 @@ func loadFirefoxCARoots(pool *x509.CertPool) bool {
|
||||
paths := []string{
|
||||
"/usr/lib/x86_64-linux-gnu/libnssckbi.so", // Debian/Ubuntu NSS module
|
||||
"/usr/lib/libnssckbi.so", // Generic path
|
||||
"/data/rsmon/docker/cert-bundles/output/mozilla/", // Our bundled Mozilla certs
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
|
||||
@@ -50,6 +50,9 @@ var (
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
if handled, code := dispatchManagementCommand(os.Args[1:]); handled {
|
||||
os.Exit(code)
|
||||
}
|
||||
loadDotEnv()
|
||||
|
||||
versionFlag := flag.Bool("version", false, "Print version and exit")
|
||||
@@ -71,6 +74,10 @@ func main() {
|
||||
if len(flag.Args()) > 0 && flag.Arg(0) == "liveness" {
|
||||
os.Exit(livenessCheck())
|
||||
}
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n", flag.Arg(0))
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.Println("rsmon-worker starting...")
|
||||
|
||||
|
||||
@@ -3,9 +3,30 @@ package main
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDispatchManagementCommand(t *testing.T) {
|
||||
if handled, _ := dispatchManagementCommand(nil); handled {
|
||||
t.Fatal("empty arguments were handled")
|
||||
}
|
||||
if handled, code := dispatchManagementCommand([]string{"install", "--help"}); !handled || code != 0 {
|
||||
t.Fatalf("install help = handled %t code %d", handled, code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretValue(t *testing.T) {
|
||||
path := t.TempDir() + "/secret"
|
||||
if err := os.WriteFile(path, []byte("value\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := secretValue("", path)
|
||||
if err != nil || got != "value" {
|
||||
t.Fatalf("secretValue() = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeLiveness(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
145
cmd/rsmon-worker/management.go
Обычный файл
145
cmd/rsmon-worker/management.go
Обычный файл
@@ -0,0 +1,145 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"rocketgit.ru/rsmon/worker/internal/installer"
|
||||
)
|
||||
|
||||
func dispatchManagementCommand(args []string) (bool, int) {
|
||||
if len(args) == 0 {
|
||||
return false, 0
|
||||
}
|
||||
switch args[0] {
|
||||
case "install":
|
||||
return true, installCommand(args[1:])
|
||||
case "deploy":
|
||||
return true, deployCommand(args[1:])
|
||||
default:
|
||||
return false, 0
|
||||
}
|
||||
}
|
||||
|
||||
func installCommand(args []string) int {
|
||||
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
var opts installer.InstallOptions
|
||||
var tokenFile string
|
||||
fs.StringVar(&opts.Binary, "binary", "", "worker binary to install (default: this executable)")
|
||||
fs.StringVar(&opts.EnvFile, "env-file", "", "existing worker environment file")
|
||||
fs.StringVar(&opts.Token, "token", "", "worker API token")
|
||||
fs.StringVar(&opts.Token, "api-key", "", "worker API token (alias for --token)")
|
||||
fs.StringVar(&tokenFile, "token-file", "", "file containing the worker API token")
|
||||
fs.StringVar(&opts.URL, "url", installer.DefaultURL, "RSMon server URL")
|
||||
fs.BoolVar(&opts.Docker, "docker", false, "run the prebuilt Docker image instead of the binary")
|
||||
fs.StringVar(&opts.Image, "image", installer.DefaultImage, "Docker image used with --docker")
|
||||
fs.BoolVar(&opts.NoStart, "no-start", false, "install and enable without starting")
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintln(fs.Output(), "Usage: rsmon-worker install --token TOKEN [--url URL] [--no-start]")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
}
|
||||
var err error
|
||||
if opts.Token, err = secretValue(opts.Token, tokenFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "install failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if err := installer.Install(opts); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "install failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "rsmon-worker installed")
|
||||
return 0
|
||||
}
|
||||
|
||||
func deployCommand(args []string) int {
|
||||
fs := flag.NewFlagSet("deploy", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
var opts installer.DeployOptions
|
||||
var tokenFile, passwordFile, passphraseFile, sudoPasswordFile string
|
||||
fs.StringVar(&opts.Host, "host", "", "SSH server hostname or address")
|
||||
fs.IntVar(&opts.Port, "port", 22, "SSH server port")
|
||||
fs.StringVar(&opts.User, "user", "", "SSH username")
|
||||
fs.StringVar(&opts.IdentityFile, "identity-file", "", "SSH private key path")
|
||||
fs.StringVar(&opts.KeyPassphrase, "key-passphrase", "", "SSH private key passphrase")
|
||||
fs.StringVar(&passphraseFile, "key-passphrase-file", "", "file containing the private key passphrase")
|
||||
fs.StringVar(&opts.Password, "password", "", "SSH login password")
|
||||
fs.StringVar(&passwordFile, "password-file", "", "file containing the SSH login password")
|
||||
fs.StringVar(&opts.SudoPassword, "sudo-password", "", "remote sudo password")
|
||||
fs.StringVar(&sudoPasswordFile, "sudo-password-file", "", "file containing the remote sudo password")
|
||||
fs.StringVar(&opts.KnownHostsFile, "known-hosts", "", "known_hosts path (default: ~/.ssh/known_hosts)")
|
||||
fs.StringVar(&opts.HostKeyFingerprint, "host-key-fingerprint", "", "expected SHA256 SSH host-key fingerprint")
|
||||
fs.BoolVar(&opts.InsecureHostKey, "insecure-host-key", false, "disable SSH host-key verification (unsafe)")
|
||||
fs.StringVar(&opts.Binary, "binary", "", "worker binary to upload (default: this executable)")
|
||||
fs.StringVar(&opts.Token, "token", "", "worker API token")
|
||||
fs.StringVar(&opts.Token, "api-key", "", "worker API token (alias for --token)")
|
||||
fs.StringVar(&tokenFile, "token-file", "", "file containing the worker API token")
|
||||
fs.StringVar(&opts.URL, "url", installer.DefaultURL, "RSMon server URL")
|
||||
fs.BoolVar(&opts.Docker, "docker", false, "install the prebuilt Docker image remotely")
|
||||
fs.StringVar(&opts.Image, "image", installer.DefaultImage, "Docker image used with --docker")
|
||||
fs.BoolVar(&opts.NoStart, "no-start", false, "install and enable without starting")
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintln(fs.Output(), "Usage: rsmon-worker deploy --host HOST --user USER --token TOKEN [SSH options]")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
}
|
||||
var err error
|
||||
if opts.Token, err = secretValue(opts.Token, tokenFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if opts.Password, err = secretValue(opts.Password, passwordFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if opts.KeyPassphrase, err = secretValue(opts.KeyPassphrase, passphraseFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if opts.SudoPassword, err = secretValue(opts.SudoPassword, sudoPasswordFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if err := installer.Deploy(opts); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "deploy failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "rsmon-worker deployed")
|
||||
return 0
|
||||
}
|
||||
|
||||
func secretValue(direct, path string) (string, error) {
|
||||
if direct != "" && path != "" {
|
||||
return "", fmt.Errorf("a secret and its file option cannot both be set")
|
||||
}
|
||||
if path == "" {
|
||||
return direct, nil
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read secret file: %w", err)
|
||||
}
|
||||
return strings.TrimRight(string(b), "\r\n"), nil
|
||||
}
|
||||
@@ -113,8 +113,25 @@ type, control host access:
|
||||
- Compose mutation: separate high-risk capability, disabled by default;
|
||||
- Raft voter: durable fsync-capable cluster data directory and mTLS transport.
|
||||
|
||||
`CAP_NET_RAW` is granted only for ping/traceroute features. The worker remains
|
||||
unprivileged otherwise.
|
||||
The simple systemd installer currently runs the worker as root, matching the
|
||||
minimal host-install model. Docker runs with the image's unprivileged user and
|
||||
adds `NET_RAW` for ping/traceroute. A future hardened systemd profile can use a
|
||||
dedicated user and narrow capabilities when host inventory requirements are
|
||||
finalized.
|
||||
|
||||
The standalone binary provides two systemd installation paths:
|
||||
|
||||
- `rsmon-worker install` installs the current binary locally and writes the
|
||||
worker URL/token to a root-owned mode-0600 environment file; `--docker`
|
||||
installs a systemd-managed prebuilt image instead;
|
||||
- `rsmon-worker deploy` verifies an SSH host key, uploads the binary and a
|
||||
temporary mode-0600 environment file, and invokes `install` remotely;
|
||||
`--docker` uploads only the environment and unit, then pulls on the target.
|
||||
|
||||
Both default to `https://rsmon.ru` and accept `--token-file` for automation.
|
||||
Direct secret flags are supported but can be visible in process listings; file
|
||||
options are preferred. SSH login credentials, sudo credentials, and the worker
|
||||
token remain separate.
|
||||
|
||||
## Worker Self-Monitoring
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@@ -413,6 +413,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
260
internal/installer/deploy.go
Обычный файл
260
internal/installer/deploy.go
Обычный файл
@@ -0,0 +1,260 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
type DeployOptions struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
IdentityFile string
|
||||
KeyPassphrase string
|
||||
Password string
|
||||
SudoPassword string
|
||||
KnownHostsFile string
|
||||
HostKeyFingerprint string
|
||||
InsecureHostKey bool
|
||||
Binary string
|
||||
Token string
|
||||
URL string
|
||||
Docker bool
|
||||
Image string
|
||||
NoStart bool
|
||||
}
|
||||
|
||||
func Deploy(opts DeployOptions) error {
|
||||
if opts.Host == "" || opts.User == "" {
|
||||
return errors.New("--host and --user are required")
|
||||
}
|
||||
if opts.Port == 0 {
|
||||
opts.Port = 22
|
||||
}
|
||||
if opts.Port < 1 || opts.Port > 65535 {
|
||||
return errors.New("SSH port must be between 1 and 65535")
|
||||
}
|
||||
if err := ValidateToken(opts.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.URL == "" {
|
||||
opts.URL = DefaultURL
|
||||
}
|
||||
if opts.Image == "" {
|
||||
opts.Image = DefaultImage
|
||||
}
|
||||
if opts.Docker {
|
||||
if err := ValidateImage(opts.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := ValidateURL(opts.URL); err != nil {
|
||||
return err
|
||||
}
|
||||
if !opts.Docker && opts.Binary == "" {
|
||||
var err error
|
||||
opts.Binary, err = os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
auth, err := sshAuth(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostKey, err := hostKeyCallback(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := ssh.Dial("tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port)), &ssh.ClientConfig{
|
||||
User: opts.User,
|
||||
Auth: auth,
|
||||
HostKeyCallback: hostKey,
|
||||
Timeout: 15 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to %s: %w", opts.Host, err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
suffix := strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
remoteBinary := "/tmp/rsmon-worker-" + suffix
|
||||
remoteEnv := remoteBinary + ".env"
|
||||
remoteUnit := remoteBinary + ".service"
|
||||
if err := uploadBytes(client, Environment(opts.URL, opts.Token), remoteEnv, 0600); err != nil {
|
||||
return fmt.Errorf("upload configuration: %w", err)
|
||||
}
|
||||
defer runRemote(client, "rm -f -- "+shellQuote(remoteBinary)+" "+shellQuote(remoteEnv)+" "+shellQuote(remoteUnit), nil) //nolint:errcheck
|
||||
if opts.Docker {
|
||||
if err := uploadBytes(client, []byte(DockerUnit(opts.Image)), remoteUnit, 0600); err != nil {
|
||||
return fmt.Errorf("upload systemd unit: %w", err)
|
||||
}
|
||||
return deployDocker(client, opts, remoteEnv, remoteUnit)
|
||||
}
|
||||
if err := upload(client, opts.Binary, remoteBinary, 0700); err != nil {
|
||||
return fmt.Errorf("upload worker: %w", err)
|
||||
}
|
||||
args := shellQuote(remoteBinary) + " install --binary " + shellQuote(remoteBinary) + " --env-file " + shellQuote(remoteEnv)
|
||||
if opts.NoStart {
|
||||
args += " --no-start"
|
||||
}
|
||||
var command string
|
||||
var stdin []byte
|
||||
if opts.User == "root" {
|
||||
command = args
|
||||
} else if opts.SudoPassword != "" {
|
||||
command = "sudo -S -p '' -- " + args
|
||||
stdin = []byte(opts.SudoPassword + "\n")
|
||||
} else {
|
||||
command = "sudo -n -- " + args
|
||||
}
|
||||
if err := runRemote(client, command, stdin); err != nil {
|
||||
return fmt.Errorf("remote install: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deployDocker(client *ssh.Client, opts DeployOptions, remoteEnv, remoteUnit string) error {
|
||||
script := "install -d -m 0755 /etc/rsmon-worker" +
|
||||
" && install -m 0600 " + shellQuote(remoteEnv) + " /etc/rsmon-worker/worker.env" +
|
||||
" && install -m 0644 " + shellQuote(remoteUnit) + " /etc/systemd/system/rsmon-worker.service" +
|
||||
" && docker pull " + shellQuote(opts.Image) +
|
||||
" && systemctl daemon-reload" +
|
||||
" && systemctl enable rsmon-worker.service"
|
||||
if !opts.NoStart {
|
||||
script += " && systemctl restart rsmon-worker.service && systemctl is-active --quiet rsmon-worker.service"
|
||||
}
|
||||
command := "sh -c " + shellQuote(script)
|
||||
var stdin []byte
|
||||
if opts.User != "root" {
|
||||
if opts.SudoPassword != "" {
|
||||
command = "sudo -S -p '' -- " + command
|
||||
stdin = []byte(opts.SudoPassword + "\n")
|
||||
} else {
|
||||
command = "sudo -n -- " + command
|
||||
}
|
||||
}
|
||||
if err := runRemote(client, command, stdin); err != nil {
|
||||
return fmt.Errorf("remote Docker install: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshAuth(opts DeployOptions) ([]ssh.AuthMethod, error) {
|
||||
var methods []ssh.AuthMethod
|
||||
if opts.IdentityFile != "" {
|
||||
key, err := os.ReadFile(opts.IdentityFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read identity file: %w", err)
|
||||
}
|
||||
var signer ssh.Signer
|
||||
if opts.KeyPassphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(opts.KeyPassphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey(key)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse identity file: %w", err)
|
||||
}
|
||||
methods = append(methods, ssh.PublicKeys(signer))
|
||||
}
|
||||
if opts.Password != "" {
|
||||
methods = append(methods, ssh.Password(opts.Password))
|
||||
}
|
||||
if len(methods) == 0 {
|
||||
return nil, errors.New("provide --identity-file or --password")
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func hostKeyCallback(opts DeployOptions) (ssh.HostKeyCallback, error) {
|
||||
if opts.HostKeyFingerprint != "" {
|
||||
want := opts.HostKeyFingerprint
|
||||
return func(_ string, _ net.Addr, key ssh.PublicKey) error {
|
||||
if got := ssh.FingerprintSHA256(key); got != want {
|
||||
return fmt.Errorf("host key fingerprint mismatch: got %s", got)
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
if opts.InsecureHostKey {
|
||||
return ssh.InsecureIgnoreHostKey(), nil //nolint:gosec // explicit operator opt-in
|
||||
}
|
||||
path := opts.KnownHostsFile
|
||||
if path == "" {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path = filepath.Join(u.HomeDir, ".ssh", "known_hosts")
|
||||
}
|
||||
return knownhosts.New(path)
|
||||
}
|
||||
|
||||
func upload(client *ssh.Client, localPath, remotePath string, mode os.FileMode) error {
|
||||
f, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return uploadReader(client, f, remotePath, mode)
|
||||
}
|
||||
|
||||
func uploadBytes(client *ssh.Client, data []byte, remotePath string, mode os.FileMode) error {
|
||||
return uploadReader(client, bytes.NewReader(data), remotePath, mode)
|
||||
}
|
||||
|
||||
func uploadReader(client *ssh.Client, src io.Reader, remotePath string, mode os.FileMode) error {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
command := "umask 077; base64 -d > " + shellQuote(remotePath) + " && chmod " + fmt.Sprintf("%#o", mode.Perm()) + " " + shellQuote(remotePath)
|
||||
if err := session.Start(command); err != nil {
|
||||
return err
|
||||
}
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, stdin)
|
||||
_, copyErr := io.Copy(encoder, src)
|
||||
closeErr := encoder.Close()
|
||||
pipeErr := stdin.Close()
|
||||
waitErr := session.Wait()
|
||||
return errors.Join(copyErr, closeErr, pipeErr, waitErr)
|
||||
}
|
||||
|
||||
func runRemote(client *ssh.Client, command string, stdin []byte) error {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
if stdin != nil {
|
||||
session.Stdin = bytes.NewReader(stdin)
|
||||
}
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
return session.Run(command)
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
||||
}
|
||||
41
internal/installer/deploy_test.go
Обычный файл
41
internal/installer/deploy_test.go
Обычный файл
@@ -0,0 +1,41 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestFingerprintHostKeyCallback(t *testing.T) {
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKey, err := ssh.NewPublicKey(privateKey.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
callback, err := hostKeyCallback(DeployOptions{HostKeyFingerprint: ssh.FingerprintSHA256(publicKey)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := callback("host", &net.TCPAddr{}, publicKey); err != nil {
|
||||
t.Fatalf("matching fingerprint rejected: %v", err)
|
||||
}
|
||||
callback, err = hostKeyCallback(DeployOptions{HostKeyFingerprint: "SHA256:wrong"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := callback("host", &net.TCPAddr{}, publicKey); err == nil {
|
||||
t.Fatal("mismatched fingerprint accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnownHostsMissingFile(t *testing.T) {
|
||||
if _, err := hostKeyCallback(DeployOptions{KnownHostsFile: t.TempDir() + "/missing"}); err == nil {
|
||||
t.Fatal("missing known_hosts file accepted")
|
||||
}
|
||||
}
|
||||
225
internal/installer/install.go
Обычный файл
225
internal/installer/install.go
Обычный файл
@@ -0,0 +1,225 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dockerImagePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/:@-]*$`)
|
||||
|
||||
const (
|
||||
DefaultURL = "https://rsmon.ru"
|
||||
DefaultImage = "reg.rsxx.ru/rsmon/rsmon-worker:latest"
|
||||
binaryPath = "/usr/local/bin/rsmon-worker"
|
||||
envPath = "/etc/rsmon-worker/worker.env"
|
||||
unitPath = "/etc/systemd/system/rsmon-worker.service"
|
||||
)
|
||||
|
||||
const systemdUnit = `[Unit]
|
||||
Description=RSMon distributed monitoring worker
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
EnvironmentFile=/etc/rsmon-worker/worker.env
|
||||
ExecStart=/usr/local/bin/rsmon-worker
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
const dockerSystemdUnit = `[Unit]
|
||||
Description=RSMon distributed monitoring worker (Docker)
|
||||
After=network-online.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStartPre=-docker rm -f rsmon-worker
|
||||
ExecStart=docker run --rm --name rsmon-worker --network host --cap-add NET_RAW --env-file /etc/rsmon-worker/worker.env -v rsmon-worker-data:/var/lib/rsmon-worker %s
|
||||
ExecStop=docker stop rsmon-worker
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
type InstallOptions struct {
|
||||
Binary string
|
||||
EnvFile string
|
||||
Token string
|
||||
URL string
|
||||
Docker bool
|
||||
Image string
|
||||
NoStart bool
|
||||
}
|
||||
|
||||
func Install(opts InstallOptions) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("install must be run as root")
|
||||
}
|
||||
if opts.URL == "" {
|
||||
opts.URL = DefaultURL
|
||||
}
|
||||
if err := ValidateURL(opts.URL); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.EnvFile == "" {
|
||||
if err := ValidateToken(opts.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.Image == "" {
|
||||
opts.Image = DefaultImage
|
||||
}
|
||||
if opts.Docker {
|
||||
if err := ValidateImage(opts.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if opts.Binary == "" {
|
||||
var err error
|
||||
opts.Binary, err = os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate worker executable: %w", err)
|
||||
}
|
||||
}
|
||||
unit := systemdUnit
|
||||
if opts.Docker {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
return errors.New("docker is required for --docker installation")
|
||||
}
|
||||
if err := command("docker", "pull", opts.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
unit = DockerUnit(opts.Image)
|
||||
} else if err := copyAtomic(opts.Binary, binaryPath, 0755); err != nil {
|
||||
return fmt.Errorf("install binary: %w", err)
|
||||
}
|
||||
if err := writeAtomic(unitPath, []byte(unit), 0644); err != nil {
|
||||
return fmt.Errorf("install systemd unit: %w", err)
|
||||
}
|
||||
if opts.EnvFile != "" {
|
||||
if err := copyAtomic(opts.EnvFile, envPath, 0600); err != nil {
|
||||
return fmt.Errorf("install environment: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := writeAtomic(envPath, Environment(opts.URL, opts.Token), 0600); err != nil {
|
||||
return fmt.Errorf("install environment: %w", err)
|
||||
}
|
||||
}
|
||||
if err := command("systemctl", "daemon-reload"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := command("systemctl", "enable", "rsmon-worker.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !opts.NoStart {
|
||||
if err := command("systemctl", "restart", "rsmon-worker.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := command("systemctl", "is-active", "--quiet", "rsmon-worker.service"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateURL(raw string) error {
|
||||
if strings.ContainsAny(raw, "\r\n") {
|
||||
return fmt.Errorf("server URL must be an absolute HTTP(S) URL")
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") {
|
||||
return fmt.Errorf("server URL must be an absolute HTTP(S) URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Environment(serverURL, token string) []byte {
|
||||
return []byte("RSMON_URL=" + serverURL + "\nRSMON_TOKEN=" + token + "\nWORKER_HOST=127.0.0.1\n")
|
||||
}
|
||||
|
||||
func ValidateToken(token string) error {
|
||||
if strings.TrimSpace(token) == "" || strings.ContainsAny(token, "\r\n") {
|
||||
return errors.New("worker token must be non-empty and contain no newlines")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateImage(image string) error {
|
||||
if !dockerImagePattern.MatchString(image) {
|
||||
return errors.New("Docker image must be one non-option argument")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DockerUnit(image string) string {
|
||||
return fmt.Sprintf(dockerSystemdUnit, image)
|
||||
}
|
||||
|
||||
func copyAtomic(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
return atomicFile(dst, mode, func(out *os.File) error {
|
||||
_, err := io.Copy(out, in)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func writeAtomic(dst string, data []byte, mode os.FileMode) error {
|
||||
return atomicFile(dst, mode, func(out *os.File) error {
|
||||
_, err := out.Write(data)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func atomicFile(dst string, mode os.FileMode, write func(*os.File) error) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(dst), ".rsmon-worker-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := tmp.Name()
|
||||
defer os.Remove(name)
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := write(tmp); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, dst)
|
||||
}
|
||||
|
||||
func command(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s failed: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
internal/installer/install_test.go
Обычный файл
68
internal/installer/install_test.go
Обычный файл
@@ -0,0 +1,68 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateURL(t *testing.T) {
|
||||
for _, raw := range []string{"https://rsmon.ru", "http://localhost:7401"} {
|
||||
if err := ValidateURL(raw); err != nil {
|
||||
t.Fatalf("ValidateURL(%q): %v", raw, err)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{"", "rsmon.ru", "file:///tmp/x"} {
|
||||
if err := ValidateURL(raw); err == nil {
|
||||
t.Fatalf("ValidateURL(%q) succeeded", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateToken(t *testing.T) {
|
||||
if err := ValidateToken("token"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, token := range []string{"", " ", "token\nRSMON_URL=https://evil.test"} {
|
||||
if err := ValidateToken(token); err == nil {
|
||||
t.Fatalf("ValidateToken(%q) succeeded", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
got := string(Environment("https://example.test", "secret"))
|
||||
for _, want := range []string{"RSMON_URL=https://example.test\n", "RSMON_TOKEN=secret\n", "WORKER_HOST=127.0.0.1\n"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("environment missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellQuote(t *testing.T) {
|
||||
if got, want := shellQuote("a'b"), `'a'\''b'`; got != want {
|
||||
t.Fatalf("shellQuote() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateImage(t *testing.T) {
|
||||
if err := ValidateImage(DefaultImage); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, image := range []string{"", "-bad", "image name", "image%stest", `image"test`} {
|
||||
if err := ValidateImage(image); err == nil {
|
||||
t.Fatalf("ValidateImage(%q) succeeded", image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemdUnits(t *testing.T) {
|
||||
if !strings.Contains(systemdUnit, "Type=simple\nUser=root\n") || !strings.Contains(systemdUnit, "ExecStart=/usr/local/bin/rsmon-worker\n") {
|
||||
t.Fatal("binary systemd unit is not the simple root service")
|
||||
}
|
||||
unit := DockerUnit(DefaultImage)
|
||||
for _, want := range []string{"ExecStartPre=-docker rm -f rsmon-worker", "docker run --rm", DefaultImage} {
|
||||
if !strings.Contains(unit, want) {
|
||||
t.Fatalf("Docker systemd unit missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,13 @@
|
||||
[Unit]
|
||||
Description=RSMon distributed monitoring worker
|
||||
Documentation=https://rocketgit.ru/rsmon/worker
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=rsmon-worker
|
||||
Group=rsmon-worker
|
||||
Environment=HOME=/var/lib/rsmon-worker
|
||||
Environment=RSMON_WEBAPP_DATA_DIR=/var/lib/rsmon-worker/webapp
|
||||
User=root
|
||||
EnvironmentFile=/etc/rsmon-worker/worker.env
|
||||
WorkingDirectory=/var/lib/rsmon-worker
|
||||
ExecStart=/usr/local/bin/rsmon-worker
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
TimeoutStopSec=20s
|
||||
AmbientCapabilities=CAP_NET_RAW
|
||||
CapabilityBoundingSet=CAP_NET_RAW
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/var/lib/rsmon-worker
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -11,8 +11,7 @@ usage() {
|
||||
Usage: sudo ./scripts/install-systemd.sh [--binary PATH] [--env PATH] [--no-start]
|
||||
|
||||
Installs rsmon-worker as /usr/local/bin/rsmon-worker and configures systemd.
|
||||
If --env is omitted on a first install, an example file is installed and the
|
||||
service is not started until its token and password are configured.
|
||||
--env is required and must contain RSMON_URL and RSMON_TOKEN.
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -56,44 +55,13 @@ if [ ! -x "$BINARY" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v chromium >/dev/null 2>&1; then
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates chromium libcap2-bin tzdata
|
||||
else
|
||||
printf 'Warning: Chromium is not installed; browser-backed HTTP checks will fail.\n' >&2
|
||||
fi
|
||||
if [ -z "$ENV_FILE" ]; then
|
||||
printf '%s\n' '--env is required; use a mode-0600 file containing RSMON_URL and RSMON_TOKEN.' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! getent group rsmon-worker >/dev/null; then
|
||||
groupadd --system rsmon-worker
|
||||
fi
|
||||
if ! id rsmon-worker >/dev/null 2>&1; then
|
||||
useradd --system --gid rsmon-worker --home-dir /var/lib/rsmon-worker --create-home --shell /usr/sbin/nologin rsmon-worker
|
||||
fi
|
||||
|
||||
install -d -m 0755 /etc/rsmon-worker
|
||||
install -d -o rsmon-worker -g rsmon-worker -m 0750 /var/lib/rsmon-worker /var/lib/rsmon-worker/webapp /var/lib/rsmon-worker/cluster
|
||||
install -m 0755 "$BINARY" /usr/local/bin/rsmon-worker
|
||||
install -m 0644 "$ROOT_DIR/packaging/systemd/rsmon-worker.service" /etc/systemd/system/rsmon-worker.service
|
||||
|
||||
if [ -n "$ENV_FILE" ]; then
|
||||
install -m 0600 "$ENV_FILE" /etc/rsmon-worker/worker.env
|
||||
elif [ ! -f /etc/rsmon-worker/worker.env ]; then
|
||||
install -m 0600 "$ROOT_DIR/packaging/systemd/worker.env.example" /etc/rsmon-worker/worker.env
|
||||
START_SERVICE=0
|
||||
printf 'Installed /etc/rsmon-worker/worker.env; configure it before starting the service.\n'
|
||||
fi
|
||||
|
||||
if command -v setcap >/dev/null 2>&1; then
|
||||
setcap cap_net_raw=+ep /usr/local/bin/rsmon-worker || true
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable rsmon-worker.service
|
||||
if [ "$START_SERVICE" -eq 1 ]; then
|
||||
systemctl restart rsmon-worker.service
|
||||
systemctl --no-pager --full status rsmon-worker.service
|
||||
else
|
||||
printf 'Start after configuration with: systemctl start rsmon-worker\n'
|
||||
ARGS=(install --binary "$BINARY" --env-file "$ENV_FILE")
|
||||
if [ "$START_SERVICE" -eq 0 ]; then
|
||||
ARGS+=(--no-start)
|
||||
fi
|
||||
exec "$BINARY" "${ARGS[@]}"
|
||||
|
||||
Ссылка в новой задаче
Block a user