@aifabrix/server-setup 1.3.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -19
- package/assets/aifabrix-apply-dev-users.sh +123 -0
- package/assets/setup-dev-server-no-node.sh +216 -76
- package/assets/setup-install-init.sh +42 -0
- package/dist/backup.spec.js +1 -1
- package/dist/cli.js +66 -3
- package/dist/install-init.d.ts +6 -0
- package/dist/install-init.js +41 -0
- package/dist/install-init.spec.d.ts +1 -0
- package/dist/install-init.spec.js +10 -0
- package/dist/install-ssh.d.ts +10 -0
- package/dist/install-ssh.js +39 -0
- package/dist/install-ssh.spec.d.ts +4 -0
- package/dist/install-ssh.spec.js +55 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.js +73 -8
- package/dist/install.spec.d.ts +1 -0
- package/dist/install.spec.js +23 -0
- package/dist/ssh.d.ts +1 -0
- package/dist/ssh.js +104 -11
- package/package.json +9 -2
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
# Run via af-server install user@host. REPO_ROOT must be set to the dir containing builder/builder-server/nginx-builder-server.conf.template.
|
|
4
4
|
# On empty server: installs Docker, nginx, admin user, optional Portainer/Mutagen; then updates nginx config and ensures builder-server container.
|
|
5
5
|
# Optional env: DEV_DOMAIN, SSL_DIR, DATA_DIR, SETUP_ADMIN_USER, SYNC_USER, BUILDER_SERVER_PORT, NGINX_CONF_DIR, SETUP_HOSTNAME, INSTALL_PORTAINER=1, SKIP_DOCKER_TLS=1.
|
|
6
|
+
# INSTALL_PHASE: infra = Docker, nginx pkg, Mutagen, data dir, apply-dev-users (no vhost, no container, no Docker TLS). server = nginx vhost, builder-server container, Docker TLS. full = both.
|
|
6
7
|
|
|
7
8
|
set -e
|
|
9
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
10
|
+
# Cache sudo so one password covers the whole script (avoids repeated "[sudo] password for ...")
|
|
11
|
+
sudo -v
|
|
8
12
|
|
|
9
13
|
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
10
14
|
DATA_DIR="${DATA_DIR:-/opt/aifabrix/builder-server/data}"
|
|
@@ -14,6 +18,7 @@ SETUP_ADMIN_USER="${SETUP_ADMIN_USER:-serveradmin}"
|
|
|
14
18
|
SYNC_USER="${SYNC_USER:-aifabrix-sync}"
|
|
15
19
|
BUILDER_SERVER_PORT="${BUILDER_SERVER_PORT:-3000}"
|
|
16
20
|
NGINX_CONF_DIR="${NGINX_CONF_DIR:-/etc/nginx/conf.d}"
|
|
21
|
+
INSTALL_PHASE="${INSTALL_PHASE:-full}"
|
|
17
22
|
|
|
18
23
|
# Sanitize user-controlled env to prevent path/command injection
|
|
19
24
|
sanitize_domain() {
|
|
@@ -25,6 +30,9 @@ sanitize_path() {
|
|
|
25
30
|
*) return 0 ;;
|
|
26
31
|
esac
|
|
27
32
|
}
|
|
33
|
+
case "$DEV_DOMAIN" in
|
|
34
|
+
*'--'*) echo "Invalid DEV_DOMAIN: '$DEV_DOMAIN' looks like a typo (e.g. missing space before --ssl-dir). Use: af-server install user@host --dev-domain DOMAIN --ssl-dir PATH"; exit 1 ;;
|
|
35
|
+
esac
|
|
28
36
|
if ! sanitize_domain "$DEV_DOMAIN"; then
|
|
29
37
|
echo "Invalid DEV_DOMAIN (use only letters, digits, dots, hyphens)."
|
|
30
38
|
exit 1
|
|
@@ -48,6 +56,9 @@ require_sudo() {
|
|
|
48
56
|
}
|
|
49
57
|
require_sudo
|
|
50
58
|
|
|
59
|
+
# --- Infra phase: hostname, system, Docker, nginx package, Mutagen, data dir, apply-dev-users ---
|
|
60
|
+
if [ "$INSTALL_PHASE" = "infra" ] || [ "$INSTALL_PHASE" = "full" ]; then
|
|
61
|
+
|
|
51
62
|
# --- Hostname (optional) ---
|
|
52
63
|
if [ -n "$SETUP_HOSTNAME" ]; then
|
|
53
64
|
current=$(hostname 2>/dev/null || true)
|
|
@@ -58,7 +69,26 @@ fi
|
|
|
58
69
|
|
|
59
70
|
# --- System updates ---
|
|
60
71
|
apt-get update -qq
|
|
61
|
-
|
|
72
|
+
apt-get upgrade -y -qq
|
|
73
|
+
|
|
74
|
+
# --- Packages for SSH users (same base as builder-server/wsl/install-wsl-ubuntu-dev.sh; no Node/Ruby) ---
|
|
75
|
+
apt-get install -y \
|
|
76
|
+
openssh-server sudo curl git ca-certificates gnupg software-properties-common apt-transport-https \
|
|
77
|
+
build-essential unzip zip jq wget vim nano less locales \
|
|
78
|
+
python3 python3-venv python3-dev python3-pip \
|
|
79
|
+
openjdk-17-jdk \
|
|
80
|
+
autoconf bison libssl-dev libyaml-dev libreadline-dev zlib1g-dev libffi-dev libgdbm-dev libncurses-dev \
|
|
81
|
+
redis-tools dbus-x11 imagemagick \
|
|
82
|
+
openssh-client rsync sshfs
|
|
83
|
+
systemctl enable ssh 2>/dev/null || systemctl enable sshd 2>/dev/null || true
|
|
84
|
+
systemctl start ssh 2>/dev/null || systemctl start sshd 2>/dev/null || true
|
|
85
|
+
|
|
86
|
+
# --- Azure CLI (az) ---
|
|
87
|
+
# Manual one-liner (sudo must apply to bash): curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
|
88
|
+
echo "=== Azure CLI (optional - remove block if not needed) ==="
|
|
89
|
+
if ! command -v az >/dev/null 2>&1; then
|
|
90
|
+
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
|
91
|
+
fi
|
|
62
92
|
|
|
63
93
|
# --- Docker ---
|
|
64
94
|
if ! command -v docker >/dev/null 2>&1; then
|
|
@@ -96,30 +126,12 @@ if [ "$INSTALL_PORTAINER" = "1" ] && ! docker ps -a --format '{{.Names}}' 2>/dev
|
|
|
96
126
|
portainer/portainer-ce:latest 2>/dev/null || true
|
|
97
127
|
fi
|
|
98
128
|
|
|
99
|
-
# --- Nginx ---
|
|
129
|
+
# --- Nginx (package only in infra; vhost + reload in server phase) ---
|
|
100
130
|
if ! command -v nginx >/dev/null 2>&1; then
|
|
101
131
|
apt-get install -y nginx
|
|
102
132
|
systemctl enable nginx
|
|
103
133
|
systemctl start nginx
|
|
104
134
|
fi
|
|
105
|
-
NGINX_CONF="$NGINX_CONF_DIR/$DEV_DOMAIN.conf"
|
|
106
|
-
NGINX_TEMPLATE="$REPO_ROOT/builder/builder-server/nginx-builder-server.conf.template"
|
|
107
|
-
# Always update builder vhost from template so re-running install applies latest config.
|
|
108
|
-
if [ -f "$NGINX_TEMPLATE" ]; then
|
|
109
|
-
sed -e "s|DEV_DOMAIN_PLACEHOLDER|$DEV_DOMAIN|g" \
|
|
110
|
-
-e "s|SSL_DIR_PLACEHOLDER|$SSL_DIR|g" \
|
|
111
|
-
-e "s|BUILDER_SERVER_PORT_PLACEHOLDER|$BUILDER_SERVER_PORT|g" \
|
|
112
|
-
-e "s|DATA_DIR_PLACEHOLDER|$DATA_DIR|g" \
|
|
113
|
-
"$NGINX_TEMPLATE" > "$NGINX_CONF"
|
|
114
|
-
elif [ ! -f "$NGINX_CONF" ]; then
|
|
115
|
-
echo "Warning: SSL prereqs required. Place $NGINX_CONF (see SETUP.md) and ensure $SSL_DIR/wildcard.crt and $SSL_DIR/wildcard.key exist."
|
|
116
|
-
fi
|
|
117
|
-
if command -v nginx >/dev/null 2>&1 && nginx -t 2>/dev/null; then
|
|
118
|
-
systemctl reload nginx
|
|
119
|
-
elif [ -f "$NGINX_TEMPLATE" ] && command -v nginx >/dev/null 2>&1; then
|
|
120
|
-
echo "Warning: nginx -t failed (config was still written to $NGINX_CONF). Nginx was NOT reloaded."
|
|
121
|
-
nginx -t 2>&1 || true
|
|
122
|
-
fi
|
|
123
135
|
|
|
124
136
|
# --- Mutagen ---
|
|
125
137
|
if ! command -v mutagen >/dev/null 2>&1; then
|
|
@@ -152,21 +164,110 @@ MUTAGEN_EOF
|
|
|
152
164
|
fi
|
|
153
165
|
fi
|
|
154
166
|
|
|
155
|
-
# ---
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
# --- Data dir and workspace/ssh-keys (for apply-dev-users) ---
|
|
168
|
+
mkdir -p "$DATA_DIR" "${DATA_DIR}/workspace" "${DATA_DIR}/ssh-keys"
|
|
169
|
+
chown -R 1001:65533 "$DATA_DIR"
|
|
170
|
+
chmod 755 "$DATA_DIR"
|
|
171
|
+
DATA_DIR_ABS=$(cd "$DATA_DIR" && pwd)
|
|
172
|
+
|
|
173
|
+
# --- Host job: apply per-developer OS users from builder-server state ---
|
|
174
|
+
APPLY_DEV_USERS_SCRIPT="/usr/local/bin/aifabrix-apply-dev-users.sh"
|
|
175
|
+
SETUP_ASSETS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
176
|
+
if [ ! -f "$APPLY_DEV_USERS_SCRIPT" ]; then
|
|
177
|
+
if [ -f "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" ]; then
|
|
178
|
+
cp "$SETUP_ASSETS_DIR/aifabrix-apply-dev-users.sh" "$APPLY_DEV_USERS_SCRIPT"
|
|
179
|
+
else
|
|
180
|
+
# Inline fallback when run from builder/builder-server context
|
|
181
|
+
cat > "$APPLY_DEV_USERS_SCRIPT" << 'APPLY_DEV_EOF'
|
|
182
|
+
#!/bin/sh
|
|
183
|
+
DATA_DIR="${DATA_DIR:-/opt/aifabrix/builder-server/data}"
|
|
184
|
+
SSH_KEYS_DIR="${DATA_DIR}/ssh-keys"
|
|
185
|
+
PENDING_REMOVALS="${DATA_DIR}/pending-removals"
|
|
186
|
+
[ ! -d "$DATA_DIR" ] && exit 0
|
|
187
|
+
if [ -f "$PENDING_REMOVALS" ]; then
|
|
188
|
+
while IFS= read -r dev_id || [ -n "$dev_id" ]; do
|
|
189
|
+
dev_id=$(echo "$dev_id" | tr -d '\r\n ')
|
|
190
|
+
[ -z "$dev_id" ] && continue
|
|
191
|
+
user_name="dev${dev_id}"
|
|
192
|
+
getent passwd "$user_name" >/dev/null 2>&1 && ( userdel -r "$user_name" 2>/dev/null || userdel "$user_name" 2>/dev/null || true )
|
|
193
|
+
done < "$PENDING_REMOVALS"
|
|
194
|
+
: > "$PENDING_REMOVALS"
|
|
195
|
+
fi
|
|
196
|
+
for key_file in "${SSH_KEYS_DIR}"/*/authorized_keys; do
|
|
197
|
+
[ -f "$key_file" ] || continue
|
|
198
|
+
dev_id=$(dirname "$key_file" | xargs basename)
|
|
199
|
+
[ -z "$dev_id" ] && continue
|
|
200
|
+
user_name="dev${dev_id}"
|
|
201
|
+
home_dir="/home/${user_name}"
|
|
202
|
+
workspace_target="${DATA_DIR}/workspace/${user_name}"
|
|
203
|
+
getent passwd "$user_name" >/dev/null 2>&1 || useradd -m -s /bin/bash "$user_name"
|
|
204
|
+
mkdir -p "${home_dir}/.ssh"
|
|
205
|
+
cp "$key_file" "${home_dir}/.ssh/authorized_keys"
|
|
206
|
+
chown -R "${user_name}:${user_name}" "${home_dir}/.ssh"
|
|
207
|
+
chmod 700 "${home_dir}/.ssh"
|
|
208
|
+
chmod 600 "${home_dir}/.ssh/authorized_keys"
|
|
209
|
+
config_file="${home_dir}/.aifabrix/config.yaml"
|
|
210
|
+
if [ ! -f "$config_file" ]; then
|
|
211
|
+
mkdir -p "${home_dir}/.aifabrix"
|
|
212
|
+
hostname_val=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "localhost")
|
|
213
|
+
printf 'user-mutagen-folder: %s\nsecrets-encryption: ""\naifabrix-secrets: ""\naifabrix-env-config: ""\nremote-server: "http://localhost:3000"\ndocker-endpoint: "tcp://%s:2376"\nsync-ssh-user: "%s"\nsync-ssh-host: "%s"\n' "$workspace_target" "$hostname_val" "$user_name" "$hostname_val" > "$config_file"
|
|
214
|
+
chown -R "${user_name}:${user_name}" "${home_dir}/.aifabrix"
|
|
215
|
+
chmod 700 "${home_dir}/.aifabrix"
|
|
216
|
+
chmod 600 "$config_file"
|
|
217
|
+
fi
|
|
218
|
+
mkdir -p "$workspace_target"
|
|
219
|
+
chown -R "${user_name}:${user_name}" "$workspace_target"
|
|
220
|
+
[ -L "${home_dir}/workspace" ] && [ "$(readlink -f "${home_dir}/workspace" 2>/dev/null)" != "$(readlink -f "$workspace_target" 2>/dev/null)" ] && rm -f "${home_dir}/workspace"
|
|
221
|
+
[ ! -e "${home_dir}/workspace" ] && ln -s "$workspace_target" "${home_dir}/workspace" && chown -h "${user_name}:${user_name}" "${home_dir}/workspace"
|
|
222
|
+
profile="${home_dir}/.profile"
|
|
223
|
+
[ ! -f "$profile" ] && touch "$profile" && chown "${user_name}:${user_name}" "$profile"
|
|
224
|
+
grep -q 'cd.*workspace' "$profile" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$profile"
|
|
225
|
+
bashrc="${home_dir}/.bashrc"
|
|
226
|
+
[ ! -f "$bashrc" ] && touch "$bashrc" && chown "${user_name}:${user_name}" "$bashrc"
|
|
227
|
+
grep -q 'cd.*workspace' "$bashrc" 2>/dev/null || echo '[ -d "$HOME/workspace" ] && cd "$HOME/workspace"' >> "$bashrc"
|
|
228
|
+
done
|
|
229
|
+
APPLY_DEV_EOF
|
|
169
230
|
fi
|
|
231
|
+
chmod 755 "$APPLY_DEV_USERS_SCRIPT"
|
|
232
|
+
fi
|
|
233
|
+
# Defaults for apply script (aifabrix-secrets, aifabrix-env-config in generated config); match builder-server env.template
|
|
234
|
+
APPLY_DEFAULTS="$DATA_DIR_ABS/apply-dev-users-defaults"
|
|
235
|
+
if [ ! -f "$APPLY_DEFAULTS" ]; then
|
|
236
|
+
{
|
|
237
|
+
echo "# Sourced by cron before aifabrix-apply-dev-users.sh; override to match builder-server .env"
|
|
238
|
+
echo 'export AIFABRIX_SECRETS="${AIFABRIX_SECRETS:-/aifabrix-miso/builder/secrets.local.yaml}"'
|
|
239
|
+
echo 'export AIFABRIX_ENV_CONFIG="${AIFABRIX_ENV_CONFIG:-aifabrix-miso/builder/env-config.yaml}"'
|
|
240
|
+
} > "$APPLY_DEFAULTS"
|
|
241
|
+
chmod 644 "$APPLY_DEFAULTS"
|
|
242
|
+
fi
|
|
243
|
+
if [ -d /etc/cron.d ] && [ ! -f /etc/cron.d/aifabrix-apply-dev-users ]; then
|
|
244
|
+
echo "*/2 * * * * root /bin/sh -c '. $APPLY_DEFAULTS 2>/dev/null; export DATA_DIR=$DATA_DIR_ABS; exec $APPLY_DEV_USERS_SCRIPT'" > /etc/cron.d/aifabrix-apply-dev-users
|
|
245
|
+
chmod 644 /etc/cron.d/aifabrix-apply-dev-users
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
fi
|
|
249
|
+
# --- End infra phase ---
|
|
250
|
+
|
|
251
|
+
# --- Server phase: nginx vhost, builder-server container, Docker TLS ---
|
|
252
|
+
if [ "$INSTALL_PHASE" = "server" ] || [ "$INSTALL_PHASE" = "full" ]; then
|
|
253
|
+
|
|
254
|
+
# --- Nginx builder vhost from template ---
|
|
255
|
+
NGINX_CONF="$NGINX_CONF_DIR/$DEV_DOMAIN.conf"
|
|
256
|
+
NGINX_TEMPLATE="$REPO_ROOT/builder/builder-server/nginx-builder-server.conf.template"
|
|
257
|
+
if [ -f "$NGINX_TEMPLATE" ]; then
|
|
258
|
+
sed -e "s|DEV_DOMAIN_PLACEHOLDER|$DEV_DOMAIN|g" \
|
|
259
|
+
-e "s|SSL_DIR_PLACEHOLDER|$SSL_DIR|g" \
|
|
260
|
+
-e "s|BUILDER_SERVER_PORT_PLACEHOLDER|$BUILDER_SERVER_PORT|g" \
|
|
261
|
+
-e "s|DATA_DIR_PLACEHOLDER|$DATA_DIR|g" \
|
|
262
|
+
"$NGINX_TEMPLATE" > "$NGINX_CONF"
|
|
263
|
+
elif [ ! -f "$NGINX_CONF" ]; then
|
|
264
|
+
echo "Warning: SSL prereqs required. Place $NGINX_CONF (see SETUP.md) and ensure $SSL_DIR/wildcard.crt and $SSL_DIR/wildcard.key exist."
|
|
265
|
+
fi
|
|
266
|
+
if command -v nginx >/dev/null 2>&1 && nginx -t 2>/dev/null; then
|
|
267
|
+
systemctl reload nginx
|
|
268
|
+
elif [ -f "$NGINX_TEMPLATE" ] && command -v nginx >/dev/null 2>&1; then
|
|
269
|
+
echo "Warning: nginx -t failed (config was still written to $NGINX_CONF). Nginx was NOT reloaded."
|
|
270
|
+
nginx -t 2>&1 || true
|
|
170
271
|
fi
|
|
171
272
|
|
|
172
273
|
# --- Builder-server data dir and container ---
|
|
@@ -217,6 +318,7 @@ if command -v docker >/dev/null 2>&1; then
|
|
|
217
318
|
-e PORT=3000 \
|
|
218
319
|
-e DATA_DIR="$CONTAINER_DATA_PATH" \
|
|
219
320
|
-e ENCRYPTION_KEY_PATH="${CONTAINER_DATA_PATH}/secrets-encryption.key" \
|
|
321
|
+
-e DOCKER_ENDPOINT="tcp://${DEV_DOMAIN}:2376" \
|
|
220
322
|
"$IMG_TO_USE"
|
|
221
323
|
else
|
|
222
324
|
echo "Builder-server image not found. Get the image from the AI Fabrix Builder (aifabrix build builder-server; then push/deploy to this host). No source or docker build on server. See builder/builder-server/README.md."
|
|
@@ -227,50 +329,88 @@ if command -v docker >/dev/null 2>&1; then
|
|
|
227
329
|
fi
|
|
228
330
|
fi
|
|
229
331
|
|
|
230
|
-
# ---
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
332
|
+
# --- Docker TLS (daemon.json + certs): use website cert for server, builder-server CA for client verification ---
|
|
333
|
+
if [ "$SKIP_DOCKER_TLS" != "1" ] && [ -d /etc/docker ]; then
|
|
334
|
+
DATA_DIR_ABS=$(cd "$DATA_DIR" 2>/dev/null && pwd) || true
|
|
335
|
+
if [ -n "$DATA_DIR_ABS" ]; then
|
|
336
|
+
# Wait for builder-server to create ca.crt (up to 30s)
|
|
337
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
|
338
|
+
[ -f "$DATA_DIR_ABS/ca.crt" ] && break
|
|
339
|
+
sleep 2
|
|
340
|
+
done
|
|
341
|
+
# Server cert/key: use existing website cert (same as nginx) so no extra cert to manage
|
|
342
|
+
if [ -f "$SSL_DIR/wildcard.crt" ] && [ -f "$SSL_DIR/wildcard.key" ]; then
|
|
343
|
+
cp "$SSL_DIR/wildcard.crt" /etc/docker/server-cert.pem
|
|
344
|
+
cp "$SSL_DIR/wildcard.key" /etc/docker/server-key.pem
|
|
345
|
+
chmod 600 /etc/docker/server-key.pem
|
|
346
|
+
fi
|
|
347
|
+
# Client verification: CA that signed developer certs (builder-server creates ca.crt on first run)
|
|
348
|
+
if [ -f "$DATA_DIR_ABS/ca.crt" ]; then
|
|
349
|
+
cp "$DATA_DIR_ABS/ca.crt" /etc/docker/ca.pem
|
|
350
|
+
fi
|
|
351
|
+
if [ -f /etc/docker/ca.pem ] && [ -f /etc/docker/server-cert.pem ] && [ -f /etc/docker/server-key.pem ]; then
|
|
352
|
+
# Allow daemon.json to set "hosts" (avoid conflict with systemd -H fd://)
|
|
353
|
+
DOCKER_DROPIN="/etc/systemd/system/docker.service.d"
|
|
354
|
+
if [ -d /etc/systemd/system ]; then
|
|
355
|
+
mkdir -p "$DOCKER_DROPIN"
|
|
356
|
+
if [ ! -f "$DOCKER_DROPIN/override.conf" ]; then
|
|
357
|
+
cat > "$DOCKER_DROPIN/override.conf" << 'DROPIN_EOF'
|
|
358
|
+
[Service]
|
|
359
|
+
ExecStart=
|
|
360
|
+
ExecStart=/usr/bin/dockerd
|
|
361
|
+
DROPIN_EOF
|
|
362
|
+
systemctl daemon-reload
|
|
363
|
+
fi
|
|
364
|
+
fi
|
|
365
|
+
[ -f /etc/docker/daemon.json ] && cp -a /etc/docker/daemon.json /etc/docker/daemon.json.bak
|
|
366
|
+
DATA_ROOT=''
|
|
367
|
+
if [ -f /etc/docker/daemon.json.bak ]; then
|
|
368
|
+
DATA_ROOT=$(grep -o '"data-root"[[:space:]]*:[[:space:]]*"[^"]*"' /etc/docker/daemon.json.bak 2>/dev/null | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
|
369
|
+
fi
|
|
370
|
+
if [ -n "$DATA_ROOT" ] && [ -d "$DATA_ROOT" ]; then
|
|
371
|
+
cat > /etc/docker/daemon.json << DOCKER_EOF
|
|
372
|
+
{
|
|
373
|
+
"data-root": "$DATA_ROOT",
|
|
374
|
+
"tls": true,
|
|
375
|
+
"tlsverify": true,
|
|
376
|
+
"tlscacert": "/etc/docker/ca.pem",
|
|
377
|
+
"tlscert": "/etc/docker/server-cert.pem",
|
|
378
|
+
"tlskey": "/etc/docker/server-key.pem",
|
|
379
|
+
"hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"]
|
|
380
|
+
}
|
|
381
|
+
DOCKER_EOF
|
|
382
|
+
else
|
|
383
|
+
cat > /etc/docker/daemon.json << 'DOCKER_EOF'
|
|
384
|
+
{
|
|
385
|
+
"tls": true,
|
|
386
|
+
"tlsverify": true,
|
|
387
|
+
"tlscacert": "/etc/docker/ca.pem",
|
|
388
|
+
"tlscert": "/etc/docker/server-cert.pem",
|
|
389
|
+
"tlskey": "/etc/docker/server-key.pem",
|
|
390
|
+
"hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"]
|
|
391
|
+
}
|
|
392
|
+
DOCKER_EOF
|
|
393
|
+
fi
|
|
394
|
+
systemctl restart docker 2>/dev/null || true
|
|
395
|
+
if ! docker info >/dev/null 2>&1; then
|
|
396
|
+
echo "Docker failed to start with TLS config. Restoring previous daemon.json so Docker can start."
|
|
397
|
+
if [ -f /etc/docker/daemon.json.bak ]; then
|
|
398
|
+
mv /etc/docker/daemon.json.bak /etc/docker/daemon.json
|
|
399
|
+
else
|
|
400
|
+
rm -f /etc/docker/daemon.json
|
|
401
|
+
fi
|
|
402
|
+
systemctl start docker 2>/dev/null || true
|
|
403
|
+
echo "Run scripts/fix-docker-daemon.sh if Docker still does not start. See SETUP.md Docker API TLS."
|
|
404
|
+
else
|
|
405
|
+
docker start aifabrix-builder-server 2>/dev/null || docker start builder-server 2>/dev/null || true
|
|
406
|
+
fi
|
|
407
|
+
else
|
|
408
|
+
echo "Docker TLS skipped: need $SSL_DIR/wildcard.crt, wildcard.key and $DATA_DIR/ca.crt (ca.crt appears after builder-server first run). Re-run af-server install after container has started. See SETUP.md Docker API TLS."
|
|
409
|
+
fi
|
|
410
|
+
fi
|
|
243
411
|
fi
|
|
244
412
|
|
|
245
|
-
# --- Workspace dir ---
|
|
246
|
-
mkdir -p "${DATA_DIR}/workspace"
|
|
247
|
-
chown -R "$SYNC_USER:$SYNC_USER" "${DATA_DIR}/workspace" 2>/dev/null || true
|
|
248
|
-
|
|
249
|
-
# --- Host job: apply managed SSH keys to sync user ---
|
|
250
|
-
APPLY_KEYS_SCRIPT="/usr/local/bin/aifabrix-apply-sync-keys.sh"
|
|
251
|
-
if [ ! -f "$APPLY_KEYS_SCRIPT" ]; then
|
|
252
|
-
cat > "$APPLY_KEYS_SCRIPT" << APPLY_EOF
|
|
253
|
-
#!/bin/sh
|
|
254
|
-
SYNC_USER="$SYNC_USER"
|
|
255
|
-
DATA_DIR="$DATA_DIR"
|
|
256
|
-
SYNC_HOME="\${DATA_DIR}/.sync-home"
|
|
257
|
-
MANAGED="\${DATA_DIR}/sync-authorized-keys"
|
|
258
|
-
if [ -f "\$MANAGED" ]; then
|
|
259
|
-
cp "\$MANAGED" "\$SYNC_HOME/.ssh/authorized_keys"
|
|
260
|
-
else
|
|
261
|
-
touch "\$SYNC_HOME/.ssh/authorized_keys"
|
|
262
|
-
fi
|
|
263
|
-
chown "\$SYNC_USER:\$SYNC_USER" "\$SYNC_HOME/.ssh/authorized_keys"
|
|
264
|
-
chmod 600 "\$SYNC_HOME/.ssh/authorized_keys"
|
|
265
|
-
if [ -d "\${DATA_DIR}/workspace" ]; then
|
|
266
|
-
chown -R "\$SYNC_USER:\$SYNC_USER" "\${DATA_DIR}/workspace"
|
|
267
|
-
fi
|
|
268
|
-
APPLY_EOF
|
|
269
|
-
chmod 755 "$APPLY_KEYS_SCRIPT"
|
|
270
|
-
fi
|
|
271
|
-
if [ -d /etc/cron.d ] && [ ! -f /etc/cron.d/aifabrix-sync-keys ]; then
|
|
272
|
-
echo "*/2 * * * * root $APPLY_KEYS_SCRIPT" > /etc/cron.d/aifabrix-sync-keys
|
|
273
|
-
chmod 644 /etc/cron.d/aifabrix-sync-keys
|
|
274
413
|
fi
|
|
414
|
+
# --- End server phase ---
|
|
275
415
|
|
|
276
416
|
echo "Setup complete. Ensure manual prerequisites (SSL at $SSL_DIR: wildcard.crt, wildcard.key; DNS $DEV_DOMAIN) are done; see SETUP.md."
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# One-time bootstrap: ensure SSH, install Node 18+ and npm, then af-server CLI on the server.
|
|
3
|
+
# Run via: af-server install-init user@host (from PC over SSH). No Docker, nginx, or builder-server.
|
|
4
|
+
# After this, user logs in to the server and runs: sudo af-server install, then sudo af-server install-server --dev-domain DOMAIN.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
8
|
+
|
|
9
|
+
# --- SSH server (so install-init and later ssh-cert install can connect) ---
|
|
10
|
+
wait_for_apt() {
|
|
11
|
+
i=0
|
|
12
|
+
while [ $i -lt 20 ]; do
|
|
13
|
+
apt-get update -qq 2>/dev/null && return 0
|
|
14
|
+
echo "Waiting for apt lock..."; sleep 6
|
|
15
|
+
i=$((i + 1))
|
|
16
|
+
done
|
|
17
|
+
return 1
|
|
18
|
+
}
|
|
19
|
+
wait_for_apt
|
|
20
|
+
apt-get install -y openssh-server
|
|
21
|
+
systemctl enable ssh 2>/dev/null || systemctl enable sshd 2>/dev/null || true
|
|
22
|
+
systemctl start ssh 2>/dev/null || systemctl start sshd 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
# --- Node.js 18+ and npm (NodeSource) ---
|
|
25
|
+
need_node=0
|
|
26
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
27
|
+
need_node=1
|
|
28
|
+
else
|
|
29
|
+
case "$(node -v 2>/dev/null)" in v1[89].*|v[2-9]*) ;; *) need_node=1 ;; esac
|
|
30
|
+
fi
|
|
31
|
+
if [ "$need_node" = "1" ]; then
|
|
32
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
33
|
+
apt-get install -y nodejs
|
|
34
|
+
fi
|
|
35
|
+
node -v
|
|
36
|
+
npm -v
|
|
37
|
+
|
|
38
|
+
# --- af-server CLI (same as on PC) ---
|
|
39
|
+
npm install -g @aifabrix/builder @aifabrix/server-setup
|
|
40
|
+
command -v af-server >/dev/null 2>&1 && af-server --version || true
|
|
41
|
+
|
|
42
|
+
echo "Bootstrap complete. Log in to the server and run: sudo af-server install, then sudo af-server install-server --dev-domain YOUR_DOMAIN"
|
package/dist/backup.spec.js
CHANGED
|
@@ -100,7 +100,7 @@ describe('runBackup', () => {
|
|
|
100
100
|
mockExec.mockResolvedValue({ stdout: '1\n', stderr: '', code: 0 });
|
|
101
101
|
mockReadFile.mockResolvedValue(Buffer.from(''));
|
|
102
102
|
const result = await runBackup({ target: 'user@host', dataDir: '/data' });
|
|
103
|
-
expect(result).toMatch(
|
|
103
|
+
expect(result).toMatch(/aifabrix-backup-\d{8}-\d{4}\.zip$/);
|
|
104
104
|
expect(fs.existsSync(result)).toBe(true);
|
|
105
105
|
try {
|
|
106
106
|
fs.unlinkSync(result);
|
package/dist/cli.js
CHANGED
|
@@ -3,21 +3,42 @@
|
|
|
3
3
|
* af-server — Install, backup, and restore AI Fabrix builder-server (config + DB) over SSH.
|
|
4
4
|
* Usage: af-server <command> [options] [user@host]
|
|
5
5
|
*/
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
6
9
|
import { Command } from 'commander';
|
|
7
|
-
import { runInstall, runInstallLocal } from './install.js';
|
|
10
|
+
import { runInstall, runInstallLocal, runInstallServerLocal } from './install.js';
|
|
8
11
|
import { runBackup, runBackupLocal } from './backup.js';
|
|
9
12
|
import { runBackupScheduleInstall, runBackupScheduleInstallLocal } from './backup-schedule.js';
|
|
10
13
|
import { runRestore, runRestoreLocal } from './restore.js';
|
|
11
14
|
import { runSshCertRequest, runSshCertInstall, runSshCertInstallLocal } from './ssh-cert.js';
|
|
15
|
+
import { runInstallSsh, runInstallSshLocal } from './install-ssh.js';
|
|
16
|
+
import { runInstallInit } from './install-init.js';
|
|
12
17
|
import { requireUbuntu } from './ubuntu.js';
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
13
20
|
const program = new Command();
|
|
14
21
|
program
|
|
15
22
|
.name('af-server')
|
|
16
23
|
.description('Install, backup, and restore AI Fabrix builder-server over SSH (config + DB only)')
|
|
17
|
-
.version(
|
|
24
|
+
.version(pkg.version);
|
|
25
|
+
program
|
|
26
|
+
.command('install-init <user@host>')
|
|
27
|
+
.description('One-time bootstrap over SSH: install on server SSH (if needed), Node 18+, npm, and af-server CLI. Run from PC only.')
|
|
28
|
+
.option('-i, --identity <path>', 'SSH private key path')
|
|
29
|
+
.action(async (target, opts) => {
|
|
30
|
+
try {
|
|
31
|
+
await runInstallInit({ target: target.trim(), privateKeyPath: opts.identity });
|
|
32
|
+
console.log('Install-init complete. Log in to the server and run: sudo af-server install, then sudo af-server install-server --dev-domain YOUR_DOMAIN');
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error(err instanceof Error ? err.message : err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
18
39
|
program
|
|
19
40
|
.command('install [user@host]')
|
|
20
|
-
.description('Run server setup on host or locally (omit target on server). Docker, nginx,
|
|
41
|
+
.description('Run server setup on host or locally (omit target on server). Docker, nginx pkg, Mutagen, cron; no builder-server. Run on server: sudo af-server install.')
|
|
21
42
|
.option('-d, --data-dir <path>', 'DATA_DIR', '/opt/aifabrix/builder-server/data')
|
|
22
43
|
.option('--dev-domain <domain>', 'DEV_DOMAIN for nginx', 'builder01.aifabrix.dev')
|
|
23
44
|
.option('--ssl-dir <path>', 'SSL_DIR', '/opt/aifabrix/ssl')
|
|
@@ -51,6 +72,29 @@ program
|
|
|
51
72
|
process.exit(1);
|
|
52
73
|
}
|
|
53
74
|
});
|
|
75
|
+
program
|
|
76
|
+
.command('install-server')
|
|
77
|
+
.description('On server only: nginx vhost, builder-server container, Docker TLS. Run after: sudo af-server install. Requires --dev-domain.')
|
|
78
|
+
.option('-d, --data-dir <path>', 'DATA_DIR', '/opt/aifabrix/builder-server/data')
|
|
79
|
+
.requiredOption('--dev-domain <domain>', 'DEV_DOMAIN for nginx (e.g. builder01.aifabrix.dev)')
|
|
80
|
+
.option('--ssl-dir <path>', 'SSL_DIR', '/opt/aifabrix/ssl')
|
|
81
|
+
.option('--builder-port <port>', 'BUILDER_SERVER_PORT', '3000')
|
|
82
|
+
.action(async (opts) => {
|
|
83
|
+
try {
|
|
84
|
+
requireUbuntu();
|
|
85
|
+
runInstallServerLocal({
|
|
86
|
+
dataDir: opts.dataDir,
|
|
87
|
+
devDomain: opts.devDomain,
|
|
88
|
+
sslDir: opts.sslDir,
|
|
89
|
+
builderServerPort: opts.builderPort ? parseInt(opts.builderPort, 10) : undefined,
|
|
90
|
+
});
|
|
91
|
+
console.log('Install-server complete.');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(err instanceof Error ? err.message : err);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
54
98
|
program
|
|
55
99
|
.command('backup [user@host]')
|
|
56
100
|
.description('On-demand backup, or --schedule to install cron (omit target for local).')
|
|
@@ -133,6 +177,25 @@ program
|
|
|
133
177
|
process.exit(1);
|
|
134
178
|
}
|
|
135
179
|
});
|
|
180
|
+
program
|
|
181
|
+
.command('install-ssh [user@host]')
|
|
182
|
+
.description('Activate SSH server (install openssh-server, enable and start ssh) without login. Omit target for local.')
|
|
183
|
+
.option('-i, --identity <path>', 'SSH private key path')
|
|
184
|
+
.action(async (target, opts) => {
|
|
185
|
+
try {
|
|
186
|
+
if (!target || !target.trim()) {
|
|
187
|
+
requireUbuntu();
|
|
188
|
+
runInstallSshLocal();
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
await runInstallSsh({ target: target.trim(), privateKeyPath: opts.identity });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error(err instanceof Error ? err.message : err);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
136
199
|
const sshCert = program.command('ssh-cert').description('Passwordless auth: install = append your SSH public key; request = stub for future SSH CA.');
|
|
137
200
|
sshCert
|
|
138
201
|
.command('request')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-init: One-time bootstrap over SSH. Installs on the server: SSH (if needed), Node 18+, npm, and af-server CLI.
|
|
3
|
+
* No Docker, nginx, or builder-server. After this, the user logs in to the server and runs install + install-server locally.
|
|
4
|
+
*/
|
|
5
|
+
import { type SSHConnectionOptions } from './ssh.js';
|
|
6
|
+
export declare function runInstallInit(options: SSHConnectionOptions): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-init: One-time bootstrap over SSH. Installs on the server: SSH (if needed), Node 18+, npm, and af-server CLI.
|
|
3
|
+
* No Docker, nginx, or builder-server. After this, the user logs in to the server and runs install + install-server locally.
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createSSHClient, exec, writeFile, close } from './ssh.js';
|
|
9
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ASSETS_DIR = path.resolve(scriptDir, '..', 'assets');
|
|
11
|
+
function toUnixLf(s) {
|
|
12
|
+
return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
13
|
+
}
|
|
14
|
+
function getInitScript() {
|
|
15
|
+
const p = path.join(ASSETS_DIR, 'setup-install-init.sh');
|
|
16
|
+
return toUnixLf(fs.readFileSync(p, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
export async function runInstallInit(options) {
|
|
19
|
+
const conn = await createSSHClient(options);
|
|
20
|
+
try {
|
|
21
|
+
const tmpDir = `/tmp/aifabrix-init-${Date.now()}`;
|
|
22
|
+
await exec(conn, `mkdir -p ${tmpDir}`);
|
|
23
|
+
await exec(conn, `chmod 755 ${tmpDir}`);
|
|
24
|
+
const script = getInitScript();
|
|
25
|
+
await writeFile(conn, `${tmpDir}/setup-install-init.sh`, script);
|
|
26
|
+
await exec(conn, `chmod +x ${tmpDir}/setup-install-init.sh`);
|
|
27
|
+
const cmd = `sudo ${tmpDir}/setup-install-init.sh`;
|
|
28
|
+
const result = await exec(conn, cmd);
|
|
29
|
+
if (result.stderr)
|
|
30
|
+
process.stderr.write(result.stderr);
|
|
31
|
+
if (result.stdout)
|
|
32
|
+
process.stdout.write(result.stdout);
|
|
33
|
+
if (result.code !== 0) {
|
|
34
|
+
throw new Error(`install-init script exited with code ${result.code}`);
|
|
35
|
+
}
|
|
36
|
+
await exec(conn, `rm -rf ${tmpDir}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
close(conn);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for install-init: bootstrap over SSH (mocked).
|
|
3
|
+
* install-init.ts uses import.meta.url; Jest (CJS) fails to load it. Skipped until ESM/import.meta is supported in tests.
|
|
4
|
+
*/
|
|
5
|
+
describe.skip('install-init', () => {
|
|
6
|
+
it('placeholder until Jest supports import.meta in transformed modules', () => {
|
|
7
|
+
expect(true).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-ssh: Activate SSH server (openssh-server) without login.
|
|
3
|
+
* Installs openssh-server if needed, enables and starts the ssh service so the server accepts SSH connections.
|
|
4
|
+
*/
|
|
5
|
+
import { type SSHConnectionOptions } from './ssh.js';
|
|
6
|
+
export declare function runInstallSsh(options: SSHConnectionOptions): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Enable and start SSH server locally (no SSH). Call requireUbuntu() before this.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runInstallSshLocal(): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-ssh: Activate SSH server (openssh-server) without login.
|
|
3
|
+
* Installs openssh-server if needed, enables and starts the ssh service so the server accepts SSH connections.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { createSSHClient, exec, close } from './ssh.js';
|
|
7
|
+
const INSTALL_SSH_SCRIPT = [
|
|
8
|
+
'wait_for_apt() { i=0; while [ $i -lt 20 ]; do',
|
|
9
|
+
' apt-get update -qq 2>/dev/null && return 0;',
|
|
10
|
+
' echo "Waiting for apt lock (another process is using apt)..."; sleep 6; i=$((i+1));',
|
|
11
|
+
' done; return 1;',
|
|
12
|
+
'};',
|
|
13
|
+
'wait_for_apt && DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server && systemctl enable ssh && systemctl start ssh',
|
|
14
|
+
].join(' ');
|
|
15
|
+
export async function runInstallSsh(options) {
|
|
16
|
+
const conn = await createSSHClient(options);
|
|
17
|
+
try {
|
|
18
|
+
const cmd = `sudo bash -c '${INSTALL_SSH_SCRIPT.replace(/'/g, "'\"'\"'")}'`;
|
|
19
|
+
const result = await exec(conn, cmd);
|
|
20
|
+
if (result.stderr)
|
|
21
|
+
process.stderr.write(result.stderr);
|
|
22
|
+
if (result.stdout)
|
|
23
|
+
process.stdout.write(result.stdout);
|
|
24
|
+
if (result.code !== 0) {
|
|
25
|
+
throw new Error(`install-ssh exited with code ${result.code}`);
|
|
26
|
+
}
|
|
27
|
+
console.log('SSH server is enabled and running. You can connect with key-based or password auth.');
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
close(conn);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Enable and start SSH server locally (no SSH). Call requireUbuntu() before this.
|
|
35
|
+
*/
|
|
36
|
+
export function runInstallSshLocal() {
|
|
37
|
+
execSync(`sudo bash -c '${INSTALL_SSH_SCRIPT.replace(/'/g, "'\"'\"'")}'`, { stdio: 'inherit' });
|
|
38
|
+
console.log('SSH server is enabled and running. You can connect with key-based or password auth.');
|
|
39
|
+
}
|