diff --git a/.gitignore b/.gitignore index d94a71c0..69cd3752 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ cofnig.link .bak config.link release/* -.history/ \ No newline at end of file +.history/ +config_init/nginx/cert/ diff --git a/README.md b/README.md index 68d170aa..1e665331 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,25 @@ $ ./jmsctl.sh tail ``` +## KOTL(企业版) + +KOTL 是企业版组件,需要在 `/opt/jumpserver/config/config.txt` 中设置 +`USE_XPACK=1`。它作为宿主机 systemd 服务安装,不加入 Docker Compose;企业版中 +默认启用,如需关闭可设置: + +```bash +KOTL_ENABLED=0 +``` + +安装器会拉取 `${NAMESPACE:-jumpserver}/kotl:${VERSION}` artifact 镜像,从 +`/dist` 提取并执行 KOTL 自带的 `scripts/install.sh` 或 `scripts/upgrade.sh`。 +离线包也会自动包含该镜像。服务跟随 `jmsctl.sh start/stop/restart/status` +管理,日志可通过 `./jmsctl.sh tail kotl` 查看。启用时还会自动为 Core 配置 +`KOTL_ENABLED=1`、`JDMC_ENABLED=1` 和 `/opt/jumpserver/data/unshare/kotl.sock`。 + +当前 KOTL 的宿主机路径固定使用 `/data/jumpserver`,因此启用时 +`VOLUME_DIR` 也必须保持为 `/data/jumpserver`。 + ## 配置文件说明 配置文件将会放在 /opt/jumpserver/config 中 diff --git a/compose/magnus.yml b/compose/magnus.yml index c0cc1b56..58b48ecb 100644 --- a/compose/magnus.yml +++ b/compose/magnus.yml @@ -9,13 +9,7 @@ services: env_file: - ${CONFIG_SAFE_FILE} ports: - - ${MAGNUS_MYSQL_PORT:-33061}:33061 - - ${MAGNUS_MARIADB_PORT:-33062}:33062 - - ${MAGNUS_REDIS_PORT:-63790}:63790 - - ${MAGNUS_POSTGRESQL_PORT:-54320}:54320 - - ${MAGNUS_SQLSERVER_PORT:-14330}:14330 - - ${MAGNUS_ORACLE_PORT:-15210}:15210 - - ${MAGNUS_MONGODB_PORT:-27018}:27018 + - ${MAGNUS_PORT:-5525}:5525 volumes: - ${CONFIG_DIR}/certs:/opt/magnus/data/certs - ${VOLUME_DIR}/magnus/data:/opt/magnus/data diff --git a/compose/mysql.yml b/compose/mysql.yml index 5f5b0cd9..dedbd951 100644 --- a/compose/mysql.yml +++ b/compose/mysql.yml @@ -1,6 +1,6 @@ services: mysql: - image: mysql:5.7-debian + image: mysql:8.0 container_name: jms_mysql restart: always command: --character-set-server=utf8 diff --git a/compose/openbao.yml b/compose/openbao.yml new file mode 100644 index 00000000..b70507b3 --- /dev/null +++ b/compose/openbao.yml @@ -0,0 +1,60 @@ +services: + core: + depends_on: + openbao-init: + condition: service_completed_successfully + + celery: + depends_on: + openbao-init: + condition: service_completed_successfully + + openbao: + image: openbao:2.6.0 + container_name: jms_openbao + hostname: openbao + # Start the image entrypoint as root so it can fix bind-mount ownership; + # the official entrypoint then launches bao via su-exec as openbao. + user: "0" + restart: always + command: server -config=/openbao/config/server.hcl + environment: + BAO_ADDR: http://127.0.0.1:8200 + VAULT_API_ADDR: ${OPENBAO_RAFT_API_ADDR:-http://openbao:8200} + VAULT_CLUSTER_ADDR: ${OPENBAO_RAFT_CLUSTER_ADDR:-http://openbao:8201} + VAULT_RAFT_NODE_ID: ${OPENBAO_RAFT_NODE_ID:-openbao} + TZ: ${TZ:-Asia/Shanghai} + volumes: + - ${CONFIG_DIR}/openbao/server.hcl:/openbao/config/server.hcl:ro + - ${VOLUME_DIR}/openbao/data:/openbao/file + ports: + - "${OPENBAO_UI_BIND:-127.0.0.1}:${OPENBAO_UI_PORT:-8200}:8200" + - "${OPENBAO_CLUSTER_BIND:-127.0.0.1}:${OPENBAO_CLUSTER_PORT:-8201}:8201" + healthcheck: + test: "bao status -address=http://127.0.0.1:8200" + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - net + + openbao-init: + image: openbao:2.6.0 + container_name: jms_openbao_init + hostname: openbao-init + # The one-shot initializer writes root-owned 0600 unseal material. + user: "0" + restart: "no" + command: sh /openbao/bootstrap/bootstrap.sh + env_file: + - ${CONFIG_FILE} + environment: + BAO_ADDR: http://openbao:8200 + TZ: ${TZ:-Asia/Shanghai} + volumes: + - ${CONFIG_DIR}/openbao:/openbao/bootstrap + depends_on: + - openbao + networks: + - net diff --git a/compose/postgresql.yml b/compose/postgresql.yml index 19aa293c..917a25aa 100644 --- a/compose/postgresql.yml +++ b/compose/postgresql.yml @@ -1,6 +1,6 @@ services: postgresql: - image: postgres:16.10-bookworm + image: postgres:16.15-bookworm container_name: jms_postgresql restart: always environment: @@ -17,4 +17,4 @@ services: retries: 3 start_period: 30s networks: - - net \ No newline at end of file + - net diff --git a/compose/redis.yml b/compose/redis.yml index 00eeaf5d..7cd1f84c 100644 --- a/compose/redis.yml +++ b/compose/redis.yml @@ -1,6 +1,6 @@ services: redis: - image: redis:7.4.6-bookworm + image: redis:7.4.10-bookworm container_name: jms_redis hostname: jms_redis restart: always diff --git a/compose/web.yml b/compose/web.yml index 8276d3ae..d8f507d4 100644 --- a/compose/web.yml +++ b/compose/web.yml @@ -24,6 +24,7 @@ services: KAEL_ENABLED: ${KAEL_ENABLED:-0} RAZOR_ENABLED: ${RAZOR_ENABLED:-1} FACELIVE_ENABLED: ${FACELIVE_ENABLED:-0} + KOTL_ENABLED: ${KOTL_ENABLED:-0} BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:-} volumes: - ${VOLUME_DIR}/core/data:/opt/jumpserver/data @@ -36,4 +37,6 @@ services: retries: 3 start_period: 90s networks: - - net \ No newline at end of file + - net + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/compose/xrdp.yml b/compose/xrdp.yml index b44b3963..47ea06a3 100644 --- a/compose/xrdp.yml +++ b/compose/xrdp.yml @@ -14,10 +14,10 @@ services: - ${VOLUME_DIR}/xrdp/data:/opt/xrdp/data - ${CONFIG_DIR}/nginx/cert:/opt/xrdp/cert healthcheck: - test: "nc -z 127.0.0.1 $$XRDP_PORT || exit 1" + test: "nc -z 127.0.0.1 3390 || exit 1" interval: 10s timeout: 5s retries: 3 start_period: 10s networks: - - net \ No newline at end of file + - net diff --git a/config-example.txt b/config-example.txt index 3b8a2b12..f64149db 100644 --- a/config-example.txt +++ b/config-example.txt @@ -129,6 +129,11 @@ DOMAINS= # CHEN_ENABLED=0 # WEB_ENABLED=0 +# KOTL is an Enterprise Edition host management console. It is distributed as +# an artifact image, then installed as a systemd service on the host instead of +# running in Compose. It requires USE_XPACK=1 and can be disabled with: +# KOTL_ENABLED=0 + # Lion enables font smoothing to optimize the experience # JUMPSERVER_ENABLE_FONT_SMOOTHING=true @@ -136,15 +141,10 @@ JUMPSERVER_ENABLE_FONT_SMOOTHING=true ################################# XPack Configuration ################################# # XPack package, invalid setting in open source version # -SSH_PORT=2222 -RDP_PORT=3389 +KOKO_SSH_PORT=2222 +RAZOR_RDP_PORT=3389 XRDP_PORT=3390 -MAGNUS_MYSQL_PORT=33061 -MAGNUS_MARIADB_PORT=33062 -MAGNUS_REDIS_PORT=63790 -MAGNUS_POSTGRESQL_PORT=54320 -MAGNUS_SQLSERVER_PORT=14330 -MAGNUS_ORACLE_PORT=15210 +MAGNUS_PORT=5525 #XRDP_ENABLED=1 @@ -162,3 +162,36 @@ USE_LB=1 # TZ=Asia/Shanghai CURRENT_VERSION= + +################################# Vault Configuration ################################# +# OpenBao runs as an independent Docker service in this installer. JumpServer connects to OpenBao Server directly. +# OpenBao 作为独立 Docker 服务部署,JumpServer 直接访问 OpenBao Server。 +# +# Disabled by default. Set to true to enable Vault storage. When the backend is +# openbao and OPENBAO_EXTERNAL=false, the installer starts the built-in service. +# 默认关闭;设为 true 后启用 Vault。当后端为 openbao 且 OPENBAO_EXTERNAL=false 时,安装器启动内置服务。 +VAULT_ENABLED=false +VAULT_BACKEND=openbao +VAULT_OPENBAO_ADDR=http://openbao:8200 +VAULT_OPENBAO_MOUNT_POINT=pam +VAULT_OPENBAO_TOKEN= +VAULT_OPENBAO_TIMEOUT=10 + +# Set to true when VAULT_OPENBAO_ADDR points to an external OpenBao cluster or HA endpoint. +# 设置为 true 时,installer 不会启动内置 OpenBao,Core 仍使用上面的 Vault 配置连接外部服务。 +OPENBAO_EXTERNAL=false + +OPENBAO_RAFT_NODE_ID=openbao +OPENBAO_RAFT_API_ADDR=http://openbao:8200 +OPENBAO_RAFT_CLUSTER_ADDR=http://openbao:8201 +OPENBAO_RAFT_BOOTSTRAP=true +# Additional Raft nodes must set OPENBAO_RAFT_BOOTSTRAP=false and receive a +# protected copy of openbao/init.json from the bootstrap node before startup. +# OPENBAO_RAFT_RETRY_JOIN=http://openbao-1:8200,http://openbao-2:8200 + +OPENBAO_UNSEAL_KEY_SHARES=5 +OPENBAO_UNSEAL_KEY_THRESHOLD=3 +OPENBAO_UI_BIND=127.0.0.1 +OPENBAO_UI_PORT=8200 +OPENBAO_CLUSTER_BIND=127.0.0.1 +OPENBAO_CLUSTER_PORT=8201 diff --git a/config_init/nginx/cert/server.crt b/config_init/nginx/cert/server.crt deleted file mode 100644 index e060a861..00000000 --- a/config_init/nginx/cert/server.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtDCCApwCCQC70xxmpUL+9zANBgkqhkiG9w0BAQUFADCBmzELMAkGA1UEBhMC -Q04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0JlaWppbmcxFDASBgNVBAoM -C0R1aVpoYW4uSW5jMQ0wCwYDVQQLDARUZWNoMRwwGgYDVQQDDBN0ZXN0Lmp1bXBz -ZXJ2ZXIub3JnMSUwIwYJKoZIhvcNAQkBFhZzdXBwb3J0QGp1bXBzZXJ2ZXIub3Jn -MB4XDTE5MDExNzA5MjYwNFoXDTI5MDExNDA5MjYwNFowgZsxCzAJBgNVBAYTAkNO -MRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5nMRQwEgYDVQQKDAtE -dWlaaGFuLkluYzENMAsGA1UECwwEVGVjaDEcMBoGA1UEAwwTdGVzdC5qdW1wc2Vy -dmVyLm9yZzElMCMGCSqGSIb3DQEJARYWc3VwcG9ydEBqdW1wc2VydmVyLm9yZzCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKOEMGaqwjlNTTDtJkQpQH/5 -0QbvXWr+Q82ihPnFV685uQsj6vLFjD4gksdehENbMnPjpVDZCvzfhBOqahLZsvM6 -7ea0S1RFtX0t7rwErn3DOs5MYxU1bVyss1Ahf+bvOWgwaQpxkOmynOe4wxEqC2OJ -mQEzhb9sPo+tF52yMKtoQdkLVzf1Ci9HkVfwwAytejktnYwhzKR65GYHjMb9j7+p -z2dp3n6QKz0R9mnkEj6d6zRaFWT4sE5TMJt4DmHSwSP43c58rAbL8wyqWV9lifRr -RevHzGgFE3ep1Y53fm4jVCK5jY17CtV8g9iP8b0ttL2qr9jvNec37kGONpYjkl8C -AwEAATANBgkqhkiG9w0BAQUFAAOCAQEAdsI48hcuVz65dK1JYaoezM/PmVTD17Nx -l9QeRWANALro/nV8UOtMFGVBPFFzji+BsLKol6o8BCdpw72nfRNIPsNFFCrCgbft -2eNNc0hFHqbfwjT6E03JLb42BU5is+x2U8/Krg90Yt5XF0LoHm9lr24Kb31/wBKx -Ilb7mpC/TPd4p2V+QG46t6Ji4Q+DufihupgobG3PgcaOtPzT51HAlrL7R8OmDciB -o3h1ALD6CsvSK38SkFo6yc3lckjlfg/q0LZLeSEAAKerzL2j/DPw3hTUwMhFmTt5 -PWL26PfEUlv6wwUi/9Soa7B9QM0XEa9ib5PAWbIRCG4VxOG8fl57UA== ------END CERTIFICATE----- diff --git a/config_init/nginx/cert/server.key b/config_init/nginx/cert/server.key deleted file mode 100644 index 742b323c..00000000 --- a/config_init/nginx/cert/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAo4QwZqrCOU1NMO0mRClAf/nRBu9dav5DzaKE+cVXrzm5CyPq -8sWMPiCSx16EQ1syc+OlUNkK/N+EE6pqEtmy8zrt5rRLVEW1fS3uvASufcM6zkxj -FTVtXKyzUCF/5u85aDBpCnGQ6bKc57jDESoLY4mZATOFv2w+j60XnbIwq2hB2QtX -N/UKL0eRV/DADK16OS2djCHMpHrkZgeMxv2Pv6nPZ2nefpArPRH2aeQSPp3rNFoV -ZPiwTlMwm3gOYdLBI/jdznysBsvzDKpZX2WJ9GtF68fMaAUTd6nVjnd+biNUIrmN -jXsK1XyD2I/xvS20vaqv2O815zfuQY42liOSXwIDAQABAoIBAHmpj0G0Z9Ku23I9 -4Szx7JXL0GTykHVdPiGwfHRDCtzLfAz36oY7yf8nyU4h2xMqtb1YcdZXxz8jJ2hi -cY4ZAHbNL9lp6GqJe2HqXSjz6siUDBsW5toO6JH9xWUnp7yx3erRqjYlDYd0aB5Z -cHpC6DplVLx6E1e8OEg6p8mjnWbKeUAYAjw5ib3Cpn/VIY+ehuuDIwpfEKLct1yc -dWTJUqOhKOHxEPMdfmgaqqFi5XsDwC+aK4kXOvmLOYYvwH9DummcRckY7WOVF4yS -vKThEs60xOYViAtKLan47XhDqBaUcLWfK09OC9X87OXlUFh9nxdrZ6cwLcmYOL2Q -ZDYqf7ECgYEA0HvB6S87I5p2eaFLRS3ZYcHo75ooGAoUSDSkf2UBqznVWQrhQgUY -TOFf+/RQnDGY/tLbd338nhuOae2qtaY5iaFnbFVe0H4gDoIqM6tYCcJ0SXSvWTFk -E5Jso0O/yPGudztc2LBYpN6TTKbmANFBbq/zsDWFigFYXYjmDw/voFsCgYEAyMjE -vWb29VEELyWaBGUs3ryezvMA/Leli7uD3VwcVzLUIlhoTFESjooZeQlIHoYDuFX5 -aHjF2U4sLJ2lft+3nbBVfdW7s4FZtsX7QM5IfcVVj8PTvM2uMWxOxA6L/DPIin01 -PiCnEe7Xz/7ypEXS7cu4SNHwZu9toNcEUFsQtU0CgYBmtCDBm+fZUTV+E6w95ylI -lDsJFfscZJK7Q1up+ntI+5OTat2vJU1kSj57o062s2Q4XG2LPwBcbxzIKDHJjJqZ -p26ImsG7mfZ2zz6093rGTAn3Sck7+i3fymlEQJLRDeYxjIffo3f3uEH+J9X0nyFJ -wtocezFO2/zJDzCuSN52MwKBgQCRCAysrzJV0xaNo8CTyi1WGrMv03H0Ggd3XpSK -kd1a0zlOMcPs1GbuFSz/M8gnXDBVt6x3XT20kPXxqFIBykGMovGt+nQh3p5aGro5 -fof4aVE7jn1klMFtq8ldbxCItTL3bifGX7muh3LWKFdGd7U71XqhBxx6jhoHIylX -jeAMxQKBgAYSePmWULgXazuqTM0VeyiVOYlBmoD5WOZPBmX3KBs3FQmaVJmCdJBx -m8eudLlU2FSjPqEYOFqkH+bIMh6GsIAy09FlCyC9GhkiU8cMETa3S1MQsXAf9JP7 -5+/8/MoppyBaDEjbYNp3kAB0AcsSNgc2tbkquoKeyPG6xWabvhxb ------END RSA PRIVATE KEY----- diff --git a/config_init/nginx/lb_http_server.conf b/config_init/nginx/lb_http_server.conf index ef12af47..b6318b34 100644 --- a/config_init/nginx/lb_http_server.conf +++ b/config_init/nginx/lb_http_server.conf @@ -8,7 +8,7 @@ server { listen 80; # listen [::]:80; # server_name demo.jumpserver.org; # 取消注释并自行修改成你自己的域名 - return 307 https://$server_name$request_uri; + return 307 https://$host$request_uri; } server { diff --git a/config_init/openbao/bootstrap.sh b/config_init/openbao/bootstrap.sh new file mode 100644 index 00000000..e3a6f369 --- /dev/null +++ b/config_init/openbao/bootstrap.sh @@ -0,0 +1,162 @@ +#!/bin/sh +set -eu + +export BAO_ADDR="${BAO_ADDR:-http://openbao:8200}" + +MOUNT_POINT="${VAULT_OPENBAO_MOUNT_POINT:-pam}" +SERVICE_TOKEN="${VAULT_OPENBAO_TOKEN:-}" +RAFT_BOOTSTRAP="${OPENBAO_RAFT_BOOTSTRAP:-true}" +UNSEAL_KEY_SHARES="${OPENBAO_UNSEAL_KEY_SHARES:-5}" +UNSEAL_KEY_THRESHOLD="${OPENBAO_UNSEAL_KEY_THRESHOLD:-3}" +INIT_FILE="/openbao/bootstrap/init.json" +POLICY_FILE="/tmp/jumpserver-policy.hcl" +SERVICE_TOKEN_FILE="/openbao/bootstrap/jumpserver-token.json" + +wait_openbao() { + i=0 + while [ "$i" -lt 60 ]; do + if bao status >/tmp/openbao-status 2>&1; then + return 0 + fi + if grep -q "Initialized" /tmp/openbao-status 2>/dev/null; then + return 0 + fi + i=$((i + 1)) + sleep 1 + done + cat /tmp/openbao-status 2>/dev/null || true + echo "OpenBao is not reachable" + exit 1 +} + +json_value() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\"\\([^\"]*\\)\".*/\\1/p" +} + +json_array_first() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\\[\"\\([^\"]*\\)\".*/\\1/p" +} + +json_array_values() { + key="$1" + tr -d '\n ' <"${INIT_FILE}" | sed -n "s/.*\"${key}\":\\[\\([^]]*\\)\\].*/\\1/p" | tr ',' '\n' | sed 's/^"//;s/"$//' +} + +is_true() { + case "$1" in + 1|true|True|TRUE|yes|Yes|YES) return 0 ;; + *) return 1 ;; + esac +} + +is_initialized() { + bao status 2>/dev/null | grep -q "Initialized[[:space:]]*true" +} + +is_uninitialized() { + bao status 2>/dev/null | grep -q "Initialized[[:space:]]*false" +} + +is_sealed() { + bao status 2>/dev/null | grep -q "Sealed[[:space:]]*true" +} + +wait_unsealed() { + i=0 + while [ "$i" -lt 30 ]; do + if ! is_sealed; then + return 0 + fi + i=$((i + 1)) + sleep 1 + done + return 1 +} + +unseal_openbao() { + if ! is_sealed; then + return 0 + fi + + json_array_values unseal_keys_b64 | while IFS= read -r key; do + [ -z "${key}" ] && continue + if ! is_sealed; then + break + fi + bao operator unseal "${key}" >/dev/null + done + + if ! wait_unsealed; then + echo "OpenBao is still sealed after applying unseal keys from ${INIT_FILE}." + exit 1 + fi +} + +wait_openbao + +if is_uninitialized; then + if is_true "${RAFT_BOOTSTRAP}"; then + bao operator init -key-shares="${UNSEAL_KEY_SHARES}" -key-threshold="${UNSEAL_KEY_THRESHOLD}" -format=json >"${INIT_FILE}" + chmod 600 "${INIT_FILE}" 2>/dev/null || true + else + i=0 + while [ "$i" -lt 60 ]; do + is_initialized && break + i=$((i + 1)) + sleep 1 + done + if is_uninitialized; then + echo "OpenBao is not initialized. Set OPENBAO_RAFT_BOOTSTRAP=true on the first Raft node, or wait for retry_join to finish." + exit 1 + fi + fi +fi + +if [ ! -f "${INIT_FILE}" ]; then + echo "OpenBao is initialized, but ${INIT_FILE} is missing; cannot unseal automatically." + echo "On an additional Raft node, copy init.json from the bootstrap node to this node before startup." + exit 1 +fi + +ROOT_TOKEN="$(json_value root_token)" + +if [ -z "${ROOT_TOKEN}" ] || [ -z "$(json_array_first unseal_keys_b64)" ]; then + echo "Invalid OpenBao initialization file: ${INIT_FILE}" + exit 1 +fi + +unseal_openbao + +export BAO_TOKEN="${ROOT_TOKEN}" + +if ! bao secrets list -format=json | grep -q "\"${MOUNT_POINT}/\""; then + bao secrets enable -path="${MOUNT_POINT}" -version=2 kv +fi + +bao write "${MOUNT_POINT}/config" max_versions=20 >/dev/null + +cat >"${POLICY_FILE}" </dev/null + +if [ -n "${SERVICE_TOKEN}" ]; then + if ! bao token lookup "${SERVICE_TOKEN}" >/dev/null 2>&1; then + bao token create \ + -id="${SERVICE_TOKEN}" \ + -policy=jumpserver \ + -orphan \ + -no-default-policy \ + -format=json >"${SERVICE_TOKEN_FILE}" + chmod 600 "${SERVICE_TOKEN_FILE}" 2>/dev/null || true + fi +fi diff --git a/config_init/openbao/server.hcl b/config_init/openbao/server.hcl new file mode 100644 index 00000000..39957de9 --- /dev/null +++ b/config_init/openbao/server.hcl @@ -0,0 +1,16 @@ +ui = true +disable_mlock = true + +storage "raft" { + path = "/openbao/file" + node_id = "openbao" +} + +listener "tcp" { + address = "0.0.0.0:8200" + cluster_address = "0.0.0.0:8201" + tls_disable = true +} + +api_addr = "http://openbao:8200" +cluster_addr = "http://openbao:8201" diff --git a/jmsctl.sh b/jmsctl.sh index 32c1b2c3..30741e85 100755 --- a/jmsctl.sh +++ b/jmsctl.sh @@ -11,6 +11,16 @@ cd "${PROJECT_DIR}" || exit 1 action=${1-} target=${2-} args=("$@") +skip_kotl=false + +if [[ "${target}" == "--skip-kotl" ]]; then + case "${action}" in + start|stop|restart|close|status|down) + skip_kotl=true + target="" + ;; + esac +fi function check_config_file() { if [[ ! -f "${CONFIG_FILE}" ]]; then @@ -32,7 +42,7 @@ function check_config_file() { if [[ ! -f "./compose/.env" ]]; then ln -s "${CONFIG_FILE}" ./compose/.env fi - gen_safe_config + gen_safe_config >/dev/null } function pre_check() { @@ -52,16 +62,17 @@ function usage() { echo echo "Management Commands: " echo " config $(gettext 'Configuration Tools')" - echo " start $(gettext 'Start JumpServer')" - echo " stop $(gettext 'Stop JumpServer')" - echo " restart $(gettext 'Restart JumpServer')" - echo " status $(gettext 'Check JumpServer')" + echo " start [--skip-kotl] $(gettext 'Start JumpServer')" + echo " stop [--skip-kotl] $(gettext 'Stop JumpServer')" + echo " restart [--skip-kotl] $(gettext 'Restart JumpServer')" + echo " status [--skip-kotl] $(gettext 'Check JumpServer')" echo " down $(gettext 'Offline JumpServer')" echo " uninstall $(gettext 'Uninstall JumpServer')" echo echo "More Commands: " echo " load_image $(gettext 'Loading docker image')" echo " backup_db $(gettext 'Backup database')" + echo " backup_no_audit $(gettext 'Backup database without audit data')" echo " backup_audit $(gettext 'Backup audits tables')" echo " restore_db [file] $(gettext 'Data recovery through database backup file')" echo " raw $(gettext 'Execute the original docker compose command')" @@ -79,41 +90,53 @@ function service_to_docker_name() { EXE="" +function should_manage_kotl() { + [[ "${skip_kotl}" != "true" ]] +} + function start() { + set_openbao || return 1 + gen_safe_config >/dev/null + EXE=$(get_docker_compose_cmd_line) ${EXE} up -d - base_dir="${PROJECT_DIR}" - to="/opt/current/installer" - if [[ "$base_dir" == "$to" ]]; then - return - fi - mkdir -p /opt/current - echo "$base_dir" > /var/run/installer.lock - if [[ ! -L "$to" || "$(readlink -f "$to")" != "$base_dir" ]]; then - rm -f "$to" - ln -s "$base_dir" "$to" - fi - if [[ -e "$base_dir" && ! -e "$to" ]]; then - ln -s "$base_dir" "$to" + ensure_current_installer_link || return 1 + if should_manage_kotl; then + start_kotl fi } function stop() { - if [[ "${target}" == "ignore_db" ]]; then + if [[ "${target}" == "kotl" ]]; then + stop_kotl + elif [[ "${target}" == "ignore_db" ]]; then + if should_manage_kotl; then + stop_kotl || return 1 + fi cmd=$(get_docker_compose_cmd_line "ignore_db") ${cmd} down -v elif [[ -n "${target}" ]]; then ${EXE} stop "${target}" && ${EXE} rm -f "${target}" else + if should_manage_kotl; then + stop_kotl || return 1 + fi ${EXE} down -v fi } function close() { if [[ -n "${target}" ]]; then + if [[ "${target}" == "kotl" ]]; then + stop_kotl + return + fi ${EXE} stop "${target}" return fi + if should_manage_kotl; then + stop_kotl || return 1 + fi services=$(get_docker_compose_services ignore_db) for i in ${services}; do ${EXE} stop "${i}" @@ -129,6 +152,10 @@ function pull() { } function restart() { + if [[ "${target}" == "kotl" ]]; then + restart_kotl + return + fi stop echo -e "\n" @@ -243,10 +270,18 @@ function main() { ;; status) ${EXE} ps + if should_manage_kotl; then + status_kotl + fi ;; down) if [[ -z "${target}" ]]; then + if should_manage_kotl; then + stop_kotl || exit 1 + fi ${EXE} down -v + elif [[ "${target}" == "kotl" ]]; then + stop_kotl else ${EXE} stop "${target}" && ${EXE} rm -f "${target}" fi @@ -263,6 +298,9 @@ function main() { backup_db) bash "${SCRIPT_DIR}/5_db_backup.sh" ;; + backup_no_audit) + bash "${SCRIPT_DIR}/5_db_backup.sh" "no_audit" + ;; backup_audit) bash "${SCRIPT_DIR}/5_db_backup.sh" "audit" ;; @@ -275,11 +313,17 @@ function main() { pull_images) pull_images ;; + pull_mysql) + docker pull registry.cn-beijing.aliyuncs.com/jumpservice/mysql:8.0 + docker tag registry.cn-beijing.aliyuncs.com/jumpservice/mysql:8.0 mysql:8.0 + ;; cmd) echo "${EXE}" ;; tail) - if [[ -z "${target}" ]]; then + if [[ "${target}" == "kotl" ]]; then + tail_kotl + elif [[ -z "${target}" ]]; then ${EXE} logs --tail 100 -f else docker_name=$(service_to_docker_name "${target}") diff --git a/locale/en/LC_MESSAGES/jumpserver-installer.mo b/locale/en/LC_MESSAGES/jumpserver-installer.mo index b20270de..3e1d0773 100644 Binary files a/locale/en/LC_MESSAGES/jumpserver-installer.mo and b/locale/en/LC_MESSAGES/jumpserver-installer.mo differ diff --git a/locale/en/LC_MESSAGES/jumpserver-installer.po b/locale/en/LC_MESSAGES/jumpserver-installer.po index 55c115ad..1d89d2ca 100644 --- a/locale/en/LC_MESSAGES/jumpserver-installer.po +++ b/locale/en/LC_MESSAGES/jumpserver-installer.po @@ -187,7 +187,7 @@ msgstr "" #: scripts/1_config_jumpserver.sh:116 msgid "Do you want to use external MySQL" -msgstr "Do you want to use external MySQL (version ≥ 5.7 required)" +msgstr "Do you want to use external MySQL (version ≥ 8.0 required)" #: scripts/1_config_jumpserver.sh:128 msgid "Do you want to use external PostgreSQL" @@ -519,6 +519,10 @@ msgstr "" msgid "Please enter SSH PORT" msgstr "" +#: scripts/config.sh:80 +msgid "Please enter MAGNUS PORT" +msgstr "" + #: scripts/config.sh:80 msgid "Please enter MAGNUS MYSQL PORT" msgstr "" diff --git a/locale/zh_CN/LC_MESSAGES/jumpserver-installer.mo b/locale/zh_CN/LC_MESSAGES/jumpserver-installer.mo index 7948f16f..c9978408 100644 Binary files a/locale/zh_CN/LC_MESSAGES/jumpserver-installer.mo and b/locale/zh_CN/LC_MESSAGES/jumpserver-installer.mo differ diff --git a/locale/zh_CN/LC_MESSAGES/jumpserver-installer.po b/locale/zh_CN/LC_MESSAGES/jumpserver-installer.po index 0ee77739..1a924620 100644 --- a/locale/zh_CN/LC_MESSAGES/jumpserver-installer.po +++ b/locale/zh_CN/LC_MESSAGES/jumpserver-installer.po @@ -187,7 +187,7 @@ msgstr "配置数据库" #: scripts/1_config_jumpserver.sh:116 msgid "Do you want to use external MySQL" -msgstr "是否使用外部 MySQL (版本需 ≥ 5.7)" +msgstr "是否使用外部 MySQL (版本需 ≥ 8.0)" #: scripts/1_config_jumpserver.sh:128 msgid "Do you want to use external PostgreSQL" @@ -522,6 +522,10 @@ msgstr "请输入 HTTPS 服务端口" msgid "Please enter SSH PORT" msgstr "请输入 KOKO SSH 服务端口" +#: scripts/config.sh:80 +msgid "Please enter MAGNUS PORT" +msgstr "请输入 MAGNUS 服务端口" + #: scripts/config.sh:80 msgid "Please enter MAGNUS MYSQL PORT" msgstr "请输入 MAGNUS MYSQL 服务端口" diff --git a/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.mo b/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.mo index 8b9b4c4d..681a12b6 100644 Binary files a/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.mo and b/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.mo differ diff --git a/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.po b/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.po index d80d1444..609db388 100644 --- a/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.po +++ b/locale/zh_Hant/LC_MESSAGES/jumpserver-installer.po @@ -187,7 +187,7 @@ msgstr "配置資料庫" #: scripts/1_config_jumpserver.sh:116 msgid "Do you want to use external MySQL" -msgstr "是否使用外部 MySQL(版本需 ≥ 5.7)" +msgstr "是否使用外部 MySQL(版本需 ≥ 8.0)" #: scripts/1_config_jumpserver.sh:128 msgid "Do you want to use external PostgreSQL" @@ -523,6 +523,10 @@ msgstr "請輸入 HTTPS 服務埠" msgid "Please enter SSH PORT" msgstr "請輸入 KOKO SSH 服務埠" +#: scripts/config.sh:80 +msgid "Please enter MAGNUS PORT" +msgstr "請輸入 MAGNUS 服務埠" + #: scripts/config.sh:80 msgid "Please enter MAGNUS MYSQL PORT" msgstr "請輸入 MAGNUS MYSQL 服務埠" diff --git a/scripts/0_prepare.sh b/scripts/0_prepare.sh index 5d84dd31..11119b02 100644 --- a/scripts/0_prepare.sh +++ b/scripts/0_prepare.sh @@ -49,6 +49,17 @@ function prepare_image_files() { fi rm -f "${IMAGE_DIR}"/* + # The offline bundle must carry optional OpenBao even when it is disabled by + # default, so it can be enabled later without registry access. + local INCLUDE_OPENBAO_IMAGE=1 + export INCLUDE_OPENBAO_IMAGE + + # KOTL is an Enterprise Edition component. Include it in the offline bundle + # only when building an XPack deployment. + if is_enterprise_edition; then + local INCLUDE_KOTL_IMAGE=1 + export INCLUDE_KOTL_IMAGE + fi pull_images images=$(get_images) diff --git a/scripts/1_config_jumpserver.sh b/scripts/1_config_jumpserver.sh index 2a2d7d88..52fe9945 100644 --- a/scripts/1_config_jumpserver.sh +++ b/scripts/1_config_jumpserver.sh @@ -83,6 +83,9 @@ function set_external_db() { read_from_input db_password "$(gettext 'Please enter DB password')" "" "${db_password}" set_db_config "${db_engine}" "${db_host}" "${db_port}" "${db_user}" "${db_password}" "${db_name}" + if [[ "${db_engine}" == "postgresql" ]]; then + remove_config POSTGRESQL_EXPOSE_PORT + fi } function set_internal_db() { @@ -100,6 +103,9 @@ function set_internal_db() { fi set_db_config "${db_engine}" "${db_host}" "${db_port}" "${db_user}" "${db_password}" "${db_name}" + if [[ "${db_engine}" == "postgresql" ]]; then + set_config POSTGRESQL_EXPOSE_PORT "127.0.0.1:5432" + fi } function set_db() { @@ -213,8 +219,8 @@ function set_redis() { function set_service() { echo_yellow "\n5. $(gettext 'Configure External Access')" http_port=$(get_config HTTP_PORT) - ssh_port=$(get_config SSH_PORT) - rdp_port=$(get_config RDP_PORT) + ssh_port=$(get_config KOKO_SSH_PORT) + rdp_port=$(get_config RAZOR_RDP_PORT) use_xpack=$(get_config_or_env USE_XPACK) confirm="n" read_from_input confirm "$(gettext 'Do you need to customize the JumpServer external port')?" "y/n" "${confirm}" @@ -224,9 +230,9 @@ function set_service() { if [[ "${use_xpack}" == "1" ]]; then read_from_input ssh_port "$(gettext 'JumpServer ssh port')" "" "${ssh_port}" - set_config SSH_PORT "${ssh_port}" + set_config KOKO_SSH_PORT "${ssh_port}" read_from_input rdp_port "$(gettext 'JumpServer rdp port')" "" "${rdp_port}" - set_config RDP_PORT "${rdp_port}" + set_config RAZOR_RDP_PORT "${rdp_port}" fi fi } @@ -258,6 +264,7 @@ function main() { if set_redis; then echo_done fi + set_openbao || return 1 if set_service; then echo_done fi diff --git a/scripts/4_install_jumpserver.sh b/scripts/4_install_jumpserver.sh index c8c18f71..0d2fcbb0 100644 --- a/scripts/4_install_jumpserver.sh +++ b/scripts/4_install_jumpserver.sh @@ -49,7 +49,7 @@ function post_install() { http_port=$(get_config HTTP_PORT) https_port=$(get_config HTTPS_PORT) server_name=$(get_config SERVER_NAME) - ssh_port=$(get_config SSH_PORT) + ssh_port=$(get_config KOKO_SSH_PORT) use_xpack=$(get_config_or_env USE_XPACK) echo_yellow "1. $(gettext 'You can use the following command to start, and then visit')" @@ -99,6 +99,10 @@ function main() { if ! bash "${BASE_DIR}/1_config_jumpserver.sh"; then exit 1 fi + if ! configure_kotl; then + log_error "Failed to configure KOTL" + exit 1 + fi echo_green "\n>>> $(gettext 'Loading Docker Image')" if ! bash "${BASE_DIR}/3_load_images.sh"; then @@ -112,6 +116,14 @@ function main() { fi ensure_core_data_symlink || log_warn "Failed to prepare host core data symlink, continue installation" + ensure_current_installer_link || { + log_error "Failed to update /opt/current/installer" + exit 1 + } + install_kotl || { + log_error "Failed to install KOTL" + exit 1 + } installation_log "install" post_install diff --git a/scripts/5_db_backup.sh b/scripts/5_db_backup.sh index f6abb88b..685bcc59 100644 --- a/scripts/5_db_backup.sh +++ b/scripts/5_db_backup.sh @@ -22,18 +22,13 @@ AUDITS_TABLES=( audits_operatelog audits_passwordchangelog audits_userloginlog - terminal terminal_session terminal_command ) -FULL_IGNORE_TABLES=( - audits_activitylog - terminal_command -) - SHARED_BACKUP_TABLES=( users_user + terminal ) MODE="full" @@ -42,8 +37,11 @@ if [[ $# -gt 0 ]]; then audit) MODE="audit" ;; + no_audit) + MODE="no_audit" + ;; *) - log_error "Usage: $0 [audit]" + log_error "Usage: $0 [audit|no_audit]" exit 1 ;; esac @@ -78,85 +76,107 @@ function cleanup_db_env() { fi } -function backup_main_db() { +function backup_main_db_mysql() { local table - case "${DB_ENGINE}" in - mysql) - DB_FILE="${BACKUP_DIR}/${DB_NAME}-${CURRENT_VERSION}-$(date +%F_%T).sql" - local dump_cmd=( - mysqldump - --skip-add-locks - --skip-lock-tables - --single-transaction - -h"${DB_HOST}" - -P"${DB_PORT}" - -u"${DB_USER}" - ) - for table in "${FULL_IGNORE_TABLES[@]}"; do - dump_cmd+=("--ignore-table=${DB_NAME}.${table}") - done - dump_cmd+=("${DB_NAME}") + local backup_type="" + local excluded_tables=() + if [[ "${MODE}" == "no_audit" ]]; then + backup_type="-no_audit" + excluded_tables=("${AUDITS_TABLES[@]}") + fi - if ! docker run --rm \ - --env MYSQL_PWD="${DB_PASSWORD}" \ - -i --network=jms_net \ - "${db_images}" \ - "${dump_cmd[@]}" > "${DB_FILE}"; then - log_error "$(gettext 'Backup failed')!" - rm -f "${DB_FILE}" - exit 1 - fi + DB_FILE="${BACKUP_DIR}/${DB_NAME}${backup_type}-${CURRENT_VERSION}-$(date +%F_%T).sql" + local dump_cmd=( + mysqldump + --skip-add-locks + --single-transaction + -h"${DB_HOST}" + -P"${DB_PORT}" + -u"${DB_USER}" + ) + for table in "${excluded_tables[@]}"; do + dump_cmd+=("--ignore-table=${DB_NAME}.${table}") + done + dump_cmd+=("${DB_NAME}") - local schema_cmd=( - mysqldump - --skip-add-locks - --skip-lock-tables - --single-transaction - --no-data - -h"${DB_HOST}" - -P"${DB_PORT}" - -u"${DB_USER}" - "${DB_NAME}" - "${FULL_IGNORE_TABLES[@]}" - ) - if ! docker run --rm \ - --env MYSQL_PWD="${DB_PASSWORD}" \ - -i --network=jms_net \ - "${db_images}" \ - "${schema_cmd[@]}" >> "${DB_FILE}"; then - log_error "$(gettext 'Backup failed')!" - rm -f "${DB_FILE}" - exit 1 - fi + if ! docker run --rm \ + --env MYSQL_PWD="${DB_PASSWORD}" \ + -i --network=jms_net \ + "${db_images}" \ + "${dump_cmd[@]}" > "${DB_FILE}"; then + log_error "$(gettext 'Backup failed')!" + rm -f "${DB_FILE}" + return 1 + fi + + if [[ ${#excluded_tables[@]} -gt 0 ]]; then + local schema_cmd=( + mysqldump + --skip-lock-tables + --no-data + -h"${DB_HOST}" + -P"${DB_PORT}" + -u"${DB_USER}" + "${DB_NAME}" + "${excluded_tables[@]}" + ) + if ! docker run --rm \ + --env MYSQL_PWD="${DB_PASSWORD}" \ + -i --network=jms_net \ + "${db_images}" \ + "${schema_cmd[@]}" >> "${DB_FILE}"; then + log_error "$(gettext 'Backup failed')!" + rm -f "${DB_FILE}" + return 1 + fi + fi +} + +function backup_main_db_postgresql() { + local table + local backup_type="" + local excluded_tables=() + if [[ "${MODE}" == "no_audit" ]]; then + backup_type="-no_audit" + excluded_tables=("${AUDITS_TABLES[@]}") + fi + + DB_FILE="${BACKUP_DIR}/${DB_NAME}${backup_type}-${CURRENT_VERSION}-$(date +%F_%T).dump" + local dump_cmd=( + pg_dump + --format=custom + --no-owner + -U "${DB_USER}" + -h "${DB_HOST}" + -p "${DB_PORT}" + -d "${DB_NAME}" + ) + for table in "${excluded_tables[@]}"; do + dump_cmd+=("--exclude-table-data=${table}") + done + + if ! docker run --rm \ + --env PGPASSWORD="${DB_PASSWORD}" \ + -i --network=jms_net \ + "${db_images}" \ + "${dump_cmd[@]}" > "${DB_FILE}"; then + log_error "$(gettext 'Backup failed')!" + rm -f "${DB_FILE}" + return 1 + fi +} + +function backup_main_db() { + case "${DB_ENGINE}" in + mysql) + backup_main_db_mysql || return 1 ;; postgresql) - DB_FILE="${BACKUP_DIR}/${DB_NAME}-${CURRENT_VERSION}-$(date +%F_%T).dump" - local dump_cmd=( - pg_dump - --format=custom - --no-owner - -U "${DB_USER}" - -h "${DB_HOST}" - -p "${DB_PORT}" - -d "${DB_NAME}" - ) - for table in "${FULL_IGNORE_TABLES[@]}"; do - dump_cmd+=("--exclude-table-data=${table}") - done - - if ! docker run --rm \ - --env PGPASSWORD="${DB_PASSWORD}" \ - -i --network=jms_net \ - "${db_images}" \ - "${dump_cmd[@]}" > "${DB_FILE}"; then - log_error "$(gettext 'Backup failed')!" - rm -f "${DB_FILE}" - exit 1 - fi + backup_main_db_postgresql || return 1 ;; *) log_error "$(gettext 'Invalid DB Engine selection')!" - exit 1 + return 1 ;; esac @@ -173,25 +193,31 @@ function backup_main_db() { function backup_audits_mysql() { local backup_file=$1 + local dump_cmd=( + mysqldump + -h"${DB_HOST}" + -P"${DB_PORT}" + -u"${DB_USER}" + --single-transaction + --no-create-info + --skip-triggers + --insert-ignore + ) + + if [[ "${db_images}" != *mariadb* ]]; then + dump_cmd+=(--set-gtid-purged=OFF) + fi + dump_cmd+=( + "${DB_NAME}" + "${AUDITS_TABLES[@]}" + "${SHARED_BACKUP_TABLES[@]}" + ) docker run --rm \ -e MYSQL_PWD="${DB_PASSWORD}" \ -i --network=jms_net \ "${db_images}" \ - mysqldump \ - -h"${DB_HOST}" \ - -P"${DB_PORT}" \ - -u"${DB_USER}" \ - --single-transaction \ - --quick \ - --set-gtid-purged=OFF \ - --no-create-info \ - --skip-triggers \ - --insert-ignore \ - --default-character-set=utf8mb4 \ - "${DB_NAME}" \ - "${AUDITS_TABLES[@]}" \ - "${SHARED_BACKUP_TABLES[@]}" | gzip > "${backup_file}" + "${dump_cmd[@]}" | gzip > "${backup_file}" } function backup_audits_postgresql() { @@ -218,17 +244,17 @@ function backup_audits_postgresql() { -d "${DB_NAME}" \ --data-only \ --inserts \ - --no-owner \ - --no-privileges \ - "${table_args[@]}" | sed '/^INSERT INTO / s/;[[:space:]]*$/ ON CONFLICT DO NOTHING;/' > "${sql_file}" || return 1 + --on-conflict-do-nothing \ + "${table_args[@]}" > "${sql_file}" || return 1 gzip -f "${sql_file}" || return 1 } function backup_audits() { + AUDIT_FILE="${BACKUP_DIR}/${DB_NAME}-audit-${CURRENT_VERSION}-$(date +%F_%H%M%S).sql.gz" + case "${DB_ENGINE}" in mysql) - AUDIT_FILE="${BACKUP_DIR}/audits_${CURRENT_VERSION}_$(date +%F_%H%M%S).sql.gz" if ! backup_audits_mysql "${AUDIT_FILE}"; then rm -f "${AUDIT_FILE}" log_error "$(gettext 'Backup failed')!" @@ -236,7 +262,6 @@ function backup_audits() { fi ;; postgresql) - AUDIT_FILE="${BACKUP_DIR}/audits_${CURRENT_VERSION}_$(date +%F_%H%M%S).sql.gz" if ! backup_audits_postgresql "${AUDIT_FILE}"; then rm -f "${AUDIT_FILE}" rm -f "${AUDIT_FILE%.gz}" @@ -267,7 +292,7 @@ function main() { prepare_db_env case "${MODE}" in - full) + full|no_audit) if ! backup_main_db; then cleanup_db_env exit 1 diff --git a/scripts/6_db_restore.sh b/scripts/6_db_restore.sh index 1c9a69fc..766e2178 100644 --- a/scripts/6_db_restore.sh +++ b/scripts/6_db_restore.sh @@ -14,59 +14,45 @@ DB_USER=$(get_config DB_USER) DB_PASSWORD=$(get_config DB_PASSWORD) DB_NAME=$(get_config DB_NAME) -function main() { - echo_warn "$(gettext 'Make sure you have a backup of data, this operation is not reversible')! \n" - - if [[ ! -f "${DB_FILE}" ]]; then - echo "$(gettext 'file does not exist'): ${DB_FILE}" - exit 1 - fi - - db_images=$(get_db_images) - - echo "$(gettext 'Start restoring database'): $DB_FILE" - - if ! docker ps | grep -w "jms_core" &>/dev/null; then - create_db_ops_env - flag=1 - fi - case "${DB_HOST}" in - mysql|postgresql) - while [[ "$(docker inspect -f "{{.State.Health.Status}}" jms_${DB_HOST})" != "healthy" ]]; do - sleep 5s - done - ;; - esac - - case "${DB_ENGINE}" in - mysql) - restore_cmd=' +function restore_mysql() { + local restore_cmd=' if [[ "${DB_FILE}" == *.gz ]]; then gzip -dc "${DB_FILE}" | mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" else mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" < "${DB_FILE}" fi ' - ;; - postgresql) - restore_file="${DB_FILE}" - tmp_restore_file="" - if [[ "${DB_FILE}" == *.gz ]]; then - tmp_restore_file=$(mktemp "${BACKUP_DIR}/.pg_restore.XXXXXX") - if ! gzip -dc "${DB_FILE}" > "${tmp_restore_file}"; then - log_error "$(gettext 'Failed to decompress backup file')!" - rm -f "${tmp_restore_file}" - exit 1 - fi - restore_file="${tmp_restore_file}" - fi + local docker_env=( + --env "DB_HOST=${DB_HOST}" --env "DB_PORT=${DB_PORT}" --env "DB_USER=${DB_USER}" + --env "DB_PASSWORD=${DB_PASSWORD}" --env "DB_NAME=${DB_NAME}" --env "DB_FILE=${DB_FILE}" + ) + + docker run --rm "${docker_env[@]}" \ + -i --network=jms_net \ + -v "${BACKUP_DIR}:${BACKUP_DIR}" \ + "${db_images}" bash -c "${restore_cmd}" +} - pg_magic=$(dd if="${restore_file}" bs=1 count=5 2>/dev/null) - if [[ "${pg_magic}" == "PGDMP" ]]; then - echo "$(gettext 'Resetting database schema before restore')..." - fi +function restore_postgresql() { + local restore_file="${DB_FILE}" + local tmp_restore_file="" + if [[ "${DB_FILE}" == *.gz ]]; then + tmp_restore_file=$(mktemp "${BACKUP_DIR}/.pg_restore.XXXXXX") + if ! gzip -dc "${DB_FILE}" > "${tmp_restore_file}"; then + log_error "$(gettext 'Failed to decompress backup file')!" + rm -f "${tmp_restore_file}" + return 1 + fi + restore_file="${tmp_restore_file}" + fi - restore_cmd=' + local pg_magic + pg_magic=$(dd if="${restore_file}" bs=1 count=5 2>/dev/null) + if [[ "${pg_magic}" == "PGDMP" ]]; then + echo "$(gettext 'Resetting database schema before restore')..." + fi + + local restore_cmd=' reset_pg_public_schema() { PGPASSWORD="${DB_PASSWORD}" psql -v ON_ERROR_STOP=1 -U "${DB_USER}" -h "${DB_HOST}" -p "${DB_PORT}" -d "${DB_NAME}" \ -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid();" \ @@ -84,30 +70,69 @@ function main() { PGPASSWORD="${DB_PASSWORD}" psql -q -v ON_ERROR_STOP=1 -U "${DB_USER}" -h "${DB_HOST}" -p "${DB_PORT}" -d "${DB_NAME}" < "${RESTORE_FILE}" >/dev/null fi ' + local docker_env=( + --env "DB_HOST=${DB_HOST}" --env "DB_PORT=${DB_PORT}" --env "DB_USER=${DB_USER}" + --env "DB_PASSWORD=${DB_PASSWORD}" --env "DB_NAME=${DB_NAME}" + --env "RESTORE_FILE=${restore_file}" + ) + + docker run --rm "${docker_env[@]}" \ + -i --network=jms_net \ + -v "${BACKUP_DIR}:${BACKUP_DIR}" \ + "${db_images}" bash -c "${restore_cmd}" + local restore_status=$? + + [[ -n "${tmp_restore_file}" ]] && rm -f "${tmp_restore_file}" + return ${restore_status} +} + +function restore_database() { + case "${DB_ENGINE}" in + mysql) + restore_mysql + ;; + postgresql) + restore_postgresql ;; *) log_error "$(gettext 'Invalid DB Engine selection')!" - exit 1 + return 1 ;; esac +} - docker_env=( - --env "DB_HOST=${DB_HOST}" --env "DB_PORT=${DB_PORT}" --env "DB_USER=${DB_USER}" - --env "DB_PASSWORD=${DB_PASSWORD}" --env "DB_NAME=${DB_NAME}" --env "DB_FILE=${DB_FILE}" - ) - if [[ "${DB_ENGINE}" == "postgresql" ]]; then - docker_env+=(--env "RESTORE_FILE=${restore_file}") +function main() { + echo_warn "$(gettext 'Make sure you have a backup of data, this operation is not reversible')! \n" + + if [[ ! -f "${DB_FILE}" ]]; then + echo "$(gettext 'file does not exist'): ${DB_FILE}" + return 1 fi - if ! docker run --rm "${docker_env[@]}" \ - -i --network=jms_net \ - -v "${BACKUP_DIR}:${BACKUP_DIR}" \ - "${db_images}" bash -c "${restore_cmd}"; then - [[ -n "${tmp_restore_file}" ]] && rm -f "${tmp_restore_file}" + db_images=$(get_db_images) + + echo "$(gettext 'Start restoring database'): $DB_FILE" + + if ! docker ps | grep -w "jms_core" &>/dev/null; then + create_db_ops_env + flag=1 + fi + case "${DB_HOST}" in + mysql|postgresql) + while [[ "$(docker inspect -f "{{.State.Health.Status}}" jms_${DB_HOST})" != "healthy" ]]; do + sleep 5s + done + ;; + esac + + if ! restore_database; then log_error "$(gettext 'Database recovery failed. Please check whether the database file is complete or try to recover manually')!" - exit 1 + if [[ -n "$flag" ]]; then + down_db_ops_env + unset flag + fi + return 1 else - [[ -n "${tmp_restore_file}" ]] && rm -f "${tmp_restore_file}" log_success "$(gettext 'Database recovered successfully')!" run_post_restore fi @@ -148,5 +173,7 @@ if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then fi stop_jms_core main + restore_status=$? start_jms_core -fi \ No newline at end of file + exit ${restore_status} +fi diff --git a/scripts/7_upgrade.sh b/scripts/7_upgrade.sh index fe1a14d8..c33061c0 100644 --- a/scripts/7_upgrade.sh +++ b/scripts/7_upgrade.sh @@ -37,6 +37,31 @@ function check_and_set_config() { fi } +function migrate_compat_config() { + local new_key=$1 + local old_key=$2 + local default_value=$3 + local new_value old_value + + new_value=$(get_config "${new_key}") + if [[ -n "${new_value}" ]]; then + remove_config "${old_key}" + return + fi + + old_value=$(get_config "${old_key}") + if [[ -n "${old_value}" ]]; then + set_config "${new_key}" "${old_value}" + remove_config "${old_key}" + return + fi + + if [[ -n "${default_value}" ]]; then + set_config "${new_key}" "${default_value}" + fi + +} + function upgrade_config() { if check_root; then check_docker_start @@ -58,8 +83,8 @@ function upgrade_config() { if docker image inspect -f '{{.Id}}' jumpserver/mariadb:10.6 &>/dev/null; then docker tag jumpserver/mariadb:10.6 mariadb:10.6 fi - if docker image inspect -f '{{.Id}}' jumpserver/mysql:5.7 &>/dev/null; then - docker tag jumpserver/mysql:5.7 mysql:5.7-debian + if docker image inspect -f '{{.Id}}' jumpserver/mysql:8.0 &>/dev/null; then + docker tag jumpserver/mysql:8.0 mysql:8.0 fi check_and_set_config "CURRENT_VERSION" "${VERSION}" check_and_set_config "CLIENT_MAX_BODY_SIZE" "4096m" @@ -67,17 +92,14 @@ function upgrade_config() { check_and_set_config "JUMPSERVER_ENABLE_FONT_SMOOTHING" "true" check_and_set_config "USE_LB" "1" check_and_set_config "VERIFY_EXTERNAL_SSL" "false" + if [[ "$(get_config DB_HOST)" == "postgresql" ]]; then + check_and_set_config "POSTGRESQL_EXPOSE_PORT" "127.0.0.1:5432" + fi # XPACK use_xpack=$(get_config_or_env USE_XPACK) if [[ "${use_xpack}" == "1" ]]; then - check_and_set_config "RDP_PORT" "3389" check_and_set_config "XRDP_PORT" "3390" - check_and_set_config "MAGNUS_MYSQL_PORT" "33061" - check_and_set_config "MAGNUS_MARIADB_PORT" "33062" - check_and_set_config "MAGNUS_REDIS_PORT" "63790" - check_and_set_config "MAGNUS_POSTGRESQL_PORT" "54320" - check_and_set_config "MAGNUS_SQLSERVER_PORT" "14330" - check_and_set_config "MAGNUS_ORACLE_PORT" "15210" + check_and_set_config "MAGNUS_PORT" "5525" fi } @@ -156,7 +178,9 @@ function migrate_data_folder() { } function migrate_config() { - prepare_config + prepare_jmsctl + migrate_compat_config "KOKO_SSH_PORT" "SSH_PORT" "2222" + migrate_compat_config "RAZOR_RDP_PORT" "RDP_PORT" "3389" } function update_config_if_need() { @@ -165,6 +189,8 @@ function update_config_if_need() { migrate_coco_to_koko migrate_config upgrade_config + set_openbao || exit 1 + configure_kotl || exit 1 clean_file } @@ -288,6 +314,8 @@ function upgrade_compose() { } function main() { + cd "${PROJECT_DIR}" || exit 1 + confirm="y" to_version="${VERSION}" if [[ -n "${target}" ]]; then @@ -344,6 +372,14 @@ function main() { upgrade_docker upgrade_compose ensure_core_data_symlink || log_warn "Failed to prepare host core data symlink, continue upgrade" + ensure_current_installer_link || { + log_error "Failed to update /opt/current/installer" + exit 1 + } + upgrade_kotl || { + log_error "Failed to upgrade KOTL" + exit 1 + } installation_log "upgrade" diff --git a/scripts/8_uninstall.sh b/scripts/8_uninstall.sh index 6c431db1..6b0c9161 100644 --- a/scripts/8_uninstall.sh +++ b/scripts/8_uninstall.sh @@ -98,6 +98,7 @@ function remove_jumpserver() { function main() { echo_yellow "\n>>> $(gettext 'Uninstall JumpServer')" stop_services + disable_kotl || log_warn "Failed to disable KOTL" installation_log "uninstall" remove_jmsctl remove_jumpserver @@ -105,4 +106,4 @@ function main() { remove_docker } -main \ No newline at end of file +main diff --git a/scripts/config.sh b/scripts/config.sh index 43eaf948..2773fbd5 100644 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -71,26 +71,14 @@ function set_port() { fi if [[ "${use_xpack}" == "1" ]]; then if [[ "${koko_enable}" != "0" ]]; then - ssh_port=$(get_config SSH_PORT) + ssh_port=$(get_config KOKO_SSH_PORT) read_from_input ssh_port "$(gettext 'Please enter SSH PORT')" "" "${ssh_port}" - set_config SSH_PORT "${ssh_port}" + set_config KOKO_SSH_PORT "${ssh_port}" fi if [[ "${magnus_enable}" != "0" ]]; then - magnus_mysql_port=$(get_config MAGNUS_MYSQL_PORT) - read_from_input magnus_mysql_port "$(gettext 'Please enter MAGNUS MYSQL PORT')" "" "${magnus_mysql_port}" - set_config MAGNUS_MYSQL_PORT "${magnus_mysql_port}" - magnus_mariadb_port=$(get_config MAGNUS_MARIADB_PORT) - read_from_input magnus_mariadb_port "$(gettext 'Please enter MAGNUS MARIADB PORT')" "" "${magnus_mariadb_port}" - set_config MAGNUS_MARIADB_PORT "${magnus_mariadb_port}" - magnus_redis_port=$(get_config MAGNUS_REDIS_PORT) - read_from_input magnus_redis_port "$(gettext 'Please enter MAGNUS REDIS PORT')" "" "${magnus_redis_port}" - set_config MAGNUS_REDIS_PORT "${magnus_redis_port}" - magnus_postgresql_port=$(get_config MAGNUS_POSTGRESQL_PORT) - read_from_input magnus_postgresql_port "$(gettext 'Please enter MAGNUS POSTGRESQL PORT')" "" "${magnus_postgresql_port}" - set_config MAGNUS_POSTGRESQL_PORT "${magnus_postgresql_port}" - magnus_sqlserver_port=$(get_config MAGNUS_SQLSERVER_PORT) - read_from_input magnus_sqlserver_port "$(gettext 'Please enter MAGNUS SQLSERVER PORT')" "" "${magnus_sqlserver_port}" - set_config MAGNUS_SQLSERVER_PORT "${magnus_sqlserver_port}" + magnus_port=$(get_config MAGNUS_PORT) + read_from_input magnus_port "$(gettext 'Please enter MAGNUS PORT')" "" "${magnus_port}" + set_config MAGNUS_PORT "${magnus_port}" fi if [[ "${xrdp_enable}" != "0" ]]; then xrdp_port=$(get_config XRDP_PORT) @@ -98,14 +86,9 @@ function set_port() { set_config XRDP_PORT "${xrdp_port}" fi if [[ "${razor_enable}" != "0" ]]; then - rdp_port=$(get_config RDP_PORT) + rdp_port=$(get_config RAZOR_RDP_PORT) read_from_input rdp_port "$(gettext 'Please enter RAZOR PORT')" "" "${rdp_port}" - set_config RDP_PORT "${rdp_port}" - fi - if [[ "${magnus_enable}" != "0" ]]; then - magnus_oracle_port=$(get_config MAGNUS_ORACLE_PORT) - read_from_input magnus_oracle_port "$(gettext 'Please enter MAGNUS ORACLE PORT')" "" "${magnus_oracle_port}" - set_config MAGNUS_ORACLE_PORT "${magnus_oracle_port}" + set_config RAZOR_RDP_PORT "${rdp_port}" fi if [[ "${nec_enable}" != "0" ]]; then nec_vnc_port=$(get_config NEC_VNC_PORT) diff --git a/scripts/const.sh b/scripts/const.sh index e0b2a26e..02f32b59 100644 --- a/scripts/const.sh +++ b/scripts/const.sh @@ -27,7 +27,7 @@ STATIC_ENV=${PROJECT_DIR}/static.env . "${STATIC_ENV}" export OS=$(uname -s) -export DOCKER_VERSION=29.6.1 +export DOCKER_VERSION=29.7.2 export DOCKER_COMPOSE_VERSION=v2.40.3 ARCH=$(uname -m) diff --git a/scripts/gists/common.sh b/scripts/gists/common.sh index f41759c2..85bb2342 100644 --- a/scripts/gists/common.sh +++ b/scripts/gists/common.sh @@ -179,3 +179,28 @@ function get_host_ip() { echo "${default_ip}" fi } + +function ensure_current_installer_link() { + local source_dir="${PROJECT_DIR}" + local current_dir="/opt/current" + local target="${current_dir}/installer" + + if [[ "${source_dir}" == "${target}" ]]; then + return 0 + fi + + mkdir -p "${current_dir}" || return 1 + echo "${source_dir}" > /var/run/installer.lock || return 1 + + if [[ -L "${target}" ]]; then + if [[ "$(readlink -f "${target}")" == "${source_dir}" ]]; then + return 0 + fi + rm -f "${target}" || return 1 + elif [[ -e "${target}" ]]; then + log_error "${target} exists and is not a symbolic link" + return 1 + fi + + ln -s "${source_dir}" "${target}" +} diff --git a/scripts/gists/conf.sh b/scripts/gists/conf.sh index 2f7c8b92..2c5a99b2 100644 --- a/scripts/gists/conf.sh +++ b/scripts/gists/conf.sh @@ -1,15 +1,4 @@ - -function is_confirm() { - read -r confirmed - if [[ "${confirmed}" == "y" || "${confirmed}" == "Y" || ${confirmed} == "" ]]; then - return 0 - else - return 1 - fi -} - - function has_config() { key=$1 if grep "^[ \t]*${key}=" "${CONFIG_FILE}" &>/dev/null; then @@ -33,19 +22,18 @@ function get_config() { echo "${value}" } -function get_env_value() { - key=$1 - default=${2-''} - value="${!key}" - echo "${value}" -} - function get_config_or_env() { key=$1 value='' default=${2-''} - value=$(get_env_value "$key") + # Bash supports ${!key}, but zsh reports "bad substitution" when this + # helper is called from an interactive shell. The installer only passes + # configuration variable names here, so use a validated, portable indirect + # expansion instead. + if [[ "${key}" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + eval "value=\${${key}:-}" + fi if [[ -z "$value" && -f "${CONFIG_FILE}" ]];then value=$(get_config "$key") fi @@ -56,7 +44,7 @@ function get_config_or_env() { echo "${value}" } -CONFIG_SAFE_EXCLUDES="DB_HOST DB_PORT DB_PASSWORD REDIS_PASSWORD" +CONFIG_SAFE_EXCLUDES="DB_HOST DB_PORT DB_PASSWORD REDIS_PASSWORD VAULT_OPENBAO_TOKEN" function is_config_excluded() { local key=$1 @@ -74,32 +62,19 @@ function gen_safe_config() { local base_config_file=${CONFIG_FILE} local output_file=${CONFIG_SAFE_FILE} local tmp_file="${output_file}.tmp.$$" - local line key value + local excluded mkdir -p "${CONFIG_DIR}" - : >"${tmp_file}" - if [[ -f "${base_config_file}" ]]; then - while IFS= read -r line || [[ -n "${line}" ]]; do - [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue - [[ "${line}" != *"="* ]] && continue - - key="${line%%=*}" - key="${key#"${key%%[![:space:]]*}"}" - key="${key%"${key##*[![:space:]]}"}" - is_config_excluded "${key}" && continue - - value="${line#*=}" - value="${value#"${value%%[![:space:]]*}"}" - echo "${key}=${value}" >>"${tmp_file}" - done <"${base_config_file}" - fi - - if [[ -s "${tmp_file}" ]]; then - sort -o "${tmp_file}" "${tmp_file}" + cp "${base_config_file}" "${tmp_file}" + for excluded in ${CONFIG_SAFE_EXCLUDES}; do + sed_in_place "/^[[:space:]]*${excluded}=/d" "${tmp_file}" + done + else + : >"${tmp_file}" fi - if [[ -f "${output_file}" ]] && cmp -s "${tmp_file}" <(sort "${output_file}"); then + if [[ -f "${output_file}" ]] && cmp -s "${tmp_file}" "${output_file}"; then rm -f "${tmp_file}" echo "${output_file}" return @@ -126,7 +101,6 @@ function set_config() { has=$(has_config "${key}") if [[ ${has} == "0" ]]; then echo "${key}=${value}" >>"${CONFIG_FILE}" - gen_safe_config >/dev/null return fi @@ -136,7 +110,15 @@ function set_config() { fi sed_in_place "s,^[ \t]*${key}=.*$,${key}=${value},g" "${CONFIG_FILE}" - gen_safe_config >/dev/null +} + +function remove_config() { + key=$1 + + has=$(has_config "${key}") + if [[ ${has} == "1" ]]; then + sed_in_place "/^[ \t]*${key}=.*$/d" "${CONFIG_FILE}" + fi } function disable_config() { @@ -145,7 +127,6 @@ function disable_config() { has=$(has_config "${key}") if [[ ${has} == "1" ]]; then sed_in_place "s,^[ \t]*${key}=.*$,# ${key}=,g" "${CONFIG_FILE}" - gen_safe_config >/dev/null fi } @@ -161,14 +142,19 @@ function get_config_enabled() { } -function prepare_config() { - cd "${PROJECT_DIR}" || exit 1 +function prepare_jmsctl() { if check_root; then echo -e "#!/usr/bin/env bash\n#" > /usr/bin/jmsctl echo -e "cd ${PROJECT_DIR}" >> /usr/bin/jmsctl echo -e './jmsctl.sh $@' >> /usr/bin/jmsctl chmod 755 /usr/bin/jmsctl fi +} + + +function prepare_config() { + cd "${PROJECT_DIR}" || exit 1 + prepare_jmsctl echo_yellow "1. $(gettext 'Check Configuration File')" echo "$(gettext 'Path to Configuration file'): ${CONFIG_DIR}" @@ -204,31 +190,41 @@ function prepare_config() { done nginx_cert_dir="${CONFIG_DIR}/nginx/cert" - if [[ ! -d ${nginx_cert_dir} ]]; then - mkdir -p "${nginx_cert_dir}" - \cp -rf "${PROJECT_DIR}/config_init/nginx/cert" "${CONFIG_DIR}/nginx" - fi - - # shellcheck disable=SC2045 - for f in $(ls "${PROJECT_DIR}/config_init/nginx/cert"); do - if [[ -f "${PROJECT_DIR}/config_init/nginx/cert/${f}" ]]; then - if [[ ! -f "${nginx_cert_dir}/${f}" ]]; then - \cp -f "${PROJECT_DIR}/config_init/nginx/cert/${f}" "${nginx_cert_dir}" - else - echo_check "${nginx_cert_dir}/${f} " - fi + nginx_cert_file="${nginx_cert_dir}/server.crt" + nginx_key_file="${nginx_cert_dir}/server.key" + mkdir -p "${nginx_cert_dir}" + if [[ ! -f "${nginx_cert_file}" && ! -f "${nginx_key_file}" ]]; then + if ! command -v openssl >/dev/null 2>&1; then + log_error "$(gettext 'OpenSSL is required to generate the initial Nginx certificate')" + exit 1 fi - done + if ! openssl_output=$(openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 3650 \ + -keyout "${nginx_key_file}" \ + -out "${nginx_cert_file}" \ + -subj "/CN=localhost" 2>&1); then + log_error "${openssl_output}" + exit 1 + fi + elif [[ ! -f "${nginx_cert_file}" || ! -f "${nginx_key_file}" ]]; then + log_error "$(gettext 'Nginx certificate and private key must both exist')" + exit 1 + else + echo_check "${nginx_cert_file}" + echo_check "${nginx_key_file}" + fi chmod 700 "${CONFIG_DIR}/../" find "${CONFIG_DIR}" -type d -exec chmod 700 {} \; find "${CONFIG_DIR}" -type f -exec chmod 600 {} \; chmod 644 "${CONFIG_DIR}/redis/redis.conf" + if [[ -f "${CONFIG_DIR}/openbao/server.hcl" ]]; then + chmod 644 "${CONFIG_DIR}/openbao/server.hcl" + fi if [[ "$(uname -m)" == "aarch64" ]]; then sed_in_place "s/# ignore-warnings ARM64-COW-BUG/ignore-warnings ARM64-COW-BUG/g" "${CONFIG_DIR}/redis/redis.conf" fi - gen_safe_config + gen_safe_config >/dev/null } function ensure_core_data_symlink() { diff --git a/scripts/gists/image.sh b/scripts/gists/image.sh index 66f90e70..7167d6a1 100644 --- a/scripts/gists/image.sh +++ b/scripts/gists/image.sh @@ -8,7 +8,7 @@ function get_db_images() { function get_pull_images() { use_xpack=$(get_config_or_env USE_XPACK) - images=("redis:7.4.6-bookworm") + images=("redis:7.4.10-bookworm") images+=("$(get_db_images)") enabled_services=$(get_enabled_services) @@ -25,12 +25,18 @@ function get_pull_images() { if [[ "${use_xpack}" == "1" ]]; then images+=("jumpserver/ansible-executor:latest") fi + if should_include_openbao_image; then + images+=("$(get_openbao_image)") + fi + if should_include_kotl_image; then + images+=("jumpserver/kotl:${VERSION}") + fi echo "${images[@]}" } function get_images() { use_xpack=$(get_config_or_env USE_XPACK) - images=("redis:7.4.6-bookworm") + images=("redis:7.4.10-bookworm") images+=("$(get_db_images)") enabled_services=$(get_enabled_services) @@ -47,6 +53,12 @@ function get_images() { if [[ "${use_xpack}" == "1" ]]; then images+=("${namespace}/ansible-executor:latest") fi + if should_include_openbao_image; then + images+=("$(get_openbao_image)") + fi + if should_include_kotl_image; then + images+=("$(get_kotl_image)") + fi echo "${images[@]}" } @@ -59,6 +71,26 @@ function image_has_prefix() { fi } +function image_uses_mirror_prefix() { + image=$1 + + # Infrastructure images are pulled directly from Docker Hub. Only + # JumpServer application images and the remaining third-party images use + # the configured internal mirror. + case "${image}" in + redis|redis:*|redis@*|postgres|postgres:*|postgres@*|openbao|openbao:*|openbao@*|openbao/openbao|openbao/openbao:*|openbao/openbao@*) + echo "0" + return + ;; + esac + + if [[ "${image}" != */* || $(image_has_prefix "${image}") == "1" ]]; then + echo "1" + else + echo "0" + fi +} + function check_image_exists() { image=$1 if docker image inspect -f '{{ .Id }}' "$image" &>/dev/null; then @@ -86,7 +118,9 @@ function get_image_full_path() { full_image_path="${image}" if [[ -n "${DOCKER_IMAGE_PREFIX}" ]]; then - if echo "${DOCKER_IMAGE_PREFIX}" | grep -q "/";then + if [[ $(image_uses_mirror_prefix "${image}") != "1" ]]; then + full_image_path="${image}" + elif echo "${DOCKER_IMAGE_PREFIX}" | grep -q "/";then app=$(echo "$image" | awk -F'/' '{ print $NF }') full_image_path="${DOCKER_IMAGE_PREFIX}/${app}" elif [[ $(image_has_prefix "${image}") != "1" ]]; then @@ -151,4 +185,3 @@ function pull_images() { trap - SIGINT SIGTERM } - diff --git a/scripts/gists/kotl.sh b/scripts/gists/kotl.sh new file mode 100644 index 00000000..67ee85da --- /dev/null +++ b/scripts/gists/kotl.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +KOTL_SERVICE_NAME=${KOTL_SERVICE_NAME:-kotl.service} +KOTL_CORE_SOCKET_PATH=${KOTL_CORE_SOCKET_PATH:-/opt/jumpserver/data/unshare/kotl.sock} + +function is_enterprise_edition() { + [[ "$(get_config_or_env USE_XPACK 0)" == "1" ]] +} + +function is_kotl_enabled() { + is_enterprise_edition && [[ "$(get_config_or_env KOTL_ENABLED 1)" == "1" ]] +} + +function should_include_kotl_image() { + is_enterprise_edition || return 1 + + case "${INCLUDE_KOTL_IMAGE:-}" in + 1|true|True|TRUE) return 0 ;; + esac + is_kotl_enabled +} + +function get_kotl_image() { + echo "${NAMESPACE:-jumpserver}/kotl:${VERSION}" +} + +function check_kotl_volume_dir() { + local volume_dir + + volume_dir=$(get_config_or_env VOLUME_DIR /data/jumpserver) + volume_dir=${volume_dir%/} + if [[ "${volume_dir}" != "/data/jumpserver" ]]; then + log_error "KOTL currently requires VOLUME_DIR=/data/jumpserver (got: ${volume_dir})" + return 1 + fi +} + +function configure_kotl() { + is_kotl_enabled || return 0 + check_kotl_volume_dir || return 1 + set_config KOTL_ENABLED 1 + set_config JDMC_ENABLED 1 + set_config JDMC_SOCK_PATH "${KOTL_CORE_SOCKET_PATH}" + gen_safe_config >/dev/null +} + +function check_kotl_runtime() { + if ! command -v systemctl &>/dev/null; then + log_error "KOTL requires systemd, but systemctl was not found" + return 1 + fi +} + +function check_kotl_installed() { + [[ -x /opt/kotl/kotl && -f "/etc/systemd/system/${KOTL_SERVICE_NAME}" ]] +} + +function run_kotl_package_action() { + local action=$1 + local image + + image=$(get_kotl_image) + if ! docker image inspect "${image}" &>/dev/null; then + log_error "KOTL artifact image not found: ${image}" + return 1 + fi + + ( + local temp_dir container_id script_path + + if ! temp_dir=$(mktemp -d -t kotl-installer.XXXXXX); then + log_error "Failed to create a temporary directory for KOTL" + exit 1 + fi + container_id="" + function cleanup_kotl_package() { + if [[ -n "${container_id}" ]]; then + docker rm -f "${container_id}" &>/dev/null || true + fi + if [[ -n "${temp_dir}" && -d "${temp_dir}" ]]; then + rm -rf "${temp_dir}" + fi + } + trap cleanup_kotl_package EXIT + + if ! container_id=$(docker create "${image}" /__kotl_artifact_placeholder__); then + log_error "Failed to create a temporary container from ${image}" + exit 1 + fi + if ! docker cp "${container_id}:/dist/." "${temp_dir}/"; then + log_error "Failed to extract /dist from ${image}" + exit 1 + fi + + script_path="${temp_dir}/scripts/${action}.sh" + if [[ ! -f "${script_path}" ]]; then + log_error "KOTL package script not found: scripts/${action}.sh" + exit 1 + fi + + chmod +x "${script_path}" || exit 1 + cd "${temp_dir}" || exit 1 + bash "./scripts/${action}.sh" + ) +} + +function install_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + configure_kotl || return 1 + + if check_kotl_installed; then + echo_check "KOTL is already installed" + return 0 + fi + + echo_yellow "\n>>> Installing KOTL" + run_kotl_package_action install +} + +function upgrade_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + configure_kotl || return 1 + + echo_yellow "\n>>> Upgrading KOTL" + if check_kotl_installed; then + run_kotl_package_action upgrade + else + run_kotl_package_action install + fi +} + +function start_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + if ! check_kotl_installed; then + log_error "KOTL is enabled but not installed; run ./jmsctl.sh install first" + return 1 + fi + systemctl start "${KOTL_SERVICE_NAME}" +} + +function stop_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + systemctl stop "${KOTL_SERVICE_NAME}" +} + +function restart_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + systemctl restart "${KOTL_SERVICE_NAME}" +} + +function status_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + systemctl status "${KOTL_SERVICE_NAME}" --no-pager || true +} + +function tail_kotl() { + is_kotl_enabled || return 0 + check_kotl_runtime || return 1 + journalctl -u "${KOTL_SERVICE_NAME}" -n 100 -f -o cat +} + +function disable_kotl() { + check_kotl_installed || return 0 + check_kotl_runtime || return 1 + systemctl stop "${KOTL_SERVICE_NAME}" || true + systemctl disable "${KOTL_SERVICE_NAME}" +} diff --git a/scripts/gists/openbao.sh b/scripts/gists/openbao.sh new file mode 100644 index 00000000..63616aaa --- /dev/null +++ b/scripts/gists/openbao.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash + +function is_internal_openbao_enabled() { + local vault_enabled vault_backend openbao_external + + vault_enabled=$(get_config_or_env VAULT_ENABLED) + vault_backend=$(get_config_or_env VAULT_BACKEND) + openbao_external=$(get_config_or_env OPENBAO_EXTERNAL "false") + + case "${vault_enabled}" in + 1|true|True|TRUE) ;; + *) return 1 ;; + esac + [[ "${vault_backend}" == "openbao" ]] || return 1 + case "${openbao_external}" in + 1|true|True|TRUE) return 1 ;; + esac + return 0 +} + +function get_openbao_image() { + get_config_or_env OPENBAO_IMAGE "openbao:2.6.0" +} + +function should_include_openbao_image() { + case "${INCLUDE_OPENBAO_IMAGE:-}" in + 1|true|True|TRUE) return 0 ;; + esac + is_internal_openbao_enabled +} + +function set_openbao() { + local vault_enabled vault_backend openbao_external vault_addr vault_token + + vault_enabled=$(get_config VAULT_ENABLED "false") + vault_backend=$(get_config VAULT_BACKEND "openbao") + openbao_external=$(get_config OPENBAO_EXTERNAL "false") + + set_config VAULT_ENABLED "${vault_enabled}" + if [[ "${vault_enabled}" != "1" && "${vault_enabled}" != "true" && "${vault_enabled}" != "True" && "${vault_enabled}" != "TRUE" ]]; then + return + fi + if [[ "${vault_backend}" != "openbao" ]]; then + return + fi + + vault_addr=$(get_config VAULT_OPENBAO_ADDR) + vault_token=$(get_config VAULT_OPENBAO_TOKEN) + + if [[ "${openbao_external}" == "1" || "${openbao_external}" == "true" || "${openbao_external}" == "True" || "${openbao_external}" == "TRUE" ]]; then + if [[ -z "${vault_token}" ]]; then + log_error "$(gettext 'VAULT_OPENBAO_TOKEN is required when using external OpenBao')" + return 1 + fi + if [[ -z "${vault_addr}" || "${vault_addr}" == "http://openbao:8200" || "${vault_addr}" == "https://openbao:8200" ]]; then + log_error "$(gettext 'Set VAULT_OPENBAO_ADDR to the external OpenBao address')" + return 1 + fi + else + if [[ -z "${vault_addr}" ]]; then + vault_addr="http://openbao:8200" + fi + if [[ -z "${vault_token}" ]]; then + vault_token=$(random_str 48) + set_config VAULT_OPENBAO_TOKEN "${vault_token}" + fi + fi + + set_config VAULT_BACKEND openbao + set_config VAULT_OPENBAO_ADDR "${vault_addr}" + set_config VAULT_OPENBAO_MOUNT_POINT "$(get_config VAULT_OPENBAO_MOUNT_POINT pam)" + set_config VAULT_OPENBAO_TIMEOUT "$(get_config VAULT_OPENBAO_TIMEOUT 10)" + set_config OPENBAO_EXTERNAL "${openbao_external}" + + if [[ "${openbao_external}" == "1" || "${openbao_external}" == "true" || "${openbao_external}" == "True" || "${openbao_external}" == "TRUE" ]]; then + return 0 + fi + + set_config OPENBAO_RAFT_NODE_ID "$(get_config OPENBAO_RAFT_NODE_ID openbao)" + set_config OPENBAO_RAFT_API_ADDR "$(get_config OPENBAO_RAFT_API_ADDR http://openbao:8200)" + set_config OPENBAO_RAFT_CLUSTER_ADDR "$(get_config OPENBAO_RAFT_CLUSTER_ADDR http://openbao:8201)" + set_config OPENBAO_RAFT_BOOTSTRAP "$(get_config OPENBAO_RAFT_BOOTSTRAP true)" + set_config OPENBAO_UNSEAL_KEY_SHARES "$(get_config OPENBAO_UNSEAL_KEY_SHARES 5)" + set_config OPENBAO_UNSEAL_KEY_THRESHOLD "$(get_config OPENBAO_UNSEAL_KEY_THRESHOLD 3)" + set_openbao_bootstrap_script + set_openbao_server_config +} + +function set_openbao_bootstrap_script() { + local source_file target_file + + source_file="${PROJECT_DIR}/config_init/openbao/bootstrap.sh" + target_file="${CONFIG_DIR}/openbao/bootstrap.sh" + mkdir -p "${CONFIG_DIR}/openbao" + cp -f "${source_file}" "${target_file}" + chmod 600 "${target_file}" 2>/dev/null || true +} + +function set_openbao_server_config() { + local config_file data_path node_id api_addr cluster_addr retry_join addr + + config_file="${CONFIG_DIR}/openbao/server.hcl" + data_path="/openbao/file" + node_id=$(get_config OPENBAO_RAFT_NODE_ID openbao) + api_addr=$(get_config OPENBAO_RAFT_API_ADDR http://openbao:8200) + cluster_addr=$(get_config OPENBAO_RAFT_CLUSTER_ADDR http://openbao:8201) + retry_join=$(get_config OPENBAO_RAFT_RETRY_JOIN) + + mkdir -p "${CONFIG_DIR}/openbao" + + cat >"${config_file}" <>"${config_file}" <>"${config_file}" </dev/null || true +} diff --git a/scripts/gists/service.sh b/scripts/gists/service.sh index 91083ad5..b8eb2be1 100644 --- a/scripts/gists/service.sh +++ b/scripts/gists/service.sh @@ -84,11 +84,11 @@ function get_db_info() { case "${info_type}" in "image") if [[ "${mysql_data_exists}" == "1" ]]; then - echo "mysql:5.7-debian" + echo "mysql:8.0" elif [[ "${mariadb_data_exists}" == "1" ]]; then echo "mariadb:10.6" elif [[ "${postgres_data_exists}" == "1" ]]; then - echo "postgres:16.10-bookworm" + echo "postgres:16.15-bookworm" fi ;; "file") @@ -140,6 +140,9 @@ function get_docker_compose_services() { [[ "${use_es}" == "1" ]] && services+=" es" [[ "${use_minio}" == "1" ]] && services+=" minio" [[ "${use_loki}" == "1" ]] && services+=" loki" + if is_internal_openbao_enabled; then + services+=" openbao" + fi echo "${services}" } @@ -173,7 +176,7 @@ function get_docker_compose_cmd_line() { fi fi - if [[ "${use_lb}" == "1" ]]; then + if [[ -n "${https_port}" ]]; then cmd+=" -f compose/web.https.yml" fi @@ -348,4 +351,4 @@ function installation_log() { version=$(get_current_version) url="https://community.fit2cloud.com/installation-analytics?product=${product}&type=${install_type}&version=${version}" curl --connect-timeout 5 -m 10 -k $url &>/dev/null -} \ No newline at end of file +} diff --git a/scripts/utils.sh b/scripts/utils.sh index 357492d1..d2e6769e 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -6,8 +6,9 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" . "${BASE_DIR}/const.sh" . "${BASE_DIR}/gists/common.sh" . "${BASE_DIR}/gists/conf.sh" +. "${BASE_DIR}/gists/openbao.sh" +. "${BASE_DIR}/gists/kotl.sh" . "${BASE_DIR}/gists/image.sh" . "${BASE_DIR}/gists/service.sh" namespace=${NAMESPACE:-jumpserver} -