dsh-scnet 0.4.1 → 0.5.0

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.
Files changed (38) hide show
  1. package/README.md +20 -113
  2. package/README_CN.md +20 -1
  3. package/docs/installation.md +10 -0
  4. package/docs/installation.zh-CN.md +13 -1
  5. package/docs/testing.md +11 -0
  6. package/docs/testing.zh-CN.md +10 -0
  7. package/index.mjs +184 -0
  8. package/package.json +4 -2
  9. package/skills/scnet-hpc/SKILL.md +63 -105
  10. package/skills/scnet-hpc/VERSION +1 -0
  11. package/skills/scnet-hpc/clusters/_template.conf +10 -4
  12. package/skills/scnet-hpc/references/adding-cluster.md +6 -1
  13. package/skills/scnet-hpc/references/backends.md +177 -0
  14. package/skills/scnet-hpc/references/notebook.md +60 -0
  15. package/skills/scnet-hpc/references/openapi.md +140 -0
  16. package/skills/scnet-hpc/references/quickstart-en.md +22 -0
  17. package/skills/scnet-hpc/references/setup.md +4 -0
  18. package/skills/scnet-hpc/scripts/__pycache__/compute-probe.cpython-312.pyc +0 -0
  19. package/skills/scnet-hpc/scripts/__pycache__/scnet.cpython-312.pyc +0 -0
  20. package/skills/scnet-hpc/scripts/__pycache__/scnet_config.cpython-312.pyc +0 -0
  21. package/skills/scnet-hpc/scripts/__pycache__/scnet_credentials.cpython-312.pyc +0 -0
  22. package/skills/scnet-hpc/scripts/__pycache__/scnet_version.cpython-312.pyc +0 -0
  23. package/skills/scnet-hpc/scripts/scnet.py +1573 -0
  24. package/skills/scnet-hpc/scripts/scnet_backends/__init__.py +53 -0
  25. package/skills/scnet-hpc/scripts/scnet_backends/base.py +64 -0
  26. package/skills/scnet-hpc/scripts/scnet_backends/external.py +92 -0
  27. package/skills/scnet-hpc/scripts/scnet_backends/openapi.py +771 -0
  28. package/skills/scnet-hpc/scripts/scnet_backends/profile.py +78 -0
  29. package/skills/scnet-hpc/scripts/scnet_backends/ssh.py +258 -0
  30. package/skills/scnet-hpc/scripts/scnet_config.py +175 -0
  31. package/skills/scnet-hpc/scripts/scnet_credentials.py +160 -0
  32. package/skills/scnet-hpc/scripts/scnet_sdk/__init__.py +11 -0
  33. package/skills/scnet-hpc/scripts/scnet_sdk/cli.py +394 -0
  34. package/skills/scnet-hpc/scripts/scnet_sdk/client.py +240 -0
  35. package/skills/scnet-hpc/scripts/scnet_sdk/notebook.py +379 -0
  36. package/skills/scnet-hpc/scripts/scnet_version.py +20 -0
  37. package/skills/scnet-hpc/scripts/setup-ssh.sh +33 -1
  38. package/skills/scnet-hpc/scripts/setup.sh +471 -0
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env bash
2
+ # SCNet HPC configuration and credential-rotation panel.
3
+ #
4
+ # This is the dependency-light entrypoint for new machines. Advanced OpenAPI
5
+ # discovery remains available through: python3 scripts/scnet.py setup
6
+
7
+ set -euo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
11
+ # shellcheck disable=SC1091
12
+ . "$SCRIPT_DIR/_common.sh"
13
+
14
+ BACKEND=""
15
+ CLUSTER=""
16
+ ACTION=""
17
+ SKIP_CONNECT=no
18
+ SSH_KEY=""
19
+ SSH_USER=""
20
+
21
+ usage() {
22
+ sed -n '2,8p' "$0" | sed 's/^# \?//'
23
+ cat <<'EOF'
24
+
25
+ 用法:
26
+ ./scripts/setup.sh
27
+ ./scripts/setup.sh new|modify|status|reset
28
+ ./scripts/setup.sh --cluster <profile> --backend ssh
29
+ ./scripts/setup.sh --skip-connect
30
+
31
+ action 可选:new、modify、status、reset。
32
+ backend 可选:ssh、openapi、both。无参数时进入交互管理面板。
33
+ EOF
34
+ }
35
+
36
+ while [ $# -gt 0 ]; do
37
+ case "$1" in
38
+ --cluster)
39
+ [ $# -ge 2 ] || die "--cluster 缺少参数"
40
+ CLUSTER="$2"
41
+ shift 2
42
+ ;;
43
+ --cluster=*) CLUSTER="${1#*=}"; shift ;;
44
+ --backend)
45
+ [ $# -ge 2 ] || die "--backend 缺少参数"
46
+ BACKEND="$2"
47
+ shift 2
48
+ ;;
49
+ --backend=*) BACKEND="${1#*=}"; shift ;;
50
+ --skip-connect) SKIP_CONNECT=yes; shift ;;
51
+ --ssh-key)
52
+ [ $# -ge 2 ] || die "--ssh-key 缺少参数"
53
+ SSH_KEY="$2"
54
+ shift 2
55
+ ;;
56
+ --ssh-key=*) SSH_KEY="${1#*=}"; shift ;;
57
+ --username)
58
+ [ $# -ge 2 ] || die "--username 缺少参数"
59
+ SSH_USER="$2"
60
+ shift 2
61
+ ;;
62
+ --username=*) SSH_USER="${1#*=}"; shift ;;
63
+ new|modify|status|reset)
64
+ [ -z "$ACTION" ] || die "只能指定一个 action"
65
+ ACTION="$1"
66
+ shift
67
+ ;;
68
+ -h|--help) usage; exit 0 ;;
69
+ *) die "未知参数: $1" ;;
70
+ esac
71
+ done
72
+
73
+ require_tty() {
74
+ [ -t 0 ] || die "配置面板需要交互式终端;非交互部署请预先生成 config.json 或使用 Python/CI 专用流程"
75
+ }
76
+
77
+ ask() {
78
+ local prompt="$1"
79
+ local default="${2:-}"
80
+ local answer
81
+ if [ -n "$default" ]; then
82
+ printf '%s [%s]: ' "$prompt" "$default" >&2
83
+ else
84
+ printf '%s: ' "$prompt" >&2
85
+ fi
86
+ IFS= read -r answer
87
+ printf '%s' "${answer:-$default}"
88
+ }
89
+
90
+ choose() {
91
+ local prompt="$1"
92
+ shift
93
+ local choices=("$@")
94
+ local index choice
95
+ if [ -t 0 ] && [ -t 2 ] && [ "${TERM:-dumb}" != "dumb" ]; then
96
+ local selected=0
97
+ local number=""
98
+ local key sequence value
99
+ local line_count=$(( ${#choices[@]} + 1 ))
100
+ printf '%s\n' "$prompt" >&2
101
+ printf '使用 ↑/↓ 移动,Enter 确认;也可输入编号后按 Enter;q 取消。\n' >&2
102
+
103
+ render_choices() {
104
+ local first="${1:-no}"
105
+ local render_index pointer typed
106
+ if [ "$first" != yes ]; then
107
+ printf '\033[%dA' "$line_count" >&2
108
+ fi
109
+ for render_index in "${!choices[@]}"; do
110
+ pointer=" "
111
+ [ "$render_index" -eq "$selected" ] && pointer="▶"
112
+ printf '\r\033[2K %s %d. %s\n' \
113
+ "$pointer" "$((render_index + 1))" \
114
+ "${choices[$render_index]}" >&2
115
+ done
116
+ typed="编号:(可直接按 Enter)"
117
+ [ -n "$number" ] && typed="编号:$number"
118
+ printf '\r\033[2K %s\n' "$typed" >&2
119
+ }
120
+
121
+ render_choices yes
122
+ while true; do
123
+ IFS= read -rsn1 key
124
+ case "$key" in
125
+ "")
126
+ if [ -n "$number" ]; then
127
+ value=$((10#$number))
128
+ if [ "$value" -ge 1 ] \
129
+ && [ "$value" -le "${#choices[@]}" ]; then
130
+ selected=$((value - 1))
131
+ break
132
+ fi
133
+ number=""
134
+ render_choices
135
+ else
136
+ break
137
+ fi
138
+ ;;
139
+ $'\033')
140
+ sequence=""
141
+ IFS= read -rsn2 -t 1 sequence || true
142
+ case "$sequence" in
143
+ "[A") selected=$(( (selected - 1 + ${#choices[@]}) % ${#choices[@]} )); number=""; render_choices ;;
144
+ "[B") selected=$(( (selected + 1) % ${#choices[@]} )); number=""; render_choices ;;
145
+ esac
146
+ ;;
147
+ q|Q) die "用户取消配置" ;;
148
+ $'\177')
149
+ number="${number%?}"
150
+ render_choices
151
+ ;;
152
+ [0-9])
153
+ number="${number}${key}"
154
+ render_choices
155
+ ;;
156
+ esac
157
+ done
158
+ printf '已选择:%d. %s\n' \
159
+ "$((selected + 1))" "${choices[$selected]}" >&2
160
+ printf '%s' "${choices[$selected]}"
161
+ return
162
+ fi
163
+
164
+ printf '%s\n' "$prompt" >&2
165
+ for index in "${!choices[@]}"; do
166
+ printf ' %d. %s\n' "$((index + 1))" "${choices[$index]}" >&2
167
+ done
168
+ printf '请输入 1-%d 的数字并按 Enter;直接按 Enter 使用默认项 1。\n' \
169
+ "${#choices[@]}" >&2
170
+ printf '当前终端不支持交互导航,已使用数字输入模式。\n' >&2
171
+ local selected
172
+ selected=$(ask "选择" "1")
173
+ [[ "$selected" =~ ^[0-9]+$ ]] || die "选择必须是数字"
174
+ [ "$selected" -ge 1 ] && [ "$selected" -le "${#choices[@]}" ] \
175
+ || die "选择超出范围"
176
+ choice="${choices[$((selected - 1))]}"
177
+ printf '%s' "$choice"
178
+ }
179
+
180
+ validate_identifier() {
181
+ local name="$1"
182
+ local value="$2"
183
+ [[ "$value" =~ ^[A-Za-z0-9._:-]+$ ]] || die "$name 包含不安全字符"
184
+ }
185
+
186
+ json_string() {
187
+ local value="$1"
188
+ value="${value//\\/\\\\}"
189
+ value="${value//\"/\\\"}"
190
+ value="${value//$'\n'/\\n}"
191
+ value="${value//$'\r'/\\r}"
192
+ printf '"%s"' "$value"
193
+ }
194
+
195
+ write_config() {
196
+ local config_root="${XDG_CONFIG_HOME:-$HOME/.config}/scnet-hpc"
197
+ local config_path="$config_root/config.json"
198
+ mkdir -p "$config_root"
199
+ chmod 700 "$config_root"
200
+
201
+ local temporary
202
+ temporary=$(mktemp "$config_root/.config.XXXXXX")
203
+ chmod 600 "$temporary"
204
+ {
205
+ printf '{\n "version": 1,\n "cluster": '
206
+ json_string "$CLUSTER"
207
+ printf ',\n "default_backend": '
208
+ json_string "$DEFAULT_BACKEND"
209
+ printf '\n}\n'
210
+ } >"$temporary"
211
+ mv "$temporary" "$config_path"
212
+ printf '%s\n' "$config_path"
213
+ }
214
+
215
+ write_ssh_profile() {
216
+ [ -n "$SSH_USER" ] || return 0
217
+ local config_root="${XDG_CONFIG_HOME:-$HOME/.config}/scnet-hpc"
218
+ local ssh_root="$config_root/ssh"
219
+ local profile_path="$ssh_root/$CLUSTER.json"
220
+ local expiry=""
221
+ local key_base=""
222
+ local installed_key="$HOME/.ssh/id_rsa_$CLUSTER_ID"
223
+ local fingerprint=""
224
+ local updated_at=""
225
+ local existing_key_path=""
226
+ local existing_source=""
227
+ local existing_expiry=""
228
+ local existing_fingerprint=""
229
+ local existing_updated=""
230
+ mkdir -p "$ssh_root"
231
+ chmod 700 "$ssh_root"
232
+ existing_key_path=$(json_string_value "$profile_path" key_path)
233
+ existing_source=$(json_string_value "$profile_path" key_source_name)
234
+ existing_expiry=$(json_string_value "$profile_path" key_expires_at)
235
+ existing_fingerprint=$(json_string_value "$profile_path" key_fingerprint)
236
+ existing_updated=$(json_string_value "$profile_path" updated_at)
237
+ if [ -n "$SSH_KEY" ]; then
238
+ key_base=$(basename "$SSH_KEY")
239
+ expiry=$(printf '%s' "$key_base" | sed -nE \
240
+ 's/.*RsaKeyExpireTime[_-]([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/p')
241
+ updated_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
242
+ if [ -f "$installed_key" ]; then
243
+ fingerprint=$(ssh-keygen -lf "$installed_key" 2>/dev/null \
244
+ | awk '{print $2; exit}')
245
+ fi
246
+ else
247
+ installed_key="$existing_key_path"
248
+ key_base="$existing_source"
249
+ expiry="$existing_expiry"
250
+ fingerprint="$existing_fingerprint"
251
+ updated_at="$existing_updated"
252
+ fi
253
+ local temporary
254
+ temporary=$(mktemp "$ssh_root/.$CLUSTER.XXXXXX")
255
+ chmod 600 "$temporary"
256
+ {
257
+ printf '{\n "username": '
258
+ json_string "$SSH_USER"
259
+ if [ -n "$installed_key" ]; then
260
+ printf ',\n "key_path": '
261
+ json_string "$installed_key"
262
+ fi
263
+ if [ -n "$key_base" ]; then
264
+ printf ',\n "key_source_name": '
265
+ json_string "$key_base"
266
+ fi
267
+ if [ -n "$updated_at" ]; then
268
+ printf ',\n "updated_at": '
269
+ json_string "$updated_at"
270
+ fi
271
+ if [ -n "$expiry" ]; then
272
+ printf ',\n "key_expires_at": '
273
+ json_string "$expiry"
274
+ fi
275
+ if [ -n "$fingerprint" ]; then
276
+ printf ',\n "key_fingerprint": '
277
+ json_string "$fingerprint"
278
+ fi
279
+ printf '\n}\n'
280
+ } >"$temporary"
281
+ mv "$temporary" "$profile_path"
282
+ }
283
+
284
+ json_string_value() {
285
+ local path="$1"
286
+ local key="$2"
287
+ [ -f "$path" ] || return 0
288
+ sed -nE "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\\1/p" \
289
+ "$path" | head -1
290
+ }
291
+
292
+ detect_ssh_user() {
293
+ local key_path="${1:-}"
294
+ local config_root="${XDG_CONFIG_HOME:-$HOME/.config}/scnet-hpc"
295
+ local saved
296
+ saved=$(json_string_value "$config_root/ssh/$CLUSTER.json" username)
297
+ [ -n "$saved" ] && { printf '%s' "$saved"; return; }
298
+
299
+ if [ -n "$key_path" ] && [ -n "${KEY_NAME_MARKER:-}" ] \
300
+ && [[ "$KEY_NAME_MARKER" != *"<"* ]]; then
301
+ local base
302
+ base=$(basename "$key_path")
303
+ case "$base" in
304
+ *"$KEY_NAME_MARKER"*)
305
+ printf '%s' "${base%%"$KEY_NAME_MARKER"*}"
306
+ return
307
+ ;;
308
+ esac
309
+ fi
310
+ if [ -n "$key_path" ] && [ -n "${SSH_HOST:-}" ]; then
311
+ local host_marker="_${SSH_HOST}_"
312
+ local base
313
+ base=$(basename "$key_path")
314
+ case "$base" in
315
+ *"$host_marker"*)
316
+ printf '%s' "${base%%"$host_marker"*}"
317
+ return
318
+ ;;
319
+ esac
320
+ fi
321
+
322
+ local resolved_host resolved_user
323
+ resolved_host=$(ssh -G "$CLUSTER_ID" 2>/dev/null \
324
+ | awk '/^hostname /{print $2; exit}')
325
+ resolved_user=$(ssh -G "$CLUSTER_ID" 2>/dev/null \
326
+ | awk '/^user /{print $2; exit}')
327
+ if [ "$resolved_host" = "${SSH_HOST:-}" ]; then
328
+ printf '%s' "$resolved_user"
329
+ fi
330
+ }
331
+
332
+ require_tty
333
+
334
+ printf '\nSCNet HPC 配置/维护面板(Bash)\n' >&2
335
+ printf '==================================\n' >&2
336
+ printf '此面板只保存非敏感选择;AK/SK/token 不会写入配置文件。\n\n' >&2
337
+ case "$(uname -s)" in
338
+ Darwin)
339
+ printf '%s\n' \
340
+ "macOS:在 Terminal 或 iTerm2 中使用 ↑/↓ 和 Enter,也可输入编号;Ctrl+C 可取消。" \
341
+ "敏感输入不会回显;OpenAPI 凭据可保存到 macOS Keychain。" >&2
342
+ ;;
343
+ Linux)
344
+ if [ -r /etc/os-release ] \
345
+ && grep -Eq '^ID=(ubuntu|debian)$' /etc/os-release; then
346
+ printf '%s\n' \
347
+ "Ubuntu/Debian:在 Terminal 中使用 ↑/↓ 和 Enter,也可输入编号;Ctrl+C 可取消。" \
348
+ "如需安全保存 AK/SK:sudo apt install libsecret-tools" >&2
349
+ else
350
+ printf '%s\n' "Linux:使用 ↑/↓ 和 Enter,也可输入编号;Ctrl+C 可取消。" >&2
351
+ fi
352
+ ;;
353
+ esac
354
+ printf '\n' >&2
355
+
356
+ available=$(list_clusters)
357
+ [ -n "$available" ] || die "clusters/ 下没有 profile"
358
+ if [ -z "$ACTION" ]; then
359
+ action_label=$(choose "选择配置操作:" \
360
+ "查看当前状态(status)" \
361
+ "新增连接(new)" \
362
+ "修改现有配置(modify)" \
363
+ "重置配置(reset)")
364
+ case "$action_label" in
365
+ *status*) ACTION=status ;;
366
+ *new*) ACTION=new ;;
367
+ *modify*) ACTION=modify ;;
368
+ *reset*) ACTION=reset ;;
369
+ esac
370
+ fi
371
+
372
+ if [ "$ACTION" = status ] || [ "$ACTION" = reset ]; then
373
+ if command -v python3 >/dev/null 2>&1; then
374
+ exec python3 "$SCRIPT_DIR/scnet.py" setup "$ACTION"
375
+ fi
376
+ [ "$ACTION" = status ] || die "reset 需要 Python 3 以执行安全的分范围重置"
377
+ config_root="${XDG_CONFIG_HOME:-$HOME/.config}/scnet-hpc"
378
+ printf '配置目录:%s\n' "$config_root"
379
+ find "$config_root" -maxdepth 2 -type f -print 2>/dev/null || true
380
+ exit 0
381
+ fi
382
+
383
+ if [ -z "$BACKEND" ]; then
384
+ BACKEND=$(choose "选择要管理的 backend:" \
385
+ "ssh(区域级用户名和私钥)" \
386
+ "openapi(平台级 AK/SK 和多区域)" \
387
+ "both(同时管理)")
388
+ case "$BACKEND" in
389
+ ssh*) BACKEND=ssh ;;
390
+ openapi*) BACKEND=openapi ;;
391
+ both*) BACKEND=both ;;
392
+ esac
393
+ fi
394
+ case "$BACKEND" in
395
+ ssh|openapi|both) ;;
396
+ *) die "backend 必须是 ssh、openapi 或 both" ;;
397
+ esac
398
+
399
+ if { [ "$BACKEND" = ssh ] || [ "$BACKEND" = both ]; } \
400
+ && [ -z "$CLUSTER" ]; then
401
+ cluster_choices=()
402
+ while IFS= read -r cluster_choice; do
403
+ [ -n "$cluster_choice" ] || continue
404
+ metadata="${XDG_CONFIG_HOME:-$HOME/.config}/scnet-hpc/ssh/$cluster_choice.json"
405
+ if [ "$ACTION" = new ] && [ ! -f "$metadata" ]; then
406
+ cluster_choices+=("$cluster_choice")
407
+ elif [ "$ACTION" = modify ] && [ -f "$metadata" ]; then
408
+ cluster_choices+=("$cluster_choice")
409
+ fi
410
+ done <<EOF
411
+ $available
412
+ EOF
413
+ [ "${#cluster_choices[@]}" -gt 0 ] || {
414
+ if [ "$ACTION" = new ]; then
415
+ die "所有 SSH profiles 已配置;请使用 setup.sh modify"
416
+ fi
417
+ die "没有已配置的 SSH profile;请使用 setup.sh new"
418
+ }
419
+ CLUSTER=$(choose "选择默认集群 profile:" "${cluster_choices[@]}")
420
+ fi
421
+
422
+ if [ "$BACKEND" = ssh ] || [ "$BACKEND" = both ]; then
423
+ [[ "$CLUSTER" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \
424
+ || die "集群 profile 名称包含不安全字符"
425
+ export SCNET_HPC_LIVE_NODE_COUNT=no
426
+ load_cluster "$CLUSTER"
427
+ unset SCNET_HPC_LIVE_NODE_COUNT
428
+ fi
429
+
430
+ DEFAULT_BACKEND="$BACKEND"
431
+ if [ "$BACKEND" = both ]; then
432
+ DEFAULT_BACKEND=$(choose "选择默认 backend:" "ssh" "openapi")
433
+ fi
434
+
435
+ if [ "$BACKEND" = ssh ] || [ "$BACKEND" = both ]; then
436
+ if [ "$SKIP_CONNECT" = no ]; then
437
+ SSH_KEY="${SSH_KEY:-$(ask "私钥文件路径")}"
438
+ SSH_KEY="${SSH_KEY/#\~/$HOME}"
439
+ [ -f "$SSH_KEY" ] || die "找不到私钥文件: $SSH_KEY"
440
+ fi
441
+ detected_user=$(detect_ssh_user "$SSH_KEY")
442
+ SSH_USER="${SSH_USER:-$(ask "远端用户名(已自动探测,可直接回车)" "$detected_user")}"
443
+ validate_identifier "远端用户名" "$SSH_USER"
444
+ [ -n "$SSH_USER" ] || die "无法自动探测 SSH 用户名,请手工输入"
445
+ if [ "$SKIP_CONNECT" = no ]; then
446
+ "$SCRIPT_DIR/setup-ssh.sh" --cluster "$CLUSTER" "$SSH_KEY" "$SSH_USER"
447
+ else
448
+ printf '已跳过 SSH 私钥安装和连接测试。\n' >&2
449
+ fi
450
+ fi
451
+
452
+ if [ "$BACKEND" = openapi ] || [ "$BACKEND" = both ]; then
453
+ if command -v python3 >/dev/null 2>&1; then
454
+ printf '%s\n' \
455
+ "OpenAPI 使用一组平台 AK/SK 自动发现全部授权区域。" >&2
456
+ openapi_args=(setup "$ACTION" --mode openapi)
457
+ [ "$SKIP_CONNECT" = yes ] && openapi_args+=(--skip-connect)
458
+ python3 "$SCRIPT_DIR/scnet.py" "${openapi_args[@]}"
459
+ else
460
+ printf '%s\n' \
461
+ "OpenAPI backend 需要 Python 3;SSH 配置不受影响。" \
462
+ "安装 Python 3 后运行:python3 scripts/scnet.py setup --mode openapi" >&2
463
+ fi
464
+ fi
465
+
466
+ write_ssh_profile
467
+ if [ "$BACKEND" = ssh ] || [ "$BACKEND" = both ]; then
468
+ config_path=$(write_config)
469
+ printf '\n已保存配置:%s\n' "$config_path" >&2
470
+ fi
471
+ printf '下一步:%s/scripts/scnet.py doctor\n' "$REPO_ROOT" >&2