Files
Qortal-Nextcloud-Integration/scripts/topology/analyze-existing-nextcloud-instance.sh

501 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/../.." && pwd)"
source "${script_dir}/../lib-nextcloud-apps-path.sh"
occ_path=""
nextcloud_path=""
output_mode="text"
output_file=""
usage() {
cat <<USAGE
Usage: ./scripts/topology/analyze-existing-nextcloud-instance.sh [options]
Read-only host analysis for adopting an existing Nextcloud instance into the
NuQloud/Qortal integration stack.
Options:
--occ-path <path> Explicit path to Nextcloud occ
--nextcloud-path <path> Explicit Nextcloud root path (used to derive occ)
--json JSON output
--text Human-readable output (default)
--output-file <path> Write output to a file instead of stdout
-h, --help Show this help
USAGE
}
json_escape() {
local input="${1:-}"
input="${input//\\/\\\\}"
input="${input//\"/\\\"}"
input="${input//$'\n'/\\n}"
input="${input//$'\r'/}"
input="${input//$'\t'/\\t}"
printf '%s' "${input}"
}
bool_json() {
if [[ "${1:-0}" == "1" ]]; then
printf 'true'
else
printf 'false'
fi
}
join_lines_json() {
local input="${1:-}"
local first="1"
local line result=""
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
if [[ "${first}" == "1" ]]; then
result="\"$(json_escape "${line}")\""
first="0"
else
result="${result},\"$(json_escape "${line}")\""
fi
done <<< "${input}"
printf '%s' "${result}"
}
apps_paths_json() {
local input="${1:-}"
local first="1"
local path=""
local url=""
local writable=""
while IFS=$'\t' read -r path url writable; do
[[ -n "${path}" ]] || continue
if [[ "${first}" == "1" ]]; then
first="0"
else
printf ','
fi
printf '{"path":"%s","url":"%s","writable":%s}' \
"$(json_escape "${path}")" \
"$(json_escape "${url}")" \
"$(bool_json "${writable}")"
done <<< "${input}"
}
append_line() {
local var_name="$1"
local line="${2:-}"
local current="${!var_name:-}"
if [[ -z "${line}" ]]; then
return 0
fi
if [[ -z "${current}" ]]; then
printf -v "${var_name}" '%s' "${line}"
else
printf -v "${var_name}" '%s\n%s' "${current}" "${line}"
fi
}
run_timeout_cmd() {
if command -v timeout >/dev/null 2>&1; then
timeout 20 "$@"
else
"$@"
fi
}
detect_occ_path() {
local candidates=(
"/var/www/nextcloud/occ"
"/var/www/html/nextcloud/occ"
"/var/www/html/occ"
"/srv/www/nextcloud/occ"
"/var/lib/nextcloud/occ"
)
local candidate
for candidate in "${candidates[@]}"; do
if [[ -f "${candidate}" ]]; then
printf '%s\n' "${candidate}"
return 0
fi
done
return 1
}
occ_runner() {
if [[ -z "${occ_path}" || ! -f "${occ_path}" ]]; then
return 1
fi
if [[ -z "$(command -v php || true)" ]]; then
return 1
fi
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
run_timeout_cmd sudo -u www-data php "${occ_path}" "$@"
return $?
fi
if command -v runuser >/dev/null 2>&1; then
run_timeout_cmd runuser -u www-data -- php "${occ_path}" "$@"
return $?
fi
fi
run_timeout_cmd php "${occ_path}" "$@"
}
detect_service_state() {
local service_name="${1:-}"
if [[ -z "${service_name}" || -z "$(command -v systemctl || true)" ]]; then
return 1
fi
systemctl is-active "${service_name}" 2>/dev/null || true
}
docker_access_cmd=""
docker_daemon_reachable="0"
docker_access_mode="unavailable"
ensure_docker_best_effort() {
local docker_path=""
docker_path="$(type -P docker || true)"
if [[ -z "${docker_path}" ]]; then
return 1
fi
if "${docker_path}" info >/dev/null 2>&1; then
docker_access_cmd="${docker_path}"
docker_daemon_reachable="1"
docker_access_mode="direct"
return 0
fi
if command -v sudo >/dev/null 2>&1 && sudo -n "${docker_path}" info >/dev/null 2>&1; then
docker_access_cmd="sudo ${docker_path}"
docker_daemon_reachable="1"
docker_access_mode="sudo"
return 0
fi
return 1
}
run_docker_cmd() {
if [[ -z "${docker_access_cmd}" ]]; then
return 1
fi
bash -lc "${docker_access_cmd} $*"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--occ-path)
occ_path="${2:-}"
shift 2
;;
--occ-path=*)
occ_path="${1#*=}"
shift
;;
--nextcloud-path)
nextcloud_path="${2:-}"
shift 2
;;
--nextcloud-path=*)
nextcloud_path="${1#*=}"
shift
;;
--json)
output_mode="json"
shift
;;
--text)
output_mode="text"
shift
;;
--output-file)
output_file="${2:-}"
shift 2
;;
--output-file=*)
output_file="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ -n "${nextcloud_path}" && "${nextcloud_path}" != /* ]]; then
nextcloud_path="$(cd "${nextcloud_path}" 2>/dev/null && pwd || true)"
fi
if [[ -n "${occ_path}" && "${occ_path}" != /* ]]; then
occ_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd)/$(basename "${occ_path}")"
fi
if [[ -z "${occ_path}" && -n "${nextcloud_path}" && -f "${nextcloud_path}/occ" ]]; then
occ_path="${nextcloud_path}/occ"
fi
if [[ -z "${occ_path}" ]]; then
occ_path="$(detect_occ_path || true)"
fi
if [[ -n "${occ_path}" && -z "${nextcloud_path}" ]]; then
nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)"
fi
host_name="$(hostname 2>/dev/null || true)"
kernel_name="$(uname -sr 2>/dev/null || true)"
current_user="$(id -un 2>/dev/null || true)"
php_path="$(command -v php || true)"
php_version="$(php -v 2>/dev/null | head -n1 || true)"
apache_state="$(detect_service_state apache2)"
httpd_state="$(detect_service_state httpd)"
nginx_state="$(detect_service_state nginx)"
caddy_state="$(detect_service_state caddy)"
docker_state="$(detect_service_state docker)"
mariadb_state="$(detect_service_state mariadb)"
mysql_state="$(detect_service_state mysql)"
postgres_state="$(detect_service_state postgresql)"
redis_state="$(detect_service_state redis-server)"
listening_ports=""
if command -v ss >/dev/null 2>&1; then
listening_ports="$(ss -ltn 2>/dev/null | awk 'NR==1 || /:80 |:80$|:443 |:443$|:8080 |:8080$|:8081 |:8081$|:9980 |:9980$|:3000 |:3000$|:3191 |:3191$|:1234 |:1234$|:12391 |:12391$|:3478 |:3478$|:3479 |:3479$/' || true)"
fi
occ_found="0"
occ_status_ok="0"
occ_status_json=""
occ_status_text=""
nc_version=""
nc_installed=""
nc_maintenance=""
nc_needs_db_upgrade=""
nc_overwrite_url=""
nc_dbtype=""
nc_dbhost=""
nc_datadirectory=""
nc_trusted_domain_0=""
nc_enabled_apps=""
nc_apps_of_interest=""
nc_apps_paths=""
nc_selected_apps_path=""
if [[ -n "${occ_path}" && -f "${occ_path}" ]]; then
occ_found="1"
occ_status_json="$(occ_runner status --output=json 2>/dev/null || true)"
if [[ -n "${occ_status_json}" ]]; then
occ_status_ok="1"
fi
occ_status_text="$(occ_runner status 2>/dev/null || true)"
nc_version="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- versionstring:[[:space:]]*//p' | head -n1)"
nc_installed="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- installed:[[:space:]]*//p' | head -n1)"
nc_maintenance="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- maintenance:[[:space:]]*//p' | head -n1)"
nc_needs_db_upgrade="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- needsDbUpgrade:[[:space:]]*//p' | head -n1)"
nc_overwrite_url="$(occ_runner config:system:get overwrite.cli.url 2>/dev/null | tr -d '\r' | head -n1 || true)"
nc_dbtype="$(occ_runner config:system:get dbtype 2>/dev/null | tr -d '\r' | head -n1 || true)"
nc_dbhost="$(occ_runner config:system:get dbhost 2>/dev/null | tr -d '\r' | head -n1 || true)"
nc_datadirectory="$(occ_runner config:system:get datadirectory 2>/dev/null | tr -d '\r' | head -n1 || true)"
nc_trusted_domain_0="$(occ_runner config:system:get trusted_domains 0 2>/dev/null | tr -d '\r' | head -n1 || true)"
nc_enabled_apps="$(occ_runner app:list 2>/dev/null || true)"
nc_apps_of_interest="$(printf '%s\n' "${nc_enabled_apps}" | grep -E 'qortal_integration|user_oidc|notify_push|spreed|richdocuments|custom_pwa|chd_admin|qortal_files_bridge|qortal_talk_bridge|nuqloud_scrum' || true)"
nc_apps_paths="$(nextcloud_apps_paths_from_config "${nextcloud_path}" 2>/dev/null || true)"
nc_selected_apps_path="$(nextcloud_select_apps_path "${nextcloud_path}" 2>/dev/null || true)"
fi
docker_cli_present="0"
if command -v docker >/dev/null 2>&1; then
docker_cli_present="1"
fi
ensure_docker_best_effort || true
docker_ps_summary=""
docker_qortal_related=""
docker_networks=""
if [[ "${docker_daemon_reachable}" == "1" ]]; then
docker_ps_summary="$(run_docker_cmd "ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'" 2>/dev/null || true)"
docker_qortal_related="$(printf '%s\n' "${docker_ps_summary}" | grep -Ei 'qortal|broker|external_auth|nextcloud|collabora|talk|caddy' || true)"
docker_networks="$(run_docker_cmd "network ls --format '{{.Name}}'" 2>/dev/null || true)"
fi
topology_id="unknown"
topology_reason="Unable to classify runtime yet."
recommended_target="manual-analysis"
recommended_next_step="./scripts/analyze-existing-nextcloud-instance.sh --text"
if [[ "${occ_found}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then
topology_id="host-nextcloud-plus-docker"
topology_reason="Detected host-level Nextcloud OCC and a reachable Docker daemon."
recommended_target="host-nextcloud-hybrid"
recommended_next_step="./scripts/install-host-nextcloud-hybrid.sh --occ-path ${occ_path}"
elif [[ "${occ_found}" == "1" ]]; then
topology_id="host-nextcloud-no-docker"
topology_reason="Detected host-level Nextcloud OCC without usable Docker access."
recommended_target="app-only-or-external-services"
recommended_next_step="Provision broker/external-auth/Qortal services separately, then wire Nextcloud with OCC."
elif [[ "${docker_daemon_reachable}" == "1" && -n "${docker_qortal_related}" ]]; then
topology_id="docker-runtime-no-host-occ"
topology_reason="Detected Docker daemon and Nextcloud/Qortal sidecar containers, but no host-level OCC path."
recommended_target="repo-devprod-or-custom-compose"
recommended_next_step="./scripts/topology/discover-cloud-topology.sh --text"
fi
report_text() {
cat <<EOF
Existing Nextcloud Instance Analysis
Host:
hostname: ${host_name:-<unknown>}
kernel: ${kernel_name:-<unknown>}
user: ${current_user:-<unknown>}
php: ${php_path:-<missing>}
php_version: ${php_version:-<unknown>}
Host services:
apache2: ${apache_state:-<unknown>}
httpd: ${httpd_state:-<unknown>}
nginx: ${nginx_state:-<unknown>}
caddy: ${caddy_state:-<unknown>}
docker: ${docker_state:-<unknown>}
mariadb: ${mariadb_state:-<unknown>}
mysql: ${mysql_state:-<unknown>}
postgresql: ${postgres_state:-<unknown>}
redis-server: ${redis_state:-<unknown>}
Nextcloud host detection:
occ_found: ${occ_found}
occ_path: ${occ_path:-<not found>}
nextcloud_path: ${nextcloud_path:-<unknown>}
occ_status_ok: ${occ_status_ok}
installed: ${nc_installed:-<unknown>}
version: ${nc_version:-<unknown>}
maintenance: ${nc_maintenance:-<unknown>}
needsDbUpgrade: ${nc_needs_db_upgrade:-<unknown>}
overwrite.cli.url: ${nc_overwrite_url:-<empty>}
trusted_domains[0]: ${nc_trusted_domain_0:-<empty>}
dbtype: ${nc_dbtype:-<empty>}
dbhost: ${nc_dbhost:-<empty>}
datadirectory: ${nc_datadirectory:-<empty>}
Apps paths:
$(if [[ -n "${nc_apps_paths}" ]]; then printf '%s\n' "${nc_apps_paths}" | while IFS=$'\t' read -r path url writable; do
[[ -n "${path}" ]] || continue
printf ' - path=%s url=%s writable=%s\n' "${path}" "${url:-<empty>}" "$( [[ "${writable}" == "1" ]] && echo true || echo false )"
done; else echo " <none detected>"; fi)
selected_apps_path: ${nc_selected_apps_path:-<default>}
Apps of interest:
$(if [[ -n "${nc_apps_of_interest}" ]]; then printf '%s\n' "${nc_apps_of_interest}" | sed 's/^/ /'; else echo " <none detected>"; fi)
Docker:
cli_present: ${docker_cli_present}
daemon_reachable: ${docker_daemon_reachable}
access_mode: ${docker_access_mode}
Qortal/Nextcloud-related containers:
$(if [[ -n "${docker_qortal_related}" ]]; then printf '%s\n' "${docker_qortal_related}" | sed 's/^/ /'; else echo " <none detected>"; fi)
Docker networks:
$(if [[ -n "${docker_networks}" ]]; then printf '%s\n' "${docker_networks}" | sed 's/^/ /'; else echo " <none detected>"; fi)
Listening ports snapshot:
$(if [[ -n "${listening_ports}" ]]; then printf '%s\n' "${listening_ports}" | sed 's/^/ /'; else echo " <not available>"; fi)
Recommendation:
topology_id: ${topology_id}
reason: ${topology_reason}
target_profile: ${recommended_target}
next_step: ${recommended_next_step}
Suggested rollout stages:
1. Run this analyzer on the target host and save the output.
2. Decide whether the target is:
- full NuQloud sidecar stack on an existing Nextcloud host
- hybrid host-Nextcloud plus Docker sidecars
- app-only/manual external services
3. Reuse existing scripts where possible:
- scripts/topology/discover-cloud-topology.sh
- scripts/topology/apply-topology-profile.sh
- scripts/nextcloud-vm-install.sh
- scripts/finish-initial-setup.sh
- scripts/ensure-*.sh helpers for URL, Talk, service auth, CustomPWA, qortal runtime
4. Use the host-hybrid installer and lifecycle wrappers after confirming the target topology.
EOF
}
report_json() {
printf '{\n'
printf ' "ok": true,\n'
printf ' "generatedAt": "%s",\n' "$(json_escape "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
printf ' "host": {\n'
printf ' "hostname": "%s",\n' "$(json_escape "${host_name}")"
printf ' "kernel": "%s",\n' "$(json_escape "${kernel_name}")"
printf ' "user": "%s",\n' "$(json_escape "${current_user}")"
printf ' "phpPath": "%s",\n' "$(json_escape "${php_path}")"
printf ' "phpVersion": "%s"\n' "$(json_escape "${php_version}")"
printf ' },\n'
printf ' "services": {\n'
printf ' "apache2": "%s",\n' "$(json_escape "${apache_state}")"
printf ' "httpd": "%s",\n' "$(json_escape "${httpd_state}")"
printf ' "nginx": "%s",\n' "$(json_escape "${nginx_state}")"
printf ' "caddy": "%s",\n' "$(json_escape "${caddy_state}")"
printf ' "docker": "%s",\n' "$(json_escape "${docker_state}")"
printf ' "mariadb": "%s",\n' "$(json_escape "${mariadb_state}")"
printf ' "mysql": "%s",\n' "$(json_escape "${mysql_state}")"
printf ' "postgresql": "%s",\n' "$(json_escape "${postgres_state}")"
printf ' "redisServer": "%s"\n' "$(json_escape "${redis_state}")"
printf ' },\n'
printf ' "nextcloud": {\n'
printf ' "occFound": %s,\n' "$(bool_json "${occ_found}")"
printf ' "occPath": "%s",\n' "$(json_escape "${occ_path}")"
printf ' "nextcloudPath": "%s",\n' "$(json_escape "${nextcloud_path}")"
printf ' "occStatusOk": %s,\n' "$(bool_json "${occ_status_ok}")"
printf ' "installed": "%s",\n' "$(json_escape "${nc_installed}")"
printf ' "version": "%s",\n' "$(json_escape "${nc_version}")"
printf ' "maintenance": "%s",\n' "$(json_escape "${nc_maintenance}")"
printf ' "needsDbUpgrade": "%s",\n' "$(json_escape "${nc_needs_db_upgrade}")"
printf ' "overwriteCliUrl": "%s",\n' "$(json_escape "${nc_overwrite_url}")"
printf ' "trustedDomain0": "%s",\n' "$(json_escape "${nc_trusted_domain_0}")"
printf ' "dbtype": "%s",\n' "$(json_escape "${nc_dbtype}")"
printf ' "dbhost": "%s",\n' "$(json_escape "${nc_dbhost}")"
printf ' "datadirectory": "%s",\n' "$(json_escape "${nc_datadirectory}")"
printf ' "appsPaths": [%s],\n' "$(apps_paths_json "${nc_apps_paths}")"
printf ' "selectedAppsPath": "%s",\n' "$(json_escape "${nc_selected_apps_path}")"
printf ' "appsOfInterest": [%s]\n' "$(join_lines_json "${nc_apps_of_interest}")"
printf ' },\n'
printf ' "docker": {\n'
printf ' "cliPresent": %s,\n' "$(bool_json "${docker_cli_present}")"
printf ' "daemonReachable": %s,\n' "$(bool_json "${docker_daemon_reachable}")"
printf ' "accessMode": "%s",\n' "$(json_escape "${docker_access_mode}")"
printf ' "qortalRelatedContainers": [%s],\n' "$(join_lines_json "${docker_qortal_related}")"
printf ' "networks": [%s]\n' "$(join_lines_json "${docker_networks}")"
printf ' },\n'
printf ' "listeningPorts": [%s],\n' "$(join_lines_json "${listening_ports}")"
printf ' "recommendation": {\n'
printf ' "topologyId": "%s",\n' "$(json_escape "${topology_id}")"
printf ' "reason": "%s",\n' "$(json_escape "${topology_reason}")"
printf ' "targetProfile": "%s",\n' "$(json_escape "${recommended_target}")"
printf ' "nextStep": "%s"\n' "$(json_escape "${recommended_next_step}")"
printf ' }\n'
printf '}\n'
}
rendered_output=""
if [[ "${output_mode}" == "json" ]]; then
rendered_output="$(report_json)"
else
rendered_output="$(report_text)"
fi
if [[ -n "${output_file}" ]]; then
mkdir -p "$(dirname "${output_file}")"
printf '%s\n' "${rendered_output}" > "${output_file}"
else
printf '%s\n' "${rendered_output}"
fi