Commit 59b40cbf by Bambang Adrian

fix(osrm): release v2.4 fixing bugs for motorcycle profile, and filepath

parent b4677cf5
...@@ -2,102 +2,63 @@ ...@@ -2,102 +2,63 @@
# #
# install-osrm-ngpm.sh # install-osrm-ngpm.sh
# OSRM + NGPM installer & manager (Rocky Linux 10) # OSRM + NGPM installer & manager (Rocky Linux 10)
# Final self-contained script (v1.0.1 - Patched) # Final self-contained script (v2.4 - Lua Profile Path Fix)
# #
set -euo pipefail set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
# ------------------------- # -------------------------
# DEFAULTS (internal variables use snake_case) # DEFAULTS
# ------------------------- # -------------------------
DEFAULT_WORK_DIR="${HOME}/data/docker-data/osrm" DEFAULT_WORK_DIR="${HOME}/data/docker-data/osrm"
DEFAULT_MAP_DIR="${DEFAULT_WORK_DIR}/dataset"
DEFAULT_PROFILE_DIR="${DEFAULT_WORK_DIR}/profiles"
DEFAULT_PATCHED_DIR="${DEFAULT_WORK_DIR}/patched-profiles"
DEFAULT_BACKUP_DIR="${DEFAULT_WORK_DIR}/backup"
DEFAULT_LOG_DIR="${DEFAULT_WORK_DIR}/logs"
DEFAULT_BACKEND_REGISTRY="ghcr.io/project-osrm/osrm-backend" DEFAULT_BACKEND_REGISTRY="ghcr.io/project-osrm/osrm-backend"
DEFAULT_FRONTEND_REGISTRY="ghcr.io/project-osrm/osrm-frontend" DEFAULT_FRONTEND_REGISTRY="ghcr.io/project-osrm/osrm-frontend"
DEFAULT_BACKEND_VERSION="latest" DEFAULT_BACKEND_VERSION="latest"
DEFAULT_FRONTEND_VERSION="latest" DEFAULT_FRONTEND_VERSION="latest"
DEFAULT_MAP_SOURCE="https://download.geofrik.de/asia/indonesia-latest.osm.pbf" DEFAULT_MAP_SOURCE="https://download.geofabrik.de/asia/indonesia-latest.osm.pbf"
DEFAULT_NETWORK="ngpm-network" DEFAULT_NETWORK="ngpm-network"
DEFAULT_PROFILES="car" DEFAULT_PROFILES="car"
DEFAULT_BACKEND_PORT=5000 DEFAULT_BACKEND_PORT=5000
DEFAULT_FRONTEND_PORT=9966 DEFAULT_FRONTEND_PORT=9966
DEFAULT_BACKEND_DOMAIN="localhost" DEFAULT_BACKEND_DOMAIN="osrm.example.com"
DEFAULT_FRONTEND_DOMAIN="localhost" DEFAULT_FRONTEND_DOMAIN="map.example.com"
DEFAULT_BACKEND_PATH="/osrm" DEFAULT_BACKEND_PATH="/osrm"
DEFAULT_API_VERSION="v1" DEFAULT_API_VERSION="v1"
DEFAULT_NO_FRONTEND=false
DEFAULT_NO_BACKEND=false
# ------------------------- # -------------------------
# RUN-TIME VARIABLES (overridden by CLI) # RUN-TIME VARIABLES
# ------------------------- # -------------------------
work_dir="${DEFAULT_WORK_DIR}" work_dir="${DEFAULT_WORK_DIR}"
map_dir="${DEFAULT_MAP_DIR}" map_name="default"
profile_dir="${DEFAULT_PROFILE_DIR}"
patched_dir="${DEFAULT_PATCHED_DIR}"
backup_dir="${DEFAULT_BACKUP_DIR}"
log_dir="${DEFAULT_LOG_DIR}"
backend_registry="${DEFAULT_BACKEND_REGISTRY}"
frontend_registry="${DEFAULT_FRONTEND_REGISTRY}"
backend_version="${DEFAULT_BACKEND_VERSION}"
frontend_version="${DEFAULT_FRONTEND_VERSION}"
map_source="${DEFAULT_MAP_SOURCE}" map_source="${DEFAULT_MAP_SOURCE}"
network_name="${DEFAULT_NETWORK}"
profiles_csv="${DEFAULT_PROFILES}" profiles_csv="${DEFAULT_PROFILES}"
network_name="${DEFAULT_NETWORK}"
backend_port="${DEFAULT_BACKEND_PORT}" backend_port="${DEFAULT_BACKEND_PORT}"
frontend_port="${DEFAULT_FRONTEND_PORT}" frontend_port="${DEFAULT_FRONTEND_PORT}"
backend_domain="${DEFAULT_BACKEND_DOMAIN}" backend_domain="${DEFAULT_BACKEND_DOMAIN}"
frontend_domain="${DEFAULT_FRONTEND_DOMAIN}" frontend_domain="${DEFAULT_FRONTEND_DOMAIN}"
backend_path="${DEFAULT_BACKEND_PATH}" backend_path="${DEFAULT_BACKEND_PATH}"
api_version="${DEFAULT_API_VERSION}" api_version="${DEFAULT_API_VERSION}"
no_frontend="${DEFAULT_NO_FRONTEND}" no_frontend=false
no_backend="${DEFAULT_NO_BACKEND}"
dry_run=false dry_run=false
print_config=false
yes_all=false yes_all=false
# update/remove flags (granular)
do_update_backend=false
do_update_frontend=false
do_update_map=false
do_update_profile=false
do_update_all=false
do_remove_backend=false
do_remove_frontend=false
do_remove_profile=false
do_remove_map=false
do_remove_all=false
do_purge=false do_purge=false
map_arg="" # explicit --map-source given (URL or path) override var map_source
# derived # derived
MAP_PATH=""
COMPOSE_FILE="" COMPOSE_FILE=""
LOGFILE="" LOGFILE=""
NGPM_HINTS="" PROFILE_ARR=()
MAP_PATH=""
# ------------------------- # -------------------------
# Logging helpers # Logging helpers
# ------------------------- # -------------------------
timestamp() { date '+%Y-%m-%d %H:%M:%S'; } timestamp() { date '+%Y-%m-%d %H:%M:%S'; }
logfile_init() {
mkdir -p "${log_dir}"
LOGFILE="${log_dir}/osrm-installer-$(date '+%Y%m%d_%H%M%S').log"
: > "${LOGFILE}"
}
_log() { _log() {
local lvl="$1"; shift local lvl="$1"; shift
local ts; ts="$(timestamp)" local ts; ts="$(timestamp)"
printf "%s [%s] %s\n" "$ts" "$lvl" "$*" | tee -a "${LOGFILE}" if [[ -n "${LOGFILE:-}" ]]; then printf "%s [%s] %s\n" "$ts" "$lvl" "$*" >> "${LOGFILE}"; fi
printf "%s [%s] %s\n" "$ts" "$lvl" "$*"
} }
info() { _log "INFO" "$*"; } info() { _log "INFO" "$*"; }
success() { _log "SUCCESS" "$*"; } success() { _log "SUCCESS" "$*"; }
...@@ -109,194 +70,94 @@ err() { _log "ERROR" "$*" >&2; } ...@@ -109,194 +70,94 @@ err() { _log "ERROR" "$*" >&2; }
# ------------------------- # -------------------------
usage() { usage() {
cat <<EOF cat <<EOF
install-osrm-ngpm.sh - OSRM + NGPM installer & manager (Rocky Linux 10) OSRM Installer & Manager v2.4 (Multi-Map Support)
Usage: Usage:
$0 <command> [options] $0 <command> --map-name <name> [options]
Commands: Commands:
install Install and run OSRM backend + frontend (unless disabled) install Install a new OSRM instance.
update <component> Update a specific component (backend, frontend, map, profile, all) update <component> Update a component (backend, frontend, map, profile, all).
remove <component> Remove a specific component (backend, frontend, profile, all) remove Remove an OSRM instance.
Install Options: Required Argument:
--map-source <url|path> Map URL or local file path (default: ${DEFAULT_MAP_SOURCE}) --map-name <name> A unique name for this map instance (e.g., 'indonesia').
--profiles <csv> Profiles to enable (car,motorcycle,bike,foot). Default '${DEFAULT_PROFILES}'
--backend-domain <domain> Domain for backend URLs (default: ${DEFAULT_BACKEND_DOMAIN}) Options:
--frontend-domain <domain> Domain for frontend (default: ${DEFAULT_FRONTEND_DOMAIN}) --map-source <url|path> Map URL or local file path.
--backend-port <port> Host start port for backend mapping (default: ${DEFAULT_BACKEND_PORT}) --profiles <csv> Profiles to enable (car,motorcycle,bike,foot).
--frontend-port <port> Host port for frontend UI (default: ${DEFAULT_FRONTEND_PORT}) --backend-port <port> Host start port for backend services.
--no-frontend Do NOT install frontend (frontend is installed by default) --frontend-port <port> Host port for the frontend UI.
--no-backend Do NOT install backend --no-frontend Do NOT install the frontend UI.
--dry-run Simulate steps --purge Also delete all data directories on remove.
--yes Non-interactive (answer Yes to all prompts) --dry-run Simulate steps without executing.
-h, --help Show this help --yes Answer 'yes' to all prompts.
-h, --help Show this help message.
Update Command:
$0 update <backend|frontend|map|profile|all> [--map-source <url|path>]
Remove Command:
$0 remove <backend|frontend|profile|all> [--purge]
Examples:
Install default:
$0 install
Install with multiple profiles:
$0 install --profiles car,motorcycle,bike,foot
Update only map (local file):
$0 update map --map-source /data/osm/indonesia-latest.osm.pbf
Remove all components and data:
$0 remove all --purge
EOF EOF
exit 0 exit 0
} }
# ------------------------- # -------------------------
# Parse args # Argument Parsing & Setup
# ------------------------- # -------------------------
if [ $# -eq 0 ]; then usage; fi parse_args() {
command_name="$1"; shift || true if [ $# -eq 0 ]; then usage; fi
command_name="$1"; shift || true
# Sub-command for update/remove sub_command=""
sub_command="" if [[ "${command_name}" == "update" ]]; then
if [[ "${command_name}" == "update" || "${command_name}" == "remove" ]]; then if [ $# -gt 0 ] && ! [[ "$1" =~ ^-- ]]; then sub_command="$1"; shift; else err "Update command requires a component."; usage; fi
if [ $# -gt 0 ] && ! [[ "$1" =~ ^-- ]]; then
sub_command="$1"
shift
fi fi
fi while (( "$#" )); do
while (( "$#" )); do
case "$1" in case "$1" in
--map-name) map_name="$2"; shift 2 ;;
--work-dir) work_dir="${2/#\~/$HOME}"; shift 2 ;; --work-dir) work_dir="${2/#\~/$HOME}"; shift 2 ;;
--map-source) map_source="${2}"; map_arg="${2}"; shift 2 ;; --map-source) map_source="$2"; shift 2 ;;
--map-dir) map_dir="${2/#\~/$HOME}"; shift 2 ;; --profiles) profiles_csv="$2"; shift 2 ;;
--profiles) profiles_csv="${2}"; shift 2 ;;
--profile-dir) profile_dir="${2/#\~/$HOME}"; shift 2 ;;
--backend-registry) backend_registry="$2"; shift 2 ;;
--frontend-registry) frontend_registry="$2"; shift 2 ;;
--backend-version) backend_version="$2"; shift 2 ;;
--frontend-version) frontend_version="$2"; shift 2 ;;
--backend-port) backend_port="$2"; shift 2 ;; --backend-port) backend_port="$2"; shift 2 ;;
--frontend-port) frontend_port="$2"; shift 2 ;; --frontend-port) frontend_port="$2"; shift 2 ;;
--network) network_name="$2"; shift 2 ;; --network) network_name="$2"; shift 2 ;;
--backend-domain) backend_domain="$2"; shift 2 ;; --backend-domain) backend_domain="$2"; shift 2 ;;
--frontend-domain) frontend_domain="$2"; shift 2 ;; --frontend-domain) frontend_domain="$2"; shift 2 ;;
--backend-path) backend_path="$2"; shift 2 ;;
--api-version) api_version="$2"; shift 2 ;;
--no-frontend) no_frontend=true; shift ;; --no-frontend) no_frontend=true; shift ;;
--no-backend) no_backend=true; shift ;;
--dry-run) dry_run=true; shift ;; --dry-run) dry_run=true; shift ;;
--print-config) print_config=true; shift ;;
--purge) do_purge=true; shift ;; --purge) do_purge=true; shift ;;
--yes) yes_all=true; shift ;; --yes) yes_all=true; shift ;;
-h|--help) usage ;; -h|--help) usage ;;
*) warn "Unknown option: $1"; shift ;; *) err "Unknown option: $1"; usage ;;
esac esac
done done
if [[ "${map_name}" == "default" && "${command_name}" != "install" ]]; then err "The --map-name argument is required."; usage; fi
# normalize & mkdirs work_dir="${work_dir/#\~/$HOME}"; local log_dir="${work_dir}/logs"; mkdir -p "${work_dir}" "${log_dir}"
work_dir="${work_dir/#\~/$HOME}" LOGFILE="${log_dir}/osrm-manager-$(date '+%Y%m%d_%H%M%S').log"
map_dir="${map_dir/#\~/$HOME}" : > "${LOGFILE}"
profile_dir="${profile_dir/#\~/$HOME}" COMPOSE_FILE="${work_dir}/docker-compose.${map_name}.yml"
patched_dir="${patched_dir/#\~/$HOME}" IFS=',' read -r -a PROFILE_ARR <<< "$(echo "${profiles_csv}" | tr -d '[:space:]')"
backup_dir="${backup_dir/#\~/$HOME}" if [ "${#PROFILE_ARR[@]}" -eq 0 ]; then PROFILE_ARR=(car); fi
log_dir="${log_dir/#\~/$HOME}" }
mkdir -p "${work_dir}" "${map_dir}" "${profile_dir}" "${patched_dir}" "${backup_dir}" "${log_dir}"
COMPOSE_FILE="${work_dir}/docker-compose.yml"
NGPM_HINTS="${work_dir}/ngpm-config.txt"
logfile_init
# parse profiles csv -> array
IFS=',' read -r -a PROFILE_ARR <<< "$(echo "${profiles_csv}" | tr -d '[:space:]')"
if [ "${#PROFILE_ARR[@]}" -eq 0 ]; then PROFILE_ARR=(car); fi
BACKEND_IMAGE="${backend_registry}:${backend_version}"
FRONTEND_IMAGE="${frontend_registry}:${frontend_version}"
info "Command: ${command_name}"
info "Work dir: ${work_dir}"
info "Log dir: ${log_dir}"
info "Network: ${network_name}"
info "Backend image: ${BACKEND_IMAGE}"
info "Frontend image: ${FRONTEND_IMAGE}"
info "Profiles: ${profiles_csv}"
info "Dry run: ${dry_run}"
[ -n "${map_arg}" ] && info "Map source override: ${map_arg}"
# ------------------------- # -------------------------
# Helper functions # Helper Functions
# ------------------------- # -------------------------
command_exists() { command -v "$1" >/dev/null 2>&1; } command_exists() { command -v "$1" >/dev/null 2>&1; }
ensure_package() {
local pkg="$1"
if command_exists "$pkg"; then
info "Dependency found: ${pkg}"
return 0
fi
info "Dependency missing: ${pkg}"
if [ "$(id -u)" -ne 0 ]; then
warn "Cannot auto-install ${pkg} because script is not running as root. Please run: sudo dnf install -y ${pkg}"
return 1
fi
info "Attempting to install ${pkg} via dnf..."
if dnf install -y "${pkg}"; then
success "Installed ${pkg}"
return 0
else
warn "Failed to auto-install ${pkg}. Please install manually."
return 1
fi
}
prereqs_check() { prereqs_check() {
info "Running prerequisite checks..."
local ok=0 local ok=0
if ! command_exists docker; then if ! command_exists docker; then err "Docker is not installed."; ok=1; fi
err "docker not found. Please install docker and ensure the docker daemon is running." if ! docker compose version >/dev/null 2>&1; then err "Docker Compose V2 plugin is not installed."; ok=1; fi
ok=1 if ! command_exists curl; then err "curl is not installed."; ok=1; fi
fi if [ ${ok} -ne 0 ]; then err "Prerequisite checks failed."; exit 1; fi
if ! docker compose version >/dev/null 2>&1; then success "All prerequisites are met."
warn "docker compose plugin not found. Attempting to install docker-compose-plugin via dnf (requires root)."
if ! ensure_package docker-compose-plugin; then
err "docker compose plugin not available. Please install docker compose manually."
ok=1
fi
fi
for util in curl wget tar awk sed; do
if ! command_exists "$util"; then
info "Utility ${util} missing; attempting to install"
if ! ensure_package "$util"; then
warn "Please install ${util} manually."
ok=1
fi
fi
done
if [ ${ok} -ne 0 ]; then
err "Prerequisite checks failed. Aborting."
exit 1
fi
} }
# SELinux label detection for mounts
SELINUX_LABEL="" SELINUX_LABEL=""
if command_exists sestatus && sestatus 2>/dev/null | grep -qi "SELinux status:.*enabled"; then setup_selinux_flag() {
SELINUX_LABEL=":z" if command_exists sestatus && sestatus 2>/dev/null | grep -qi "SELinux status:.*enabled"; then
info "SELinux is enabled. Will append ':z' to bind mounts for context." SELINUX_LABEL=",z"
fi info "SELinux is enabled. Appending ',z' to volume mounts."
# prompt helper
ask_confirm() {
local prompt="$1"; shift
if [ "${yes_all}" = true ]; then
info "[auto-yes] ${prompt} -> yes"
return 0
fi fi
}
ask_confirm() {
local prompt="$1"
if [ "${yes_all}" = true ]; then info "[auto-yes] ${prompt} -> yes"; return 0; fi
while true; do while true; do
printf "%s [y/N]: " "${prompt}" printf "%s [y/N]: " "${prompt}"
read -r yn read -r yn
...@@ -309,198 +170,83 @@ ask_confirm() { ...@@ -309,198 +170,83 @@ ask_confirm() {
} }
# ------------------------- # -------------------------
# Map selection logic # Core Logic
# ------------------------- # -------------------------
select_map() { download_map() {
info "Selecting map..." info "Preparing map from source: ${map_source}"
local map_target_source="${map_arg:-${map_source}}" local map_instance_dir="${work_dir}/${map_name}/dataset"
mkdir -p "${map_instance_dir}"
if [[ "${map_target_source}" =~ ^https?:// ]]; then MAP_PATH="${map_instance_dir}/$(basename "${map_source}")"
MAP_PATH="${map_dir}/$(basename "${map_target_source}")" if [[ -f "${MAP_PATH}" ]]; then info "Map file already exists. Skipping download."; return 0; fi
download_map_if_needed "${map_target_source}" "${MAP_PATH}" if [[ "${map_source}" =~ ^https?:// ]]; then
return 0 if [ "${dry_run}" = true ]; then warn "[DRY RUN] Would download map to ${MAP_PATH}"; return 0; fi
fi info "Downloading map with curl..."; if ! curl -L --fail -o "${MAP_PATH}.tmp" "${map_source}"; then rm -f "${MAP_PATH}.tmp"; err "Download failed."; exit 1; fi
mv "${MAP_PATH}.tmp" "${MAP_PATH}"; success "Map downloaded successfully."
if [ -f "${map_target_source}" ]; then elif [[ -f "${map_source}" ]]; then
MAP_PATH="$(realpath "${map_target_source}")" if [ "${dry_run}" = true ]; then warn "[DRY RUN] Would copy local map to ${MAP_PATH}"; return 0; fi
info "Using user-provided local map: ${MAP_PATH}" info "Copying local map file..."; cp -v "${map_source}" "${MAP_PATH}"; success "Local map copied."
return 0 else err "Map source '${map_source}' is not a valid URL or an existing file."; exit 1;
fi fi
# Fallback to default if nothing works
local default_file="${map_dir}/$(basename "${DEFAULT_MAP_SOURCE}")"
if [ -f "${default_file}" ]; then
MAP_PATH="${default_file}"
info "Found existing default map: ${MAP_PATH}"
return 0
fi
MAP_PATH="${default_file}"
download_map_if_needed "${DEFAULT_MAP_SOURCE}" "${MAP_PATH}"
} }
generate_profiles() {
download_map_if_needed() { info "Generating profile files for map instance '${map_name}'"
local url="$1"; local dest="$2" local profile_dir="${work_dir}/${map_name}/profiles"; local patched_dir="${work_dir}/${map_name}/patched-profiles"
info "Preparing map: ${url} -> ${dest}" mkdir -p "${profile_dir}" "${patched_dir}"
if [ -f "${dest}" ]; then
info "Map already exists at ${dest}. Skipping download."
return 0
fi
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would download ${url} to ${dest}"
# Create a dummy file for dry-run to proceed
touch "${dest}"
return 0
fi
mkdir -p "$(dirname "${dest}")"
info "Downloading with curl..."
if ! curl -L --fail -o "${dest}.tmp" "${url}"; then
rm -f "${dest}.tmp"
err "Download failed for ${url}"
exit 1
fi
mv "${dest}.tmp" "${dest}"
success "Map downloaded: ${dest}"
}
# -------------------------
# Profile generation & patching
# -------------------------
generate_profile_wrappers() {
info "Generating profile wrappers in ${profile_dir} (if missing)"
mkdir -p "${profile_dir}"
for profile in "${PROFILE_ARR[@]}"; do for profile in "${PROFILE_ARR[@]}"; do
local pf="${profile_dir}/${profile}.lua" local pf="${profile_dir}/${profile}.lua"
if [ -f "${pf}" ]; then if [ ! -f "${pf}" ]; then
info "Profile exists: ${pf}"
continue
fi
case "${profile}" in case "${profile}" in
car|bicycle|bike|foot) car|bicycle|bike|foot)
info "Generating wrapper for ${profile}" info "Generating wrapper for built-in profile: ${profile}"
# This generic wrapper loads the built-in profile from the container # --- FIX IS HERE (v2.4) ---
# The correct path inside the container is /opt/*.lua, not /opt/profiles/*.lua
cat > "${pf}" <<LUA cat > "${pf}" <<LUA
-- wrapper: attempt to load builtin profile '${profile}' return dofile('/opt/${profile}.lua')
local function load_builtin(name)
local cands = {"/opt/profiles/"..name..".lua", "/opt/"..name..".lua"}
for _,p in ipairs(cands) do
local f = io.open(p,"r")
if f then f:close(); return dofile(p) end
end
error("builtin profile not found: "..name)
end
return load_builtin('${profile}')
LUA LUA
success "Generated wrapper: ${pf}"
;; ;;
motorcycle) motorcycle)
info "Generating motorcycle wrapper based on car" info "Generating custom 'motorcycle' profile based on 'car'"
# FIX: This version correctly modifies the car profile without breaking its internal scope. # --- FIX IS HERE (v2.4) ---
cat > "${pf}" <<'LUA' cat > "${pf}" <<'LUA'
-- motorcycle wrapper derived from car profile (v2 - Robust) local car_module = dofile('/opt/car.lua')
local function load_builtin(name) local profile = car_module.setup()
local candidates = { profile.properties.max_speed = 100
"/opt/profiles/" .. name .. ".lua", profile.properties.u_turn_penalty = 60
"/opt/" .. name .. ".lua" profile.restrictions = profile.restrictions or {}
} profile.restrictions["toll"] = "prohibited"
for _, path in ipairs(candidates) do car_module.setup = function() return profile end
local f = io.open(path, "r")
if f then
f:close()
return dofile(path)
end
end
error("Built-in profile not found: " .. name)
end
-- Load the entire module returned by the car profile
local car_module = load_builtin('car')
-- Get the profile data table by calling its setup function
local profile_table = car_module.setup()
-- Modify the profile table in-place
profile_table.properties.max_speed = 100
profile_table.properties.u_turn_penalty = 60
profile_table.restrictions = profile_table.restrictions or {}
profile_table.restrictions["toll"] = "prohibited"
-- Redefine the setup function in the original module to return our modified profile
car_module.setup = function()
return profile_table
end
-- Return the entire, modified module. This preserves all functions
-- like process_node, process_way, etc. in their original context.
return car_module return car_module
LUA LUA
success "Generated motorcycle wrapper: ${pf}"
;;
*)
warn "No generator for profile '${profile}'. Provide ${pf} manually."
;; ;;
*) warn "No generator for profile '${profile}'."; continue;;
esac esac
done fi; cp "${pf}" "${patched_dir}/${profile}.lua"
done; success "Profile generation complete."
} }
create_patched_profiles() {
info "Creating patched profiles in ${patched_dir} (safe copies with arg fallback)"
mkdir -p "${patched_dir}"
for profile in "${PROFILE_ARR[@]}"; do
local src="${profile_dir}/${profile}.lua"
local dst="${patched_dir}/${profile}.lua"
if [ ! -f "${src}" ]; then
warn "Profile source not found: ${src} (skipping patch)"
continue
fi
# This patch helps profiles find their own name if they rely on arg[0]
{
printf 'local arg = arg or {}\n'
printf 'arg[0] = arg[0] or "%s"\n' "${profile}"
printf '\n'
cat "${src}"
} > "${dst}.tmp"
mv "${dst}.tmp" "${dst}"
info "Patched profile written: ${dst}"
done
}
# -------------------------
# Docker Compose generation (absolute host paths)
# -------------------------
generate_docker_compose() { generate_docker_compose() {
info "Generating docker-compose.yml at ${COMPOSE_FILE}" info "Generating Docker Compose file: ${COMPOSE_FILE}"
mkdir -p "$(dirname "${COMPOSE_FILE}")" local map_instance_dir="${work_dir}/${map_name}"; local selabel="${SELINUX_LABEL:-}"; local services=""; local idx=0
local selabel="${SELINUX_LABEL:-}"
local services=""
local idx=0
for profile in "${PROFILE_ARR[@]}"; do for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx+1)) idx=$((idx+1)); local host_port=$((backend_port + idx - 1)); local pdata="${map_instance_dir}/${profile}-data"; mkdir -p "${pdata}"
local host_port=$((backend_port + idx - 1))
local pdata="${work_dir}/${profile}-data"
mkdir -p "${pdata}"
services+=" services+="
osrm-preprocess-${profile}: osrm-preprocess-${map_name}-${profile}:
image: ${BACKEND_IMAGE} image: ${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}
container_name: osrm-preprocess-${profile} container_name: osrm-preprocess-${map_name}-${profile}
command: > command: >
sh -c \"osrm-extract -p /opt/custom_profiles/${profile}.lua /data/map.osm.pbf && sh -c \"osrm-extract -p /opt/custom_profiles/${profile}.lua /data/map.osm.pbf &&
osrm-partition /data/map.osrm && osrm-partition /data/map.osrm &&
osrm-customize /data/map.osrm\" osrm-customize /data/map.osrm\"
volumes: volumes:
- ${patched_dir}:/opt/custom_profiles:ro${selabel} - ${map_instance_dir}/patched-profiles:/opt/custom_profiles:ro${selabel}
- ${pdata}:/data${selabel} - ${pdata}:/data:rw${selabel}
networks: networks:
- ${network_name} - ${network_name}
osrm-routed-${profile}: osrm-routed-${map_name}-${profile}:
image: ${BACKEND_IMAGE} image: ${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}
container_name: osrm-routed-${profile} container_name: osrm-routed-${map_name}-${profile}
restart: unless-stopped restart: unless-stopped
depends_on: command: osrm-routed --algorithm mld /data/map.osrm
- osrm-preprocess-${profile}
command: osrm-routed --algorithm mld --max-table-size 10000 /data/map.osrm
volumes: volumes:
- ${pdata}:/data:ro${selabel} - ${pdata}:/data:ro${selabel}
ports: ports:
...@@ -509,26 +255,14 @@ generate_docker_compose() { ...@@ -509,26 +255,14 @@ generate_docker_compose() {
- ${network_name} - ${network_name}
" "
done done
local frontend_block=""; if [ "${no_frontend}" != true ]; then
local frontend_block="" local backend_urls=""; local idx2=0
if [ "${no_frontend}" != true ]; then for profile in "${PROFILE_ARR[@]}"; do idx2=$((idx2+1)); local host_port=$((backend_port + idx2 - 1)); local url="http://${backend_domain}:${host_port}${backend_path}/${api_version}/${profile}"; backend_urls+="${url},"; done
local backend_urls="" backend_urls=${backend_urls%,}
local idx2=0
for profile in "${PROFILE_ARR[@]}"; do
idx2=$((idx2+1))
local host_port=$((backend_port + idx2 - 1))
local url="http://${backend_domain}:${host_port}${backend_path}/${api_version}/${profile}"
if [ -z "${backend_urls}" ]; then
backend_urls="${url}"
else
backend_urls="${backend_urls},${url}"
fi
done
frontend_block+=" frontend_block+="
osrm-frontend: osrm-frontend-${map_name}:
image: ${FRONTEND_IMAGE} image: ${DEFAULT_FRONTEND_REGISTRY}:${DEFAULT_FRONTEND_VERSION}
container_name: osrm-frontend container_name: osrm-frontend-${map_name}
restart: unless-stopped restart: unless-stopped
environment: environment:
- VITE_DEFAULT_BACKEND_URLS=${backend_urls} - VITE_DEFAULT_BACKEND_URLS=${backend_urls}
...@@ -538,365 +272,116 @@ generate_docker_compose() { ...@@ -538,365 +272,116 @@ generate_docker_compose() {
- ${network_name} - ${network_name}
" "
fi fi
# FIX: Removed the obsolete 'version' attribute
cat > "${COMPOSE_FILE}" <<EOF cat > "${COMPOSE_FILE}" <<EOF
services: services:${services}${frontend_block}
${services}
${frontend_block}
networks: networks:
${network_name}: ${network_name}:
external: true external: true
name: ${network_name} name: ${network_name}
EOF EOF
success "Docker Compose file written successfully."
success "docker-compose.yml written to ${COMPOSE_FILE}"
} }
# ------------------------- # -------------------------
# Ensure docker network # Command Implementations
# -------------------------
ensure_network() {
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would ensure docker network '${network_name}' exists"
return 0
fi
if docker network inspect "${network_name}" >/dev/null 2>&1; then
info "Docker network '${network_name}' exists"
else
info "Creating docker network '${network_name}'"
docker network create "${network_name}"
fi
}
# -------------------------
# Pull images
# -------------------------
pull_images() {
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would pull images"
return 0
fi
info "Pulling latest images..."
docker pull "${BACKEND_IMAGE}" || warn "docker pull backend failed"
if [ "${no_frontend}" != true ]; then
docker pull "${FRONTEND_IMAGE}" || warn "docker pull frontend failed"
fi
}
# ------------------------- # -------------------------
# Preprocess sequential run_install() {
# ------------------------- info "Starting OSRM installation for map instance: '${map_name}'"
preprocess_sequential() { if [ -f "${COMPOSE_FILE}" ] && ! ask_confirm "Instance '${map_name}' may exist. Re-running will recreate containers. Continue?"; then info "Installation aborted."; exit 0; fi
info "Starting sequential preprocessing for profiles: ${profiles_csv}" download_map; generate_profiles; generate_docker_compose
if [ ! -f "${MAP_PATH}" ]; then if [ "${dry_run}" = true ]; then warn "[DRY RUN] Skipping network and container startup."; return; fi
err "Map file not found at ${MAP_PATH}. Cannot preprocess." info "Ensuring Docker network '${network_name}' exists..."; docker network create "${network_name}" >/dev/null 2>&1 || info "Network already exists."
return 1 info "Pulling latest Docker images..."; docker pull "${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}"; if [[ "${no_frontend}" != "true" ]]; then docker pull "${DEFAULT_FRONTEND_REGISTRY}:${DEFAULT_FRONTEND_VERSION}"; fi
fi info "Starting data preprocessing..."
local any_failed=false
local idx=0
for profile in "${PROFILE_ARR[@]}"; do for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx+1)) local pdata_dir="${work_dir}/${map_name}/${profile}-data"
info "=== Preprocessing (${idx}/${#PROFILE_ARR[@]}): ${profile} ===" info "Preparing map file for profile '${profile}'..."
local pdata="${work_dir}/${profile}-data" cp -vf "${MAP_PATH}" "${pdata_dir}/map.osm.pbf"
mkdir -p "${pdata}" local service_name="osrm-preprocess-${map_name}-${profile}"
safe_copy_map "${MAP_PATH}" "${pdata}/map.osm.pbf" info "--- Processing profile: ${profile} ---"
local service_name="osrm-preprocess-${profile}"
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would run preprocessing for ${profile}"
continue
fi
# FIX: Use --exit-code-from for reliable error detection
info "Running preprocessor container for '${profile}'... This may take a long time."
if docker compose -f "${COMPOSE_FILE}" up --build --exit-code-from "${service_name}" "${service_name}"; then if docker compose -f "${COMPOSE_FILE}" up --build --exit-code-from "${service_name}" "${service_name}"; then
success "Preprocessing for '${profile}' completed successfully." success "Preprocessing for '${profile}' completed."
else else
err "Preprocessing for '${profile}' FAILED. Check container logs above for details." err "Preprocessing for '${profile}' FAILED. Check logs for details."
any_failed=true docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
if ! ask_confirm "Preprocessing for '${profile}' failed. Continue with other profiles?"; then if ! ask_confirm "Continue with other profiles?"; then err "Aborting installation."; exit 1; fi
err "Aborting installation due to preprocessing failure."
exit 1
fi
fi fi
# Clean up the one-shot container
docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
done done
info "Starting all services for instance '${map_name}'..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
if [[ "${any_failed}" == "true" ]]; then success "Installation for '${map_name}' completed!"; print_summary
warn "One or more preprocessing steps failed. The corresponding routing services will not be started."
fi
}
safe_copy_map() {
local src="$1"; local dest="$2"
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would copy ${src} -> ${dest}"
return 0
fi
mkdir -p "$(dirname "${dest}")"
if [ ! -f "${src}" ]; then
err "Map source file does not exist: ${src}"
exit 1
fi
info "Copying map to processing directory: ${dest}"
cp -f "${src}" "${dest}"
} }
run_update() {
# ------------------------- info "Starting update for instance '${map_name}', component '${sub_command}'"
# Start routed services if [ ! -f "${COMPOSE_FILE}" ]; then err "Instance '${map_name}' not found."; exit 1; fi
# ------------------------- case "${sub_command}" in
start_routed_services() { backend|frontend|all)
info "Starting routed services..." info "Pulling latest images..."; docker pull "${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}"
if [ "${dry_run}" = true ]; then if [[ "${sub_command}" != "backend" ]]; then docker pull "${DEFAULT_FRONTEND_REGISTRY}:${DEFAULT_FRONTEND_VERSION}"; fi
warn "[DRY RUN] Would start routed containers via docker compose" info "Recreating services..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
return 0 ;;
fi map|profile)
if [[ "${sub_command}" == "map" ]]; then download_map; fi
local services_to_start=() if [[ "${sub_command}" == "profile" ]]; then generate_profiles; fi
info "Re-running preprocessing..."
for profile in "${PROFILE_ARR[@]}"; do for profile in "${PROFILE_ARR[@]}"; do
local pdata="${work_dir}/${profile}-data" local pdata_dir="${work_dir}/${map_name}/${profile}-data"
# Check if preprocessing was successful by looking for the output files info "Preparing map file for profile '${profile}'..."
if [ -f "${pdata}/map.osrm" ]; then cp -vf "${MAP_PATH}" "${pdata_dir}/map.osm.pbf"
services_to_start+=("osrm-routed-${profile}") local service_name="osrm-preprocess-${map_name}-${profile}"
else docker compose -f "${COMPOSE_FILE}" up --build --exit-code-from "${service_name}" "${service_name}" || err "Preprocessing failed for ${profile}"
warn "Skipping 'osrm-routed-${profile}' because preprocessed data was not found in ${pdata}" docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
fi
done done
info "Restarting services..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
if [ ${#services_to_start[@]} -eq 0 ]; then ;;
err "No successfully preprocessed profiles found. Cannot start any routing services." *) err "Invalid update component."; exit 1;;
return 1 esac
fi success "Update for '${map_name}' completed."
info "Starting services: ${services_to_start[*]}"
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans "${services_to_start[@]}"
success "Routed services started."
} }
run_remove() {
# ------------------------- info "Starting removal of OSRM instance: '${map_name}'"
# Start frontend if [ ! -f "${COMPOSE_FILE}" ]; then warn "Instance '${map_name}' not found."; fi
# ------------------------- if ask_confirm "This will STOP and REMOVE all containers for instance '${map_name}'. Are you sure?"; then
start_frontend() { if [ -f "${COMPOSE_FILE}" ]; then docker compose -f "${COMPOSE_FILE}" down -v || warn "Could not stop containers."; rm -f "${COMPOSE_FILE}"; fi
if [ "${no_frontend}" = true ]; then success "All containers for '${map_name}' have been removed."
info "Frontend installation skipped (--no-frontend)" if [[ "${do_purge}" == "true" ]]; then
return 0 local instance_dir="${work_dir}/${map_name}"
if ask_confirm "PURGE ENABLED. This will DELETE '${instance_dir}'. IRREVERSIBLE. Proceed?"; then
rm -rf "${instance_dir}"; success "Instance data for '${map_name}' has been purged."
fi fi
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would start osrm-frontend via docker compose"
return 0
fi fi
info "Starting frontend service..." else info "Removal aborted by user."; fi
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans osrm-frontend
success "Frontend started on host port ${frontend_port}"
} }
print_summary() {
# ------------------------- echo "--- Instance '${map_name}' Summary ---"
# NGPM hints echo "Compose File: ${COMPOSE_FILE}"; echo "Map Source: ${MAP_PATH}"; echo "Profiles: ${profiles_csv}"
# ------------------------- echo; echo "ACCESS POINTS (Example):"
generate_ngpm_hints() {
info "Generating NGPM hints at ${NGPM_HINTS}"
{
echo "--- OSRM & NGPM (Nginx Proxy Manager) Configuration ---"
echo "Date: $(date)"
echo
echo "This file provides hints for configuring your public-facing domains in NGPM."
echo "Ensure your NGPM container is connected to the '${network_name}' Docker network."
echo
echo "BACKEND SERVICES:"
printf "%-15s | %-25s | %-35s\n" "Profile" "Container Target" "Public URL Example"
echo "--------------------------------------------------------------------------------"
local idx=0
for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx+1))
local host_port=$((backend_port + idx - 1))
local container_target="osrm-routed-${profile}:5000"
local url_example="https://${backend_domain}${backend_path}/${api_version}/${profile}"
printf "%-15s | %-25s | %-35s\n" "${profile}" "${container_target}" "${url_example}"
done
echo
if [ "${no_frontend}" != true ]; then
echo "FRONTEND UI:"
printf "%-15s | %-25s | %-35s\n" "UI" "osrm-frontend:9966" "https://${frontend_domain}"
echo "--------------------------------------------------------------------------------"
fi
} > "${NGPM_HINTS}"
success "NGPM hints saved to ${NGPM_HINTS}"
info "--- NGPM Hints Preview ---"
cat "${NGPM_HINTS}"
info "------------------------"
}
# -------------------------
# Print config
# -------------------------
print_configuration() {
echo "--- Installation Summary ---"
echo "Work Directory: ${work_dir}"
echo "Docker Compose: ${COMPOSE_FILE}"
echo "Docker Network: ${network_name}"
echo "Map Source Used: ${MAP_PATH}"
echo "Profiles Active: ${profiles_csv}"
echo
echo "ACCESS POINTS:"
local idx=0 local idx=0
for profile in "${PROFILE_ARR[@]}"; do for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx + 1)) idx=$((idx + 1)); local host_port=$((backend_port + idx - 1))
local host_port=$((backend_port + idx - 1)) echo " - Backend (${profile}): http://localhost:${host_port}${backend_path}/${api_version}/${profile}"
echo " - Backend (${profile}): http://${backend_domain}:${host_port}${backend_path}/${api_version}/${profile}" echo " (NGPM Target: osrm-routed-${map_name}-${profile}:5000)"
done done
if [ "${no_frontend}" != true ]; then if [ "${no_frontend}" != true ]; then
echo " - Frontend UI: http://${frontend_domain}:${frontend_port}" echo " - Frontend UI: http://localhost:${frontend_port}"; echo " (NGPM Target: osrm-frontend-${map_name}:9966)"
fi fi
echo "--------------------------" echo "--------------------------"
} }
# ------------------------- # -------------------------
# Update logic # Main Dispatcher
# -------------------------
run_update() {
info "Update flow started for component: ${sub_command}"
prereqs_check
case "${sub_command}" in
backend)
pull_images
start_routed_services
;;
frontend)
pull_images
start_frontend
;;
map)
select_map
preprocess_sequential
start_routed_services
;;
profile)
generate_profile_wrappers
create_patched_profiles
preprocess_sequential
start_routed_services
;;
all)
pull_images
generate_profile_wrappers
create_patched_profiles
select_map
preprocess_sequential
start_routed_services
start_frontend
;;
*)
err "Invalid update component '${sub_command}'. Use one of: backend, frontend, map, profile, all."
exit 1
;;
esac
success "Update flow completed."
}
# -------------------------
# Remove logic
# ------------------------- # -------------------------
run_remove() { main() {
info "Remove flow started for component: ${sub_command}" parse_args "$@"
if [ ! -f "${COMPOSE_FILE}" ]; then prereqs_check; setup_selinux_flag
warn "Docker compose file not found at ${COMPOSE_FILE}. Assuming components are already removed." info "Starting script execution."; info "Command: ${command_name}, Map Instance: ${map_name}"
fi case "${command_name}" in
install) run_install ;;
local services_to_remove=() update) run_update ;;
case "${sub_command}" in remove) run_remove ;;
backend) *) err "Unknown command: ${command_name}"; usage ;;
for profile in "${PROFILE_ARR[@]}"; do
services_to_remove+=("osrm-routed-${profile}")
done
;;
frontend)
services_to_remove+=("osrm-frontend")
;;
profile)
if ask_confirm "This will delete all profile files from ${profile_dir} and ${patched_dir}. Continue?"; then
rm -rf "${profile_dir}" "${patched_dir}"
success "Profile directories removed."
fi
return
;;
all)
if ask_confirm "This will STOP and REMOVE ALL OSRM containers. Proceed?"; then
docker compose -f "${COMPOSE_FILE}" down -v --remove-orphans || warn "Could not stop all containers. They may not be running."
success "All containers stopped and removed."
if [[ "${do_purge}" == "true" ]]; then
if ask_confirm "PURGE active. This will DELETE the entire work directory: ${work_dir}. THIS IS IRREVERSIBLE. Proceed?"; then
rm -rf "${work_dir}"
success "Work directory purged."
fi
fi
fi
return
;;
*)
err "Invalid remove component '${sub_command}'. Use one of: backend, frontend, profile, all."
exit 1
;;
esac esac
info "Script execution finished."
if [ ${#services_to_remove[@]} -gt 0 ]; then
info "Stopping and removing services: ${services_to_remove[*]}"
docker compose -f "${COMPOSE_FILE}" rm -fsv "${services_to_remove[@]}" || warn "Failed to remove some services."
if [[ "${do_purge}" == "true" ]]; then
info "Purging data for removed services..."
for profile in "${PROFILE_ARR[@]}"; do
if [[ "${sub_command}" == "backend" || "${sub_command}" == "all" ]]; then
rm -rf "${work_dir}/${profile}-data"
info "Purged ${work_dir}/${profile}-data"
fi
done
fi
fi
success "Remove flow completed."
} }
# ------------------------- main "$@"
# Main dispatcher \ No newline at end of file
# -------------------------
case "${command_name}" in
install)
info "Install flow started"
prereqs_check
select_map
generate_profile_wrappers
create_patched_profiles
pull_images
generate_docker_compose
ensure_network
preprocess_sequential
start_routed_services
start_frontend
generate_ngpm_hints
success "Install flow completed"
print_configuration
;;
update)
run_update
;;
remove)
run_remove
;;
*)
err "Unknown command: ${command_name}"
usage
;;
esac
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment