Compare commits
163 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f467c20c | ||
|
|
a8084981b4 | ||
|
|
3a80e1067a | ||
|
|
3149b15cef | ||
|
|
6fd758bb1f | ||
|
|
98bd0d7d23 | ||
|
|
348bf39517 | ||
|
|
d955bbff0c | ||
|
|
f5eaf58e9b | ||
|
|
79707c9395 | ||
|
|
853aad8a36 | ||
|
|
c17a44f077 | ||
|
|
448a05856a | ||
|
|
6f1037950a | ||
|
|
85f6d42920 | ||
|
|
8bbf2ccc25 | ||
|
|
c2bfffa668 | ||
|
|
678fa83f31 | ||
|
|
9c6ddd2da1 | ||
|
|
202f0e2dc8 | ||
|
|
a2650e5c36 | ||
|
|
30fb7b2010 | ||
|
|
f9c2771984 | ||
|
|
c4f2c8fadb | ||
|
|
643a8fbe23 | ||
|
|
f72ea3f348 | ||
|
|
3d8b85d918 | ||
|
|
9d10516a63 | ||
|
|
6a79d6b5b6 | ||
|
|
30d247c411 | ||
|
|
0171438505 | ||
|
|
30c24da698 | ||
|
|
f3c82258ac | ||
|
|
d8b40f4999 | ||
|
|
f394d5fa97 | ||
|
|
1d4b6c9673 | ||
|
|
b981b82c5d | ||
|
|
c6e163a767 | ||
|
|
95e66f21c8 | ||
|
|
1748371b34 | ||
|
|
aba98108b0 | ||
|
|
d6fdbbe8db | ||
|
|
84612e4c7e | ||
|
|
d1c3f69f99 | ||
|
|
676202f625 | ||
|
|
d7a1d790be | ||
|
|
e9e9b2976e | ||
|
|
ebde840b28 | ||
|
|
3ba8e6e6c2 | ||
|
|
128e59c0e4 | ||
|
|
587aa70554 | ||
|
|
e25ea0560b | ||
|
|
fd641905e0 | ||
|
|
d75a9a34c9 | ||
|
|
c747e9da63 | ||
|
|
156c81f230 | ||
|
|
3037f5f8ef | ||
|
|
a906b21b99 | ||
|
|
b3cf1c8250 | ||
|
|
350434e690 | ||
|
|
10fa528e77 | ||
|
|
b8b46daf00 | ||
|
|
d277d976d0 | ||
|
|
06c1ecfe85 | ||
|
|
c8fd8bc37e | ||
|
|
1974fd0293 | ||
|
|
d28d77661d | ||
|
|
112bffb742 | ||
|
|
8ea81cdb17 | ||
|
|
944df2d394 | ||
|
|
2049bcf617 | ||
|
|
a2a658caaa | ||
|
|
278d82441d | ||
|
|
31013b3e82 | ||
|
|
076b801eda | ||
|
|
03f56964b9 | ||
|
|
43dfc6ec06 | ||
|
|
31cbb56629 | ||
|
|
e62efa5534 | ||
|
|
0e781b157e | ||
|
|
1924d5b1df | ||
|
|
8f9c0ac10c | ||
|
|
216b24b9ac | ||
|
|
c49ee64b0b | ||
|
|
11dade0c5e | ||
|
|
1c2dbb9540 | ||
|
|
3d076221e2 | ||
|
|
e5e1697402 | ||
|
|
c96fb88fbd | ||
|
|
221b1968b0 | ||
|
|
86a172a4ed | ||
|
|
f4e931bbd4 | ||
|
|
6a9337553a | ||
|
|
3884f094fd | ||
|
|
aeb8be3ebc | ||
|
|
6c8b9dc166 | ||
|
|
56468d3366 | ||
|
|
50ba28c2f9 | ||
|
|
6f5c281089 | ||
|
|
08653eca9d | ||
|
|
310ee0a8a4 | ||
|
|
8b49978ea1 | ||
|
|
a6359beb61 | ||
|
|
938aa168b3 | ||
|
|
7f7bff15ca | ||
|
|
e4b1021c52 | ||
|
|
da56eaf164 | ||
|
|
842024a6ea | ||
|
|
452976ab43 | ||
|
|
d5252b3be0 | ||
|
|
ddf0dd9884 | ||
|
|
bf3ba41179 | ||
|
|
b01f5d6a52 | ||
|
|
e9dff30d84 | ||
|
|
cbe6d51ac6 | ||
|
|
57e0c8798a | ||
|
|
f7689b4d6a | ||
|
|
8484ca46a7 | ||
|
|
0922102226 | ||
|
|
bc66018af3 | ||
|
|
4432855a2d | ||
|
|
8638f198ff | ||
|
|
d405c98ff8 | ||
|
|
9cf6a2a66b | ||
|
|
bb45f8606d | ||
|
|
a1d71b3eac | ||
|
|
c38535398d | ||
|
|
91b8b0e980 | ||
|
|
fd3a005fe8 | ||
|
|
02e9fd2793 | ||
|
|
83f73c0e9c | ||
|
|
b44da10ae7 | ||
|
|
02f3368fa6 | ||
|
|
af8df1a659 | ||
|
|
7875dbca13 | ||
|
|
d3db8ce4c0 | ||
|
|
5a0cfe92e1 | ||
|
|
b86d9d3604 | ||
|
|
d8390eab2a | ||
|
|
0a301fbd05 | ||
|
|
0ac2fc82cf | ||
|
|
f299f7f895 | ||
|
|
82a28c6101 | ||
|
|
2e6eca019d | ||
|
|
0ce12f4742 | ||
|
|
5923f684ec | ||
|
|
157373ea16 | ||
|
|
c1e1be2ab2 | ||
|
|
5dc449b7af | ||
|
|
78e316af64 | ||
|
|
e2c0f5af08 | ||
|
|
f5919b4c85 | ||
|
|
b1c596ccf2 | ||
|
|
d0ad1c5243 | ||
|
|
ed9655ee79 | ||
|
|
e378c2a436 | ||
|
|
f5ddb145cb | ||
|
|
a6e12d54cc | ||
|
|
91add96b7d | ||
|
|
56a22756f8 | ||
|
|
8b610b1bdb | ||
|
|
c169887fed | ||
|
|
58faccbb27 |
36 changed files with 1212 additions and 5949 deletions
15
.env
15
.env
|
|
@ -1,15 +0,0 @@
|
|||
# Wallarm Preflight Check Results
|
||||
# Generated: 2026-04-21 15:47:43
|
||||
# Script: ./wallarm-ct-check.sh
|
||||
|
||||
result=pass
|
||||
os_name=darwin
|
||||
os_version=25.3.0
|
||||
architecture=x86_64
|
||||
init_system=darwin
|
||||
us_cloud_reachable=true
|
||||
eu_cloud_reachable=true
|
||||
registry_reachable=false
|
||||
download_reachable=false
|
||||
git_reachable=true
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -11,3 +11,6 @@ notes/
|
|||
wallarm
|
||||
bin/
|
||||
wallarm-upx
|
||||
bin/
|
||||
wallarm-linux-amd64
|
||||
deploy
|
||||
|
|
|
|||
212
JUMP_SERVER.md
Normal file
212
JUMP_SERVER.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Jump Server Setup for Wallarm Remote Assistance
|
||||
|
||||
This guide explains how to configure a Linux VM as a jump server so engineers
|
||||
can securely access customer Wallarm nodes through the `wallarm --tunnel` feature.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Customer VM (VM-A) Jump Server (VM-B) Engineer
|
||||
┌──────────────────┐ TLS:443 ┌──────────────────┐ SSH ┌──────────┐
|
||||
│ wallarm binary │───────────►│ sshd │◄─────────│ You │
|
||||
│ (reverse tunnel) │◄───────────│ │ │ │
|
||||
│ │ │ localhost:9042 │ │ │
|
||||
│ │ │ → VM-A:22 │ │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
VM-A opens an outbound TLS connection to VM-B on port 443 (looks like HTTPS to
|
||||
firewalls). VM-B terminates TLS and proxies to its internal SSH server. VM-A
|
||||
then requests a reverse port forward: VM-B:9042 → VM-A:22. The engineer SSH's
|
||||
into VM-B, then jumps through localhost:9042 to reach VM-A.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Linux VM** (any distro) with a public IP or reachable via Zoraxy/Nginx reverse proxy
|
||||
- **OpenSSH server** 7.6+
|
||||
- **Port 443** accessible from customer environments (outbound only, no inbound needed)
|
||||
|
||||
## Step 1: Install and Configure SSH Server
|
||||
|
||||
```bash
|
||||
# Install SSH server
|
||||
apt-get install -y openssh-server # Debian/Ubuntu
|
||||
yum install -y openssh-server # RHEL/CentOS
|
||||
|
||||
# Edit SSH config
|
||||
nano /etc/ssh/sshd_config
|
||||
```
|
||||
|
||||
Add or uncomment these lines:
|
||||
|
||||
```ini
|
||||
# Required: allow TCP forwarding for the reverse tunnel
|
||||
AllowTcpForwarding yes
|
||||
|
||||
# Required: keep idle tunnels alive
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
|
||||
# Optional: allow remote binds on non-localhost addresses
|
||||
# (only needed if you want engineers to connect directly to VM-B:9042
|
||||
# instead of SSH'ing into VM-B first)
|
||||
GatewayPorts yes
|
||||
```
|
||||
|
||||
Restart SSH:
|
||||
|
||||
```bash
|
||||
systemctl restart sshd
|
||||
```
|
||||
|
||||
## Step 2: Create the Tunnel User
|
||||
|
||||
This user is used by the `wallarm` binary on customer VMs to authenticate
|
||||
and open the reverse tunnel. It should NOT have a shell — it exists only
|
||||
for port forwarding.
|
||||
|
||||
```bash
|
||||
# Create tunnel-only user (no shell, no home directory needed)
|
||||
useradd -m -s /bin/false wallarm-tunnel
|
||||
|
||||
# Create SSH directory
|
||||
mkdir -p ~wallarm-tunnel/.ssh
|
||||
chmod 700 ~wallarm-tunnel/.ssh
|
||||
```
|
||||
|
||||
### Option A: Password Authentication (simpler for customers)
|
||||
|
||||
```bash
|
||||
# Set a password for the tunnel user
|
||||
passwd wallarm-tunnel
|
||||
```
|
||||
|
||||
The customer enters this password when prompted by `wallarm`.
|
||||
|
||||
### Option B: SSH Key Authentication (more secure)
|
||||
|
||||
Generate a key pair and share the private key with customers:
|
||||
|
||||
```bash
|
||||
# On the jump server
|
||||
ssh-keygen -t ed25519 -f ~wallarm-tunnel/.ssh/wallarm_tunnel -N "" -C "wallarm-tunnel"
|
||||
cat ~wallarm-tunnel/.ssh/wallarm_tunnel.pub >> ~wallarm-tunnel/.ssh/authorized_keys
|
||||
chmod 600 ~wallarm-tunnel/.ssh/authorized_keys
|
||||
chown -R wallarm-tunnel:wallarm-tunnel ~wallarm-tunnel/.ssh
|
||||
|
||||
# Share the private key securely with customers
|
||||
cat ~wallarm-tunnel/.ssh/wallarm_tunnel
|
||||
```
|
||||
|
||||
The customer provides the key path when prompted by `wallarm`.
|
||||
|
||||
## Step 3: Zoraxy / Reverse Proxy (Optional)
|
||||
|
||||
If the jump server sits behind a Zoraxy edge proxy, configure it to forward
|
||||
TLS:443 → internal SSH:22. This lets `wallarm` connect over port 443 (which
|
||||
passes through most corporate firewalls).
|
||||
|
||||
### Zoraxy Configuration
|
||||
|
||||
1. Add a new proxy rule:
|
||||
- **Domain**: `ssh.sechpoint.app`
|
||||
- **Target**: `tcp://<jump-server-ip>:22`
|
||||
- **TLS**: Enabled (auto-cert or custom)
|
||||
|
||||
2. No WebSocket or HTTP mode needed — Zoraxy proxies raw TCP.
|
||||
|
||||
### Manual Nginx Stream Proxy (Alternative)
|
||||
|
||||
```nginx
|
||||
stream {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
proxy_pass <jump-server-ip>:22;
|
||||
ssl_certificate /etc/ssl/certs/jump.crt;
|
||||
ssl_certificate_key /etc/ssl/private/jump.key;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Test the Setup
|
||||
|
||||
### From the Jump Server Itself
|
||||
|
||||
```bash
|
||||
# Verify SSH is listening
|
||||
ss -tlnp | grep 22
|
||||
|
||||
# Verify tunnel user can authenticate (no shell expected)
|
||||
ssh wallarm-tunnel@localhost echo test
|
||||
# Expected: command fails (user has no shell), authentication succeeds
|
||||
```
|
||||
|
||||
### From an External Machine (Customer VM)
|
||||
|
||||
```bash
|
||||
# Test TLS connection (if Zoraxy is configured)
|
||||
openssl s_client -connect ssh.sechpoint.app:443 </dev/null 2>/dev/null | head -5
|
||||
|
||||
# Test SSH over TLS
|
||||
ssh -p 443 wallarm-tunnel@ssh.sechpoint.app
|
||||
# Expected: "This account is currently not available" or immediate disconnect
|
||||
# This confirms authentication works but no shell is allowed — correct.
|
||||
```
|
||||
|
||||
## How wallarm Uses It
|
||||
|
||||
On the customer VM, the `wallarm` binary offers two paths:
|
||||
|
||||
1. **`wallarm --tunnel`** — starts tunnel with pre-configured credentials (build-time or env var)
|
||||
2. **`wallarm` → Remote Assistance** — prompts for jumphost URL, username, password/key interactively
|
||||
|
||||
The binary does:
|
||||
1. TLS dial to `ssh.sechpoint.app:443`
|
||||
2. SSH authenticate as `wallarm-tunnel`
|
||||
3. Request reverse forward: `0.0.0.0:9042 → localhost:22` (or `localhost:9042` if GatewayPorts is off)
|
||||
4. Keep tunnel alive with 30s heartbeats
|
||||
|
||||
## Engineer Connection (Assistance Flow)
|
||||
|
||||
After the customer starts the tunnel, the engineer connects with two hops:
|
||||
|
||||
```bash
|
||||
# Step 1: SSH into the jump server
|
||||
ssh engineer@ssh.sechpoint.app
|
||||
|
||||
# Step 2: Jump through the tunnel to the customer VM
|
||||
ssh root@localhost -p 9042
|
||||
```
|
||||
|
||||
Or as a single command:
|
||||
|
||||
```bash
|
||||
ssh -o ProxyJump=engineer@ssh.sechpoint.app root@localhost -p 9042
|
||||
```
|
||||
|
||||
The engineer now has a root shell on the customer VM and can run the deployment
|
||||
wizard or troubleshoot directly.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The `wallarm-tunnel` user has no shell (`/bin/false`) — authentication only succeeds for port forwarding
|
||||
- The tunnel is outbound-only from the customer VM — no inbound ports opened
|
||||
- TLS encrypts the connection end-to-end (VM-A → Zoraxy → VM-B)
|
||||
- Add `Match User wallarm-tunnel` blocks in `sshd_config` to further restrict:
|
||||
```ini
|
||||
Match User wallarm-tunnel
|
||||
PermitTTY no
|
||||
PermitTunnel no
|
||||
X11Forwarding no
|
||||
AllowAgentForwarding no
|
||||
ForceCommand /bin/false
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|---------|-------|
|
||||
| "Connection refused" from customer VM | Jump server port 443 reachable? Zoraxy running? |
|
||||
| "Permission denied" | Tunnel user password/key correct? `~wallarm-tunnel/.ssh/authorized_keys` permissions 600? |
|
||||
| Tunnel opens but engineer can't reach VM-A | `AllowTcpForwarding yes` in sshd_config? SSH server restarted? |
|
||||
| Tunnel drops after a few minutes | `ClientAliveInterval` set? Check firewall idle timeout |
|
||||
36
Makefile
36
Makefile
|
|
@ -1,36 +0,0 @@
|
|||
.PHONY: all linux-amd64 linux-arm64 clean test release
|
||||
|
||||
BINARY := wallarm
|
||||
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
|
||||
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||
# Embed tunnel key at build time: make TUNNEL_KEY=~/.wallarm/tunnel_key
|
||||
ifdef TUNNEL_KEY
|
||||
LDFLAGS += -X main.tunnelKey=$(shell cat $(TUNNEL_KEY))
|
||||
endif
|
||||
|
||||
all: linux-amd64 linux-arm64
|
||||
|
||||
linux-amd64:
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/wallarm/
|
||||
upx --best --lzma $(BINARY)-linux-amd64 -o $(BINARY)-linux-amd64.tmp 2>/dev/null && mv $(BINARY)-linux-amd64.tmp $(BINARY)-linux-amd64 || true
|
||||
|
||||
linux-arm64:
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/wallarm/
|
||||
upx --best --lzma $(BINARY)-linux-arm64 -o $(BINARY)-linux-arm64.tmp 2>/dev/null && mv $(BINARY)-linux-arm64.tmp $(BINARY)-linux-arm64 || true
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY) $(BINARY)-linux-*
|
||||
|
||||
test:
|
||||
go test ./internal/...
|
||||
|
||||
# Build and prepare for Gitea release.
|
||||
# Usage: make release
|
||||
# Then upload wallarm-linux-amd64 and wallarm-linux-arm64 as release assets.
|
||||
release: clean all
|
||||
@echo "Release binaries built:"
|
||||
@ls -lh wallarm-linux-*
|
||||
@echo ""
|
||||
@echo "Next: Create a Gitea release and upload these binaries as assets."
|
||||
@echo " Tag: v$(VERSION)"
|
||||
|
||||
116
README.md
116
README.md
|
|
@ -1,90 +1,78 @@
|
|||
# Wallarm Native Node Manager
|
||||
# Wallarm Docker Node Manager
|
||||
|
||||
Single-binary deployment and management for Wallarm Native Nodes (connector mode, no Docker).
|
||||
One command to get started, one TUI to manage everything.
|
||||
Single-command Docker-based deployment for Wallarm filtering nodes.
|
||||
Multiple nodes on one host, no conflicts, zero native dependencies.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/deploy.sh" | bash
|
||||
sudo ./deploy/wallarm
|
||||
curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/setup.sh" | bash
|
||||
sudo /opt/fw/deploy.sh
|
||||
```
|
||||
|
||||
That's it. The binary runs preflight checks, then opens an interactive TUI:
|
||||
- **First run**: configuration wizard (cloud region, API token, node name, listen address)
|
||||
- **Subsequent runs**: dashboard with node list, add/remove/configure actions
|
||||
Setup installs Python + Docker, downloads deploy script. Menu handles everything.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single binary** — 2MB, zero runtime dependencies, works on any Linux
|
||||
- **Interactive TUI** — bubbletea-powered forms and dashboard
|
||||
- **Preflight checks** — runs on every start: system, network, cloud reachability, resources
|
||||
- **Multi-node** — manage multiple Wallarm nodes on the same host via systemd
|
||||
- **Remote tunnel** — `wallarm --tunnel` opens a reverse SSH tunnel over TLS:443 via Zoraxy
|
||||
- **State persistence** — `~/.wallarm/state.json` tracks all deployments
|
||||
|
||||
## Commands
|
||||
## Menu
|
||||
|
||||
```
|
||||
wallarm Interactive TUI (wizard or dashboard)
|
||||
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app
|
||||
wallarm --version Show version
|
||||
wallarm --help Show help
|
||||
[1] Deploy all Deploy all nodes from fw.conf
|
||||
[2] Add a node Interactive config + deploy
|
||||
[3] Status Show running containers
|
||||
[4] Remove a node Stop + delete container
|
||||
[5] Edit a node Update port, upstream, mode in fw.conf
|
||||
```
|
||||
|
||||
## Dashboard
|
||||
## Configuration
|
||||
|
||||
```
|
||||
📊 Wallarm Dashboard
|
||||
Type: native | Cloud: EU (api.wallarm.com)
|
||||
──────────────────────────────────────────────────
|
||||
`/opt/fw/app/fw.conf` — JSON with node configs:
|
||||
|
||||
Nodes:
|
||||
● srv1 — running (0.0.0.0:8081)
|
||||
● srv2 — running (0.0.0.0:8082)
|
||||
|
||||
Actions:
|
||||
[a] Add node
|
||||
[c] Configure
|
||||
[r] Remove node
|
||||
[t] Start tunnel
|
||||
[q] Quit
|
||||
```json
|
||||
{
|
||||
"srv1": {
|
||||
"token": "your-wallarm-token",
|
||||
"cloud": "EU",
|
||||
"port": "8081",
|
||||
"upstream_ip": "10.1.0.10",
|
||||
"upstream_port": "8081",
|
||||
"mode": "monitoring"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
wallarm/
|
||||
├── cmd/wallarm/main.go Entrypoint: preflight → TUI
|
||||
├── internal/
|
||||
│ ├── shared/ System detection, validation, connectivity
|
||||
│ ├── preflight/ Mandatory checks on every start
|
||||
│ ├── state/ ~/.wallarm/state.json persistence
|
||||
│ ├── native/ Systemd units, installer, node management
|
||||
│ ├── tunnel/ Reverse SSH over TLS:443 via Zoraxy
|
||||
│ └── ui/ Bubbletea TUI (wizard + dashboard)
|
||||
├── bin/
|
||||
│ └── wallarm-linux-amd64 Pre-built binary
|
||||
├── deploy.sh One-command bootstrap
|
||||
├── go.mod / go.sum
|
||||
└── Makefile Cross-compile targets
|
||||
deploy.py Single Python file, ~200 lines
|
||||
├── Docker image: wallarm/node:6.13.0
|
||||
├── Named volumes for persistence
|
||||
└── Unique ports per node
|
||||
|
||||
/opt/fw/
|
||||
├── deploy.sh Entry wrapper
|
||||
├── app/
|
||||
│ ├── main.py Deploy script
|
||||
│ ├── fw.conf Node config
|
||||
│ └── state.json Deployment state
|
||||
└── wallarm-aio.sh Cached installer (native fallback)
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
## Requirements
|
||||
|
||||
```bash
|
||||
go build -ldflags "-s -w -X main.version=$(git describe --tags)" -o wallarm ./cmd/wallarm/
|
||||
make linux-amd64 # Cross-compile
|
||||
make all # All targets
|
||||
```
|
||||
- Linux with systemd
|
||||
- Docker (auto-installed by setup.sh)
|
||||
- Python 3.10+ (auto-installed by setup.sh)
|
||||
- Outbound to api.wallarm.com (EU) or us1.api.wallarm.com (US)
|
||||
|
||||
## Prerequisites
|
||||
## Remote Assistance
|
||||
|
||||
- Linux (systemd required)
|
||||
- x86_64 or aarch64
|
||||
- 2GB+ RAM, 10GB+ disk
|
||||
- Outbound connectivity to Wallarm cloud (US/EU)
|
||||
See [JUMP_SERVER.md](JUMP_SERVER.md) for jump server setup.
|
||||
|
||||
## License
|
||||
## Agent Rules (for AI assistants working on this project)
|
||||
|
||||
Proprietary — see repository for terms.
|
||||
**CRITICAL — These override all other considerations:**
|
||||
|
||||
1. **NEVER change code without explicit user approval.** Discuss ideas first. Ask before implementing.
|
||||
2. **Production defaults must be explicit.** Never rely on Docker image defaults — nginx config, headers, client_max_body_size, upload limits must be set explicitly in our code. Defaults cause production outages.
|
||||
3. **Header forwarding is not default.** `proxy_set_header X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` must be explicitly configured.
|
||||
4. **client_max_body_size** must be set high enough for production (e.g., `1024m`) — default 1m blocks legitimate uploads.
|
||||
5. **The user owns this project.** You are assisting, not leading. Propose, don't impose.
|
||||
|
|
|
|||
86
changelog.md
86
changelog.md
|
|
@ -1,86 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to date-based versioning (YYYY-MM.x).
|
||||
|
||||
## [2026-08.1] - 2026-08-01
|
||||
### Fixed
|
||||
- **setup.sh interactive prompt broken under `curl ... | bash`**: the deployment-type prompt and overwrite confirmation read from stdin, which is the script pipe (not the terminal) when piped to bash — the prompt was silently skipped and only Docker scripts were downloaded. setup.sh is now **non-interactive by default and downloads BOTH deployment types** (`docker/` + `native/`), so the native option is always available. Use `DEPLOYMENT_TYPE=docker|native` to download only one type.
|
||||
|
||||
### Added
|
||||
- **Native deployment**: Wallarm filtering node can now be deployed directly on the OS **without Docker** via the unified manager `native/wallarm-native.sh` (Wallarm Native Node, go-node, `connector-server` mode)
|
||||
- `--preflight` checks (root, systemd, architecture, required commands, installer + Wallarm cloud connectivity, disk/memory, listen-port availability); auto-run before `--install`
|
||||
- Interactive parallel multi-node installation with per-node systemd template units (`wallarm-node@<name>.service`)
|
||||
- `--config` (address/token/labels, safe env rewrite), `--remove`, `--status [NODE]`
|
||||
- All-in-one installer from `repo.wallarm.com` (overridable via `WALLARM_INSTALLER_URL`/`WALLARM_INSTALLER_ARCH`)
|
||||
- **Shared library**: `common/wallarm-lib.sh` extracted and reused by both deployment types
|
||||
- Colors, logging (`log_message`, `fail_with_remediation`), early error handler
|
||||
- System detection (OS/arch/init), network connectivity tests
|
||||
- Preflight `.env` parsing (`load_env_file`), cloud region selection (`select_cloud_region`)
|
||||
- Validation helpers (IP, CIDR, port), artifact download + checksum verification
|
||||
|
||||
### Changed
|
||||
- **Repository structure** now separates deployment types:
|
||||
- `docker/` – all Docker-based scripts moved here (`git mv`, history preserved)
|
||||
- `docker/binaries/` and `docker/images/` – Docker artifacts moved into the Docker tree
|
||||
- `native/` – native (no-Docker) deployment, containing only the unified `wallarm-native.sh`
|
||||
- `common/` – shared library
|
||||
- **Removed** the `native/wallarm-ct-*.sh` scripts (NGINX-module based native deployment) so the native deployment is represented solely by the unified `wallarm-native.sh`; the `wallarm-ct-*` family is now Docker-only
|
||||
- **Artifact URLs** updated to the `docker/` prefix (`/docker/binaries/...`, `/docker/images/...`)
|
||||
- **Docker scripts** refactored to source `common/wallarm-lib.sh` (removed duplicated helper functions; behavior preserved)
|
||||
- **setup.sh** downloads the shared library and scripts per deployment type into `docker/`/`native/` (native = `wallarm-native.sh`); supports `DEPLOYMENT_TYPE=docker|native` to download only one type
|
||||
- **README.md** rewritten to document both deployment types, the new structure, and the unified native manager
|
||||
|
||||
### Notes
|
||||
- The `wallarm-ct-*` script family is Docker-only; native deployment uses `wallarm-native.sh`
|
||||
- Native multi-node is supported via per-node systemd template units
|
||||
- Docker deployment behavior is unchanged apart from the new directory layout
|
||||
|
||||
## [2026-04.1] - 2026-04-21
|
||||
### Added
|
||||
- Initial changelog file with versioning schema
|
||||
- Date-based versioning system (YYYY-MM.x)
|
||||
|
||||
### Changed
|
||||
- **Variable renaming**: All `GITLAB_*` variables renamed to `GIT_*` prefix
|
||||
- `GITLAB_BASE_URL` → `GIT_BASE_URL`
|
||||
- `GITLAB_RAW_URL` → `GIT_RAW_URL` (with updated path)
|
||||
- `GITLAB_DOCKER_BINARY_URL` → `GIT_DOCKER_BINARY_URL`
|
||||
- `GITLAB_DOCKER_CHECKSUM_URL` → `GIT_DOCKER_CHECKSUM_URL`
|
||||
- `GITLAB_WALLARM_IMAGE_URL` → `GIT_WALLARM_IMAGE_URL`
|
||||
- `GITLAB_WALLARM_CHECKSUM_URL` → `GIT_WALLARM_CHECKSUM_URL`
|
||||
- `GITLAB_REACHABLE` → `GIT_REACHABLE`
|
||||
- **URL structure**: Updated `GIT_RAW_URL` from `/-/raw/main` to `/raw/branch/main` path (corrected for download compatibility)
|
||||
- **Terminology**: Replaced all "GitLab" references in comments and log messages with "Git Repositorys"
|
||||
- **Documentation**: Updated README.md to reflect new terminology
|
||||
- **URL correction**: Corrected setup.sh download URL in README.md back to `/raw/branch/main/` pattern for download compatibility
|
||||
- **Branding**: Removed all Forgejo references from codebase and documentation for neutrality
|
||||
- **Fallback chains**: Simplified from three-tier to two-tier approach
|
||||
- Docker binary: `Git Repositorys → local dir → current dir` (removed `→ internal proxy`)
|
||||
- Wallarm image: `Git Repositorys → local dir → current dir` (removed `→ internal registry`)
|
||||
|
||||
### Removed
|
||||
- Internal registry fallback options and related variables:
|
||||
- `INTERNAL_DOCKER_REGISTRY` and `INTERNAL_DOCKER_DOWNLOAD`
|
||||
- `DOCKER_REGISTRY_HOST` and `DOCKER_DOWNLOAD_HOST`
|
||||
- `DOCKER_STATIC_BASE_URL` and `WALLARM_IMAGE_SOURCE`
|
||||
- Connectivity tests for internal registry/download servers
|
||||
- Remediation instructions mentioning internal fallback options
|
||||
- All references to internal proxy/registry in error messages
|
||||
|
||||
### Technical Details
|
||||
- **Commits**:
|
||||
- `3158ee7` (chore: refactor git references and remove internal registry fallback)
|
||||
- `509909d` (chore: remove Forgejo references and fix setup URL)
|
||||
- **Files modified**: 4 files changed, additional modifications
|
||||
- `README.md` - Documentation updates and URL fixes
|
||||
- `setup.sh` - URL base update and Forgejo reference removal
|
||||
- `wallarm-ct-check.sh` - Variable renaming and logic simplification
|
||||
- `wallarm-ct-deploy.sh` - Variable renaming and fallback chain updates
|
||||
|
||||
### Notes
|
||||
- Scripts maintain backward compatibility with existing artifact URLs
|
||||
- Simplified error handling focuses on primary Git Repositorys source and local files
|
||||
- No functional changes to core deployment logic
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
// wallarm — single-binary Wallarm deployment manager.
|
||||
//
|
||||
// On every start: run preflight checks → detect existing deployment → route to wizard or dashboard.
|
||||
//
|
||||
// Commands:
|
||||
//
|
||||
// wallarm Auto-detect state, show wizard or dashboard
|
||||
// wallarm --tunnel Start reverse SSH tunnel over TLS:443
|
||||
// wallarm --help Show help
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
|
||||
)
|
||||
|
||||
// Embedded tunnel key — set at build time with:
|
||||
//
|
||||
// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)"
|
||||
var tunnelKey string
|
||||
|
||||
// Version is set at build time with -ldflags "-X main.version=1.0.0"
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, `wallarm — Wallarm Deployment Manager
|
||||
|
||||
Usage:
|
||||
wallarm Start interactive deployment wizard/dashboard
|
||||
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app
|
||||
wallarm --help Show this help
|
||||
|
||||
On first run, wallarm checks system readiness, then guides you through
|
||||
deployment. On subsequent runs, it shows your existing deployments.
|
||||
`)
|
||||
}
|
||||
help := flag.Bool("help", false, "Show help")
|
||||
versionFlag := flag.Bool("version", false, "Show version")
|
||||
tunnelFlag := flag.Bool("tunnel", false, "Start reverse SSH tunnel")
|
||||
flag.Parse()
|
||||
|
||||
if *help {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
if *versionFlag {
|
||||
fmt.Println("wallarm version", version)
|
||||
return
|
||||
}
|
||||
|
||||
// ── Tunnel mode ──────────────────────────────────────────────
|
||||
if *tunnelFlag {
|
||||
cfg := tunnel.DefaultConfig()
|
||||
if tunnelKey != "" {
|
||||
cfg.KeyBytes = []byte(tunnelKey)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "No tunnel key configured. Set WALLARM_TUNNEL_KEY or build with -ldflags.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := tunnel.Start(cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ── Default: preflight → TUI wizard/dashboard ────────────────
|
||||
fmt.Println("═══ Wallarm Deployment Manager ═══")
|
||||
fmt.Println()
|
||||
|
||||
// 1. Preflight checks (always run on start)
|
||||
fmt.Println("Running preflight checks...")
|
||||
result := preflight.Run()
|
||||
for _, c := range result.Checks {
|
||||
marker := "✅"
|
||||
if !c.Passed {
|
||||
marker = "❌"
|
||||
} else if c.Warning {
|
||||
marker = "⚠️"
|
||||
}
|
||||
fmt.Printf(" %s %s — %s\n", marker, c.Name, c.Detail)
|
||||
}
|
||||
|
||||
if !result.Passed {
|
||||
fmt.Println("\n❌ Preflight checks failed. Fix the issues above and re-run.")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("✅ Preflight checks passed.")
|
||||
fmt.Println()
|
||||
|
||||
// 2. Launch bubbletea TUI (handles wizard vs dashboard routing)
|
||||
if err := ui.Run(result); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,530 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# WALLARM COMMON LIBRARY - shared functions for docker/ and native/ deployment
|
||||
# ==============================================================================
|
||||
# Purpose: Single source of truth for functionality shared by both deployment
|
||||
# types (Docker container vs native NGINX install).
|
||||
# Usage: Scripts source this file AFTER setting `set -euo pipefail` and before
|
||||
# defining their own functions:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../common/wallarm-lib.sh"
|
||||
# The library does NOT set the error trap itself; each script owns its
|
||||
# error handling configuration.
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# COLOR DEFINITIONS (for better UX)
|
||||
# ------------------------------------------------------------------------------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# SSL SECURITY SETTINGS
|
||||
# WALLARM_INSECURE_SSL=1 disables SSL certificate validation (for self-signed
|
||||
# certs). Kept as a default of 1 for backward compatibility with existing usage.
|
||||
# ------------------------------------------------------------------------------
|
||||
INSECURE_SSL="${WALLARM_INSECURE_SSL:-1}"
|
||||
if [ "$INSECURE_SSL" = "1" ]; then
|
||||
CURL_INSECURE_FLAG="-k"
|
||||
else
|
||||
CURL_INSECURE_FLAG=""
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# EARLY ERROR HANDLER
|
||||
# Use with: trap early_error_handler ERR
|
||||
# Handles failures before log_message is available (or when logging is not set).
|
||||
# ------------------------------------------------------------------------------
|
||||
early_error_handler() {
|
||||
echo -e "${RED}${BOLD}[ERROR]${NC} Script failed at line $LINENO. Command: $BASH_COMMAND" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# LOGGING
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Log a message to stderr (colored) and to $LOG_FILE (if set).
|
||||
log_message() {
|
||||
local level="$1"
|
||||
local message="$2"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
local color="$NC"
|
||||
case "$level" in
|
||||
"INFO") color="${BLUE}" ;;
|
||||
"SUCCESS") color="${GREEN}" ;;
|
||||
"WARNING") color="${YELLOW}" ;;
|
||||
"ERROR") color="${RED}" ;;
|
||||
"DEBUG") color="${CYAN}" ;;
|
||||
esac
|
||||
|
||||
echo -e "${color}[${timestamp}] ${level}: ${message}${NC}" >&2
|
||||
if [ -n "${LOG_FILE:-}" ]; then
|
||||
echo "[${timestamp}] ${level}: ${message}" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Log an ERROR, print a remediation banner, and exit non-zero.
|
||||
fail_with_remediation() {
|
||||
local error_msg="$1"
|
||||
local remediation="$2"
|
||||
|
||||
log_message "ERROR" "$error_msg"
|
||||
echo -e "\n${RED}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${RED}${BOLD}║ DEPLOYMENT FAILED ║${NC}"
|
||||
echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "\n${YELLOW}${BOLD}Root Cause:${NC} $error_msg"
|
||||
echo -e "\n${YELLOW}${BOLD}How to Fix:${NC}"
|
||||
echo -e "$remediation"
|
||||
echo -e "\n${YELLOW}Check the full log for details:${NC} ${LOG_FILE:-stdout}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# MISC HELPERS
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Extract hostname from a URL, stripping protocol and credentials for safe logging.
|
||||
extract_hostname_from_url() {
|
||||
local url="$1"
|
||||
local hostpart="${url#*://}"
|
||||
hostpart="${hostpart#*@}"
|
||||
hostpart="${hostpart%%[:/]*}"
|
||||
echo "$hostpart"
|
||||
}
|
||||
|
||||
# Check whether a command exists (respects PATH + common system directories).
|
||||
command_exists() {
|
||||
local cmd="$1"
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
local system_dirs=("/usr/sbin" "/sbin" "/usr/local/sbin" "/usr/bin" "/bin" "/usr/local/bin")
|
||||
for dir in "${system_dirs[@]}"; do
|
||||
if [ -x "$dir/$cmd" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validate an IPv4 address (basic format + octet range check).
|
||||
validate_ip_address() {
|
||||
local ip="$1"
|
||||
|
||||
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r i1 i2 i3 i4 <<< "$ip"
|
||||
if [ "$i1" -gt 255 ] || [ "$i2" -gt 255 ] || [ "$i3" -gt 255 ] || [ "$i4" -gt 255 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate an IP or CIDR entry (IPv4 with optional /prefix). Returns 0 if valid.
|
||||
validate_ip_or_cidr() {
|
||||
local entry="$1"
|
||||
|
||||
if [[ ! "$entry" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS='/' read -r ip cidr <<< "$entry"
|
||||
IFS='.' read -r o1 o2 o3 o4 <<< "$ip"
|
||||
if [ "$o1" -gt 255 ] || [ "$o2" -gt 255 ] || [ "$o3" -gt 255 ] || [ "$o4" -gt 255 ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$cidr" ] && { [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; }; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if a TCP/UDP port is currently in use. Returns 0 when available.
|
||||
check_port_available() {
|
||||
local port="$1"
|
||||
local protocol="${2:-tcp}"
|
||||
|
||||
log_message "DEBUG" "Checking port $port/$protocol availability..."
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -"${protocol:0:1}"ln | grep -q ":$port "; then
|
||||
return 1 # Port in use
|
||||
fi
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
if netstat -tulpn 2>/dev/null | grep -E ":$port\s" >/dev/null 2>&1; then
|
||||
return 1 # Port in use
|
||||
fi
|
||||
else
|
||||
log_message "WARNING" "Neither ss nor netstat available, cannot check port $port"
|
||||
fi
|
||||
|
||||
return 0 # Port available (or cannot check)
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# SYSTEM DETECTION
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Detect OS name and version. Prints "name:version".
|
||||
detect_os_and_version() {
|
||||
log_message "INFO" "Detecting OS and version..."
|
||||
|
||||
local os_name=""
|
||||
local os_version=""
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
os_name="$ID"
|
||||
os_version="$VERSION_ID"
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
os_name="rhel"
|
||||
os_version=$(sed -e 's/.*release \([0-9]\+\)\..*/\1/' /etc/redhat-release)
|
||||
elif [ -f /etc/alpine-release ]; then
|
||||
os_name="alpine"
|
||||
os_version=$(cat /etc/alpine-release)
|
||||
else
|
||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
os_version=$(uname -r)
|
||||
fi
|
||||
|
||||
os_name="${os_name//[$'\t\r\n']/}"
|
||||
os_version="${os_version//[$'\t\r\n']/}"
|
||||
|
||||
case "$os_name" in
|
||||
"ubuntu"|"debian"|"centos"|"rhel"|"alpine"|"amzn"|"ol"|"rocky"|"almalinux")
|
||||
log_message "SUCCESS" "OS detected: $os_name $os_version (supported)"
|
||||
;;
|
||||
*)
|
||||
log_message "WARNING" "OS '$os_name' not explicitly tested but may work"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$os_name:$os_version"
|
||||
}
|
||||
|
||||
# Detect architecture. Prints a normalized value (x86_64/aarch64/armhf/unknown).
|
||||
detect_architecture() {
|
||||
log_message "INFO" "Detecting system architecture..."
|
||||
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
local docker_arch=""
|
||||
|
||||
case "$arch" in
|
||||
x86_64|x64|amd64)
|
||||
docker_arch="x86_64"
|
||||
log_message "SUCCESS" "Architecture: x86_64 (Intel/AMD 64-bit)"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
docker_arch="aarch64"
|
||||
log_message "SUCCESS" "Architecture: aarch64 (ARM 64-bit)"
|
||||
;;
|
||||
armv7l|armhf)
|
||||
docker_arch="armhf"
|
||||
log_message "SUCCESS" "Architecture: armhf (ARM 32-bit)"
|
||||
;;
|
||||
*)
|
||||
log_message "ERROR" "Unsupported architecture: $arch"
|
||||
docker_arch="unknown"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$docker_arch"
|
||||
}
|
||||
|
||||
# Detect the init system. Prints one of systemd/openrc/sysvinit/upstart/unknown.
|
||||
detect_init_system() {
|
||||
log_message "INFO" "Detecting init system..."
|
||||
|
||||
local init_system="unknown"
|
||||
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
init_system="darwin"
|
||||
log_message "SUCCESS" "Init system: darwin (macOS)"
|
||||
elif command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
|
||||
init_system="systemd"
|
||||
log_message "SUCCESS" "Init system: systemd"
|
||||
elif { [ -d /etc/init.d ] && [ -x /sbin/initctl ]; } || [ -x /sbin/init ]; then
|
||||
init_system="sysvinit"
|
||||
log_message "SUCCESS" "Init system: sysvinit"
|
||||
elif [ -d /etc/rc.d ] && [ -x /sbin/rc-service ]; then
|
||||
init_system="openrc"
|
||||
log_message "SUCCESS" "Init system: openrc (Alpine)"
|
||||
elif [ -x /sbin/upstart ]; then
|
||||
init_system="upstart"
|
||||
log_message "SUCCESS" "Init system: upstart"
|
||||
else
|
||||
log_message "WARNING" "Could not determine init system"
|
||||
fi
|
||||
|
||||
echo "$init_system"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# NETWORK CONNECTIVITY
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Test connectivity to a host/URL. Returns 0 when reachable.
|
||||
test_connectivity() {
|
||||
local host="$1"
|
||||
local description="$2"
|
||||
local timeout="${3:-10}"
|
||||
|
||||
local display_host
|
||||
display_host=$(extract_hostname_from_url "$host")
|
||||
log_message "INFO" "Testing connectivity to $description ($display_host)..."
|
||||
|
||||
local url="$host"
|
||||
if [[ ! "$host" =~ ^https?:// ]]; then
|
||||
url="https://$host"
|
||||
fi
|
||||
|
||||
if curl -sL $CURL_INSECURE_FLAG --connect-timeout "$timeout" "$url" >/dev/null 2>&1; then
|
||||
log_message "SUCCESS" "$description is reachable"
|
||||
return 0
|
||||
else
|
||||
log_message "ERROR" "$description is NOT reachable"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test a set of cloud endpoints. Prints "true" if all reachable, else "false".
|
||||
test_cloud_endpoints() {
|
||||
local cloud_name="$1"
|
||||
shift
|
||||
local endpoints=("$@")
|
||||
|
||||
log_message "INFO" "Testing $cloud_name cloud endpoints..."
|
||||
|
||||
local all_reachable=true
|
||||
local endpoint
|
||||
for endpoint in "${endpoints[@]}"; do
|
||||
if ! test_connectivity "$endpoint" "$cloud_name cloud endpoint $endpoint"; then
|
||||
all_reachable=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$all_reachable" = "true" ]; then
|
||||
log_message "SUCCESS" "All $cloud_name cloud endpoints reachable"
|
||||
echo "true"
|
||||
else
|
||||
log_message "WARNING" "Some $cloud_name cloud endpoints unreachable"
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ENVIRONMENT FILE HANDLING
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Load a preflight .env file into global variables. Returns 1 if file missing.
|
||||
load_env_file() {
|
||||
local env_file="${1:-$ENV_FILE}"
|
||||
|
||||
if [ ! -f "$env_file" ]; then
|
||||
log_message "ERROR" "Environment file not found: $env_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local key value
|
||||
while IFS='=' read -r key value; do
|
||||
[[ "$key" =~ ^#.*$ ]] && continue
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
|
||||
case "$key" in
|
||||
result) CHECK_RESULT="$value" ;;
|
||||
os_name) OS_NAME="$value" ;;
|
||||
os_version) OS_VERSION="$value" ;;
|
||||
architecture) ARCHITECTURE="$value" ;;
|
||||
init_system) INIT_SYSTEM="$value" ;;
|
||||
us_cloud_reachable) US_CLOUD_REACHABLE="$value" ;;
|
||||
eu_cloud_reachable) EU_CLOUD_REACHABLE="$value" ;;
|
||||
registry_reachable) REGISTRY_REACHABLE="$value" ;;
|
||||
download_reachable) DOWNLOAD_REACHABLE="$value" ;;
|
||||
git_reachable) GIT_REACHABLE="$value" ;;
|
||||
installer_reachable) INSTALLER_REACHABLE="$value" ;;
|
||||
esac
|
||||
done < "$env_file"
|
||||
|
||||
log_message "SUCCESS" "Loaded preflight results from $env_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# CLOUD REGION SELECTION
|
||||
# Sets CLOUD_REGION and API_HOST based on reachability from the preflight check.
|
||||
# ------------------------------------------------------------------------------
|
||||
select_cloud_region() {
|
||||
log_message "INFO" "Selecting Wallarm Cloud region..."
|
||||
|
||||
echo -e "\n${CYAN}${BOLD}Wallarm Cloud Region Selection:${NC}"
|
||||
|
||||
local available_options=()
|
||||
|
||||
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
|
||||
echo -e "1. ${YELLOW}US Cloud${NC} (us1.api.wallarm.com) - For US-based deployments"
|
||||
available_options+=("1" "US")
|
||||
fi
|
||||
|
||||
if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then
|
||||
echo -e "2. ${YELLOW}EU Cloud${NC} (api.wallarm.com) - For EU-based deployments"
|
||||
available_options+=("2" "EU")
|
||||
fi
|
||||
|
||||
if [ ${#available_options[@]} -eq 0 ]; then
|
||||
fail_with_remediation "No cloud regions available" \
|
||||
"Preflight check showed no reachable cloud regions.
|
||||
1. Check network connectivity to Wallarm endpoints
|
||||
2. Run the preflight check again
|
||||
3. Contact network administrator if behind firewall"
|
||||
fi
|
||||
|
||||
local pattern
|
||||
pattern="^($(IFS='|'; echo "${available_options[*]}"))$"
|
||||
|
||||
local cloud_choice=""
|
||||
while [[ ! "$cloud_choice" =~ $pattern ]]; do
|
||||
if [ ${#available_options[@]} -eq 2 ]; then
|
||||
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
|
||||
cloud_choice="US"
|
||||
break
|
||||
else
|
||||
cloud_choice="EU"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
read -r -p "$(echo -e "${YELLOW}Enter choice [1/US or 2/EU]: ${NC}")" cloud_choice
|
||||
cloud_choice=$(echo "$cloud_choice" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
case "$cloud_choice" in
|
||||
1|"US")
|
||||
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
|
||||
CLOUD_REGION="US"
|
||||
API_HOST="us1.api.wallarm.com"
|
||||
log_message "INFO" "Selected US Cloud"
|
||||
else
|
||||
echo -e "${RED}US Cloud is not reachable (per preflight check)${NC}"
|
||||
cloud_choice=""
|
||||
fi
|
||||
;;
|
||||
2|"EU")
|
||||
if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then
|
||||
CLOUD_REGION="EU"
|
||||
API_HOST="api.wallarm.com"
|
||||
log_message "INFO" "Selected EU Cloud"
|
||||
else
|
||||
echo -e "${RED}EU Cloud is not reachable (per preflight check)${NC}"
|
||||
cloud_choice=""
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -n "$cloud_choice" ]; then
|
||||
echo -e "${RED}Invalid choice. Select from available options above.${NC}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log_message "SUCCESS" "Cloud region selected: $CLOUD_REGION ($API_HOST)"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ARTIFACT DOWNLOAD (Git Repositorys primary source)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Download a file from Git Repositorys. Returns 0 on success.
|
||||
download_from_git() {
|
||||
local url="$1"
|
||||
local output_path="$2"
|
||||
local description="$3"
|
||||
|
||||
log_message "INFO" "Attempting to download $description from Git Repositorys..."
|
||||
log_message "DEBUG" "URL: $url"
|
||||
log_message "DEBUG" "Output path: $output_path"
|
||||
|
||||
if curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 30 --max-time 300 --progress-bar "$url" -o "$output_path"; then
|
||||
log_message "SUCCESS" "Downloaded $description to $output_path"
|
||||
return 0
|
||||
else
|
||||
local curl_exit=$?
|
||||
log_message "ERROR" "Failed to download $description from Git Repositorys (curl exit: $curl_exit)"
|
||||
if [ -f "$output_path" ]; then
|
||||
rm -f "$output_path"
|
||||
log_message "DEBUG" "Removed partial download: $output_path"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify a file against a checksum file or URL. Returns 0 on success;
|
||||
# skips verification (returns 0) when the checksum cannot be obtained.
|
||||
verify_checksum() {
|
||||
local file_path="$1"
|
||||
local checksum_file_or_url="$2"
|
||||
local description="$3"
|
||||
|
||||
log_message "INFO" "Verifying $description checksum..."
|
||||
|
||||
local checksum_file=""
|
||||
if [[ "$checksum_file_or_url" =~ ^https?:// ]]; then
|
||||
checksum_file="/tmp/$(basename "$checksum_file_or_url")"
|
||||
log_message "DEBUG" "Downloading checksum from URL: $checksum_file_or_url"
|
||||
if ! curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 10 --max-time 30 -s "$checksum_file_or_url" -o "$checksum_file"; then
|
||||
log_message "WARNING" "Could not download checksum file, skipping verification"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
checksum_file="$checksum_file_or_url"
|
||||
fi
|
||||
|
||||
if [ ! -f "$checksum_file" ]; then
|
||||
log_message "WARNING" "Checksum file not found: $checksum_file, skipping verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local expected_checksum
|
||||
expected_checksum=$(awk '{print $1}' "$checksum_file" 2>/dev/null)
|
||||
if [ -z "$expected_checksum" ]; then
|
||||
log_message "WARNING" "Could not read checksum from $checksum_file, skipping verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_message "DEBUG" "Computing SHA256 checksum of $file_path..."
|
||||
local actual_checksum
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual_checksum=$(sha256sum "$file_path" | awk '{print $1}')
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
actual_checksum=$(shasum -a 256 "$file_path" | awk '{print $1}')
|
||||
else
|
||||
log_message "WARNING" "sha256sum or shasum not available, skipping checksum verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$expected_checksum" = "$actual_checksum" ]; then
|
||||
log_message "SUCCESS" "$description checksum verified successfully"
|
||||
return 0
|
||||
else
|
||||
log_message "ERROR" "$description checksum verification FAILED"
|
||||
log_message "DEBUG" "Expected: $expected_checksum"
|
||||
log_message "DEBUG" "Actual: $actual_checksum"
|
||||
rm -f "$file_path"
|
||||
log_message "INFO" "Removed corrupted file: $file_path"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
106
deploy.sh
Normal file → Executable file
106
deploy.sh
Normal file → Executable file
|
|
@ -1,103 +1,5 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Wallarm Deployment — Single Binary Bootstrap
|
||||
# ==============================================================================
|
||||
# Downloads the wallarm binary from the Git repository and places it in
|
||||
# ~/deploy/. One command to get started:
|
||||
#
|
||||
# curl -fsSL ".../deploy.sh" | bash
|
||||
# sudo ./deploy/wallarm
|
||||
#
|
||||
# The binary handles everything: preflight → TUI wizard → deployment →
|
||||
# dashboard with multi-node management.
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[1m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) BIN_ARCH="amd64" ;;
|
||||
aarch64|arm64) BIN_ARCH="arm64" ;;
|
||||
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
REPO="https://git.sechpoint.app"
|
||||
REPO_PATH="customer-engineering/wallarm"
|
||||
API_URL="${REPO}/api/v1/repos/${REPO_PATH}/releases"
|
||||
|
||||
# Fetch latest release info
|
||||
echo -e "${YELLOW}Checking latest release...${NC}"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
RELEASE_JSON=$(curl -fsSL "${API_URL}?limit=1" 2>/dev/null)
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
RELEASE_JSON=$(wget -qO- "${API_URL}?limit=1" 2>/dev/null)
|
||||
else
|
||||
echo -e "${RED}Neither curl nor wget found. Install one and re-run.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse release tag and asset URL
|
||||
RELEASE_TAG=$(echo "$RELEASE_JSON" | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
if [[ -z "$RELEASE_TAG" ]]; then
|
||||
echo -e "${RED}No release found. Create a Gitea release first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BIN_URL="${REPO}/api/v1/repos/${REPO_PATH}/releases/tags/${RELEASE_TAG}/assets"
|
||||
echo -e "${GREEN} Latest release: ${RELEASE_TAG}${NC}"
|
||||
|
||||
# Download the binary via release assets API
|
||||
ASSETS_JSON=$(curl -fsSL "${REPO}/api/v1/repos/${REPO_PATH}/releases/tags/${RELEASE_TAG}" 2>/dev/null)
|
||||
DOWNLOAD_URL=$(echo "$ASSETS_JSON" | grep -o "\"browser_download_url\":\"[^\"]*wallarm-linux-${BIN_ARCH}[^\"]*" | head -1 | cut -d'"' -f4)
|
||||
DEPLOY_DIR="${HOME:-/root}/deploy"
|
||||
BIN_PATH="${DEPLOY_DIR}/wallarm"
|
||||
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
echo -e "${BOLD}Wallarm Deployment Bootstrap${NC}"
|
||||
echo
|
||||
|
||||
# Download the binary
|
||||
if [[ -z "$DOWNLOAD_URL" ]]; then
|
||||
echo -e "${RED}No linux-${BIN_ARCH} binary found in release ${RELEASE_TAG}.${NC}"
|
||||
echo -e "${YELLOW}Available assets:${NC}"
|
||||
echo "$ASSETS_JSON" | grep -o '"name":"[^"]*"' | cut -d'"' -f4
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Downloading wallarm (${RELEASE_TAG}, linux-${BIN_ARCH})...${NC}"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --progress-bar "$DOWNLOAD_URL" -o "$BIN_PATH"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q --show-progress "$DOWNLOAD_URL" -O "$BIN_PATH"
|
||||
else
|
||||
echo -e "${RED}Neither curl nor wget found. Install one and re-run.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$BIN_PATH"
|
||||
echo -e "${GREEN} Success: wallarm installed to ${BIN_PATH}${NC}"
|
||||
echo
|
||||
|
||||
# Quick preflight (just to confirm binary works)
|
||||
echo -e "${YELLOW}Testing binary...${NC}"
|
||||
if "$BIN_PATH" --version 2>/dev/null; then
|
||||
echo -e "${GREEN} Binary OK${NC}"
|
||||
else
|
||||
echo -e "${RED} Binary verification failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "${GREEN}${BOLD}Ready!${NC}"
|
||||
echo
|
||||
echo -e " ${CYAN}sudo ${BIN_PATH}${NC} — Start the TUI (wizard on first run, dashboard after)"
|
||||
echo -e " ${CYAN}${BIN_PATH} --help${NC} — Show all commands"
|
||||
echo -e " ${CYAN}${BIN_PATH} --tunnel${NC} — Start remote access tunnel"
|
||||
export DOCKER_HOST=unix:///opt/fw/docker/docker.sock
|
||||
# Wallarm Node Manager — wrapper
|
||||
export PATH="/opt/fw/docker/bin:$PATH"
|
||||
/usr/bin/env python3 /opt/fw/app/main.py "$@"
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
# Docker Static Binaries
|
||||
|
||||
This directory contains Docker static binaries for offline installation.
|
||||
|
||||
- `docker-29.2.1.tgz`: Docker 29.2.1 static binary for x86_64
|
||||
- `docker-29.2.1.tgz.sha256`: SHA256 checksum for verification
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
# Verify integrity
|
||||
sha256sum -c docker-29.2.1.tgz.sha256
|
||||
|
||||
# Extract and install
|
||||
tar xzvf docker-29.2.1.tgz
|
||||
sudo cp docker/* /usr/bin/
|
||||
```
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Wallarm Docker Images
|
||||
|
||||
This directory contains Wallarm node Docker images for offline deployment.
|
||||
|
||||
- `wallarm-node-6.11.0-rc1.tar.gz`: Wallarm node version 6.11.0-rc1
|
||||
- `wallarm-node-6.11.0-rc1.tar.gz.sha256`: SHA256 checksum for verification
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
# Verify integrity
|
||||
sha256sum -c wallarm-node-6.11.0-rc1.tar.gz.sha256
|
||||
|
||||
# Load into Docker
|
||||
gunzip -c wallarm-node-6.11.0-rc1.tar.gz | docker load
|
||||
```
|
||||
|
|
@ -1,549 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# WALLARM PREFLIGHT CHECK SCRIPT - V1.3 (Docker deployment)
|
||||
# ==============================================================================
|
||||
# Purpose: Validate system readiness for Wallarm Docker deployment
|
||||
# Features:
|
||||
# - Non-interactive system validation (sudo, OS, architecture, init system)
|
||||
# - Network connectivity testing (US/EU cloud)
|
||||
# - Docker artifact source validation (Git Repositorys / local binaries/images)
|
||||
# - Outputs results to .env file for deployment script
|
||||
# - DAU-friendly error messages with remediation
|
||||
# ==============================================================================
|
||||
|
||||
# Script location and shared library (colors, logging, validation, detection, connectivity)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./wallarm-lib.sh
|
||||
source "$SCRIPT_DIR/wallarm-lib.sh"
|
||||
|
||||
# Strict error handling
|
||||
set -euo pipefail
|
||||
trap early_error_handler ERR
|
||||
|
||||
# Configuration
|
||||
ENV_FILE=".env"
|
||||
LOG_FILE="${HOME:-.}/logs/wallarm-check.log"
|
||||
|
||||
# Git Repositorys artifact URLs (primary source) - Docker deployment artifacts
|
||||
GIT_BASE_URL="https://git.sechpoint.app/customer-engineering/wallarm"
|
||||
GIT_RAW_URL="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main"
|
||||
GIT_DOCKER_BINARY_URL="${GIT_RAW_URL}/docker/binaries/docker-29.2.1.tgz"
|
||||
GIT_WALLARM_IMAGE_URL="${GIT_RAW_URL}/docker/images/wallarm-node-6.11.0-rc1.tar.gz"
|
||||
|
||||
# Local artifact directories (relative to script location)
|
||||
LOCAL_BINARY_DIR="${SCRIPT_DIR}/binaries"
|
||||
LOCAL_IMAGE_DIR="${SCRIPT_DIR}/images"
|
||||
|
||||
# Cloud endpoints (from Wallarm documentation)
|
||||
EU_DATA_NODES=("api.wallarm.com" "node-data0.eu1.wallarm.com" "node-data1.eu1.wallarm.com")
|
||||
US_DATA_NODES=("us1.api.wallarm.com" "node-data0.us1.wallarm.com" "node-data1.us1.wallarm.com")
|
||||
|
||||
# Global result tracking
|
||||
CHECK_RESULT="pass"
|
||||
CHECK_ERRORS=()
|
||||
GIT_REACHABLE="false"
|
||||
|
||||
# ==============================================================================
|
||||
# RESULT TRACKING & ENV FILE
|
||||
# ==============================================================================
|
||||
|
||||
add_error() {
|
||||
local error_msg="$1"
|
||||
CHECK_ERRORS+=("$error_msg")
|
||||
CHECK_RESULT="fail"
|
||||
log_message "ERROR" "$error_msg"
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local os_name="$1"
|
||||
local os_version="$2"
|
||||
local architecture="$3"
|
||||
local init_system="$4"
|
||||
local us_cloud_reachable="$5"
|
||||
local eu_cloud_reachable="$6"
|
||||
local registry_reachable="$7"
|
||||
local download_reachable="$8"
|
||||
local git_reachable="${9:-false}"
|
||||
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# Wallarm Preflight Check Results
|
||||
# Generated: $(date '+%Y-%m-%d %H:%M:%S')
|
||||
# Script: $0
|
||||
|
||||
result=$CHECK_RESULT
|
||||
os_name=$os_name
|
||||
os_version=$os_version
|
||||
architecture=$architecture
|
||||
init_system=$init_system
|
||||
us_cloud_reachable=$us_cloud_reachable
|
||||
eu_cloud_reachable=$eu_cloud_reachable
|
||||
registry_reachable=$registry_reachable
|
||||
download_reachable=$download_reachable
|
||||
git_reachable=$git_reachable
|
||||
|
||||
EOF
|
||||
|
||||
if [ ${#CHECK_ERRORS[@]} -gt 0 ]; then
|
||||
echo "# Errors:" >> "$ENV_FILE"
|
||||
for i in "${!CHECK_ERRORS[@]}"; do
|
||||
echo "error_$i=\"${CHECK_ERRORS[$i]}\"" >> "$ENV_FILE"
|
||||
done
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Check results written to $ENV_FILE"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PRE-FLIGHT VALIDATION FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
validate_sudo_access() {
|
||||
log_message "INFO" "Validating sudo access..."
|
||||
|
||||
local os_name
|
||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
add_error "sudo command not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$os_name" = "darwin" ]; then
|
||||
log_message "WARNING" "macOS detected: sudo authentication test skipped (may prompt during deployment)"
|
||||
log_message "INFO" "Note: macOS is not a supported deployment target. This check is for Linux servers."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! sudo -v; then
|
||||
add_error "sudo authentication failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Sudo access validated"
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_required_commands() {
|
||||
log_message "INFO" "Validating required system commands..."
|
||||
|
||||
local missing_commands=()
|
||||
|
||||
local os_name
|
||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Core commands required for both check and deployment scripts
|
||||
local core_commands=(
|
||||
"tar" # Required for extracting Docker binaries in deployment
|
||||
"curl" # Required for connectivity testing
|
||||
"grep" # Used extensively
|
||||
"cut" # Used for parsing output
|
||||
"tr" # Used for text transformations
|
||||
"sed" # Used for text processing
|
||||
"head" # Used for limiting output
|
||||
"tail" # Used for limiting output
|
||||
"ls" # Used for file listing
|
||||
"date" # Used for logging timestamps
|
||||
"mkdir" # Used for creating directories
|
||||
"chmod" # Used for permission changes
|
||||
"stat" # Used for file information (required for file size checks)
|
||||
"tee" # Required for writing configuration files
|
||||
"cp" # Required for copying Docker binaries
|
||||
"rm" # Required for cleanup operations
|
||||
)
|
||||
|
||||
# Linux-specific commands (not available on macOS)
|
||||
if [ "$os_name" != "darwin" ]; then
|
||||
core_commands+=(
|
||||
"getent" # Required for checking group existence
|
||||
"groupadd" # Required for creating docker group (sudo)
|
||||
"usermod" # Required for adding user to docker group (sudo)
|
||||
"iptables" # Required for Docker network bridge creation (Docker static binaries v1.4+)
|
||||
)
|
||||
fi
|
||||
|
||||
# Check each core command (command_exists comes from common library)
|
||||
local cmd
|
||||
for cmd in "${core_commands[@]}"; do
|
||||
if ! command_exists "$cmd"; then
|
||||
missing_commands+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for port checking utility (ss or netstat)
|
||||
if [ "$os_name" != "darwin" ]; then
|
||||
if ! command_exists ss && ! command_exists netstat; then
|
||||
missing_commands+=("ss or netstat")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect init system and validate its control command
|
||||
if [ "$os_name" != "darwin" ]; then
|
||||
local init_system
|
||||
init_system=$(detect_init_system)
|
||||
|
||||
case "$init_system" in
|
||||
"systemd")
|
||||
if ! command_exists systemctl; then
|
||||
missing_commands+=("systemctl")
|
||||
fi
|
||||
;;
|
||||
"openrc")
|
||||
if ! command_exists rc-service; then
|
||||
missing_commands+=("rc-service")
|
||||
fi
|
||||
;;
|
||||
"sysvinit")
|
||||
if ! command_exists service; then
|
||||
missing_commands+=("service")
|
||||
fi
|
||||
;;
|
||||
"upstart")
|
||||
if ! command_exists initctl; then
|
||||
missing_commands+=("initctl")
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log_message "WARNING" "Unknown init system '$init_system', cannot validate init command"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
log_message "INFO" "Skipping init system validation on macOS (not a deployment target)"
|
||||
fi
|
||||
|
||||
if [ ${#missing_commands[@]} -gt 0 ]; then
|
||||
local missing_list
|
||||
missing_list=$(IFS=', '; echo "${missing_commands[*]}")
|
||||
add_error "Missing required commands: $missing_list"
|
||||
log_message "ERROR" "Please install missing commands and run the check again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Special check: iptables version must be 1.4 or higher for Docker static binaries
|
||||
if [ "$os_name" != "darwin" ]; then
|
||||
log_message "INFO" "Checking iptables version (requires 1.4+ for Docker)..."
|
||||
if command_exists iptables; then
|
||||
local iptables_version
|
||||
iptables_version=$(iptables --version 2>/dev/null | head -1 | grep -o '[0-9]\+\.[0-9]\+' | head -1)
|
||||
if [ -n "$iptables_version" ]; then
|
||||
log_message "INFO" "Found iptables version $iptables_version"
|
||||
local major_version minor_version
|
||||
major_version=$(echo "$iptables_version" | cut -d. -f1)
|
||||
minor_version=$(echo "$iptables_version" | cut -d. -f2)
|
||||
|
||||
if [ "$major_version" -lt 1 ] || ([ "$major_version" -eq 1 ] && [ "$minor_version" -lt 4 ]); then
|
||||
add_error "iptables version $iptables_version is too old. Docker requires iptables 1.4 or higher."
|
||||
log_message "ERROR" "Please upgrade iptables to version 1.4 or higher."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_message "WARNING" "Could not determine iptables version, continuing anyway"
|
||||
fi
|
||||
else
|
||||
add_error "iptables command not found (required for Docker network bridge)"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_message "INFO" "Skipping iptables check on macOS (not a deployment target)"
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "All required system commands are available"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# NETWORK CONNECTIVITY & ARTIFACT SOURCE TESTING
|
||||
# ==============================================================================
|
||||
|
||||
perform_network_tests() {
|
||||
log_message "INFO" "=== NETWORK CONNECTIVITY TESTING ==="
|
||||
|
||||
# Test US cloud endpoints
|
||||
local us_reachable
|
||||
us_reachable=$(test_cloud_endpoints "US" "${US_DATA_NODES[@]}")
|
||||
|
||||
# Test EU cloud endpoints
|
||||
local eu_reachable
|
||||
eu_reachable=$(test_cloud_endpoints "EU" "${EU_DATA_NODES[@]}")
|
||||
|
||||
local registry_reachable="false"
|
||||
local download_reachable="false"
|
||||
|
||||
# Check for local fallback resources (multiple locations)
|
||||
log_message "INFO" "Checking for local artifact fallback resources..."
|
||||
|
||||
# Docker binary locations (priority: local binaries directory -> current directory)
|
||||
local has_local_docker=false
|
||||
local docker_sources=()
|
||||
|
||||
if [ -d "$LOCAL_BINARY_DIR" ]; then
|
||||
log_message "INFO" "Checking local binaries directory: $LOCAL_BINARY_DIR"
|
||||
local binary_files
|
||||
binary_files=$(ls "$LOCAL_BINARY_DIR"/*.tgz 2>/dev/null | head -5)
|
||||
if [ -n "$binary_files" ]; then
|
||||
log_message "SUCCESS" "Found local Docker binaries in $LOCAL_BINARY_DIR:"
|
||||
while IFS= read -r file; do
|
||||
log_message "SUCCESS" " - $(basename "$file")"
|
||||
done <<< "$binary_files"
|
||||
has_local_docker=true
|
||||
docker_sources+=("$LOCAL_BINARY_DIR/")
|
||||
fi
|
||||
fi
|
||||
|
||||
local current_docker_files
|
||||
current_docker_files=$(ls docker-*.tgz 2>/dev/null | head -5)
|
||||
if [ -n "$current_docker_files" ]; then
|
||||
log_message "SUCCESS" "Found local Docker binaries in current directory:"
|
||||
while IFS= read -r file; do
|
||||
log_message "SUCCESS" " - $file"
|
||||
done <<< "$current_docker_files"
|
||||
has_local_docker=true
|
||||
docker_sources+=("current directory")
|
||||
fi
|
||||
|
||||
if [ "$has_local_docker" = "false" ]; then
|
||||
log_message "WARNING" "No local Docker binaries found in $LOCAL_BINARY_DIR/ or current directory"
|
||||
else
|
||||
log_message "INFO" "Docker binary sources: ${docker_sources[*]}"
|
||||
fi
|
||||
|
||||
# Wallarm image locations (priority: local images directory -> current directory)
|
||||
local has_local_wallarm=false
|
||||
local wallarm_sources=()
|
||||
|
||||
if [ -d "$LOCAL_IMAGE_DIR" ]; then
|
||||
log_message "INFO" "Checking local images directory: $LOCAL_IMAGE_DIR"
|
||||
local image_files
|
||||
image_files=$(ls "$LOCAL_IMAGE_DIR"/*.tar.gz "$LOCAL_IMAGE_DIR"/*.tar 2>/dev/null | head -5)
|
||||
if [ -n "$image_files" ]; then
|
||||
log_message "SUCCESS" "Found local Wallarm images in $LOCAL_IMAGE_DIR:"
|
||||
while IFS= read -r file; do
|
||||
log_message "SUCCESS" " - $(basename "$file")"
|
||||
done <<< "$image_files"
|
||||
has_local_wallarm=true
|
||||
wallarm_sources+=("$LOCAL_IMAGE_DIR/")
|
||||
fi
|
||||
fi
|
||||
|
||||
local current_image_files
|
||||
current_image_files=$(ls wallarm-node-*.tar.gz wallarm-node-*.tar 2>/dev/null | head -5)
|
||||
if [ -n "$current_image_files" ]; then
|
||||
log_message "SUCCESS" "Found local Wallarm images in current directory:"
|
||||
while IFS= read -r file; do
|
||||
log_message "SUCCESS" " - $file"
|
||||
done <<< "$current_image_files"
|
||||
has_local_wallarm=true
|
||||
wallarm_sources+=("current directory")
|
||||
fi
|
||||
|
||||
if [ "$has_local_wallarm" = "false" ]; then
|
||||
log_message "WARNING" "No local Wallarm images found in $LOCAL_IMAGE_DIR/ or current directory"
|
||||
else
|
||||
log_message "INFO" "Wallarm image sources: ${wallarm_sources[*]}"
|
||||
fi
|
||||
|
||||
echo "$us_reachable:$eu_reachable:$registry_reachable:$download_reachable"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# MAIN FUNCTION
|
||||
# ==============================================================================
|
||||
|
||||
main() {
|
||||
clear
|
||||
echo -e "${BLUE}${BOLD}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ WALLARM PREFLIGHT CHECK SCRIPT (Docker) - V1.3 ║"
|
||||
echo "║ System Readiness Validation for Deployment ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "\n${YELLOW}Starting preflight check at: $(date)${NC}"
|
||||
|
||||
# Initialize logging
|
||||
local log_dir="${HOME:-.}/logs"
|
||||
if [ ! -d "$log_dir" ]; then
|
||||
if ! mkdir -p "$log_dir"; then
|
||||
echo -e "${YELLOW}Cannot create log directory $log_dir, falling back to current directory...${NC}"
|
||||
log_dir="."
|
||||
fi
|
||||
fi
|
||||
|
||||
LOG_FILE="$log_dir/wallarm-check.log"
|
||||
if ! : > "$LOG_FILE"; then
|
||||
echo -e "${RED}Cannot create log file at $LOG_FILE${NC}"
|
||||
echo -e "${YELLOW}Falling back to current directory...${NC}"
|
||||
LOG_FILE="./wallarm-check.log"
|
||||
: > "$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if ! chmod 644 "$LOG_FILE" 2>/dev/null; then
|
||||
echo -e "${YELLOW}Warning: Could not set permissions on log file${NC}"
|
||||
fi
|
||||
|
||||
log_message "INFO" "=== Wallarm Preflight Check Started ==="
|
||||
|
||||
if [ "$INSECURE_SSL" = "1" ]; then
|
||||
log_message "WARNING" "SSL certificate validation is DISABLED (insecure). Set WALLARM_INSECURE_SSL=0 to enable validation."
|
||||
fi
|
||||
|
||||
# Phase 1: System validation
|
||||
log_message "INFO" "=== PHASE 1: SYSTEM VALIDATION ==="
|
||||
|
||||
if ! validate_required_commands; then
|
||||
add_error "Required system commands validation failed"
|
||||
fi
|
||||
|
||||
if ! validate_sudo_access; then
|
||||
add_error "Sudo access validation failed"
|
||||
fi
|
||||
|
||||
local os_info
|
||||
os_info=$(detect_os_and_version)
|
||||
local os_name
|
||||
os_name=$(echo "$os_info" | cut -d: -f1)
|
||||
local os_version
|
||||
os_version=$(echo "$os_info" | cut -d: -f2)
|
||||
|
||||
local architecture
|
||||
architecture=$(detect_architecture)
|
||||
if [ "$architecture" = "unknown" ]; then
|
||||
add_error "Unsupported architecture detected"
|
||||
fi
|
||||
|
||||
local init_system
|
||||
init_system=$(detect_init_system)
|
||||
|
||||
log_message "SUCCESS" "System validation completed:"
|
||||
log_message "SUCCESS" " OS: $os_name $os_version"
|
||||
log_message "SUCCESS" " Architecture: $architecture"
|
||||
log_message "SUCCESS" " Init System: $init_system"
|
||||
|
||||
# Phase 2: Network connectivity testing
|
||||
log_message "INFO" "=== PHASE 2: NETWORK CONNECTIVITY TESTING ==="
|
||||
|
||||
log_message "INFO" "Testing connectivity to Git Repositorys artifact repository..."
|
||||
GIT_REACHABLE="false"
|
||||
if test_connectivity "$GIT_DOCKER_BINARY_URL" "Git Repositorys Docker artifact"; then
|
||||
GIT_REACHABLE="true"
|
||||
log_message "SUCCESS" "Git Repositorys Docker artifact is reachable (primary source)"
|
||||
else
|
||||
log_message "WARNING" "Git Repositorys Docker artifact is not reachable - will use fallback sources"
|
||||
fi
|
||||
|
||||
local network_results
|
||||
network_results=$(perform_network_tests)
|
||||
local us_reachable
|
||||
us_reachable=$(echo "$network_results" | cut -d: -f1)
|
||||
local eu_reachable
|
||||
eu_reachable=$(echo "$network_results" | cut -d: -f2)
|
||||
local registry_reachable
|
||||
registry_reachable=$(echo "$network_results" | cut -d: -f3)
|
||||
local download_reachable
|
||||
download_reachable=$(echo "$network_results" | cut -d: -f4)
|
||||
|
||||
# Critical check: Need at least one source for Docker and Wallarm
|
||||
# Priority: Git Repositorys (primary) -> local files
|
||||
|
||||
if [ "$GIT_REACHABLE" = "true" ]; then
|
||||
log_message "SUCCESS" "Git Repositorys artifact repository is reachable (primary source available)"
|
||||
else
|
||||
log_message "WARNING" "Git Repositorys artifact repository is not reachable - checking fallback sources"
|
||||
|
||||
local has_local_docker=false
|
||||
local has_local_wallarm=false
|
||||
|
||||
if [ -d "$LOCAL_BINARY_DIR" ] && [ -n "$(ls "$LOCAL_BINARY_DIR"/*.tgz 2>/dev/null)" ]; then
|
||||
has_local_docker=true
|
||||
log_message "INFO" "Found local Docker binaries in $LOCAL_BINARY_DIR/"
|
||||
elif [ -n "$(ls docker-*.tgz 2>/dev/null)" ]; then
|
||||
has_local_docker=true
|
||||
log_message "INFO" "Found local Docker binaries in current directory"
|
||||
fi
|
||||
|
||||
if [ -d "$LOCAL_IMAGE_DIR" ] && [ -n "$(ls "$LOCAL_IMAGE_DIR"/*.tar.gz "$LOCAL_IMAGE_DIR"/*.tar 2>/dev/null)" ]; then
|
||||
has_local_wallarm=true
|
||||
log_message "INFO" "Found local Wallarm images in $LOCAL_IMAGE_DIR/"
|
||||
elif [ -n "$(ls wallarm-node-*.tar.gz wallarm-node-*.tar 2>/dev/null)" ]; then
|
||||
has_local_wallarm=true
|
||||
log_message "INFO" "Found local Wallarm images in current directory"
|
||||
fi
|
||||
|
||||
local has_sufficient_resources=true
|
||||
|
||||
if [ "$has_local_docker" = "false" ]; then
|
||||
log_message "ERROR" "No Docker binary source available"
|
||||
log_message "ERROR" " - Git Repositorys artifacts unreachable: $GIT_RAW_URL"
|
||||
log_message "ERROR" " - Local binaries not found in $LOCAL_BINARY_DIR/ or current directory"
|
||||
|
||||
has_sufficient_resources=false
|
||||
fi
|
||||
|
||||
if [ "$has_local_wallarm" = "false" ]; then
|
||||
log_message "ERROR" "No Wallarm image source available"
|
||||
log_message "ERROR" " - Git Repositorys artifacts unreachable: $GIT_RAW_URL"
|
||||
log_message "ERROR" " - Local images not found in $LOCAL_IMAGE_DIR/ or current directory"
|
||||
|
||||
has_sufficient_resources=false
|
||||
fi
|
||||
|
||||
if [ "$has_sufficient_resources" = "false" ]; then
|
||||
add_error "Insufficient resources: Need at least one source for Docker and Wallarm artifacts.
|
||||
|
||||
Possible sources:
|
||||
1. Git Repositorys (primary): Ensure network access to $GIT_RAW_URL
|
||||
2. Local files: Place artifacts in:
|
||||
- Docker binary: $LOCAL_BINARY_DIR/docker-29.2.1.tgz or current directory
|
||||
- Wallarm image: $LOCAL_IMAGE_DIR/wallarm-node-6.11.0-rc1.tar.gz or current directory"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Network testing completed:"
|
||||
log_message "SUCCESS" " Git Repositorys Artifacts Reachable: $GIT_REACHABLE"
|
||||
log_message "SUCCESS" " US Cloud Reachable: $us_reachable"
|
||||
log_message "SUCCESS" " EU Cloud Reachable: $eu_reachable"
|
||||
log_message "SUCCESS" " Fallback Registry Reachable: $registry_reachable"
|
||||
log_message "SUCCESS" " Fallback Download Reachable: $download_reachable"
|
||||
|
||||
# Phase 3: Write results
|
||||
log_message "INFO" "=== PHASE 3: WRITING RESULTS ==="
|
||||
|
||||
write_env_file "$os_name" "$os_version" "$architecture" "$init_system" \
|
||||
"$us_reachable" "$eu_reachable" "$registry_reachable" "$download_reachable" \
|
||||
"$GIT_REACHABLE"
|
||||
|
||||
# Final summary
|
||||
if [ "$CHECK_RESULT" = "pass" ]; then
|
||||
log_message "SUCCESS" "=== PREFLIGHT CHECK PASSED ==="
|
||||
echo -e "\n${GREEN}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}${BOLD}║ PREFLIGHT CHECK PASSED - SYSTEM READY ║${NC}"
|
||||
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "\n${CYAN}System is ready for Wallarm Docker deployment.${NC}"
|
||||
echo -e "${YELLOW}Check results: $ENV_FILE${NC}"
|
||||
echo -e "${YELLOW}Full log: $LOG_FILE${NC}"
|
||||
echo -e "\n${GREEN}Next step: Run ./docker/wallarm-ct-deploy.sh to proceed with deployment${NC}"
|
||||
exit 0
|
||||
else
|
||||
log_message "ERROR" "=== PREFLIGHT CHECK FAILED ==="
|
||||
echo -e "\n${RED}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${RED}${BOLD}║ PREFLIGHT CHECK FAILED - SYSTEM NOT READY ║${NC}"
|
||||
echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "\n${YELLOW}${BOLD}Issues found:${NC}"
|
||||
for error in "${CHECK_ERRORS[@]}"; do
|
||||
echo -e " ${RED}•${NC} $error"
|
||||
done
|
||||
echo -e "\n${YELLOW}Check results: $ENV_FILE${NC}"
|
||||
echo -e "${YELLOW}Full log: $LOG_FILE${NC}"
|
||||
echo -e "\n${CYAN}Please fix the issues above and run the check again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SCRIPT EXECUTION
|
||||
# ==============================================================================
|
||||
|
||||
# Ensure we're in bash
|
||||
if [ -z "$BASH_VERSION" ]; then
|
||||
echo "Error: This script must be run with bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,268 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# WALLARM RECONFIGURATION SCRIPT - V1.1 (Docker deployment)
|
||||
# ==============================================================================
|
||||
# Purpose: Modify nginx configuration of an existing Wallarm Docker node
|
||||
# Features:
|
||||
# - Update set_real_ip_from (trusted proxy IPs/CIDRs)
|
||||
# - Change wallarm_mode (monitoring/block)
|
||||
# - Backup current config before changes
|
||||
# - Interactive prompts with validation
|
||||
# ==============================================================================
|
||||
|
||||
# Script location and shared library (colors, logging, validation)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./wallarm-lib.sh
|
||||
source "$SCRIPT_DIR/wallarm-lib.sh"
|
||||
|
||||
set -euo pipefail
|
||||
trap early_error_handler ERR
|
||||
|
||||
# ==============================================================================
|
||||
# CHECK FOR SUDO / ROOT PRIVILEGES
|
||||
# ==============================================================================
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}${BOLD}ERROR:${NC} This script must be run with sudo or as root."
|
||||
echo -e "${YELLOW}Please run: sudo $0${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================
|
||||
INSTANCE_DIR="/opt"
|
||||
INSTANCE_NAME=""
|
||||
|
||||
# ==============================================================================
|
||||
# FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# Function to find the Wallarm instance directory
|
||||
find_wallarm_instance() {
|
||||
local dirs=()
|
||||
while IFS= read -r dir; do
|
||||
if [[ -d "$dir" && -f "$dir/nginx.conf" && -f "$dir/start.sh" ]]; then
|
||||
dirs+=("$dir")
|
||||
fi
|
||||
done < <(find "$INSTANCE_DIR" -maxdepth 1 -type d -name "wallarm-*" 2>/dev/null)
|
||||
|
||||
if [ ${#dirs[@]} -eq 0 ]; then
|
||||
echo -e "${RED}No Wallarm instance found in $INSTANCE_DIR.${NC}"
|
||||
exit 1
|
||||
elif [ ${#dirs[@]} -eq 1 ]; then
|
||||
INSTANCE_DIR="${dirs[0]}"
|
||||
INSTANCE_NAME=$(basename "$INSTANCE_DIR")
|
||||
echo -e "${GREEN}Found instance: $INSTANCE_NAME${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Multiple Wallarm instances found:${NC}"
|
||||
for i in "${!dirs[@]}"; do
|
||||
echo "$((i+1)). $(basename "${dirs[$i]}")"
|
||||
done
|
||||
read -r -p "Select instance number: " choice
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#dirs[@]} ]; then
|
||||
INSTANCE_DIR="${dirs[$((choice-1))]}"
|
||||
INSTANCE_NAME=$(basename "$INSTANCE_DIR")
|
||||
else
|
||||
echo -e "${RED}Invalid selection.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate IP/CIDR format
|
||||
validate_proxy() {
|
||||
local proxy="$1"
|
||||
if [[ "$proxy" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
|
||||
IFS='/' read -r ip cidr <<< "$proxy"
|
||||
IFS='.' read -r o1 o2 o3 o4 <<< "$ip"
|
||||
if [ "$o1" -le 255 ] && [ "$o2" -le 255 ] && [ "$o3" -le 255 ] && [ "$o4" -le 255 ]; then
|
||||
if [ -z "$cidr" ] || ( [ "$cidr" -ge 0 ] && [ "$cidr" -le 32 ] ); then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parse current configuration to get existing values
|
||||
parse_current_config() {
|
||||
local config_file="$1"
|
||||
# Get current wallarm_mode
|
||||
current_mode=$(grep -oP 'wallarm_mode\s+\K\S+' "$config_file" | head -1)
|
||||
# Get current set_real_ip_from lines
|
||||
current_proxies=$(grep -oP 'set_real_ip_from\s+\K\S+' "$config_file")
|
||||
}
|
||||
|
||||
# Update configuration
|
||||
update_config() {
|
||||
local config_file="$1"
|
||||
local backup_file="$config_file.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
echo -e "${YELLOW}Backing up current config to $backup_file${NC}"
|
||||
cp "$config_file" "$backup_file"
|
||||
|
||||
# Read new values interactively
|
||||
echo -e "\n${CYAN}${BOLD}Current set_real_ip_from entries:${NC}"
|
||||
if [ -n "$current_proxies" ]; then
|
||||
echo "$current_proxies" | while read -r proxy; do
|
||||
echo " $proxy"
|
||||
done
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}Do you want to change the trusted proxy IPs/CIDRs? (y/N)${NC}"
|
||||
read -r change_proxy
|
||||
if [[ "$change_proxy" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Enter new trusted proxy IPs/CIDRs (space-separated, or empty to remove all):${NC}"
|
||||
read -r new_proxies_input
|
||||
new_proxies=()
|
||||
if [[ -n "$new_proxies_input" ]]; then
|
||||
IFS=' ' read -ra proxy_array <<< "$new_proxies_input"
|
||||
for proxy in "${proxy_array[@]}"; do
|
||||
proxy=$(echo "$proxy" | xargs)
|
||||
if validate_proxy "$proxy"; then
|
||||
new_proxies+=("$proxy")
|
||||
else
|
||||
echo -e "${RED}Invalid format: $proxy. Skipping.${NC}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Keep existing
|
||||
while read -r proxy; do
|
||||
new_proxies+=("$proxy")
|
||||
done <<< "$current_proxies"
|
||||
fi
|
||||
|
||||
echo -e "\n${CYAN}${BOLD}Current wallarm_mode:${NC} ${current_mode:-not set}"
|
||||
echo -e "${YELLOW}Do you want to change the wallarm_mode? (y/N)${NC}"
|
||||
read -r change_mode
|
||||
if [[ "$change_mode" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Select new mode:${NC}"
|
||||
echo "1. monitoring"
|
||||
echo "2. block"
|
||||
read -r mode_choice
|
||||
case "$mode_choice" in
|
||||
1) new_mode="monitoring" ;;
|
||||
2) new_mode="block" ;;
|
||||
*) echo -e "${RED}Invalid choice, keeping current mode.${NC}"; new_mode="$current_mode" ;;
|
||||
esac
|
||||
else
|
||||
new_mode="$current_mode"
|
||||
fi
|
||||
|
||||
# Now rebuild the config file
|
||||
# We'll create a temporary file and replace the original
|
||||
temp_config=$(mktemp)
|
||||
|
||||
# Read original config line by line and modify as needed
|
||||
in_server_block=false
|
||||
while IFS= read -r line; do
|
||||
# Detect start of server block
|
||||
if [[ "$line" =~ ^[[:space:]]*server[[:space:]]*{ ]]; then
|
||||
in_server_block=true
|
||||
fi
|
||||
|
||||
# If we are inside server block, we may need to replace lines
|
||||
if $in_server_block; then
|
||||
# Replace set_real_ip_from lines with new ones
|
||||
if [[ "$line" =~ ^[[:space:]]*set_real_ip_from[[:space:]]+ ]]; then
|
||||
# Skip original set_real_ip_from lines (will be added later)
|
||||
continue
|
||||
fi
|
||||
# Replace wallarm_mode line
|
||||
if [[ "$line" =~ ^[[:space:]]*wallarm_mode[[:space:]]+ ]]; then
|
||||
# We'll add new line after processing all lines
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write line to temp file
|
||||
echo "$line" >> "$temp_config"
|
||||
|
||||
# After writing the line, if we are at the end of the server block, we may need to insert new directives
|
||||
if $in_server_block && [[ "$line" =~ ^[[:space:]]*}$ ]]; then
|
||||
in_server_block=false
|
||||
# Insert the new set_real_ip_from lines just before the closing brace
|
||||
if [ ${#new_proxies[@]} -gt 0 ]; then
|
||||
for proxy in "${new_proxies[@]}"; do
|
||||
echo " set_real_ip_from $proxy;" >> "$temp_config"
|
||||
done
|
||||
echo " real_ip_header X-Real-IP;" >> "$temp_config"
|
||||
echo " real_ip_recursive on;" >> "$temp_config"
|
||||
elif [ -n "$current_proxies" ]; then
|
||||
# If we removed all proxies, we should also remove the real_ip_header and real_ip_recursive lines
|
||||
# But that's tricky; we'll just not add them, but they might remain in the file if they were separate.
|
||||
# Simpler: after rebuild, we need to ensure they are not there. We'll do a final cleanup.
|
||||
echo -e "${YELLOW}Removing all set_real_ip_from directives.${NC}"
|
||||
fi
|
||||
# Insert new wallarm_mode
|
||||
if [ -n "$new_mode" ]; then
|
||||
echo " wallarm_mode $new_mode;" >> "$temp_config"
|
||||
fi
|
||||
fi
|
||||
done < "$config_file"
|
||||
|
||||
# After building the temp file, we need to ensure any leftover real_ip_header lines are removed if no proxies.
|
||||
if [ ${#new_proxies[@]} -eq 0 ]; then
|
||||
# Remove lines containing real_ip_header and real_ip_recursive if they exist
|
||||
sed -i '/real_ip_header/d' "$temp_config"
|
||||
sed -i '/real_ip_recursive/d' "$temp_config"
|
||||
fi
|
||||
|
||||
# Replace the original config with the new one
|
||||
mv "$temp_config" "$config_file"
|
||||
chmod 644 "$config_file"
|
||||
|
||||
echo -e "${GREEN}Configuration updated.${NC}"
|
||||
}
|
||||
|
||||
restart_container() {
|
||||
local container_name="$1"
|
||||
echo -e "${YELLOW}Restarting container $container_name to apply changes...${NC}"
|
||||
if docker ps --format "{{.Names}}" | grep -q "^$container_name$"; then
|
||||
docker restart "$container_name"
|
||||
echo -e "${GREEN}Container restarted.${NC}"
|
||||
else
|
||||
echo -e "${RED}Container $container_name is not running. Starting it...${NC}"
|
||||
if [ -f "$INSTANCE_DIR/start.sh" ]; then
|
||||
"$INSTANCE_DIR/start.sh"
|
||||
else
|
||||
echo -e "${RED}No start script found. Please start manually: docker start $container_name${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "${BLUE}${BOLD}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ WALLARM RECONFIGURATION SCRIPT - V1.0 ║"
|
||||
echo "║ Modify nginx.conf (trusted proxies / mode) ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
find_wallarm_instance
|
||||
|
||||
local config_file="$INSTANCE_DIR/nginx.conf"
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo -e "${RED}Configuration file not found: $config_file${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
parse_current_config "$config_file"
|
||||
update_config "$config_file"
|
||||
|
||||
echo -e "${YELLOW}Do you want to restart the container now? (Y/n)${NC}"
|
||||
read -r restart_choice
|
||||
if [[ ! "$restart_choice" =~ ^[Nn]$ ]]; then
|
||||
restart_container "$INSTANCE_NAME"
|
||||
else
|
||||
echo -e "${YELLOW}Changes will take effect after container restart.${NC}"
|
||||
echo -e "You can restart later with: docker restart $INSTANCE_NAME"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}${BOLD}Reconfiguration completed.${NC}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,512 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# WALLARM UNINSTALL SCRIPT - V1.1 (Docker deployment)
|
||||
# ==============================================================================
|
||||
# Purpose: Safely remove a Wallarm Docker node and cleanup Docker installation
|
||||
# Features:
|
||||
# - Interactive confirmation with safety checks
|
||||
# - Stops and removes Wallarm container and image
|
||||
# - Removes Docker service files created by deployment script
|
||||
# - Optional cleanup of Docker binaries (if no other containers exist)
|
||||
# - Preserves user data and logs (with option to remove)
|
||||
# - DAU-friendly warnings and confirmations
|
||||
# ==============================================================================
|
||||
|
||||
# Script location and shared library (colors, logging, validation)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./wallarm-lib.sh
|
||||
source "$SCRIPT_DIR/wallarm-lib.sh"
|
||||
|
||||
# Strict error handling
|
||||
set -euo pipefail
|
||||
trap early_error_handler ERR
|
||||
|
||||
# ==============================================================================
|
||||
# FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# Ask for confirmation
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
local default="${2:-n}"
|
||||
local options="[y/N]"
|
||||
|
||||
if [ "$default" = "y" ]; then
|
||||
options="[Y/n]"
|
||||
fi
|
||||
|
||||
echo -e -n "${YELLOW}${prompt} ${options}${NC} "
|
||||
read -r response
|
||||
|
||||
case "$response" in
|
||||
[yY][eE][sS]|[yY])
|
||||
return 0
|
||||
;;
|
||||
[nN][oO]|[nN])
|
||||
return 1
|
||||
;;
|
||||
"")
|
||||
# Use default
|
||||
if [ "$default" = "y" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Invalid input, treat as no
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check if running as root or with sudo
|
||||
check_sudo() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_message "INFO" "This script requires sudo privileges"
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
log_message "INFO" "Please enter your sudo password when prompted"
|
||||
sudo -v
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect init system
|
||||
detect_init_system() {
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
|
||||
echo "systemd"
|
||||
elif [ -d /run/openrc ]; then
|
||||
echo "openrc"
|
||||
elif [ -f /etc/init.d/docker ]; then
|
||||
echo "sysvinit"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if Docker is installed and running
|
||||
check_docker() {
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
log_message "WARNING" "Docker command not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! sudo docker info >/dev/null 2>&1; then
|
||||
log_message "WARNING" "Docker is not running"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check for other Docker containers (besides Wallarm)
|
||||
check_other_containers() {
|
||||
local wallarm_container="wallarm-node"
|
||||
local all_containers
|
||||
all_containers=$(sudo docker ps -a -q 2>/dev/null | wc -l)
|
||||
local wallarm_containers
|
||||
wallarm_containers=$(sudo docker ps -a --filter "name=${wallarm_container}" -q 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$all_containers" -gt "$wallarm_containers" ]; then
|
||||
log_message "WARNING" "Found other Docker containers besides Wallarm"
|
||||
sudo docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" | grep -v "$wallarm_container" || true
|
||||
return 0 # Other containers exist
|
||||
fi
|
||||
|
||||
return 1 # Only Wallarm containers or no containers
|
||||
}
|
||||
|
||||
# Stop and remove Wallarm container
|
||||
remove_wallarm_container() {
|
||||
local container_name="wallarm-node"
|
||||
|
||||
log_message "INFO" "Looking for Wallarm container..."
|
||||
|
||||
if sudo docker ps -a --filter "name=${container_name}" --format "{{.Names}}" | grep -q "${container_name}"; then
|
||||
log_message "INFO" "Found Wallarm container: ${container_name}"
|
||||
|
||||
# Stop container if running
|
||||
if sudo docker ps --filter "name=${container_name}" --filter "status=running" --format "{{.Names}}" | grep -q "${container_name}"; then
|
||||
log_message "INFO" "Stopping Wallarm container..."
|
||||
sudo docker stop "${container_name}" || {
|
||||
log_message "WARNING" "Failed to stop container, attempting force stop"
|
||||
sudo docker kill "${container_name}" 2>/dev/null || true
|
||||
}
|
||||
fi
|
||||
|
||||
# Remove container
|
||||
log_message "INFO" "Removing Wallarm container..."
|
||||
sudo docker rm -f "${container_name}" 2>/dev/null || {
|
||||
log_message "WARNING" "Failed to remove container, it may already be removed"
|
||||
}
|
||||
|
||||
log_message "SUCCESS" "Wallarm container removed"
|
||||
else
|
||||
log_message "INFO" "No Wallarm container found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove Wallarm image
|
||||
remove_wallarm_image() {
|
||||
local image_name="wallarm/node"
|
||||
|
||||
log_message "INFO" "Looking for Wallarm image..."
|
||||
|
||||
if sudo docker images --format "{{.Repository}}" | grep -q "^${image_name}"; then
|
||||
log_message "INFO" "Found Wallarm image: ${image_name}"
|
||||
|
||||
# Check if image is used by any containers
|
||||
local used_by
|
||||
used_by=$(sudo docker ps -a --filter "ancestor=${image_name}" -q 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$used_by" -gt 0 ]; then
|
||||
log_message "WARNING" "Image ${image_name} is still in use by containers, skipping removal"
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove image
|
||||
log_message "INFO" "Removing Wallarm image..."
|
||||
sudo docker rmi "${image_name}:latest" 2>/dev/null || {
|
||||
log_message "WARNING" "Failed to remove image, it may be in use or already removed"
|
||||
}
|
||||
|
||||
# Also try to remove by ID if tag removal failed
|
||||
local image_id
|
||||
image_id=$(sudo docker images --filter "reference=${image_name}" --format "{{.ID}}" 2>/dev/null | head -1)
|
||||
if [ -n "$image_id" ]; then
|
||||
sudo docker rmi -f "$image_id" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Wallarm image removed"
|
||||
else
|
||||
log_message "INFO" "No Wallarm image found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove Docker service files (created by deployment script)
|
||||
remove_docker_service_files() {
|
||||
local init_system
|
||||
init_system=$(detect_init_system)
|
||||
|
||||
log_message "INFO" "Removing Docker service files for init system: ${init_system}"
|
||||
|
||||
case "$init_system" in
|
||||
"systemd")
|
||||
# Stop and disable Docker service
|
||||
if sudo systemctl is-active docker --quiet 2>/dev/null; then
|
||||
log_message "INFO" "Stopping Docker service..."
|
||||
sudo systemctl stop docker 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if sudo systemctl is-enabled docker --quiet 2>/dev/null; then
|
||||
log_message "INFO" "Disabling Docker service..."
|
||||
sudo systemctl disable docker 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove systemd unit files (if they exist and were created by our script)
|
||||
local systemd_files=(
|
||||
"/etc/systemd/system/docker.socket"
|
||||
"/etc/systemd/system/docker.service"
|
||||
"/usr/lib/systemd/system/docker.socket"
|
||||
"/usr/lib/systemd/system/docker.service"
|
||||
)
|
||||
|
||||
for file in "${systemd_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
log_message "INFO" "Removing systemd file: $file"
|
||||
sudo rm -f "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
sudo systemctl daemon-reload 2>/dev/null || true
|
||||
;;
|
||||
|
||||
"openrc")
|
||||
# Stop and remove from runlevels
|
||||
if sudo rc-service docker status 2>/dev/null | grep -q "started"; then
|
||||
log_message "INFO" "Stopping Docker service (OpenRC)..."
|
||||
sudo rc-service docker stop 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f /etc/init.d/docker ]; then
|
||||
log_message "INFO" "Removing OpenRC init script..."
|
||||
sudo rc-update del docker default 2>/dev/null || true
|
||||
sudo rm -f /etc/init.d/docker
|
||||
fi
|
||||
;;
|
||||
|
||||
"sysvinit")
|
||||
# Stop service
|
||||
if [ -f /etc/init.d/docker ]; then
|
||||
log_message "INFO" "Stopping Docker service (SysV init)..."
|
||||
sudo service docker stop 2>/dev/null || true
|
||||
|
||||
# Remove from startup
|
||||
if command -v update-rc.d >/dev/null 2>&1; then
|
||||
sudo update-rc.d -f docker remove 2>/dev/null || true
|
||||
elif command -v chkconfig >/dev/null 2>&1; then
|
||||
sudo chkconfig --del docker 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_message "INFO" "Removing SysV init script..."
|
||||
sudo rm -f /etc/init.d/docker
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
log_message "WARNING" "Unknown init system, skipping service file cleanup"
|
||||
;;
|
||||
esac
|
||||
|
||||
log_message "SUCCESS" "Docker service files removed"
|
||||
}
|
||||
|
||||
# Remove Docker binaries (optional, only if no other containers exist)
|
||||
remove_docker_binaries() {
|
||||
local docker_binaries=(
|
||||
"/usr/bin/docker"
|
||||
"/usr/bin/dockerd"
|
||||
"/usr/bin/docker-init"
|
||||
"/usr/bin/docker-proxy"
|
||||
"/usr/bin/containerd"
|
||||
"/usr/bin/containerd-shim"
|
||||
"/usr/bin/containerd-shim-runc-v1"
|
||||
"/usr/bin/containerd-shim-runc-v2"
|
||||
"/usr/bin/runc"
|
||||
)
|
||||
|
||||
log_message "INFO" "Checking Docker binaries..."
|
||||
|
||||
local binaries_found=0
|
||||
for binary in "${docker_binaries[@]}"; do
|
||||
if [ -f "$binary" ]; then
|
||||
binaries_found=$((binaries_found + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$binaries_found" -eq 0 ]; then
|
||||
log_message "INFO" "No Docker binaries found in /usr/bin/"
|
||||
return
|
||||
fi
|
||||
|
||||
if confirm "Remove Docker binaries from /usr/bin/? (Only do this if Docker was installed by wallarm-ct-deploy.sh)" "n"; then
|
||||
log_message "WARNING" "Removing Docker binaries..."
|
||||
|
||||
for binary in "${docker_binaries[@]}"; do
|
||||
if [ -f "$binary" ]; then
|
||||
log_message "INFO" "Removing $binary"
|
||||
sudo rm -f "$binary"
|
||||
fi
|
||||
done
|
||||
|
||||
# Also remove CNI plugins if they exist
|
||||
if [ -d "/opt/cni/bin" ]; then
|
||||
log_message "INFO" "Removing CNI plugins from /opt/cni/bin/"
|
||||
sudo rm -rf /opt/cni/bin/*
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Docker binaries removed"
|
||||
else
|
||||
log_message "INFO" "Skipping Docker binary removal"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove Docker configuration files
|
||||
remove_docker_config() {
|
||||
local config_files=(
|
||||
"/etc/docker/daemon.json"
|
||||
"/etc/containerd/config.toml"
|
||||
"/var/lib/docker" # Warning: This removes all Docker data!
|
||||
)
|
||||
|
||||
log_message "INFO" "Checking Docker configuration files..."
|
||||
|
||||
# Only remove daemon.json if it was created by our script
|
||||
if [ -f "/etc/docker/daemon.json" ]; then
|
||||
log_message "INFO" "Found /etc/docker/daemon.json"
|
||||
if grep -q "storage-driver.*vfs" "/etc/docker/daemon.json" 2>/dev/null; then
|
||||
log_message "INFO" "This appears to be the VFS configuration from wallarm-ct-deploy.sh"
|
||||
if confirm "Remove /etc/docker/daemon.json?" "n"; then
|
||||
sudo rm -f "/etc/docker/daemon.json"
|
||||
log_message "SUCCESS" "Docker configuration removed"
|
||||
fi
|
||||
else
|
||||
log_message "WARNING" "/etc/docker/daemon.json doesn't appear to be from wallarm-ct-deploy.sh, skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Warn about Docker data directory
|
||||
if [ -d "/var/lib/docker" ]; then
|
||||
log_message "WARNING" "/var/lib/docker contains Docker data (images, containers, volumes)"
|
||||
log_message "WARNING" "Removing this directory will delete ALL Docker data on the system"
|
||||
if confirm "Remove /var/lib/docker? (WARNING: Deletes ALL Docker data)" "n"; then
|
||||
log_message "WARNING" "Removing /var/lib/docker - this may take a while..."
|
||||
sudo rm -rf /var/lib/docker
|
||||
log_message "SUCCESS" "Docker data directory removed"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove docker group (if empty)
|
||||
remove_docker_group() {
|
||||
log_message "INFO" "Checking docker group..."
|
||||
|
||||
if getent group docker >/dev/null; then
|
||||
local group_users
|
||||
group_users=$(getent group docker | cut -d: -f4)
|
||||
|
||||
if [ -z "$group_users" ]; then
|
||||
log_message "INFO" "Docker group exists and has no users"
|
||||
if confirm "Remove docker group?" "n"; then
|
||||
sudo groupdel docker 2>/dev/null || {
|
||||
log_message "WARNING" "Failed to remove docker group (may be system group)"
|
||||
}
|
||||
log_message "SUCCESS" "Docker group removed"
|
||||
fi
|
||||
else
|
||||
log_message "WARNING" "Docker group has users: $group_users"
|
||||
log_message "INFO" "Skipping docker group removal (users still present)"
|
||||
fi
|
||||
else
|
||||
log_message "INFO" "Docker group not found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove Wallarm-specific files and logs
|
||||
remove_wallarm_files() {
|
||||
local wallarm_files=(
|
||||
"$HOME/wallarm-start.sh"
|
||||
"$HOME/wallarm-stop.sh"
|
||||
"$HOME/wallarm-status.sh"
|
||||
"/usr/local/bin/wallarm-start"
|
||||
"/usr/local/bin/wallarm-stop"
|
||||
"/usr/local/bin/wallarm-status"
|
||||
)
|
||||
|
||||
log_message "INFO" "Removing Wallarm scripts and logs..."
|
||||
|
||||
# Remove scripts
|
||||
for file in "${wallarm_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
log_message "INFO" "Removing $file"
|
||||
sudo rm -f "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove log directory (if empty)
|
||||
local log_dir="$HOME/logs"
|
||||
if [ -d "$log_dir" ]; then
|
||||
log_message "INFO" "Found log directory: $log_dir"
|
||||
if [ -z "$(ls -A "$log_dir" 2>/dev/null)" ]; then
|
||||
log_message "INFO" "Log directory is empty, removing..."
|
||||
sudo rmdir "$log_dir" 2>/dev/null || true
|
||||
else
|
||||
log_message "INFO" "Log directory contains files, preserving..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove .env file if it exists
|
||||
if [ -f ".env" ]; then
|
||||
log_message "INFO" "Removing .env file..."
|
||||
rm -f ".env"
|
||||
fi
|
||||
|
||||
log_message "SUCCESS" "Wallarm files cleaned up"
|
||||
}
|
||||
|
||||
# Main uninstall function
|
||||
main() {
|
||||
echo -e "${CYAN}${BOLD}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ WALLARM UNINSTALLATION ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
echo -e "${YELLOW}This script will remove Wallarm filtering node and cleanup Docker installation.${NC}"
|
||||
echo -e "${YELLOW}You will be asked for confirmation before each destructive operation.${NC}"
|
||||
echo ""
|
||||
|
||||
if ! confirm "Do you want to continue with the uninstallation?" "n"; then
|
||||
log_message "INFO" "Uninstallation cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check sudo
|
||||
check_sudo
|
||||
|
||||
# Check Docker
|
||||
if check_docker; then
|
||||
log_message "INFO" "Docker is installed and running"
|
||||
|
||||
# Check for other containers
|
||||
if check_other_containers; then
|
||||
log_message "WARNING" "Other Docker containers exist on this system"
|
||||
echo -e "${YELLOW}Warning: Removing Docker may affect other containers.${NC}"
|
||||
echo -e "${YELLOW}Consider leaving Docker installed if you need it for other purposes.${NC}"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
log_message "WARNING" "Docker is not running or not installed"
|
||||
fi
|
||||
|
||||
# Step 1: Remove Wallarm container and image
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Step 1: Remove Wallarm container and image${NC}"
|
||||
if confirm "Stop and remove Wallarm container and image?" "y"; then
|
||||
remove_wallarm_container
|
||||
remove_wallarm_image
|
||||
else
|
||||
log_message "INFO" "Skipping Wallarm container/image removal"
|
||||
fi
|
||||
|
||||
# Step 2: Remove Docker service files
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Step 2: Remove Docker service files${NC}"
|
||||
if confirm "Remove Docker service files (systemd/OpenRC/SysV init scripts)?" "y"; then
|
||||
remove_docker_service_files
|
||||
else
|
||||
log_message "INFO" "Skipping Docker service file removal"
|
||||
fi
|
||||
|
||||
# Step 3: Optional Docker binary removal
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Step 3: Docker binaries and configuration${NC}"
|
||||
remove_docker_binaries
|
||||
remove_docker_config
|
||||
|
||||
# Step 4: Remove docker group
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Step 4: System cleanup${NC}"
|
||||
remove_docker_group
|
||||
|
||||
# Step 5: Remove Wallarm files
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Step 5: Wallarm files and logs${NC}"
|
||||
if confirm "Remove Wallarm scripts and log files?" "y"; then
|
||||
remove_wallarm_files
|
||||
else
|
||||
log_message "INFO" "Skipping Wallarm file cleanup"
|
||||
fi
|
||||
|
||||
# Final message
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}${BOLD}║ UNINSTALLATION COMPLETE ║${NC}"
|
||||
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Wallarm filtering node has been removed.${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note:${NC}"
|
||||
echo -e " • Docker may still be installed on your system"
|
||||
echo -e " • Docker data in /var/lib/docker may still exist"
|
||||
echo -e " • User may still be in docker group (check with 'groups')"
|
||||
echo ""
|
||||
echo -e "To completely remove Docker, you may need to:"
|
||||
echo -e " 1. Remove Docker package using your system's package manager"
|
||||
echo -e " 2. Remove /var/lib/docker directory (contains all Docker data)"
|
||||
echo -e " 3. Remove user from docker group: sudo gpasswd -d \$USER docker"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Wallarm Docker Node Manager - Unified single-script manager
|
||||
# ==============================================================================
|
||||
# Delegates to the individual docker/* scripts for preflight, deployment,
|
||||
# reconfiguration, and removal — providing a single entry point matching the
|
||||
# native/wallarm-native.sh command interface.
|
||||
#
|
||||
# Commands:
|
||||
# --preflight Run preflight checks only (no installation).
|
||||
# --install Interactive deployment of a Wallarm Docker node.
|
||||
# --config Reconfigure an existing Docker node.
|
||||
# --remove Remove a Docker node completely.
|
||||
# --status Show running Wallarm containers.
|
||||
# --help|-h Show help.
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Shell script files this wrapper delegates to (must be in the same directory)
|
||||
CHECK_SCRIPT="${SCRIPT_DIR}/wallarm-ct-check.sh"
|
||||
DEPLOY_SCRIPT="${SCRIPT_DIR}/wallarm-ct-deploy.sh"
|
||||
RECONF_SCRIPT="${SCRIPT_DIR}/wallarm-ct-reconfigure.sh"
|
||||
REMOVE_SCRIPT="${SCRIPT_DIR}/wallarm-ct-uninstall.sh"
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $0 COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
--preflight Run system validation and preflight checks
|
||||
--install Interactive deployment of a Wallarm Docker node
|
||||
--config Reconfigure an existing Docker node (proxies, mode)
|
||||
--remove Uninstall a Wallarm Docker node
|
||||
--status Show running Wallarm containers
|
||||
|
||||
Examples:
|
||||
$0 --preflight
|
||||
sudo $0 --install
|
||||
sudo $0 --config
|
||||
sudo $0 --remove
|
||||
EOF
|
||||
}
|
||||
|
||||
# Ensure the delegated script exists
|
||||
require_script() {
|
||||
local script="$1"
|
||||
local name="$2"
|
||||
if [[ ! -x "$script" ]]; then
|
||||
echo "!!! $name script not found or not executable: $script" >&2
|
||||
echo " Run setup.sh first to download all deployment scripts." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--preflight)
|
||||
require_script "$CHECK_SCRIPT" "Preflight (wallarm-ct-check.sh)"
|
||||
exec "$CHECK_SCRIPT" "${@:2}"
|
||||
;;
|
||||
--install)
|
||||
require_script "$DEPLOY_SCRIPT" "Deploy (wallarm-ct-deploy.sh)"
|
||||
exec sudo "$DEPLOY_SCRIPT" "${@:2}"
|
||||
;;
|
||||
--config)
|
||||
require_script "$RECONF_SCRIPT" "Reconfigure (wallarm-ct-reconfigure.sh)"
|
||||
exec sudo "$RECONF_SCRIPT" "${@:2}"
|
||||
;;
|
||||
--remove)
|
||||
require_script "$REMOVE_SCRIPT" "Uninstall (wallarm-ct-uninstall.sh)"
|
||||
exec sudo "$REMOVE_SCRIPT" "${@:2}"
|
||||
;;
|
||||
--status)
|
||||
echo "Wallarm Docker Nodes:"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker ps --filter "name=wallarm-" \
|
||||
--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || true
|
||||
else
|
||||
echo " Docker is not installed or not in PATH."
|
||||
fi
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
38
go.mod
38
go.mod
|
|
@ -1,38 +0,0 @@
|
|||
module git.sechpoint.app/customer-engineering/wallarm
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/huh v1.0.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
77
go.sum
77
go.sum
|
|
@ -1,77 +0,0 @@
|
|||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
||||
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
// Package native implements Wallarm Native Node deployment (no Docker).
|
||||
// Ported from native/wallarm-native.sh.
|
||||
package native
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
||||
)
|
||||
|
||||
// Constants matching the bash script
|
||||
const (
|
||||
BaseDir = "/opt/wallarm"
|
||||
NodesDir = BaseDir + "/nodes"
|
||||
SystemdTemplate = "/etc/systemd/system/wallarm-node@.service"
|
||||
InstallerBaseURL = "https://repo.wallarm.com/linux/wallarm-native-node/latest/all-in-one"
|
||||
InstallerArch = "x86_64"
|
||||
)
|
||||
|
||||
// InstallerURL returns the all-in-one installer URL.
|
||||
var InstallerURL = fmt.Sprintf("%s/wallarm-native-node-aio-%s-latest.sh", InstallerBaseURL, InstallerArch)
|
||||
|
||||
// InstallNode deploys a single native Wallarm node.
|
||||
// node: node metadata, apiToken: Wallarm API token, apiHost: cloud API host, labels: optional node labels.
|
||||
func InstallNode(node state.Node, apiToken, apiHost, labels string) error {
|
||||
if apiToken == "" {
|
||||
return fmt.Errorf("API token required")
|
||||
}
|
||||
if apiHost == "" {
|
||||
return fmt.Errorf("API host required")
|
||||
}
|
||||
|
||||
workDir := filepath.Join(NodesDir, node.Name)
|
||||
installerPath := filepath.Join(workDir, "wallarm-native-node-aio.sh")
|
||||
|
||||
// 1. Create directories
|
||||
for _, d := range []string{
|
||||
filepath.Join(workDir, "etc"),
|
||||
filepath.Join(workDir, "var", "log"),
|
||||
filepath.Join(workDir, "var", "run"),
|
||||
} {
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", d, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Write go-node.yaml config
|
||||
configPath := filepath.Join(workDir, "etc", "go-node.yaml")
|
||||
config := fmt.Sprintf(`mode: connector-server
|
||||
connector:
|
||||
address: "%s"
|
||||
`, node.Address)
|
||||
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
||||
return fmt.Errorf("write config: %w", err)
|
||||
}
|
||||
|
||||
// 3. Download installer if missing
|
||||
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
||||
fmt.Printf("[%s] Downloading installer...\n", node.Name)
|
||||
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, InstallerURL)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("download installer: %w\n%s", err, string(out))
|
||||
}
|
||||
if err := os.Chmod(installerPath, 0755); err != nil {
|
||||
return fmt.Errorf("chmod installer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Write environment file
|
||||
envFile := filepath.Join(workDir, "env")
|
||||
envContent := fmt.Sprintf(`WALLARM_API_TOKEN=%s
|
||||
WALLARM_API_HOST=%s
|
||||
WALLARM_LABELS=%s
|
||||
WALLARM_CONFIG_PATH=%s
|
||||
`, apiToken, apiHost, labels, configPath)
|
||||
if err := os.WriteFile(envFile, []byte(envContent), 0600); err != nil {
|
||||
return fmt.Errorf("write env: %w", err)
|
||||
}
|
||||
|
||||
// 5. Run the all-in-one installer
|
||||
fmt.Printf("[%s] Running Wallarm installer...\n", node.Name)
|
||||
installCmd := exec.Command(installerPath, "install",
|
||||
"--", "--config-dir", filepath.Join(workDir, "etc"),
|
||||
"--", "--log-dir", filepath.Join(workDir, "var", "log"),
|
||||
"--", "--pid-dir", filepath.Join(workDir, "var", "run"),
|
||||
)
|
||||
installCmd.Dir = workDir
|
||||
installCmd.Env = append(os.Environ(),
|
||||
"WALLARM_API_TOKEN="+apiToken,
|
||||
"WALLARM_API_HOST="+apiHost,
|
||||
"WALLARM_LABELS="+labels,
|
||||
"WALLARM_CONFIG_PATH="+configPath,
|
||||
)
|
||||
|
||||
logFile, err := os.Create(filepath.Join(workDir, "install.log"))
|
||||
if err == nil {
|
||||
installCmd.Stdout = logFile
|
||||
installCmd.Stderr = logFile
|
||||
defer logFile.Close()
|
||||
}
|
||||
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("installation failed — check %s/install.log: %w", workDir, err)
|
||||
}
|
||||
|
||||
// 6. Enable and start systemd service
|
||||
serviceName := "wallarm-node@" + node.Name
|
||||
for _, action := range []string{"enable", "start"} {
|
||||
cmd := exec.Command("systemctl", action, serviceName)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl %s %s: %w\n%s", action, serviceName, err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Installation successful. Service: %s\n", node.Name, serviceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNode stops the systemd service and deletes the node directory.
|
||||
func RemoveNode(nodeName string) error {
|
||||
serviceName := "wallarm-node@" + nodeName
|
||||
workDir := filepath.Join(NodesDir, nodeName)
|
||||
|
||||
// Stop and disable
|
||||
for _, action := range []string{"stop", "disable"} {
|
||||
exec.Command("systemctl", action, serviceName).Run() // ignore errors
|
||||
}
|
||||
|
||||
// Remove directory
|
||||
if err := os.RemoveAll(workDir); err != nil {
|
||||
return fmt.Errorf("remove %s: %w", workDir, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Node %s removed.\n", nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the systemd status for a node, or all nodes if nodeName is empty.
|
||||
func Status(nodeName string) (string, error) {
|
||||
if nodeName != "" {
|
||||
cmd := exec.Command("systemctl", "status", "wallarm-node@"+nodeName, "--no-pager")
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(NodesDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Wallarm Nodes:\n")
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
name := e.Name()
|
||||
sb.WriteString(fmt.Sprintf("--- %s ---\n", name))
|
||||
cmd := exec.Command("systemctl", "status", "wallarm-node@"+name, "--no-pager")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
sb.WriteString(string(out))
|
||||
} else {
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for i, l := range lines {
|
||||
if i >= 5 {
|
||||
break
|
||||
}
|
||||
sb.WriteString(l + "\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// GenerateSystemdTemplate creates the wallarm-node@.service template unit if it doesn't exist.
|
||||
func GenerateSystemdTemplate() error {
|
||||
if _, err := os.Stat(SystemdTemplate); err == nil {
|
||||
return nil // already exists
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`[Unit]
|
||||
Description=Wallarm Native Node - %%I
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%s/%%i
|
||||
EnvironmentFile=%s/%%i/env
|
||||
ExecStart=%s/%%i/wallarm-native-node-aio.sh start
|
||||
ExecStop=%s/%%i/wallarm-native-node-aio.sh stop
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, NodesDir, NodesDir, NodesDir, NodesDir)
|
||||
|
||||
if err := os.WriteFile(SystemdTemplate, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("write systemd template: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("systemctl", "daemon-reload")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("daemon-reload: %w\n%s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNodesDir ensures the /opt/wallarm/nodes directory exists.
|
||||
func CreateNodesDir() error {
|
||||
return os.MkdirAll(NodesDir, 0755)
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
// Package preflight runs system readiness checks before any deployment.
|
||||
// It is called on every binary start and returns a structured report.
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/shared"
|
||||
)
|
||||
|
||||
// Result holds the outcome of all preflight checks.
|
||||
type Result struct {
|
||||
Passed bool `json:"passed"`
|
||||
Checks []Check `json:"checks"`
|
||||
USReachable bool `json:"us_reachable"`
|
||||
EUReachable bool `json:"eu_reachable"`
|
||||
}
|
||||
|
||||
// Check represents a single preflight check.
|
||||
type Check struct {
|
||||
Name string `json:"name"`
|
||||
Passed bool `json:"passed"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Warning bool `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
// Cloud endpoints (from wallarm-lib.sh)
|
||||
var euEndpoints = []string{
|
||||
"api.wallarm.com:443",
|
||||
"node-data0.eu1.wallarm.com:443",
|
||||
"node-data1.eu1.wallarm.com:443",
|
||||
}
|
||||
|
||||
var usEndpoints = []string{
|
||||
"us1.api.wallarm.com:443",
|
||||
"node-data0.us1.wallarm.com:443",
|
||||
"node-data1.us1.wallarm.com:443",
|
||||
}
|
||||
|
||||
// Run executes all preflight checks and returns the result.
|
||||
func Run() Result {
|
||||
r := Result{Passed: true}
|
||||
|
||||
// 1. Root check
|
||||
if os.Geteuid() != 0 {
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "root", Passed: false, Detail: "must run as root for package installation and system config",
|
||||
})
|
||||
r.Passed = false
|
||||
} else {
|
||||
r.Checks = append(r.Checks, Check{Name: "root", Passed: true})
|
||||
}
|
||||
|
||||
// 2. Init system
|
||||
initSys := shared.InitSystem()
|
||||
if initSys != "systemd" {
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "init", Passed: false, Detail: fmt.Sprintf("requires systemd, detected: %s", initSys),
|
||||
})
|
||||
r.Passed = false
|
||||
} else {
|
||||
r.Checks = append(r.Checks, Check{Name: "init", Passed: true, Detail: "systemd"})
|
||||
}
|
||||
|
||||
// 3. Architecture
|
||||
arch := shared.Arch()
|
||||
supported := arch == "x86_64" || arch == "aarch64"
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "arch", Passed: supported, Detail: arch,
|
||||
})
|
||||
if !supported {
|
||||
r.Passed = false
|
||||
}
|
||||
|
||||
// 4. OS
|
||||
id, ver := shared.OSInfo()
|
||||
r.Checks = append(r.Checks, Check{Name: "os", Passed: true, Detail: id + " " + ver})
|
||||
|
||||
// 5. Required commands
|
||||
requiredCmds := []string{"curl", "systemctl", "sed", "mkdir", "rm"}
|
||||
for _, cmd := range requiredCmds {
|
||||
ok := shared.CommandExists(cmd)
|
||||
r.Checks = append(r.Checks, Check{Name: "cmd:" + cmd, Passed: ok})
|
||||
if !ok {
|
||||
r.Passed = false
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Installer reachability
|
||||
installerOk := shared.HTTPHead("https://repo.wallarm.com")
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "installer_reachable", Passed: installerOk,
|
||||
Detail: "repo.wallarm.com",
|
||||
})
|
||||
if !installerOk {
|
||||
r.Checks[len(r.Checks)-1].Warning = true
|
||||
}
|
||||
|
||||
// 7. Cloud endpoints
|
||||
r.USReachable = checkEndpoints(usEndpoints)
|
||||
r.EUReachable = checkEndpoints(euEndpoints)
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "cloud:US", Passed: r.USReachable,
|
||||
Detail: fmt.Sprintf("%d/%d reachable", countReachable(usEndpoints), len(usEndpoints)),
|
||||
})
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "cloud:EU", Passed: r.EUReachable,
|
||||
Detail: fmt.Sprintf("%d/%d reachable", countReachable(euEndpoints), len(euEndpoints)),
|
||||
})
|
||||
if !r.USReachable && !r.EUReachable {
|
||||
r.Passed = false
|
||||
}
|
||||
|
||||
// 8. Disk space (>= 2GB)
|
||||
free, err := shared.FreeDiskMB("/opt")
|
||||
if err == nil && free < 2048 {
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "disk", Passed: false,
|
||||
Detail: fmt.Sprintf("%d MB free (need >= 2048 MB)", free),
|
||||
})
|
||||
r.Passed = false
|
||||
} else {
|
||||
r.Checks = append(r.Checks, Check{Name: "disk", Passed: true, Detail: fmt.Sprintf("%d MB free", free)})
|
||||
}
|
||||
|
||||
// 9. Memory (>= 2GB, warning only)
|
||||
mem, err := shared.FreeMemoryMB()
|
||||
if err == nil && mem < 2048 {
|
||||
r.Checks = append(r.Checks, Check{
|
||||
Name: "memory", Passed: true, Warning: true,
|
||||
Detail: fmt.Sprintf("%d MB (2GB+ recommended)", mem),
|
||||
})
|
||||
} else {
|
||||
r.Checks = append(r.Checks, Check{Name: "memory", Passed: true, Detail: fmt.Sprintf("%d MB", mem)})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func checkEndpoints(endpoints []string) bool {
|
||||
for _, ep := range endpoints {
|
||||
host, _ := splitHostPort(ep)
|
||||
if shared.TCPConnect(host, 443) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func countReachable(endpoints []string) int {
|
||||
n := 0
|
||||
for _, ep := range endpoints {
|
||||
host, _ := splitHostPort(ep)
|
||||
if shared.TCPConnect(host, 443) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func splitHostPort(addr string) (string, string) {
|
||||
for i := len(addr) - 1; i >= 0; i-- {
|
||||
if addr[i] == ':' {
|
||||
return addr[:i], addr[i+1:]
|
||||
}
|
||||
}
|
||||
return addr, ""
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
// Package shared provides validation, connectivity, and system detection
|
||||
// utilities ported from the bash wallarm-lib.sh library.
|
||||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ─── System Detection ────────────────────────────────────────────────
|
||||
|
||||
// InitSystem returns the detected init system: systemd, openrc, sysvinit, upstart, or unknown.
|
||||
func InitSystem() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "darwin"
|
||||
}
|
||||
if _, err := exec.LookPath("systemctl"); err == nil {
|
||||
return "systemd"
|
||||
}
|
||||
if _, err := os.Stat("/sbin/openrc-run"); err == nil {
|
||||
return "openrc"
|
||||
}
|
||||
if _, err := os.Stat("/etc/init.d"); err == nil {
|
||||
return "sysvinit"
|
||||
}
|
||||
if _, err := os.Stat("/sbin/upstart"); err == nil {
|
||||
return "upstart"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// OSInfo returns (os_id, version_id) from /etc/os-release.
|
||||
func OSInfo() (id, version string) {
|
||||
data, err := os.ReadFile("/etc/os-release")
|
||||
if err != nil {
|
||||
return strings.ToLower(runtime.GOOS), "unknown"
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
|
||||
}
|
||||
if strings.HasPrefix(line, "VERSION_ID=") {
|
||||
version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`)
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
id = strings.ToLower(runtime.GOOS)
|
||||
}
|
||||
return id, version
|
||||
}
|
||||
|
||||
// Arch returns the normalized architecture: x86_64, aarch64, or armhf.
|
||||
func Arch() string {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "arm64":
|
||||
return "aarch64"
|
||||
case "arm":
|
||||
return "armhf"
|
||||
default:
|
||||
return runtime.GOARCH
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Validation ──────────────────────────────────────────────────────
|
||||
|
||||
// ValidateIP checks whether s is a valid IPv4 address.
|
||||
func ValidateIP(s string) error {
|
||||
parts := strings.Split(s, ".")
|
||||
if len(parts) != 4 {
|
||||
return fmt.Errorf("invalid IPv4: %s", s)
|
||||
}
|
||||
for _, p := range parts {
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil || n < 0 || n > 255 {
|
||||
return fmt.Errorf("invalid IPv4 octet: %s", p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCIDR checks whether s is a valid IPv4 address with optional /prefix.
|
||||
func ValidateCIDR(s string) error {
|
||||
ip := s
|
||||
prefix := ""
|
||||
if idx := strings.IndexByte(s, '/'); idx != -1 {
|
||||
ip, prefix = s[:idx], s[idx+1:]
|
||||
}
|
||||
if err := ValidateIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
if prefix != "" {
|
||||
n, err := strconv.Atoi(prefix)
|
||||
if err != nil || n < 0 || n > 32 {
|
||||
return fmt.Errorf("invalid CIDR prefix: %s", prefix)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandExists returns true if cmd is in PATH or in common system directories.
|
||||
func CommandExists(cmd string) bool {
|
||||
if _, err := exec.LookPath(cmd); err == nil {
|
||||
return true
|
||||
}
|
||||
for _, dir := range []string{"/usr/sbin", "/sbin", "/usr/local/sbin", "/usr/bin", "/bin", "/usr/local/bin"} {
|
||||
if _, err := os.Stat(dir + "/" + cmd); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ─── Resource Checks ─────────────────────────────────────────────────
|
||||
|
||||
// FreeDiskMB returns available disk space in MB for the given path.
|
||||
func FreeDiskMB(path string) (int64, error) {
|
||||
cmd := exec.Command("df", "-k", path)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
if len(lines) < 2 {
|
||||
return 0, fmt.Errorf("unexpected df output")
|
||||
}
|
||||
fields := strings.Fields(lines[1])
|
||||
if len(fields) < 4 {
|
||||
return 0, fmt.Errorf("unexpected df fields")
|
||||
}
|
||||
kb, err := strconv.ParseInt(fields[3], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return kb / 1024, nil // convert KB to MB
|
||||
}
|
||||
|
||||
// FreeMemoryMB returns available memory in MB.
|
||||
func FreeMemoryMB() (int64, error) {
|
||||
cmd := exec.Command("free", "-m")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
if len(lines) < 2 {
|
||||
return 0, fmt.Errorf("unexpected free output")
|
||||
}
|
||||
fields := strings.Fields(lines[1])
|
||||
if len(fields) < 2 {
|
||||
return 0, fmt.Errorf("unexpected free fields")
|
||||
}
|
||||
return strconv.ParseInt(fields[1], 10, 64)
|
||||
}
|
||||
|
||||
// ─── Connectivity ────────────────────────────────────────────────────
|
||||
|
||||
// TCPConnect tests whether host:port accepts a TCP connection.
|
||||
func TCPConnect(host string, port int) bool {
|
||||
cmd := exec.Command("timeout", "5", "bash", "-c",
|
||||
fmt.Sprintf("echo >/dev/tcp/%s/%d 2>/dev/null", host, port))
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// HTTPHead returns true if the URL returns a successful status.
|
||||
func HTTPHead(url string) bool {
|
||||
cmd := exec.Command("curl", "-fsSL", "--connect-timeout", "10", url)
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
// Package state manages the persistent deployment state file (~/.wallarm/state.json).
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StateDir is where wallarm stores its state.
|
||||
const StateDir = ".wallarm"
|
||||
|
||||
// State represents the persistent deployment state.
|
||||
type State struct {
|
||||
DeploymentType string `json:"deployment_type,omitempty"` // docker or native
|
||||
CloudRegion string `json:"cloud_region,omitempty"` // US or EU
|
||||
APIHost string `json:"api_host,omitempty"` // e.g., api.wallarm.com
|
||||
APIToken string `json:"api_token,omitempty"` // Wallarm API token (sensitive)
|
||||
Nodes []Node `json:"nodes"`
|
||||
}
|
||||
|
||||
// Node represents a single deployed Wallarm node (docker container or native systemd unit).
|
||||
type Node struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // docker or native
|
||||
Address string `json:"address,omitempty"` // listen address for native
|
||||
Port int `json:"port,omitempty"` // ingress port for docker
|
||||
UpstreamIP string `json:"upstream_ip,omitempty"` // docker
|
||||
UpstreamPort int `json:"upstream_port,omitempty"` // docker
|
||||
Status string `json:"status"` // running, stopped, unknown
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// Path returns the full path to the state file.
|
||||
func Path() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "/root"
|
||||
}
|
||||
return filepath.Join(home, StateDir, "state.json")
|
||||
}
|
||||
|
||||
// Load reads and parses the state file. Returns nil if the file doesn't exist.
|
||||
func Load() (*State, error) {
|
||||
p := Path()
|
||||
data, err := os.ReadFile(p)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read state: %w", err)
|
||||
}
|
||||
var s State
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, fmt.Errorf("parse state: %w", err)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Save writes the state to disk, creating directories as needed.
|
||||
func Save(s *State) error {
|
||||
p := Path()
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
|
||||
return fmt.Errorf("create state dir: %w", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal state: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(p, data, 0600); err != nil {
|
||||
return fmt.Errorf("write state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasDeployment returns true if a state file exists with at least one node.
|
||||
func HasDeployment() bool {
|
||||
s, err := Load()
|
||||
if err != nil || s == nil {
|
||||
return false
|
||||
}
|
||||
return len(s.Nodes) > 0
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
// Package tunnel provides a reverse SSH tunnel over TLS:443 via a Zoraxy edge proxy.
|
||||
// It opens an outbound TLS connection to the configured jumphost, authenticates
|
||||
// via SSH, and establishes a reverse port forward so you can reach the target VM
|
||||
// through sechpoint.app.
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Config holds the tunnel connection parameters.
|
||||
type Config struct {
|
||||
Jumphost string // e.g., "ssh.sechpoint.app:443"
|
||||
RemotePort int // Port on the jumphost that forwards to target's SSH
|
||||
LocalSSHPort int // SSH port on the target VM (usually 22)
|
||||
User string // SSH user on the jumphost
|
||||
KeyPath string // Path to private key for authentication
|
||||
KeyBytes []byte // Raw private key bytes (takes precedence over KeyPath)
|
||||
}
|
||||
|
||||
// DefaultConfig returns a Config with sensible defaults.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Jumphost: "ssh.sechpoint.app:443",
|
||||
RemotePort: 9042,
|
||||
LocalSSHPort: 22,
|
||||
User: "wallarm-tunnel",
|
||||
}
|
||||
}
|
||||
|
||||
// Start opens a reverse SSH tunnel over TLS and keeps it alive.
|
||||
// It blocks until SIGINT or connection failure.
|
||||
func Start(cfg Config) error {
|
||||
// Load the private key
|
||||
var signer ssh.Signer
|
||||
if len(cfg.KeyBytes) > 0 {
|
||||
var err error
|
||||
signer, err = ssh.ParsePrivateKey(cfg.KeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse embedded key: %w", err)
|
||||
}
|
||||
} else {
|
||||
keyBytes, err := os.ReadFile(cfg.KeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read key %s: %w", cfg.KeyPath, err)
|
||||
}
|
||||
signer, err = ssh.ParsePrivateKey(keyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: cfg.User,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil // Accept all host keys (trusted infrastructure)
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// TLS dial to the Zoraxy edge (port 443)
|
||||
tlsConn, err := tls.Dial("tcp", cfg.Jumphost, &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS dial %s: %w", cfg.Jumphost, err)
|
||||
}
|
||||
|
||||
// SSH over TLS
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.Jumphost, sshConfig)
|
||||
if err != nil {
|
||||
tlsConn.Close()
|
||||
return fmt.Errorf("SSH handshake: %w", err)
|
||||
}
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
defer client.Close()
|
||||
|
||||
// Request reverse port forward: jumphost:RemotePort -> localhost:LocalSSHPort
|
||||
remoteAddr := fmt.Sprintf("0.0.0.0:%d", cfg.RemotePort)
|
||||
localAddr := fmt.Sprintf("localhost:%d", cfg.LocalSSHPort)
|
||||
|
||||
listener, err := client.Listen("tcp", remoteAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remote listen %s: %w", remoteAddr, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("Tunnel established: %s -> %s\n", remoteAddr, localAddr)
|
||||
fmt.Printf("Connect: ssh -p %d root@%s\n", cfg.RemotePort, cfg.Jumphost)
|
||||
|
||||
// Handle incoming connections on the remote listener
|
||||
go func() {
|
||||
for {
|
||||
remoteConn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go forwardConnection(remoteConn, localAddr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep alive until signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
|
||||
// Heartbeat every 30s
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
fmt.Println("\nTunnel closed.")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
_, _, err := client.SendRequest("keepalive@wallarm", true, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("keepalive failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func forwardConnection(remoteConn net.Conn, localAddr string) {
|
||||
defer remoteConn.Close()
|
||||
localConn, err := net.DialTimeout("tcp", localAddr, 10*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer localConn.Close()
|
||||
|
||||
go func() {
|
||||
io.Copy(localConn, remoteConn)
|
||||
remoteConn.Close()
|
||||
}()
|
||||
io.Copy(remoteConn, localConn)
|
||||
}
|
||||
|
|
@ -1,339 +0,0 @@
|
|||
// Package ui provides the bubbletea terminal UI for wallarm:
|
||||
// - Wizard: guides new deployments (type → region → config → deploy)
|
||||
// - Dashboard: lists existing nodes with actions (add, config, remove, tunnel)
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/native"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
||||
)
|
||||
|
||||
// Styles
|
||||
var (
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).MarginBottom(1)
|
||||
goodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("227"))
|
||||
badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true)
|
||||
)
|
||||
|
||||
// Model is the top-level bubbletea model.
|
||||
type Model struct {
|
||||
state stateView
|
||||
width int
|
||||
height int
|
||||
status string // feedback message during deployment
|
||||
deploying bool // true while deployment is in progress
|
||||
}
|
||||
|
||||
type stateView int
|
||||
|
||||
const (
|
||||
viewPreflight stateView = iota
|
||||
viewRoute
|
||||
viewWizard
|
||||
viewDashboard
|
||||
viewDone
|
||||
viewError
|
||||
)
|
||||
|
||||
var preflightResult preflight.Result
|
||||
|
||||
// deployCompleteMsg is sent when deployment finishes.
|
||||
type deployCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Run starts the bubbletea TUI.
|
||||
func Run(r preflight.Result) error {
|
||||
preflightResult = r
|
||||
m := Model{state: viewRoute}
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "1", "2":
|
||||
if m.state == viewWizard && !m.deploying {
|
||||
return m, m.startDeploy()
|
||||
}
|
||||
case "enter":
|
||||
if m.state == viewDone || m.state == viewError {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
case deployCompleteMsg:
|
||||
m.deploying = false
|
||||
if msg.err != nil {
|
||||
m.status = fmt.Sprintf("Deployment failed: %v", msg.err)
|
||||
m.state = viewError
|
||||
} else {
|
||||
m.status = "Deployment successful!"
|
||||
m.state = viewDone
|
||||
}
|
||||
return m, nil
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case viewRoute:
|
||||
if state.HasDeployment() {
|
||||
m.state = viewDashboard
|
||||
} else {
|
||||
m.state = viewWizard
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case viewRoute:
|
||||
return ""
|
||||
case viewWizard:
|
||||
return wizardView(m)
|
||||
case viewDashboard:
|
||||
return dashboardView(m)
|
||||
case viewDone:
|
||||
return goodStyle.Render("✅ " + m.status) + "\n\n" + dimStyle.Render("Press Enter to exit.")
|
||||
case viewError:
|
||||
return badStyle.Render("❌ " + m.status) + "\n\n" + dimStyle.Render("Press Enter to exit.")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// startDeploy runs the Huh form in a goroutine, then deploys.
|
||||
func (m Model) startDeploy() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
m.deploying = true
|
||||
|
||||
s, err := runDeployForm()
|
||||
if err != nil {
|
||||
return deployCompleteMsg{err}
|
||||
}
|
||||
|
||||
// Deploy native node
|
||||
err = deployNative(s)
|
||||
|
||||
if err == nil {
|
||||
state.Save(&s)
|
||||
}
|
||||
|
||||
return deployCompleteMsg{err}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Wizard ──────────────────────────────────────────────────────────
|
||||
|
||||
func wizardView(m Model) string {
|
||||
s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n"
|
||||
|
||||
if m.deploying {
|
||||
s += dimStyle.Render("Configuring deployment — follow the prompts below...") + "\n"
|
||||
return s
|
||||
}
|
||||
|
||||
s += dimStyle.Render("No existing deployment found. Let's set one up.") + "\n\n"
|
||||
s += activeStyle.Render("Wallarm Native Node Deployment") + "\n"
|
||||
s += " Press Enter to begin configuration" + "\n\n"
|
||||
|
||||
s += activeStyle.Render("Step 2: Cloud region") + "\n"
|
||||
r := preflightResult
|
||||
if r.USReachable {
|
||||
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
|
||||
} else {
|
||||
s += dimStyle.Render(" US — not reachable") + "\n"
|
||||
}
|
||||
if r.EUReachable {
|
||||
s += goodStyle.Render(" EU") + " — api.wallarm.com (reachable)\n"
|
||||
} else {
|
||||
s += dimStyle.Render(" EU — not reachable") + "\n"
|
||||
}
|
||||
s += dimStyle.Render("\nPress q to quit")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// runDeployForm collects configuration via huh interactive forms.
|
||||
func runDeployForm() (state.State, error) {
|
||||
var s state.State
|
||||
s.DeploymentType = "native"
|
||||
|
||||
// Step 1: Cloud region
|
||||
regionOptions := []huh.Option[string]{}
|
||||
if preflightResult.USReachable {
|
||||
regionOptions = append(regionOptions, huh.NewOption("US (us1.api.wallarm.com)", "US"))
|
||||
}
|
||||
if preflightResult.EUReachable {
|
||||
regionOptions = append(regionOptions, huh.NewOption("EU (api.wallarm.com)", "EU"))
|
||||
}
|
||||
|
||||
if len(regionOptions) == 0 {
|
||||
return s, fmt.Errorf("no cloud regions reachable")
|
||||
}
|
||||
|
||||
var region string
|
||||
if len(regionOptions) == 1 {
|
||||
region = regionOptions[0].Value // auto-select
|
||||
} else {
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Choose Wallarm cloud region").
|
||||
Options(regionOptions...).
|
||||
Value(®ion),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).Run()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
}
|
||||
s.CloudRegion = region
|
||||
if region == "US" {
|
||||
s.APIHost = "us1.api.wallarm.com"
|
||||
} else {
|
||||
s.APIHost = "api.wallarm.com"
|
||||
}
|
||||
|
||||
// Step 2: API Token
|
||||
var apiToken string
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Wallarm API Token (Deploy role)").
|
||||
Placeholder("Paste your token here").
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&apiToken).
|
||||
Validate(func(v string) error {
|
||||
if len(v) < 10 {
|
||||
return fmt.Errorf("token too short")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).Run()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s.APIToken = apiToken
|
||||
|
||||
// Step 3: Node configuration
|
||||
var nodeName, address, labels string
|
||||
nodeForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Node name").Value(&nodeName).Validate(func(v string) error {
|
||||
if v == "" {
|
||||
return fmt.Errorf("required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Listen address (IP:Port)").Placeholder("0.0.0.0:8081").Value(&address).Validate(func(v string) error {
|
||||
if v == "" {
|
||||
return fmt.Errorf("required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Labels (optional)").Placeholder("group=prod").Value(&labels),
|
||||
),
|
||||
)
|
||||
|
||||
err = nodeForm.WithTheme(huh.ThemeCharm()).Run()
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
s.Nodes = append(s.Nodes, state.Node{
|
||||
Name: nodeName,
|
||||
Type: "native",
|
||||
Address: address,
|
||||
Status: "deploying",
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// deployNative executes the native deployment.
|
||||
func deployNative(s state.State) error {
|
||||
if err := native.CreateNodesDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := native.GenerateSystemdTemplate(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range s.Nodes {
|
||||
labels := "group=" + node.Name
|
||||
if err := native.InstallNode(node, s.APIToken, s.APIHost, labels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── Dashboard ───────────────────────────────────────────────────────
|
||||
|
||||
func dashboardView(m Model) string {
|
||||
s, err := state.Load()
|
||||
if err != nil || s == nil {
|
||||
return badStyle.Render("Error loading state.") + "\nPress q to quit"
|
||||
}
|
||||
|
||||
out := titleStyle.Render("📊 Wallarm Dashboard") + "\n"
|
||||
out += fmt.Sprintf("Type: %s | Cloud: %s (%s)\n", s.DeploymentType, s.CloudRegion, s.APIHost)
|
||||
out += dimStyle.Render(strings.Repeat("─", 50)) + "\n\n"
|
||||
|
||||
if len(s.Nodes) == 0 {
|
||||
out += dimStyle.Render("No nodes deployed yet.") + "\n\n"
|
||||
} else {
|
||||
out += activeStyle.Render("Nodes:") + "\n"
|
||||
for _, n := range s.Nodes {
|
||||
marker := "●"
|
||||
style := goodStyle
|
||||
if n.Status != "running" {
|
||||
marker = "○"
|
||||
style = warnStyle
|
||||
}
|
||||
detail := n.Address
|
||||
if detail == "" && n.Port != 0 {
|
||||
detail = fmt.Sprintf(":%d → %s:%d", n.Port, n.UpstreamIP, n.UpstreamPort)
|
||||
}
|
||||
out += style.Render(fmt.Sprintf(" %s %s — %s", marker, n.Name, n.Status))
|
||||
if detail != "" {
|
||||
out += dimStyle.Render(fmt.Sprintf(" (%s)", detail))
|
||||
}
|
||||
out += "\n"
|
||||
}
|
||||
out += "\n"
|
||||
}
|
||||
|
||||
out += "Actions:\n"
|
||||
out += fmt.Sprintf(" %s\n", activeStyle.Render("[a] Add node"))
|
||||
out += fmt.Sprintf(" %s\n", activeStyle.Render("[c] Configure"))
|
||||
out += fmt.Sprintf(" %s\n", activeStyle.Render("[r] Remove node"))
|
||||
out += fmt.Sprintf(" %s\n", activeStyle.Render("[t] Start tunnel"))
|
||||
out += fmt.Sprintf(" %s\n", dimStyle.Render("[q] Quit"))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
@ -1,498 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Wallarm Native Node Manager - Install, Configure, Remove, and Control
|
||||
# ==============================================================================
|
||||
# Unified single-script manager for the Wallarm Native Node (connector mode,
|
||||
# NO Docker). Manages multiple isolated nodes under ${BASE_DIR}/nodes with a
|
||||
# systemd template unit (wallarm-node@<name>.service).
|
||||
#
|
||||
# NOTE: This targets the Wallarm Native Node product (go-node, connector-server
|
||||
# mode, all-in-one installer) - distinct from the NGINX-module based native
|
||||
# deployment in ./wallarm-ct-deploy.sh.
|
||||
#
|
||||
# Commands:
|
||||
# --preflight Run preflight checks only (no installation).
|
||||
# --install Interactive installation of one or more nodes (parallel).
|
||||
# --config Update an existing node's configuration.
|
||||
# Options: --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
|
||||
# --remove Remove a node completely.
|
||||
# Options: --node NAME
|
||||
# --status [NODE] Show systemd status for a node, or all nodes.
|
||||
# --help|-h Show help.
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script location and shared library (logging, detection, connectivity, validation)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./wallarm-lib.sh
|
||||
source "$SCRIPT_DIR/wallarm-lib.sh"
|
||||
|
||||
# --- Constants ---
|
||||
BASE_DIR="/opt/wallarm"
|
||||
NODES_DIR="${BASE_DIR}/nodes"
|
||||
SYSTEMD_TEMPLATE="/etc/systemd/system/wallarm-node@.service"
|
||||
|
||||
# Wallarm Native Node all-in-one installer (latest, x86_64 by default)
|
||||
# Override WALLARM_INSTALLER_URL to pin a version; WALLARM_INSTALLER_ARCH to
|
||||
# select the architecture suffix.
|
||||
INSTALLER_BASE_URL="https://repo.wallarm.com/linux/wallarm-native-node/latest/all-in-one"
|
||||
INSTALLER_ARCH="${WALLARM_INSTALLER_ARCH:-x86_64}"
|
||||
INSTALLER_URL="${WALLARM_INSTALLER_URL:-${INSTALLER_BASE_URL}/wallarm-native-node-aio-${INSTALLER_ARCH}-latest.sh}"
|
||||
|
||||
# Wallarm cloud endpoints (for connectivity checks)
|
||||
EU_DATA_NODES=("api.wallarm.com" "node-data0.eu1.wallarm.com" "node-data1.eu1.wallarm.com")
|
||||
US_DATA_NODES=("us1.api.wallarm.com" "node-data0.us1.wallarm.com" "node-data1.us1.wallarm.com")
|
||||
|
||||
# Cloud region selection (populated by preflight, used by select_cloud_region)
|
||||
US_CLOUD_REACHABLE="false"
|
||||
EU_CLOUD_REACHABLE="false"
|
||||
CLOUD_REGION=""
|
||||
API_HOST=""
|
||||
|
||||
# --- Helper functions ---
|
||||
log() { echo ">>> $*"; }
|
||||
err() { echo "!!! $*" >&2; }
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
err "This script must be run as root (for systemd and /opt write access)."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "${NODES_DIR}"
|
||||
}
|
||||
|
||||
generate_systemd_template() {
|
||||
if [[ ! -f "${SYSTEMD_TEMPLATE}" ]]; then
|
||||
log "Creating systemd template: ${SYSTEMD_TEMPLATE}"
|
||||
cat > "${SYSTEMD_TEMPLATE}" <<EOF
|
||||
[Unit]
|
||||
Description=Wallarm Native Node - %I
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${NODES_DIR}/%i
|
||||
EnvironmentFile=${NODES_DIR}/%i/env
|
||||
ExecStart=${NODES_DIR}/%i/wallarm-native-node-aio.sh start
|
||||
ExecStop=${NODES_DIR}/%i/wallarm-native-node-aio.sh stop
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local node_name="$1"
|
||||
local api_token="$2"
|
||||
local api_host="$3"
|
||||
local labels="$4"
|
||||
local config_path="${NODES_DIR}/${node_name}/etc/go-node.yaml"
|
||||
local env_file="${NODES_DIR}/${node_name}/env"
|
||||
|
||||
cat > "${env_file}" <<EOF
|
||||
WALLARM_API_TOKEN=${api_token}
|
||||
WALLARM_API_HOST=${api_host}
|
||||
WALLARM_LABELS=${labels}
|
||||
WALLARM_CONFIG_PATH=${config_path}
|
||||
EOF
|
||||
chmod 600 "${env_file}" # token is sensitive
|
||||
}
|
||||
|
||||
# --- Preflight checks ---
|
||||
run_preflight() {
|
||||
local failed=0
|
||||
|
||||
log "Running preflight checks..."
|
||||
|
||||
# 1. Root privileges
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
err "Preflight failed: must be run as root."
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 2. Init system must be systemd (template unit management)
|
||||
local init_system
|
||||
init_system=$(detect_init_system)
|
||||
if [[ "$init_system" != "systemd" ]]; then
|
||||
err "Preflight failed: this manager requires systemd (detected: $init_system)."
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 3. Architecture
|
||||
local arch
|
||||
arch=$(detect_architecture)
|
||||
if [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]]; then
|
||||
err "Preflight failed: unsupported architecture '$arch' for the native node installer."
|
||||
failed=1
|
||||
fi
|
||||
log "Architecture: ${arch} (installer suffix: ${INSTALLER_ARCH})"
|
||||
|
||||
# 4. Required commands
|
||||
local required_cmds=(curl systemctl sed mkdir rm sleep)
|
||||
local missing=()
|
||||
local cmd
|
||||
for cmd in "${required_cmds[@]}"; do
|
||||
if ! command_exists "$cmd"; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
err "Preflight failed: missing required commands: ${missing[*]}"
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 5. Installer reachability
|
||||
if ! test_connectivity "$INSTALLER_URL" "Wallarm native node installer"; then
|
||||
err "Preflight failed: installer not reachable: $INSTALLER_URL"
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 6. Wallarm cloud reachability (must have at least one reachable region)
|
||||
local us eu
|
||||
us=$(test_cloud_endpoints "US" "${US_DATA_NODES[@]}")
|
||||
eu=$(test_cloud_endpoints "EU" "${EU_DATA_NODES[@]}")
|
||||
US_CLOUD_REACHABLE="$us"
|
||||
EU_CLOUD_REACHABLE="$eu"
|
||||
if [[ "$us" != "true" && "$eu" != "true" ]]; then
|
||||
err "Preflight failed: no Wallarm cloud region reachable (US/EU)."
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 7. Disk space (>= 2GB free on the nodes volume)
|
||||
local avail_kb
|
||||
avail_kb=$(df -k "$BASE_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || true)
|
||||
if [[ -n "$avail_kb" ]] && (( avail_kb < 2097152 )); then
|
||||
err "Preflight failed: insufficient disk space on $BASE_DIR (need >= 2GB free)."
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# 8. Memory (>= 2GB recommended; warning only)
|
||||
if command_exists free; then
|
||||
local mem_mb
|
||||
mem_mb=$(free -m 2>/dev/null | awk '/Mem:/ {print $2}')
|
||||
if [[ -n "$mem_mb" ]] && (( mem_mb < 2048 )); then
|
||||
err "Warning: only ${mem_mb}MB RAM detected (2GB+ recommended)."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $failed -ne 0 ]]; then
|
||||
err "Preflight check FAILED. Resolve the issues above and re-run."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Preflight checks passed."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate a listen address (IP:PORT) and check its port is free
|
||||
check_listen_port() {
|
||||
local address="$1"
|
||||
local port="${address##*:}"
|
||||
if [[ ! "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
||||
err "Invalid listen address (expected IP:PORT): $address"
|
||||
return 1
|
||||
fi
|
||||
if ! check_port_available "$port"; then
|
||||
err "Listen port $port (for $address) is already in use."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Core actions ---
|
||||
install_single_node() {
|
||||
local node_name="$1"
|
||||
local listen_address="$2"
|
||||
local api_token="$3"
|
||||
local api_host="$4"
|
||||
local labels="${5:-group=${node_name}}"
|
||||
|
||||
local work_dir="${NODES_DIR}/${node_name}"
|
||||
local installer_path="${work_dir}/wallarm-native-node-aio.sh"
|
||||
|
||||
log "[${node_name}] Installing (listening on ${listen_address})..."
|
||||
|
||||
mkdir -p "${work_dir}/etc" "${work_dir}/var/log" "${work_dir}/var/run"
|
||||
|
||||
# 1. Write config
|
||||
cat > "${work_dir}/etc/go-node.yaml" <<EOF
|
||||
mode: connector-server
|
||||
connector:
|
||||
address: "${listen_address}"
|
||||
EOF
|
||||
|
||||
# 2. Download installer if missing
|
||||
if [[ ! -f "${installer_path}" ]]; then
|
||||
log "[${node_name}] Downloading installer..."
|
||||
curl -fsSL -o "${installer_path}" "${INSTALLER_URL}" || {
|
||||
err "[${node_name}] Download failed"
|
||||
return 1
|
||||
}
|
||||
chmod +x "${installer_path}"
|
||||
fi
|
||||
|
||||
# 3. Write environment file (used by systemd and manual scripts)
|
||||
write_env_file "${node_name}" "${api_token}" "${api_host}" "${labels}"
|
||||
|
||||
# 4. Run installer
|
||||
cd "${work_dir}" || return 1
|
||||
if WALLARM_API_TOKEN="${api_token}" \
|
||||
WALLARM_API_HOST="${api_host}" \
|
||||
WALLARM_LABELS="${labels}" \
|
||||
WALLARM_CONFIG_PATH="${work_dir}/etc/go-node.yaml" \
|
||||
./wallarm-native-node-aio.sh install \
|
||||
-- --config-dir "${work_dir}/etc" \
|
||||
-- --log-dir "${work_dir}/var/log" \
|
||||
-- --pid-dir "${work_dir}/var/run" \
|
||||
> "${work_dir}/install.log" 2>&1; then
|
||||
log "[${node_name}] Installation successful."
|
||||
# Enable and start the systemd service
|
||||
systemctl enable "wallarm-node@${node_name}" 2>/dev/null || true
|
||||
systemctl start "wallarm-node@${node_name}"
|
||||
log "[${node_name}] Service started (systemctl status wallarm-node@${node_name})"
|
||||
else
|
||||
err "[${node_name}] Installation failed. Check ${work_dir}/install.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_preflight() {
|
||||
check_root
|
||||
echo ""
|
||||
if run_preflight; then
|
||||
log "Preflight passed - system ready for --install."
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
check_root
|
||||
if ! run_preflight; then
|
||||
exit 1
|
||||
fi
|
||||
ensure_dirs
|
||||
generate_systemd_template
|
||||
|
||||
read -p "Enter Wallarm API Token (with Deploy role): " WALLARM_API_TOKEN
|
||||
if [[ -z "$WALLARM_API_TOKEN" ]]; then
|
||||
err "API Token cannot be empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Select Wallarm cloud region (US/EU)
|
||||
select_cloud_region
|
||||
|
||||
echo ""
|
||||
echo "Enter each node's name and listening address (format: name IP:Port)"
|
||||
echo "Example: node1 0.0.0.0:8081"
|
||||
echo "Leave name blank to finish."
|
||||
|
||||
declare -a NODE_NAMES=()
|
||||
declare -a NODE_ADDRESSES=()
|
||||
|
||||
while true; do
|
||||
read -p "Node name (blank to stop): " name
|
||||
[[ -z "$name" ]] && break
|
||||
read -p "Listening address (e.g., 0.0.0.0:8081): " address
|
||||
if [[ -z "$address" ]]; then
|
||||
err "Address cannot be empty, skipping."
|
||||
continue
|
||||
fi
|
||||
NODE_NAMES+=("$name")
|
||||
NODE_ADDRESSES+=("$address")
|
||||
done
|
||||
|
||||
if [[ ${#NODE_NAMES[@]} -eq 0 ]]; then
|
||||
err "No nodes provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate listen ports before installing anything
|
||||
local address
|
||||
for address in "${NODE_ADDRESSES[@]}"; do
|
||||
if ! check_listen_port "$address"; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Will install ${#NODE_NAMES[@]} nodes in parallel:"
|
||||
for i in "${!NODE_NAMES[@]}"; do
|
||||
echo " - ${NODE_NAMES[$i]} -> ${NODE_ADDRESSES[$i]}"
|
||||
done
|
||||
read -p "Proceed? (y/N): " confirm
|
||||
[[ ! "$confirm" =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||
|
||||
echo ""
|
||||
log "Starting parallel installations..."
|
||||
|
||||
declare -a INSTALL_PIDS=()
|
||||
for i in "${!NODE_NAMES[@]}"; do
|
||||
install_single_node "${NODE_NAMES[$i]}" "${NODE_ADDRESSES[$i]}" "$WALLARM_API_TOKEN" "$API_HOST" &
|
||||
INSTALL_PIDS+=($!)
|
||||
done
|
||||
|
||||
FAILED=0
|
||||
local pid
|
||||
for pid in "${INSTALL_PIDS[@]}"; do
|
||||
wait "$pid" || ((FAILED++))
|
||||
done
|
||||
|
||||
if [[ $FAILED -eq 0 ]]; then
|
||||
log "All nodes installed and started via systemd."
|
||||
else
|
||||
err "$FAILED node(s) failed. Check individual install.log files."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_config() {
|
||||
# Usage: --config --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
|
||||
check_root
|
||||
local node_name="" address="" token="" labels=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--node) [[ $# -ge 2 ]] || { err "--node requires a value"; exit 1; }; node_name="$2"; shift 2 ;;
|
||||
--address) [[ $# -ge 2 ]] || { err "--address requires a value"; exit 1; }; address="$2"; shift 2 ;;
|
||||
--token) [[ $# -ge 2 ]] || { err "--token requires a value"; exit 1; }; token="$2"; shift 2 ;;
|
||||
--labels) [[ $# -ge 2 ]] || { err "--labels requires a value"; exit 1; }; labels="$2"; shift 2 ;;
|
||||
*) err "Unknown config option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$node_name" ]]; then
|
||||
err "Missing --node"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local work_dir="${NODES_DIR}/${node_name}"
|
||||
if [[ ! -d "$work_dir" ]]; then
|
||||
err "Node '$node_name' does not exist in ${NODES_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update config file
|
||||
if [[ -n "$address" ]]; then
|
||||
if ! check_listen_port "$address"; then
|
||||
exit 1
|
||||
fi
|
||||
log "Updating listening address to $address"
|
||||
sed -i "s|^\([[:space:]]*address: \).*|\1\"${address}\"|" "${work_dir}/etc/go-node.yaml"
|
||||
fi
|
||||
|
||||
# Update env file if token or labels provided (rewrite to avoid sed escaping issues)
|
||||
if [[ -n "$token" || -n "$labels" ]]; then
|
||||
local env_file="${work_dir}/env"
|
||||
[[ -f "$env_file" ]] || { err "env file not found"; exit 1; }
|
||||
local current_token current_labels
|
||||
current_token=$(grep '^WALLARM_API_TOKEN=' "$env_file" | cut -d= -f2-)
|
||||
current_labels=$(grep '^WALLARM_LABELS=' "$env_file" | cut -d= -f2-)
|
||||
write_env_file "$node_name" "${token:-$current_token}" "${labels:-$current_labels}"
|
||||
log "Token/labels updated for $node_name."
|
||||
fi
|
||||
|
||||
log "Configuration updated for $node_name. Restart with: systemctl restart wallarm-node@${node_name}"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
check_root
|
||||
local node_name=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--node) [[ $# -ge 2 ]] || { err "--node requires a value"; exit 1; }; node_name="$2"; shift 2 ;;
|
||||
*) err "Unknown remove option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$node_name" ]]; then
|
||||
err "Missing --node"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local work_dir="${NODES_DIR}/${node_name}"
|
||||
if [[ ! -d "$work_dir" ]]; then
|
||||
err "Node '$node_name' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Stopping and disabling service..."
|
||||
systemctl stop "wallarm-node@${node_name}" 2>/dev/null || true
|
||||
systemctl disable "wallarm-node@${node_name}" 2>/dev/null || true
|
||||
|
||||
log "Removing directory ${work_dir}..."
|
||||
rm -rf "$work_dir"
|
||||
log "Node $node_name removed."
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
# Show systemd status for all found nodes or a specific one
|
||||
local node_name="${1:-}"
|
||||
if [[ -n "$node_name" ]]; then
|
||||
systemctl status "wallarm-node@${node_name}" --no-pager
|
||||
else
|
||||
echo "Wallarm Nodes status:"
|
||||
local dir name
|
||||
for dir in "${NODES_DIR}"/*/; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
name=$(basename "$dir")
|
||||
echo "--- $name ---"
|
||||
systemctl status "wallarm-node@${name}" --no-pager | head -5
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Help ---
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $0 [COMMAND] [OPTIONS]
|
||||
|
||||
Commands:
|
||||
--preflight Run preflight checks only (no installation).
|
||||
--install Interactive installation of one or more nodes (parallel).
|
||||
--config Update an existing node's configuration.
|
||||
Options: --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
|
||||
--remove Remove a node completely.
|
||||
Options: --node NAME
|
||||
--status [NODE] Show systemd status for a node, or all nodes.
|
||||
|
||||
Environment:
|
||||
WALLARM_INSTALLER_URL Override the all-in-one installer URL (default: repo.wallarm.com latest).
|
||||
WALLARM_INSTALLER_ARCH Installer architecture suffix (default: x86_64).
|
||||
|
||||
Examples:
|
||||
$0 --preflight
|
||||
$0 --install
|
||||
$0 --config --node node1 --address 0.0.0.0:9090
|
||||
$0 --remove --node node2
|
||||
$0 --status
|
||||
EOF
|
||||
}
|
||||
|
||||
# --- Main argument parsing ---
|
||||
if [[ $# -eq 0 ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
--preflight) shift; cmd_preflight "$@" ;;
|
||||
--install) shift; cmd_install "$@" ;;
|
||||
--config) shift; cmd_config "$@" ;;
|
||||
--remove) shift; cmd_remove "$@" ;;
|
||||
--status) shift; cmd_status "$@" ;;
|
||||
--help|-h) show_help ;;
|
||||
*) err "Unknown command: $1"; show_help; exit 1 ;;
|
||||
esac
|
||||
26
remove.sh
Executable file
26
remove.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
# remove.sh — uninstall a running Wallarm node
|
||||
set -e
|
||||
NAME="${1:-}"
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "Usage: $0 <node-name>"
|
||||
echo " $0 --all Remove all nodes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$NAME" = "--all" ]; then
|
||||
docker ps -q --filter 'name=wallarm-' | xargs -r docker rm -f
|
||||
rm -f /opt/fw/app/state.json
|
||||
echo "All nodes removed."
|
||||
else
|
||||
docker rm -f "wallarm-${NAME}" 2>/dev/null
|
||||
python3 -c "
|
||||
import json
|
||||
try:
|
||||
with open('/opt/fw/app/state.json') as f: s = json.load(f)
|
||||
s['nodes'] = [n for n in s.get('nodes',[]) if n['name'] != '${NAME}']
|
||||
with open('/opt/fw/app/state.json','w') as f: json.dump(s, f, indent=2)
|
||||
except: pass
|
||||
" 2>/dev/null || true
|
||||
echo "Node ${NAME} removed."
|
||||
fi
|
||||
272
setup.sh
Executable file → Normal file
272
setup.sh
Executable file → Normal file
|
|
@ -1,191 +1,129 @@
|
|||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Wallarm Deployment Setup Script
|
||||
# Wallarm Docker Deployment Bootstrap
|
||||
# ==============================================================================
|
||||
# Downloads Wallarm deployment scripts from the Git repository and places
|
||||
# them in a single deploy/ directory.
|
||||
# Flow: check connectivity → install Docker → download deploy script
|
||||
# All under /opt/fw/ — self-contained, user-editable config.
|
||||
#
|
||||
# When run interactively (or piped via curl|bash to a terminal), asks which
|
||||
# deployment type to download:
|
||||
# 1. docker - Wallarm filtering node as a Docker container
|
||||
# 2. native - Wallarm filtering node installed directly on the OS (no Docker)
|
||||
# 3. both - All scripts (docker + native)
|
||||
#
|
||||
# Falls back to downloading BOTH only when fully headless (no /dev/tty).
|
||||
# Override with the DEPLOYMENT_TYPE env var:
|
||||
# DEPLOYMENT_TYPE=native curl -fsSL ".../setup.sh" | bash
|
||||
# DEPLOYMENT_TYPE=docker curl -fsSL ".../setup.sh" | bash
|
||||
#
|
||||
# Downloads the repo archive once, then copies only the needed folders
|
||||
# (common + docker and/or native) into deploy/. No per-file curl calls.
|
||||
# curl -fsSL ".../setup.sh" | bash
|
||||
# sudo /opt/fw/deploy.sh
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Color definitions
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
REPO="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main"
|
||||
WALLARM_DIR="/opt/fw"
|
||||
OFFLINE_DIR="${WALLARM_DIR}/offline"
|
||||
|
||||
# Git repository archive URL (Gitea-style)
|
||||
REPO_ARCHIVE="https://git.sechpoint.app/customer-engineering/wallarm/archive/main.tar.gz"
|
||||
echo -e "${BOLD}Wallarm Bootstrap${NC}"
|
||||
|
||||
# Temp directory for extracted archive
|
||||
TEMP_DIR=$(mktemp -d /tmp/wallarm-setup.XXXXXX)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
# Detect download command
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOAD_NAME="curl"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
DOWNLOAD_NAME="wget"
|
||||
# ── 1. Connectivity check ─────────────────────────────────────────────
|
||||
echo -e "${YELLOW}Checking connectivity...${NC}"
|
||||
if curl -fsSL --connect-timeout 5 -o /dev/null https://git.sechpoint.app 2>/dev/null; then
|
||||
echo -e "${GREEN} git.sechpoint.app reachable${NC}"
|
||||
else
|
||||
echo -e "${RED}${BOLD}ERROR:${NC} Neither curl nor wget is installed."
|
||||
echo -e "Please install one of them and run this script again."
|
||||
echo -e "${RED} Cannot reach git.sechpoint.app${NC}"
|
||||
echo -e "${RED} Check network/firewall. Aborting.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Archive download and extraction ---
|
||||
# ── 2. Ensure curl (for downloads) ────────────────────────────────────
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
apt-get update -qq 2>/dev/null && apt-get install -y -qq curl 2>/dev/null || true
|
||||
fi
|
||||
|
||||
download_archive() {
|
||||
echo -e "${YELLOW}Downloading deployment scripts from repository...${NC}"
|
||||
if [ "$DOWNLOAD_NAME" = "curl" ]; then
|
||||
curl -fsSL "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null
|
||||
else
|
||||
wget -qO- "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null
|
||||
fi
|
||||
|
||||
if [[ ! -d "$TEMP_DIR/common" ]]; then
|
||||
echo -e "${RED}${BOLD}ERROR:${NC} Failed to download or extract repository archive."
|
||||
echo -e "Check network connectivity to: ${REPO_ARCHIVE}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN} Success: archive downloaded and extracted.${NC}"
|
||||
}
|
||||
|
||||
deploy_type() {
|
||||
local type="$1"
|
||||
|
||||
# Copy common library (always needed)
|
||||
cp "$TEMP_DIR/common/"* "deploy/" 2>/dev/null || true
|
||||
|
||||
# Copy the selected deployment type's folder
|
||||
if [[ -d "$TEMP_DIR/$type" ]]; then
|
||||
cp "$TEMP_DIR/$type/"* "deploy/" 2>/dev/null || true
|
||||
chmod +x deploy/*.sh 2>/dev/null || true
|
||||
echo -e "${GREEN} Copied: common + ${type} scripts → deploy/${NC}"
|
||||
else
|
||||
echo -e "${RED} Folder '$type' not found in archive.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
clear 2>/dev/null || true
|
||||
echo -e "${BLUE}${BOLD}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ WALLARM DEPLOYMENT SETUP SCRIPT ║"
|
||||
echo "║ Downloads all necessary deployment tools ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo
|
||||
|
||||
# Decide which deployment type(s) to download
|
||||
DEPLOY_TYPES=()
|
||||
|
||||
# Priority 1: explicit DEPLOYMENT_TYPE env var (non-interactive override)
|
||||
if [[ "${DEPLOYMENT_TYPE:-}" =~ ^(docker|native)$ ]]; then
|
||||
DEPLOY_TYPES+=("$DEPLOYMENT_TYPE")
|
||||
echo -e "${GREEN}Downloading only: $DEPLOYMENT_TYPE (from DEPLOYMENT_TYPE env var)${NC}"
|
||||
|
||||
# Priority 2 + 3: try interactive prompt via terminal or /dev/tty
|
||||
# ── 3. Ensure Python 3.10+ ────────────────────────────────────────────
|
||||
PYTHON_BIN=""
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then
|
||||
PYTHON_BIN="python3"
|
||||
else
|
||||
# Determine where we can read user input from
|
||||
INTERACTIVE=""
|
||||
if [[ -t 0 ]]; then
|
||||
INTERACTIVE="/dev/stdin"
|
||||
elif [[ -c /dev/tty ]]; then
|
||||
INTERACTIVE="/dev/tty"
|
||||
fi
|
||||
echo -e "${YELLOW}Installing Python 3...${NC}"
|
||||
apt-get update -qq 2>/dev/null && apt-get install -y -qq python3 2>/dev/null || \
|
||||
yum install -y -q python3 2>/dev/null || \
|
||||
dnf install -y -q python3 2>/dev/null || \
|
||||
apk add --no-cache python3 2>/dev/null || true
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}"
|
||||
|
||||
if [[ -n "$INTERACTIVE" ]]; then
|
||||
echo -e "${CYAN}Which deployment type do you need?${NC}"
|
||||
echo -e " ${YELLOW}1${NC}) Docker only — Wallarm node as a container"
|
||||
echo -e " ${YELLOW}2${NC}) Native only — Wallarm node directly on this OS (no Docker)"
|
||||
echo -e " ${YELLOW}3${NC}) Both — docker + native scripts"
|
||||
echo
|
||||
while true; do
|
||||
read -r -p "$(echo -e "${YELLOW}Enter choice [1/2/3]: ${NC}")" choice <"$INTERACTIVE" 2>/dev/null || {
|
||||
echo -e "${YELLOW}Input unavailable, defaulting to BOTH.${NC}"
|
||||
DEPLOY_TYPES+=("docker" "native")
|
||||
break
|
||||
}
|
||||
case "$choice" in
|
||||
1) DEPLOY_TYPES+=("docker"); break ;;
|
||||
2) DEPLOY_TYPES+=("native"); break ;;
|
||||
3) DEPLOY_TYPES+=("docker" "native"); break ;;
|
||||
*) echo -e "${RED}Invalid choice. Enter 1, 2, or 3.${NC}" ;;
|
||||
esac
|
||||
done
|
||||
echo
|
||||
# ── 4. Install Docker under /opt/fw/docker/ ───────────────────────────
|
||||
DOCKER_DIR="${WALLARM_DIR}/docker"
|
||||
DOCKER_URL="${DOCKER_DOWNLOAD_URL:-https://download.docker.com/linux/static/stable/x86_64/docker-29.2.1.tgz}"
|
||||
WALLARM_IMAGE_URL="${WALLARM_IMAGE_URL:-https://git.sechpoint.app/customer-engineering/wallarm/releases/download/v1.0.0/wallarm-node-6.11.0-rc1.tar.gz}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Downloading Docker...${NC}"
|
||||
mkdir -p "${DOCKER_DIR}/bin" "${DOCKER_DIR}/data"
|
||||
|
||||
if [ -f "${OFFLINE_DIR}/docker-29.2.1.tgz" ]; then
|
||||
echo -e "${GREEN} Using offline binary${NC}"
|
||||
tar -xzf "${OFFLINE_DIR}/docker-29.2.1.tgz" -C "${DOCKER_DIR}/bin/"
|
||||
else
|
||||
# Fully headless — default to both
|
||||
DEPLOY_TYPES+=("docker" "native")
|
||||
echo -e "${CYAN}No terminal available: downloading BOTH deployment types.${NC}"
|
||||
echo -e "${YELLOW}To download only one type, set DEPLOYMENT_TYPE=docker or DEPLOYMENT_TYPE=native${NC}"
|
||||
curl -fsSL "${DOCKER_URL}" -o /tmp/docker.tgz 2>/dev/null && \
|
||||
tar --strip-components=1 -xzf /tmp/docker.tgz -C "${DOCKER_DIR}/bin/" && \
|
||||
rm -f /tmp/docker.tgz
|
||||
fi
|
||||
|
||||
# Start dockerd with custom data root
|
||||
if [ -x "${DOCKER_DIR}/bin/dockerd" ]; then
|
||||
# Symlink binaries
|
||||
ln -sf "${DOCKER_DIR}/bin/docker" /usr/bin/docker 2>/dev/null
|
||||
ln -sf "${DOCKER_DIR}/bin/dockerd" /usr/bin/dockerd 2>/dev/null
|
||||
# Start containerd first, then dockerd — and make them reboot-resistant
|
||||
cat > /etc/systemd/system/wallarm-containerd.service << UNIT
|
||||
[Unit]
|
||||
Description=Wallarm containerd
|
||||
After=network.target
|
||||
[Service]
|
||||
ExecStart=${DOCKER_DIR}/bin/containerd
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
cat > /etc/systemd/system/wallarm-dockerd.service << UNIT
|
||||
[Unit]
|
||||
Description=Wallarm Docker daemon
|
||||
After=wallarm-containerd.service
|
||||
Requires=wallarm-containerd.service
|
||||
[Service]
|
||||
ExecStart=${DOCKER_DIR}/bin/dockerd --data-root ${DOCKER_DIR}/data --exec-root ${DOCKER_DIR}/exec --pidfile ${DOCKER_DIR}/docker.pid --userland-proxy=false
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable wallarm-containerd wallarm-dockerd
|
||||
systemctl start wallarm-containerd wallarm-dockerd
|
||||
export PATH="${DOCKER_DIR}/bin:$PATH"
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
|
||||
# Download archive once, then copy only the selected folders
|
||||
echo
|
||||
mkdir -p "deploy"
|
||||
download_archive
|
||||
# ── 5. Pre-load Wallarm image if available ────────────────────────────
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if [ -f "${OFFLINE_DIR}/wallarm-node.tar" ]; then
|
||||
echo -e "${YELLOW}Loading offline Wallarm image...${NC}"
|
||||
docker load -i "${OFFLINE_DIR}/wallarm-node.tar" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
for type in "${DEPLOY_TYPES[@]}"; do
|
||||
deploy_type "$type"
|
||||
done
|
||||
|
||||
# Clean exit trap removes temp dir
|
||||
|
||||
# Patch library sourcing path for flat deploy/ structure
|
||||
# (repo scripts still reference ../common/ — fix until they're updated)
|
||||
for script in deploy/*.sh; do
|
||||
[[ "$script" == "deploy/wallarm-lib.sh" ]] && continue
|
||||
sed -i 's|source "\$SCRIPT_DIR/\.\./common/wallarm-lib\.sh"|source "$SCRIPT_DIR/wallarm-lib.sh"|' "$script"
|
||||
sed -i 's|# shellcheck source=\.\./common/wallarm-lib\.sh|# shellcheck source=./wallarm-lib.sh|' "$script"
|
||||
done
|
||||
# ── 6. Download deploy script ─────────────────────────────────────────
|
||||
APP_DIR="${WALLARM_DIR}/app"
|
||||
mkdir -p "${APP_DIR}"
|
||||
echo -e "${YELLOW}Downloading deploy script...${NC}"
|
||||
curl -fsSL "${REPO}/sources/python/main.py" -o "${APP_DIR}/main.py"
|
||||
curl -fsSL "${REPO}/../deploy.sh" -o "${WALLARM_DIR}/deploy.sh" 2>/dev/null || {
|
||||
# Fallback: create wrapper
|
||||
cat > "${WALLARM_DIR}/deploy.sh" << 'WRAPPER'
|
||||
#!/bin/bash
|
||||
exec python3 /opt/fw/app/main.py "$@"
|
||||
WRAPPER
|
||||
}
|
||||
chmod +x "${APP_DIR}/main.py" "${WALLARM_DIR}/deploy.sh"
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}${BOLD}Setup complete - requested scripts downloaded!${NC}"
|
||||
echo -e "${GREEN}${BOLD}Ready!${NC}"
|
||||
echo -e " ${GREEN}sudo /opt/fw/deploy.sh${NC}"
|
||||
echo
|
||||
|
||||
# Show next steps only for the deployment types that were downloaded
|
||||
for deploy_type in "${DEPLOY_TYPES[@]}"; do
|
||||
case "$deploy_type" in
|
||||
docker)
|
||||
echo -e "${CYAN}Docker deployment next steps:${NC}"
|
||||
echo -e " 1. Run the preflight check: ${YELLOW}./deploy/wallarm-docker.sh --preflight${NC}"
|
||||
echo -e " 2. Deploy a Wallarm node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --install${NC}"
|
||||
echo -e " 3. Reconfigure existing node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --config${NC}"
|
||||
echo -e " 4. Uninstall a node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --remove${NC}"
|
||||
echo -e " 5. Show node status: ${YELLOW}./deploy/wallarm-docker.sh --status${NC}"
|
||||
echo
|
||||
;;
|
||||
native)
|
||||
echo -e "${CYAN}Native deployment next steps (no Docker):${NC}"
|
||||
echo -e " 1. Run the preflight check: ${YELLOW}sudo ./deploy/wallarm-native.sh --preflight${NC}"
|
||||
echo -e " 2. Deploy Wallarm nodes: ${YELLOW}sudo ./deploy/wallarm-native.sh --install${NC}"
|
||||
echo -e " 3. Update a node's config: ${YELLOW}sudo ./deploy/wallarm-native.sh --config --node NAME --address IP:PORT${NC}"
|
||||
echo -e " 4. Remove a node: ${YELLOW}sudo ./deploy/wallarm-native.sh --remove --node NAME${NC}"
|
||||
echo -e " 5. Show node status: ${YELLOW}./deploy/wallarm-native.sh --status${NC}"
|
||||
echo
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${YELLOW}Note: Some scripts require sudo. Run them with: sudo ./<script>${NC}"
|
||||
echo -e "${YELLOW}Make sure you have the required information ready (see documentation).${NC}"
|
||||
echo -e " Config: /opt/fw/app/fw.conf"
|
||||
echo -e " Docker: /opt/fw/docker/"
|
||||
echo -e " Offline: /opt/fw/offline/ (drop binaries here for air-gapped)"
|
||||
|
|
|
|||
1
sources/binaries/README.md
Normal file
1
sources/binaries/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Docker static binaries for offline/air-gapped deployment.
|
||||
1
sources/images/README.md
Normal file
1
sources/images/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Wallarm Docker images for offline/air-gapped deployment.
|
||||
252
sources/python/deploy.py
Normal file
252
sources/python/deploy.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wallarm Docker Node Manager"""
|
||||
|
||||
import json, os, sys, time, subprocess, shutil
|
||||
|
||||
VERSION = "2.0.0"
|
||||
BASE = "/opt/fw"
|
||||
APP = f"{BASE}/app"
|
||||
CONF = f"{APP}/fw.conf"
|
||||
STATE = f"{APP}/state.json"
|
||||
DOCKER_DIR = f"{BASE}/docker"
|
||||
OFFLINE_DIR = f"{BASE}/offline"
|
||||
WALLARM_IMAGE = os.environ.get("WALLARM_IMAGE", "wallarm/node:6.11.0-rc1")
|
||||
|
||||
# Ensure Docker is in PATH
|
||||
os.environ["PATH"] = f"{DOCKER_DIR}/bin:{os.environ.get('PATH', '')}"
|
||||
|
||||
def run(cmd, timeout=120):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
def load_config():
|
||||
if not os.path.exists(CONF): return {}
|
||||
with open(CONF) as f: return json.load(f)
|
||||
|
||||
def load_state():
|
||||
if not os.path.exists(STATE): return {"nodes": []}
|
||||
with open(STATE) as f: return json.load(f)
|
||||
|
||||
def save_state(s):
|
||||
os.makedirs(APP, exist_ok=True)
|
||||
with open(STATE, "w") as f: json.dump(s, f, indent=2)
|
||||
|
||||
# ─── Docker ────────────────────────────────────────────────────────────
|
||||
|
||||
def install_docker():
|
||||
if shutil.which("docker"):
|
||||
return
|
||||
print("Installing Docker...")
|
||||
run("apt-get update -qq && apt-get install -y -qq docker.io 2>/dev/null || yum install -y -q docker 2>/dev/null", timeout=120)
|
||||
run("systemctl enable docker && systemctl start docker")
|
||||
|
||||
def deploy_container(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
|
||||
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
|
||||
|
||||
# Ensure Docker is running
|
||||
install_docker()
|
||||
|
||||
# Pull image if needed
|
||||
if run(f"docker images -q {WALLARM_IMAGE}").stdout.strip() == "":
|
||||
print(f"[{name}] Pulling Wallarm image...")
|
||||
run(f"docker pull {WALLARM_IMAGE}", timeout=300)
|
||||
|
||||
# Remove old container if exists
|
||||
container = f"wallarm-{name}"
|
||||
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||
|
||||
# Run container
|
||||
monitoring_port = int(port) + 10
|
||||
print(f"[{name}] Starting container on port {port}...")
|
||||
cmd = f"""docker run -d \
|
||||
--name {container} \
|
||||
--restart unless-stopped \
|
||||
-p {port}:{port} \
|
||||
-v wallarm-data-{name}:/opt/wallarm \
|
||||
-p {monitoring_port}:{monitoring_port} \
|
||||
-e WALLARM_API_TOKEN={token} \
|
||||
-e WALLARM_API_HOST={api_host} \
|
||||
-e WALLARM_MODE={mode} \
|
||||
-e NGINX_PORT={port}"""
|
||||
if upstream_ip:
|
||||
cmd += f" -e WALLARM_UPSTREAM=http://{upstream_ip}:{upstream_port}"
|
||||
cmd += f" {WALLARM_IMAGE}"
|
||||
|
||||
r = run(cmd, timeout=30)
|
||||
if r.returncode != 0:
|
||||
print(f"❌ {name}: {r.stderr}")
|
||||
return False
|
||||
|
||||
# Save state
|
||||
s = load_state()
|
||||
s.setdefault("nodes", []).append({
|
||||
"name": name, "type": "docker", "port": int(port),
|
||||
"upstream_ip": upstream_ip, "upstream_port": int(upstream_port),
|
||||
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
})
|
||||
save_state(s)
|
||||
print(f"✅ {name} running on port {port} (docker ps --filter name={container})")
|
||||
return True
|
||||
|
||||
def remove_container(name):
|
||||
container = f"wallarm-{name}"
|
||||
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||
shutil.rmtree(f"/opt/wallarm-{name}", ignore_errors=True)
|
||||
s = load_state()
|
||||
s["nodes"] = [n for n in s.get("nodes", []) if n["name"] != name]
|
||||
save_state(s)
|
||||
print(f"✅ {name} removed")
|
||||
|
||||
def container_status():
|
||||
r = run("docker ps --filter 'name=wallarm-' --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'")
|
||||
if not r.stdout.strip():
|
||||
print("No containers running.")
|
||||
return
|
||||
print("\n─── Containers ───")
|
||||
for line in r.stdout.strip().split("\n"):
|
||||
print(f" {line}")
|
||||
|
||||
# ─── Deploy All ───────────────────────────────────────────────────────
|
||||
|
||||
def deploy_all():
|
||||
cfg = load_config()
|
||||
nodes = cfg.get("nodes", cfg) # support both {"nodes":{...}} and flat format
|
||||
if not nodes:
|
||||
print("No nodes in fw.conf")
|
||||
return
|
||||
for name, nc in nodes.items():
|
||||
if not isinstance(nc, dict): continue
|
||||
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
|
||||
str(nc.get("upstream_port","80")), nc.get("labels",""),
|
||||
nc.get("mode","monitoring"))
|
||||
|
||||
# ─── Menu ─────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
|
||||
return deploy_all()
|
||||
|
||||
print(f"═══ Wallarm Node Manager v{VERSION} ═══\n")
|
||||
print("Preflight checks...")
|
||||
if not shutil.which("docker"):
|
||||
print(" ❌ Docker not found. Run setup.sh first.")
|
||||
sys.exit(1)
|
||||
print(" ✅ Docker ready\n")
|
||||
|
||||
cfg = load_config()
|
||||
deployed = {n["name"] for n in load_state().get("nodes", [])}
|
||||
|
||||
while True:
|
||||
undeployed = [k for k in cfg if k not in deployed]
|
||||
|
||||
print("─── Menu ───")
|
||||
if undeployed:
|
||||
print(f" Nodes ready to deploy: {', '.join(undeployed)}")
|
||||
else:
|
||||
print(" No nodes pending deployment.")
|
||||
|
||||
if undeployed:
|
||||
print(" [1] Deploy all pending nodes")
|
||||
print(" [2] Add a new node")
|
||||
if deployed:
|
||||
print(" [3] Show status")
|
||||
print(" [4] Remove a node")
|
||||
print(" [q] Quit")
|
||||
|
||||
c = input("\nChoice: ").strip()
|
||||
|
||||
if c == "1" and undeployed:
|
||||
for name in undeployed:
|
||||
nc = cfg[name]
|
||||
print(f"\nDeploying {name}...")
|
||||
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||
str(nc.get("port","8081")), nc.get("upstream_ip",""),
|
||||
str(nc.get("upstream_port","80")), "",
|
||||
nc.get("mode","monitoring"))
|
||||
elif c == "2":
|
||||
add_node()
|
||||
cfg = load_config()
|
||||
deployed = {n["name"] for n in load_state().get("nodes", [])}
|
||||
elif c == "3" and deployed:
|
||||
container_status()
|
||||
elif c == "4" and deployed:
|
||||
remove_menu()
|
||||
elif c.lower() == "q":
|
||||
break
|
||||
|
||||
def create_config():
|
||||
print("\n─── Create Node Config ───")
|
||||
name = input("Node name: ").strip()
|
||||
if not name: return
|
||||
token = input("Wallarm token: ").strip()
|
||||
cloud = input("Cloud [EU]: ").strip() or "EU"
|
||||
port = input("Port [8081]: ").strip() or "8081"
|
||||
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
|
||||
upstream_port = input("Upstream port [80]: ").strip() or "80"
|
||||
mode = input("Mode [monitoring]: ").strip() or "monitoring"
|
||||
|
||||
cfg = load_config()
|
||||
cfg[name] = {"token": token, "cloud": cloud, "port": port,
|
||||
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
|
||||
"labels": f"group={name}", "mode": mode}
|
||||
os.makedirs(APP, exist_ok=True)
|
||||
with open(CONF, "w") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
print(f"✅ {name} added to fw.conf")
|
||||
|
||||
def add_node():
|
||||
print("\n─── Add New Node ───")
|
||||
name = input("Node name: ").strip()
|
||||
if not name: return
|
||||
token = input("Wallarm token: ").strip()
|
||||
if not token: print("Token required."); return
|
||||
cloud = input("Cloud region [EU]: ").strip() or "EU"
|
||||
port = input("Port [8081]: ").strip() or "8081"
|
||||
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
|
||||
upstream_port = input("Upstream port [80]: ").strip() or "80"
|
||||
mode = input("Mode [monitoring]: ").strip() or "monitoring"
|
||||
|
||||
# Save to fw.conf
|
||||
cfg = load_config()
|
||||
cfg[name] = {"token": token, "cloud": cloud, "port": port,
|
||||
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
|
||||
"labels": f"group={name}", "mode": mode}
|
||||
os.makedirs(APP, exist_ok=True)
|
||||
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
|
||||
|
||||
# Deploy immediately
|
||||
print(f"\nDeploying {name}...")
|
||||
deploy_container(name, token, cloud, port, upstream_ip, upstream_port, f"group={name}", mode)
|
||||
|
||||
def remove_menu():
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes deployed."); return
|
||||
print("\n─── Remove Node ───")
|
||||
for i, n in enumerate(s):
|
||||
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
|
||||
c = input("\nSelect number: ").strip()
|
||||
if not c.isdigit() or int(c) < 1 or int(c) > len(s): return
|
||||
remove_container(s[int(c)-1]["name"])
|
||||
|
||||
def edit_menu():
|
||||
cfg = load_config()
|
||||
if not cfg: print("No nodes in fw.conf"); return
|
||||
print("\n─── Edit Node ───")
|
||||
names = list(cfg.keys())
|
||||
for i, name in enumerate(names):
|
||||
nc = cfg[name]
|
||||
print(f" [{i+1}] {name} (port {nc.get('port','')})")
|
||||
c = input("\nSelect number: ").strip()
|
||||
if not c.isdigit() or int(c) < 1 or int(c) > len(names): return
|
||||
name = names[int(c)-1]
|
||||
nc = cfg[name]
|
||||
print(f"\nEditing {name}. Leave blank to keep current value:")
|
||||
for key, prompt in [("port","Port"), ("upstream_ip","Upstream IP"),
|
||||
("upstream_port","Upstream port"), ("mode","Mode")]:
|
||||
v = input(f"{prompt} [{nc.get(key,'')}]: ").strip()
|
||||
if v: nc[key] = v
|
||||
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
|
||||
print(f"✅ {name} updated in fw.conf. Re-deploy to apply changes.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
471
sources/python/main.py
Normal file
471
sources/python/main.py
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wallarm Docker Node Manager"""
|
||||
|
||||
import json, os, sys, time, subprocess, shutil
|
||||
|
||||
VERSION = "2.0.0"
|
||||
BASE = "/opt/fw"
|
||||
APP = f"{BASE}/app"
|
||||
CONF = f"{APP}/fw.conf"
|
||||
STATE = f"{APP}/state.json"
|
||||
DOCKER_DIR = f"{BASE}/docker"
|
||||
OFFLINE_DIR = f"{BASE}/offline"
|
||||
WALLARM_IMAGE = os.environ.get("WALLARM_IMAGE", "wallarm/node:6.11.0-rc1")
|
||||
|
||||
# Ensure Docker is in PATH
|
||||
os.environ["PATH"] = f"{DOCKER_DIR}/bin:{os.environ.get('PATH', '')}"
|
||||
|
||||
def run(cmd, timeout=120):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
def load_config():
|
||||
if not os.path.exists(CONF): return {}
|
||||
with open(CONF) as f: return json.load(f)
|
||||
|
||||
def load_state():
|
||||
if not os.path.exists(STATE): return {"nodes": []}
|
||||
with open(STATE) as f: return json.load(f)
|
||||
|
||||
def save_state(s):
|
||||
os.makedirs(APP, exist_ok=True)
|
||||
with open(STATE, "w") as f: json.dump(s, f, indent=2)
|
||||
|
||||
# ─── Docker ────────────────────────────────────────────────────────────
|
||||
|
||||
def install_docker():
|
||||
if shutil.which("docker"):
|
||||
return
|
||||
print("Installing Docker...")
|
||||
run("apt-get update -qq && apt-get install -y -qq docker.io 2>/dev/null || yum install -y -q docker 2>/dev/null", timeout=120)
|
||||
run("systemctl enable docker && systemctl start docker")
|
||||
|
||||
def deploy_container(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
|
||||
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
|
||||
|
||||
# Ensure Docker is running
|
||||
install_docker()
|
||||
|
||||
# Pull image if needed
|
||||
if run(f"docker images -q {WALLARM_IMAGE}").stdout.strip() == "":
|
||||
print(f"[{name}] Pulling Wallarm image...")
|
||||
run(f"docker pull {WALLARM_IMAGE}", timeout=300)
|
||||
|
||||
# Remove old container
|
||||
container = f"wallarm-{name}"
|
||||
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||
|
||||
host_dir = f"{BASE}/{name}"
|
||||
os.makedirs(host_dir, exist_ok=True)
|
||||
|
||||
# Generate start.sh
|
||||
upstream = f"{upstream_ip}:{upstream_port}"
|
||||
start_path = f"{host_dir}/start.sh"
|
||||
with open(start_path, "w") as f:
|
||||
f.write(f"""#!/bin/bash
|
||||
# Wallarm node: {name}
|
||||
docker rm -f {container} 2>/dev/null || true
|
||||
docker run -d \\
|
||||
--name {container} \\
|
||||
--restart always \\
|
||||
-p {port}:80 \\
|
||||
-e WALLARM_API_TOKEN='{token}' \\
|
||||
-e WALLARM_API_HOST={api_host} \\
|
||||
-e WALLARM_MODE={mode} \\
|
||||
-e NGINX_BACKEND={upstream} \\
|
||||
-v {host_dir}/nginx.conf:/etc/nginx/http.d/default.conf:ro \\
|
||||
{WALLARM_IMAGE}
|
||||
sleep 3
|
||||
docker ps --filter name={container} --format '{{{{.Status}}}}'
|
||||
""")
|
||||
os.chmod(start_path, 0o755)
|
||||
|
||||
# Write nginx.conf (always regenerate to apply template updates)
|
||||
nginx_conf = f"{host_dir}/nginx.conf"
|
||||
with open(nginx_conf, "w") as f:
|
||||
f.write(f"""# Wallarm node: {name}
|
||||
# Mount with: -v $(pwd)/nginx.conf:/etc/nginx/http.d/default.conf:ro
|
||||
server {{
|
||||
listen 80;
|
||||
server_name _;
|
||||
client_max_body_size 1024m;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
# Pass all headers through — preserve auth cookies
|
||||
proxy_pass_request_headers on;
|
||||
|
||||
location /wallarm-status {{
|
||||
wallarm_status on;
|
||||
allow 127.0.0.0/8;
|
||||
deny all;
|
||||
}}
|
||||
|
||||
location / {{
|
||||
proxy_pass http://{upstream};
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
print(f"[{name}] Starting...")
|
||||
r = run(f"bash {start_path}", timeout=30)
|
||||
if r.returncode != 0 or "Error" in r.stderr or "Error" in r.stdout:
|
||||
print(f"❌ {name}: {r.stderr or r.stdout}")
|
||||
return False
|
||||
|
||||
# Verify container is running
|
||||
time.sleep(2)
|
||||
check = run(f"docker inspect -f '{{{{.State.Running}}}}' {container} 2>/dev/null")
|
||||
if check.stdout.strip() != "true":
|
||||
logs = run(f"docker logs {container} 2>&1").stdout[-500:]
|
||||
print(f"❌ {name} failed to start:\n{logs}")
|
||||
return False
|
||||
|
||||
# Save state (dedup)
|
||||
s = load_state()
|
||||
s.setdefault("nodes", [])
|
||||
s["nodes"] = [n for n in s["nodes"] if n["name"] != name] # remove old
|
||||
s["nodes"].append({
|
||||
"name": name, "type": "docker", "port": int(port),
|
||||
"upstream_ip": upstream_ip, "upstream_port": int(upstream_port),
|
||||
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
})
|
||||
save_state(s)
|
||||
print(f"✅ {name} running on port {port} (docker ps --filter name={container})")
|
||||
return True
|
||||
|
||||
def remove_container(name):
|
||||
container = f"wallarm-{name}"
|
||||
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||
shutil.rmtree(f"/opt/wallarm-{name}", ignore_errors=True)
|
||||
s = load_state()
|
||||
s["nodes"] = [n for n in s.get("nodes", []) if n["name"] != name]
|
||||
save_state(s)
|
||||
print(f"✅ {name} removed")
|
||||
|
||||
def container_status():
|
||||
r = run("docker ps --filter 'name=wallarm-' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null")
|
||||
if not r.stdout.strip():
|
||||
print("No containers running.")
|
||||
return
|
||||
print("\n─── Containers ───")
|
||||
print(r.stdout)
|
||||
|
||||
# ─── Deploy All ───────────────────────────────────────────────────────
|
||||
|
||||
def deploy_all():
|
||||
cfg = load_config()
|
||||
nodes = cfg.get("nodes", cfg) # support both {"nodes":{...}} and flat format
|
||||
if not nodes:
|
||||
print("No nodes in fw.conf")
|
||||
return
|
||||
for name, nc in nodes.items():
|
||||
if not isinstance(nc, dict): continue
|
||||
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
|
||||
str(nc.get("upstream_port","80")), nc.get("labels",""),
|
||||
nc.get("mode","monitoring"))
|
||||
|
||||
# ─── Menu ─────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
|
||||
return deploy_all()
|
||||
|
||||
print(f"═══ Wallarm Node Manager v{VERSION} ═══\n")
|
||||
print("Preflight checks...")
|
||||
if not shutil.which("docker"):
|
||||
print(" ❌ Docker not found. Run setup.sh first.")
|
||||
sys.exit(1)
|
||||
print(" ✅ Docker ready\n")
|
||||
|
||||
cfg = load_config()
|
||||
deployed = {n["name"] for n in load_state().get("nodes", [])}
|
||||
|
||||
while True:
|
||||
undeployed = [k for k in cfg if k not in deployed]
|
||||
|
||||
print("\n─── Menu ───")
|
||||
print(" [1] Add a new node")
|
||||
|
||||
n = 2
|
||||
node_map = {}
|
||||
ops = {}
|
||||
for name in undeployed:
|
||||
print(f" [{n}] Deploy {name} (port {cfg[name].get('port','?')})")
|
||||
node_map[str(n)] = name
|
||||
n += 1
|
||||
|
||||
if deployed:
|
||||
print(f" [{n}] Update nodes"); ops['update'] = str(n); n += 1
|
||||
print(f" [{n}] Edit nodes"); ops['edit'] = str(n); n += 1
|
||||
print(f" [{n}] Delete a node"); ops['del'] = str(n); n += 1
|
||||
print(f" [{n}] Status"); ops['status'] = str(n); n += 1
|
||||
print(f" [{n}] Remote tunnel"); ops['tunnel'] = str(n); n += 1
|
||||
print(f" [{n}] Debug (tap proxy)"); ops['debug'] = str(n); n += 1
|
||||
print(" [q] Quit")
|
||||
|
||||
c = input("\nChoice: ").strip()
|
||||
|
||||
if c == "1":
|
||||
add_node()
|
||||
cfg = load_config()
|
||||
deployed = {n["name"] for n in load_state().get("nodes", [])}
|
||||
elif c in node_map:
|
||||
name = node_map[c]
|
||||
nc = cfg[name]
|
||||
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||
str(nc.get("port","8081")), nc.get("upstream_ip",""),
|
||||
str(nc.get("upstream_port","80")), "",
|
||||
nc.get("mode","monitoring"))
|
||||
deployed.add(name)
|
||||
elif c == ops.get('update') and deployed:
|
||||
update_nodes()
|
||||
elif c == ops.get('edit') and deployed:
|
||||
edit_nodes()
|
||||
elif c == ops.get('del') and deployed:
|
||||
remove_menu()
|
||||
elif c == ops.get('status'):
|
||||
container_status()
|
||||
elif c == ops.get('tunnel'):
|
||||
start_tunnel()
|
||||
elif c == ops.get('debug'):
|
||||
start_debug()
|
||||
elif c.lower() == "q":
|
||||
break
|
||||
|
||||
|
||||
def add_node():
|
||||
print("\n─── Add New Node ───")
|
||||
name = input("Node name: ").strip()
|
||||
if not name: return
|
||||
token = input("Wallarm token: ").strip()
|
||||
if not token: print("Token required."); return
|
||||
print("Cloud region:")
|
||||
print(" [1] EU (api.wallarm.com)")
|
||||
print(" [2] US (us1.api.wallarm.com)")
|
||||
c = input("Choose [1]: ").strip()
|
||||
cloud = "EU" if c == "2" else "EU"
|
||||
if c == "2": cloud = "US"
|
||||
port = input("Port [8081]: ").strip() or "8081"
|
||||
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
|
||||
upstream_port = input("Upstream port [80]: ").strip() or "80"
|
||||
print("\nTraffic mode:")
|
||||
print(" [1] monitoring — detect only")
|
||||
print(" [2] safe_blocking — block definitely malicious")
|
||||
print(" [3] block — block all attacks")
|
||||
print(" [4] off — disable")
|
||||
m = input("Choose [1]: ").strip()
|
||||
mode = {"1":"monitoring","2":"safe_blocking","3":"block","4":"off"}.get(m, "monitoring")
|
||||
|
||||
# Save to fw.conf
|
||||
cfg = load_config()
|
||||
cfg[name] = {"token": token, "cloud": cloud, "port": port,
|
||||
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
|
||||
"labels": f"group={name}", "mode": mode}
|
||||
os.makedirs(APP, exist_ok=True)
|
||||
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
|
||||
|
||||
# Deploy immediately
|
||||
print(f"\nDeploying {name}...")
|
||||
deploy_container(name, token, cloud, port, upstream_ip, upstream_port, f"group={name}", mode)
|
||||
|
||||
def remove_menu():
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes deployed."); return
|
||||
print("\n─── Remove Node ───")
|
||||
for i, n in enumerate(s):
|
||||
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
|
||||
c = input("\nSelect number: ").strip()
|
||||
if not c.isdigit() or int(c) < 1 or int(c) > len(s): return
|
||||
remove_container(s[int(c)-1]["name"])
|
||||
|
||||
def edit_menu():
|
||||
cfg = load_config()
|
||||
if not cfg: print("No nodes in fw.conf"); return
|
||||
print("\n─── Edit Node ───")
|
||||
names = list(cfg.keys())
|
||||
for i, name in enumerate(names):
|
||||
nc = cfg[name]
|
||||
print(f" [{i+1}] {name} (port {nc.get('port','')})")
|
||||
c = input("\nSelect number: ").strip()
|
||||
if not c.isdigit() or int(c) < 1 or int(c) > len(names): return
|
||||
name = names[int(c)-1]
|
||||
nc = cfg[name]
|
||||
print(f"\nEditing {name}. Leave blank to keep current value:")
|
||||
for key, prompt in [("port","Port"), ("upstream_ip","Upstream IP"),
|
||||
("upstream_port","Upstream port"), ("mode","Mode")]:
|
||||
v = input(f"{prompt} [{nc.get(key,'')}]: ").strip()
|
||||
if v: nc[key] = v
|
||||
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
|
||||
print(f"✅ {name} updated in fw.conf. Re-deploy to apply changes.")
|
||||
|
||||
main()
|
||||
def start_tunnel():
|
||||
host = input("Jumphost URL [ssh.sechpoint.app:443]: ").strip() or "ssh.sechpoint.app:443"
|
||||
user = input("Username [wallarm-tunnel]: ").strip() or "wallarm-tunnel"
|
||||
pw = input("Password or SSH key path: ").strip()
|
||||
if not pw: print("No credentials."); return
|
||||
print(f"\nShare: ssh -p 9042 {user}@{host}\nCtrl+C to close.\n")
|
||||
# Requires paramiko: pip install paramiko
|
||||
try:
|
||||
import paramiko, socket, threading
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((host.split(":")[0], 443))
|
||||
t = paramiko.Transport(sock)
|
||||
if pw.startswith("/"): t.connect(username=user, key_filename=pw)
|
||||
else: t.connect(username=user, password=pw)
|
||||
t.request_port_forward("", 9042, "localhost", 22)
|
||||
print("Tunnel active. Ctrl+C to close.")
|
||||
threading.Event().wait()
|
||||
except ImportError:
|
||||
print("Install paramiko: pip3 install paramiko")
|
||||
except Exception as e:
|
||||
print(f"Tunnel failed: {e}")
|
||||
def edit_nodes():
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes."); return
|
||||
print("\n─── Edit Node ───")
|
||||
for i, n in enumerate(s):
|
||||
r = run(f"docker inspect -f '{{{{.State.Status}}}}' wallarm-{n['name']} 2>/dev/null")
|
||||
st = "●" if r.stdout.strip() == "running" else "○"
|
||||
print(f" [{i+1}] {st} {n['name']} (port {n.get('port','')})")
|
||||
print(" [b] Back")
|
||||
c = input("\nSelect node: ").strip()
|
||||
if c.lower() == 'b': return
|
||||
if not c.isdigit(): return
|
||||
idx = int(c) - 1
|
||||
if idx < 0 or idx >= len(s): return
|
||||
n = s[idx]
|
||||
|
||||
cfg = load_config()
|
||||
nc = cfg.get(n['name'], {})
|
||||
|
||||
while True:
|
||||
print(f"\n─── Editing: {n['name']} ───")
|
||||
print(f" [1] Token: {'*'*8}")
|
||||
print(f" [2] Cloud: {nc.get('cloud','EU')}")
|
||||
print(f" [3] Port: {n.get('port','')}")
|
||||
print(f" [4] Upstream: {nc.get('upstream_ip','')}:{nc.get('upstream_port','')}")
|
||||
print(f" [5] Mode: {nc.get('mode','monitoring')}")
|
||||
print(f" [s] Save & restart")
|
||||
print(f" [b] Back")
|
||||
|
||||
c = input("\nEdit field: ").strip()
|
||||
if c == '1': nc['token'] = input("New token: ").strip() or nc.get('token','')
|
||||
elif c == '2':
|
||||
print(" [1] EU [2] US")
|
||||
r = input("Cloud: ").strip()
|
||||
nc['cloud'] = "US" if r == "2" else "EU"
|
||||
elif c == '3': nc['port'] = input("Port: ").strip() or nc.get('port','')
|
||||
elif c == '4':
|
||||
up = input("Upstream (ip:port): ").strip()
|
||||
if ':' in up:
|
||||
ip, p = up.split(':')
|
||||
nc['upstream_ip'] = ip; nc['upstream_port'] = p
|
||||
elif c == '5':
|
||||
print(" [1] monitoring [2] safe_blocking [3] block [4] off")
|
||||
m = input("Mode: ").strip()
|
||||
nc['mode'] = {"1":"monitoring","2":"safe_blocking","3":"block","4":"off"}.get(m, nc.get('mode','monitoring'))
|
||||
elif c.lower() == 's':
|
||||
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
|
||||
deploy_container(n['name'], nc['token'], nc.get('cloud','EU'),
|
||||
str(nc.get('port','')), nc.get('upstream_ip',''),
|
||||
str(nc.get('upstream_port','')), "",
|
||||
nc.get('mode','monitoring'))
|
||||
print(f"✅ {n['name']} updated & restarted.")
|
||||
break
|
||||
elif c.lower() == 'b': break
|
||||
def remove_menu():
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes."); return
|
||||
print("\n─── Delete Node ───")
|
||||
for i, n in enumerate(s):
|
||||
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
|
||||
print(" [b] Back")
|
||||
c = input("\nSelect: ").strip()
|
||||
if c.lower() == 'b': return
|
||||
if not c.isdigit(): return
|
||||
idx = int(c) - 1
|
||||
if 0 <= idx < len(s):
|
||||
remove_container(s[idx]["name"])
|
||||
|
||||
def start_debug():
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes."); return
|
||||
print("\n─── Debug (Tap Proxy) ───")
|
||||
for i, n in enumerate(s):
|
||||
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
|
||||
c = input("\nSelect node: ").strip()
|
||||
if not c.isdigit(): return
|
||||
idx = int(c) - 1
|
||||
if idx < 0 or idx >= len(s): return
|
||||
n = s[idx]
|
||||
port = str(n.get('port', '8081'))
|
||||
backend = f"{n.get('upstream_ip','127.0.0.1')}:{n.get('upstream_port','80')}"
|
||||
logdir = f"{BASE}/{n['name']}"
|
||||
os.makedirs(logdir, exist_ok=True)
|
||||
|
||||
tap_script = f"{BASE}/app/tap-proxy.py"
|
||||
|
||||
# Kill any stale tap proxy on this port
|
||||
run(f"kill -9 $(ss -tlnp 'sport = :{port}' | grep -oP 'pid=\\K[0-9]+') 2>/dev/null", timeout=5)
|
||||
|
||||
# Stop container, start tap
|
||||
print(f"\nStopping wallarm-{n['name']}...")
|
||||
run(f"docker stop wallarm-{n['name']} 2>/dev/null", timeout=10)
|
||||
run(f"docker rm -f wallarm-{n['name']} 2>/dev/null", timeout=5)
|
||||
# Wait for port to free
|
||||
import time
|
||||
for _ in range(10):
|
||||
r = run(f"ss -tlnp | grep ':{port}' 2>/dev/null")
|
||||
if not r.stdout.strip():
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
print(f"Port {port} still in use — force killing...")
|
||||
run(f"ss -K 'sport = :{port}' 2>/dev/null; kill -9 $(ss -tlnp 'sport = :{port}' | grep -oP 'pid=\\K[0-9]+') 2>/dev/null", timeout=5)
|
||||
time.sleep(2)
|
||||
print(f"Tap proxy :{port} → {backend}")
|
||||
print(f"Log: {logdir}/tap-*.log")
|
||||
print("Press Ctrl+C to stop — container will restart automatically.\n")
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.call(["python3", tap_script, port, backend, logdir])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
print(f"\nRestarting wallarm-{n['name']}...")
|
||||
run(f"docker start wallarm-{n['name']} 2>/dev/null || bash {BASE}/{n['name']}/start.sh", timeout=30)
|
||||
|
||||
|
||||
def update_nodes():
|
||||
"""Rebuild containers using current config — no config changes, just apply template updates."""
|
||||
cfg = load_config()
|
||||
s = load_state().get("nodes", [])
|
||||
if not s: print("No nodes."); return
|
||||
print("\n─── Update Nodes ───")
|
||||
for i, n in enumerate(s):
|
||||
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
|
||||
print(" [a] All [b] Back")
|
||||
c = input("\nSelect: ").strip()
|
||||
if c.lower() == 'b': return
|
||||
|
||||
nodes_to_update = s if c.lower() == 'a' else ([s[int(c)-1]] if c.isdigit() and 1 <= int(c) <= len(s) else [])
|
||||
if not nodes_to_update: return
|
||||
|
||||
for n in nodes_to_update:
|
||||
name = n['name']
|
||||
nc = cfg.get(name, {})
|
||||
if not nc: continue
|
||||
print(f"\nUpdating {name}...")
|
||||
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||
str(nc.get("port","8081")), nc.get("upstream_ip",""),
|
||||
str(nc.get("upstream_port","80")), "",
|
||||
nc.get("mode","monitoring"))
|
||||
print(f"✅ {name} updated.")
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
85
sources/python/tap-proxy.py
Normal file
85
sources/python/tap-proxy.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tap Proxy — logs HTTP headers. Usage: python3 tap-proxy.py <port> <backend_host:port> <log_dir>"""
|
||||
import http.server, urllib.request, sys, socket
|
||||
from datetime import datetime
|
||||
|
||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8081
|
||||
BACKEND = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:80"
|
||||
LOG = (sys.argv[3] if len(sys.argv) > 3 else "/tmp") + "/tap-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".log"
|
||||
BACKEND_URL = "http://" + BACKEND
|
||||
|
||||
# Don't follow redirects — preserve Set-Cookie
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def http_error_302(self, req, fp, code, msg, headers):
|
||||
fp.status, fp.reason = code, msg
|
||||
return fp
|
||||
http_error_301 = http_error_302
|
||||
http_error_303 = http_error_302
|
||||
http_error_307 = http_error_302
|
||||
http_error_308 = http_error_302
|
||||
|
||||
_opener = urllib.request.build_opener(NoRedirect)
|
||||
|
||||
class Tap(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self): self._tap('GET')
|
||||
def do_POST(self): self._tap('POST')
|
||||
def do_PUT(self): self._tap('PUT')
|
||||
def do_DELETE(self): self._tap('DELETE')
|
||||
|
||||
def _tap(self, method):
|
||||
now = datetime.now().strftime('%H:%M:%S')
|
||||
msg = "\n" + "="*60 + "\n[" + now + "] " + method + " " + self.path
|
||||
msg += "\nClient: " + self.client_address[0] + "\n" + "-"*60 + "\n"
|
||||
for k, v in sorted(self.headers.items()):
|
||||
msg += " " + k + ": " + v + "\n"
|
||||
|
||||
# Handle chunked transfer-encoding
|
||||
body = None
|
||||
cl = self.headers.get('Content-Length')
|
||||
te = self.headers.get('Transfer-Encoding', '').lower()
|
||||
if cl:
|
||||
body = self.rfile.read(int(cl))
|
||||
elif 'chunked' in te:
|
||||
body = b''
|
||||
while True:
|
||||
line = self.rfile.readline().strip()
|
||||
if not line: continue
|
||||
size = int(line, 16)
|
||||
if size == 0: break
|
||||
body += self.rfile.read(size)
|
||||
self.rfile.readline()
|
||||
while True:
|
||||
line = self.rfile.readline().strip()
|
||||
if not line: break
|
||||
|
||||
req = urllib.request.Request(BACKEND_URL + self.path, data=body, method=method)
|
||||
# Pass through all headers including Host
|
||||
for k, v in self.headers.items():
|
||||
if k.lower() not in ('connection',):
|
||||
req.add_header(k, v)
|
||||
|
||||
try:
|
||||
resp = _opener.open(req, timeout=30)
|
||||
msg += "\nRESPONSE: " + str(resp.status) + " " + str(resp.reason) + "\n"
|
||||
for k, v in resp.getheaders():
|
||||
msg += " " + k + ": " + v + "\n"
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.getheaders():
|
||||
if k.lower() not in ('transfer-encoding', 'connection'):
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.read())
|
||||
except Exception as e:
|
||||
msg += "\nBACKEND: " + str(e) + "\n"
|
||||
self.send_response(502)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Backend unreachable: " + str(e).encode())
|
||||
|
||||
with open(LOG, "a") as f:
|
||||
f.write(msg)
|
||||
|
||||
def log_message(self, *args): pass
|
||||
|
||||
print("Tap proxy :{0} -> {1}\nLog: {2}\nCtrl+C to stop".format(PORT, BACKEND_URL, LOG))
|
||||
http.server.HTTPServer(('0.0.0.0', PORT), Tap).serve_forever()
|
||||
Loading…
Reference in a new issue