Commit 59b40cbf by Bambang Adrian

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

parent b4677cf5
......@@ -2,102 +2,63 @@
#
# install-osrm-ngpm.sh
# 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
IFS=$'\n\t'
# -------------------------
# DEFAULTS (internal variables use snake_case)
# DEFAULTS
# -------------------------
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_FRONTEND_REGISTRY="ghcr.io/project-osrm/osrm-frontend"
DEFAULT_BACKEND_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_PROFILES="car"
DEFAULT_BACKEND_PORT=5000
DEFAULT_FRONTEND_PORT=9966
DEFAULT_BACKEND_DOMAIN="localhost"
DEFAULT_FRONTEND_DOMAIN="localhost"
DEFAULT_BACKEND_DOMAIN="osrm.example.com"
DEFAULT_FRONTEND_DOMAIN="map.example.com"
DEFAULT_BACKEND_PATH="/osrm"
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}"
map_dir="${DEFAULT_MAP_DIR}"
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_name="default"
map_source="${DEFAULT_MAP_SOURCE}"
network_name="${DEFAULT_NETWORK}"
profiles_csv="${DEFAULT_PROFILES}"
network_name="${DEFAULT_NETWORK}"
backend_port="${DEFAULT_BACKEND_PORT}"
frontend_port="${DEFAULT_FRONTEND_PORT}"
backend_domain="${DEFAULT_BACKEND_DOMAIN}"
frontend_domain="${DEFAULT_FRONTEND_DOMAIN}"
backend_path="${DEFAULT_BACKEND_PATH}"
api_version="${DEFAULT_API_VERSION}"
no_frontend="${DEFAULT_NO_FRONTEND}"
no_backend="${DEFAULT_NO_BACKEND}"
no_frontend=false
dry_run=false
print_config=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
map_arg="" # explicit --map-source given (URL or path) override var map_source
# derived
MAP_PATH=""
COMPOSE_FILE=""
LOGFILE=""
NGPM_HINTS=""
MAP_PATH=""
PROFILE_ARR=()
# -------------------------
# Logging helpers
# -------------------------
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() {
local lvl="$1"; shift
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" "$*"; }
success() { _log "SUCCESS" "$*"; }
......@@ -109,194 +70,94 @@ err() { _log "ERROR" "$*" >&2; }
# -------------------------
usage() {
cat <<EOF
install-osrm-ngpm.sh - OSRM + NGPM installer & manager (Rocky Linux 10)
OSRM Installer & Manager v2.4 (Multi-Map Support)
Usage:
$0 <command> [options]
$0 <command> --map-name <name> [options]
Commands:
install Install and run OSRM backend + frontend (unless disabled)
update <component> Update a specific component (backend, frontend, map, profile, all)
remove <component> Remove a specific component (backend, frontend, profile, all)
Install Options:
--map-source <url|path> Map URL or local file path (default: ${DEFAULT_MAP_SOURCE})
--profiles <csv> Profiles to enable (car,motorcycle,bike,foot). Default '${DEFAULT_PROFILES}'
--backend-domain <domain> Domain for backend URLs (default: ${DEFAULT_BACKEND_DOMAIN})
--frontend-domain <domain> Domain for frontend (default: ${DEFAULT_FRONTEND_DOMAIN})
--backend-port <port> Host start port for backend mapping (default: ${DEFAULT_BACKEND_PORT})
--frontend-port <port> Host port for frontend UI (default: ${DEFAULT_FRONTEND_PORT})
--no-frontend Do NOT install frontend (frontend is installed by default)
--no-backend Do NOT install backend
--dry-run Simulate steps
--yes Non-interactive (answer Yes to all prompts)
-h, --help Show this help
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
install Install a new OSRM instance.
update <component> Update a component (backend, frontend, map, profile, all).
remove Remove an OSRM instance.
Required Argument:
--map-name <name> A unique name for this map instance (e.g., 'indonesia').
Options:
--map-source <url|path> Map URL or local file path.
--profiles <csv> Profiles to enable (car,motorcycle,bike,foot).
--backend-port <port> Host start port for backend services.
--frontend-port <port> Host port for the frontend UI.
--no-frontend Do NOT install the frontend UI.
--purge Also delete all data directories on remove.
--dry-run Simulate steps without executing.
--yes Answer 'yes' to all prompts.
-h, --help Show this help message.
EOF
exit 0
}
# -------------------------
# Parse args
# Argument Parsing & Setup
# -------------------------
if [ $# -eq 0 ]; then usage; fi
command_name="$1"; shift || true
# Sub-command for update/remove
sub_command=""
if [[ "${command_name}" == "update" || "${command_name}" == "remove" ]]; then
if [ $# -gt 0 ] && ! [[ "$1" =~ ^-- ]]; then
sub_command="$1"
shift
parse_args() {
if [ $# -eq 0 ]; then usage; fi
command_name="$1"; shift || true
sub_command=""
if [[ "${command_name}" == "update" ]]; then
if [ $# -gt 0 ] && ! [[ "$1" =~ ^-- ]]; then sub_command="$1"; shift; else err "Update command requires a component."; usage; fi
fi
fi
while (( "$#" )); do
while (( "$#" )); do
case "$1" in
--map-name) map_name="$2"; shift 2 ;;
--work-dir) work_dir="${2/#\~/$HOME}"; shift 2 ;;
--map-source) map_source="${2}"; map_arg="${2}"; shift 2 ;;
--map-dir) map_dir="${2/#\~/$HOME}"; 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 ;;
--map-source) map_source="$2"; shift 2 ;;
--profiles) profiles_csv="$2"; shift 2 ;;
--backend-port) backend_port="$2"; shift 2 ;;
--frontend-port) frontend_port="$2"; shift 2 ;;
--network) network_name="$2"; shift 2 ;;
--backend-domain) backend_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-backend) no_backend=true; shift ;;
--dry-run) dry_run=true; shift ;;
--print-config) print_config=true; shift ;;
--purge) do_purge=true; shift ;;
--yes) yes_all=true; shift ;;
-h|--help) usage ;;
*) warn "Unknown option: $1"; shift ;;
*) err "Unknown option: $1"; usage ;;
esac
done
# normalize & mkdirs
work_dir="${work_dir/#\~/$HOME}"
map_dir="${map_dir/#\~/$HOME}"
profile_dir="${profile_dir/#\~/$HOME}"
patched_dir="${patched_dir/#\~/$HOME}"
backup_dir="${backup_dir/#\~/$HOME}"
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}"
done
if [[ "${map_name}" == "default" && "${command_name}" != "install" ]]; then err "The --map-name argument is required."; usage; fi
work_dir="${work_dir/#\~/$HOME}"; local log_dir="${work_dir}/logs"; mkdir -p "${work_dir}" "${log_dir}"
LOGFILE="${log_dir}/osrm-manager-$(date '+%Y%m%d_%H%M%S').log"
: > "${LOGFILE}"
COMPOSE_FILE="${work_dir}/docker-compose.${map_name}.yml"
IFS=',' read -r -a PROFILE_ARR <<< "$(echo "${profiles_csv}" | tr -d '[:space:]')"
if [ "${#PROFILE_ARR[@]}" -eq 0 ]; then PROFILE_ARR=(car); fi
}
# -------------------------
# Helper functions
# Helper Functions
# -------------------------
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() {
info "Running prerequisite checks..."
local ok=0
if ! command_exists docker; then
err "docker not found. Please install docker and ensure the docker daemon is running."
ok=1
fi
if ! docker compose version >/dev/null 2>&1; then
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
if ! command_exists docker; then err "Docker is not installed."; ok=1; fi
if ! docker compose version >/dev/null 2>&1; then err "Docker Compose V2 plugin is not installed."; ok=1; fi
if ! command_exists curl; then err "curl is not installed."; ok=1; fi
if [ ${ok} -ne 0 ]; then err "Prerequisite checks failed."; exit 1; fi
success "All prerequisites are met."
}
# SELinux label detection for mounts
SELINUX_LABEL=""
if command_exists sestatus && sestatus 2>/dev/null | grep -qi "SELinux status:.*enabled"; then
SELINUX_LABEL=":z"
info "SELinux is enabled. Will append ':z' to bind mounts for context."
fi
# prompt helper
ask_confirm() {
local prompt="$1"; shift
if [ "${yes_all}" = true ]; then
info "[auto-yes] ${prompt} -> yes"
return 0
setup_selinux_flag() {
if command_exists sestatus && sestatus 2>/dev/null | grep -qi "SELinux status:.*enabled"; then
SELINUX_LABEL=",z"
info "SELinux is enabled. Appending ',z' to volume mounts."
fi
}
ask_confirm() {
local prompt="$1"
if [ "${yes_all}" = true ]; then info "[auto-yes] ${prompt} -> yes"; return 0; fi
while true; do
printf "%s [y/N]: " "${prompt}"
read -r yn
......@@ -309,198 +170,83 @@ ask_confirm() {
}
# -------------------------
# Map selection logic
# Core Logic
# -------------------------
select_map() {
info "Selecting map..."
local map_target_source="${map_arg:-${map_source}}"
if [[ "${map_target_source}" =~ ^https?:// ]]; then
MAP_PATH="${map_dir}/$(basename "${map_target_source}")"
download_map_if_needed "${map_target_source}" "${MAP_PATH}"
return 0
fi
if [ -f "${map_target_source}" ]; then
MAP_PATH="$(realpath "${map_target_source}")"
info "Using user-provided local map: ${MAP_PATH}"
return 0
download_map() {
info "Preparing map from source: ${map_source}"
local map_instance_dir="${work_dir}/${map_name}/dataset"
mkdir -p "${map_instance_dir}"
MAP_PATH="${map_instance_dir}/$(basename "${map_source}")"
if [[ -f "${MAP_PATH}" ]]; then info "Map file already exists. Skipping download."; return 0; fi
if [[ "${map_source}" =~ ^https?:// ]]; then
if [ "${dry_run}" = true ]; then warn "[DRY RUN] Would download map to ${MAP_PATH}"; return 0; 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."
elif [[ -f "${map_source}" ]]; then
if [ "${dry_run}" = true ]; then warn "[DRY RUN] Would copy local map to ${MAP_PATH}"; return 0; fi
info "Copying local map file..."; cp -v "${map_source}" "${MAP_PATH}"; success "Local map copied."
else err "Map source '${map_source}' is not a valid URL or an existing file."; exit 1;
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}"
}
download_map_if_needed() {
local url="$1"; local dest="$2"
info "Preparing map: ${url} -> ${dest}"
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}"
generate_profiles() {
info "Generating profile files for map instance '${map_name}'"
local profile_dir="${work_dir}/${map_name}/profiles"; local patched_dir="${work_dir}/${map_name}/patched-profiles"
mkdir -p "${profile_dir}" "${patched_dir}"
for profile in "${PROFILE_ARR[@]}"; do
local pf="${profile_dir}/${profile}.lua"
if [ -f "${pf}" ]; then
info "Profile exists: ${pf}"
continue
fi
if [ ! -f "${pf}" ]; then
case "${profile}" in
car|bicycle|bike|foot)
info "Generating wrapper for ${profile}"
# This generic wrapper loads the built-in profile from the container
info "Generating wrapper for built-in profile: ${profile}"
# --- FIX IS HERE (v2.4) ---
# The correct path inside the container is /opt/*.lua, not /opt/profiles/*.lua
cat > "${pf}" <<LUA
-- wrapper: attempt to load builtin profile '${profile}'
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}')
return dofile('/opt/${profile}.lua')
LUA
success "Generated wrapper: ${pf}"
;;
motorcycle)
info "Generating motorcycle wrapper based on car"
# FIX: This version correctly modifies the car profile without breaking its internal scope.
info "Generating custom 'motorcycle' profile based on 'car'"
# --- FIX IS HERE (v2.4) ---
cat > "${pf}" <<'LUA'
-- motorcycle wrapper derived from car profile (v2 - Robust)
local function load_builtin(name)
local candidates = {
"/opt/profiles/" .. name .. ".lua",
"/opt/" .. name .. ".lua"
}
for _, path in ipairs(candidates) do
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.
local car_module = dofile('/opt/car.lua')
local profile = car_module.setup()
profile.properties.max_speed = 100
profile.properties.u_turn_penalty = 60
profile.restrictions = profile.restrictions or {}
profile.restrictions["toll"] = "prohibited"
car_module.setup = function() return profile end
return car_module
LUA
success "Generated motorcycle wrapper: ${pf}"
;;
*)
warn "No generator for profile '${profile}'. Provide ${pf} manually."
;;
*) warn "No generator for profile '${profile}'."; continue;;
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() {
info "Generating docker-compose.yml at ${COMPOSE_FILE}"
mkdir -p "$(dirname "${COMPOSE_FILE}")"
local selabel="${SELINUX_LABEL:-}"
local services=""
local idx=0
info "Generating Docker Compose file: ${COMPOSE_FILE}"
local map_instance_dir="${work_dir}/${map_name}"; local selabel="${SELINUX_LABEL:-}"; local services=""; local idx=0
for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx+1))
local host_port=$((backend_port + idx - 1))
local pdata="${work_dir}/${profile}-data"
mkdir -p "${pdata}"
idx=$((idx+1)); local host_port=$((backend_port + idx - 1)); local pdata="${map_instance_dir}/${profile}-data"; mkdir -p "${pdata}"
services+="
osrm-preprocess-${profile}:
image: ${BACKEND_IMAGE}
container_name: osrm-preprocess-${profile}
osrm-preprocess-${map_name}-${profile}:
image: ${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}
container_name: osrm-preprocess-${map_name}-${profile}
command: >
sh -c \"osrm-extract -p /opt/custom_profiles/${profile}.lua /data/map.osm.pbf &&
osrm-partition /data/map.osrm &&
osrm-customize /data/map.osrm\"
volumes:
- ${patched_dir}:/opt/custom_profiles:ro${selabel}
- ${pdata}:/data${selabel}
- ${map_instance_dir}/patched-profiles:/opt/custom_profiles:ro${selabel}
- ${pdata}:/data:rw${selabel}
networks:
- ${network_name}
osrm-routed-${profile}:
image: ${BACKEND_IMAGE}
container_name: osrm-routed-${profile}
osrm-routed-${map_name}-${profile}:
image: ${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}
container_name: osrm-routed-${map_name}-${profile}
restart: unless-stopped
depends_on:
- osrm-preprocess-${profile}
command: osrm-routed --algorithm mld --max-table-size 10000 /data/map.osrm
command: osrm-routed --algorithm mld /data/map.osrm
volumes:
- ${pdata}:/data:ro${selabel}
ports:
......@@ -509,26 +255,14 @@ generate_docker_compose() {
- ${network_name}
"
done
local frontend_block=""
if [ "${no_frontend}" != true ]; then
local 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
local frontend_block=""; if [ "${no_frontend}" != true ]; then
local 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}"; backend_urls+="${url},"; done
backend_urls=${backend_urls%,}
frontend_block+="
osrm-frontend:
image: ${FRONTEND_IMAGE}
container_name: osrm-frontend
osrm-frontend-${map_name}:
image: ${DEFAULT_FRONTEND_REGISTRY}:${DEFAULT_FRONTEND_VERSION}
container_name: osrm-frontend-${map_name}
restart: unless-stopped
environment:
- VITE_DEFAULT_BACKEND_URLS=${backend_urls}
......@@ -538,365 +272,116 @@ generate_docker_compose() {
- ${network_name}
"
fi
# FIX: Removed the obsolete 'version' attribute
cat > "${COMPOSE_FILE}" <<EOF
services:
${services}
${frontend_block}
services:${services}${frontend_block}
networks:
${network_name}:
external: true
name: ${network_name}
EOF
success "docker-compose.yml written to ${COMPOSE_FILE}"
success "Docker Compose file written successfully."
}
# -------------------------
# Ensure docker network
# -------------------------
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
}
# Command Implementations
# -------------------------
# Preprocess sequential
# -------------------------
preprocess_sequential() {
info "Starting sequential preprocessing for profiles: ${profiles_csv}"
if [ ! -f "${MAP_PATH}" ]; then
err "Map file not found at ${MAP_PATH}. Cannot preprocess."
return 1
fi
local any_failed=false
local idx=0
run_install() {
info "Starting OSRM installation for map instance: '${map_name}'"
if [ -f "${COMPOSE_FILE}" ] && ! ask_confirm "Instance '${map_name}' may exist. Re-running will recreate containers. Continue?"; then info "Installation aborted."; exit 0; fi
download_map; generate_profiles; generate_docker_compose
if [ "${dry_run}" = true ]; then warn "[DRY RUN] Skipping network and container startup."; return; fi
info "Ensuring Docker network '${network_name}' exists..."; docker network create "${network_name}" >/dev/null 2>&1 || info "Network already exists."
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
info "Starting data preprocessing..."
for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx+1))
info "=== Preprocessing (${idx}/${#PROFILE_ARR[@]}): ${profile} ==="
local pdata="${work_dir}/${profile}-data"
mkdir -p "${pdata}"
safe_copy_map "${MAP_PATH}" "${pdata}/map.osm.pbf"
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."
local pdata_dir="${work_dir}/${map_name}/${profile}-data"
info "Preparing map file for profile '${profile}'..."
cp -vf "${MAP_PATH}" "${pdata_dir}/map.osm.pbf"
local service_name="osrm-preprocess-${map_name}-${profile}"
info "--- Processing profile: ${profile} ---"
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
err "Preprocessing for '${profile}' FAILED. Check container logs above for details."
any_failed=true
if ! ask_confirm "Preprocessing for '${profile}' failed. Continue with other profiles?"; then
err "Aborting installation due to preprocessing failure."
exit 1
fi
err "Preprocessing for '${profile}' FAILED. Check logs for details."
docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
if ! ask_confirm "Continue with other profiles?"; then err "Aborting installation."; exit 1; fi
fi
# Clean up the one-shot container
docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
done
if [[ "${any_failed}" == "true" ]]; then
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}"
info "Starting all services for instance '${map_name}'..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
success "Installation for '${map_name}' completed!"; print_summary
}
# -------------------------
# Start routed services
# -------------------------
start_routed_services() {
info "Starting routed services..."
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would start routed containers via docker compose"
return 0
fi
local services_to_start=()
run_update() {
info "Starting update for instance '${map_name}', component '${sub_command}'"
if [ ! -f "${COMPOSE_FILE}" ]; then err "Instance '${map_name}' not found."; exit 1; fi
case "${sub_command}" in
backend|frontend|all)
info "Pulling latest images..."; docker pull "${DEFAULT_BACKEND_REGISTRY}:${DEFAULT_BACKEND_VERSION}"
if [[ "${sub_command}" != "backend" ]]; then docker pull "${DEFAULT_FRONTEND_REGISTRY}:${DEFAULT_FRONTEND_VERSION}"; fi
info "Recreating services..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
;;
map|profile)
if [[ "${sub_command}" == "map" ]]; then download_map; fi
if [[ "${sub_command}" == "profile" ]]; then generate_profiles; fi
info "Re-running preprocessing..."
for profile in "${PROFILE_ARR[@]}"; do
local pdata="${work_dir}/${profile}-data"
# Check if preprocessing was successful by looking for the output files
if [ -f "${pdata}/map.osrm" ]; then
services_to_start+=("osrm-routed-${profile}")
else
warn "Skipping 'osrm-routed-${profile}' because preprocessed data was not found in ${pdata}"
fi
local pdata_dir="${work_dir}/${map_name}/${profile}-data"
info "Preparing map file for profile '${profile}'..."
cp -vf "${MAP_PATH}" "${pdata_dir}/map.osm.pbf"
local service_name="osrm-preprocess-${map_name}-${profile}"
docker compose -f "${COMPOSE_FILE}" up --build --exit-code-from "${service_name}" "${service_name}" || err "Preprocessing failed for ${profile}"
docker compose -f "${COMPOSE_FILE}" rm -fsv "${service_name}" >/dev/null 2>&1 || true
done
if [ ${#services_to_start[@]} -eq 0 ]; then
err "No successfully preprocessed profiles found. Cannot start any routing services."
return 1
fi
info "Starting services: ${services_to_start[*]}"
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans "${services_to_start[@]}"
success "Routed services started."
info "Restarting services..."; docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
;;
*) err "Invalid update component."; exit 1;;
esac
success "Update for '${map_name}' completed."
}
# -------------------------
# Start frontend
# -------------------------
start_frontend() {
if [ "${no_frontend}" = true ]; then
info "Frontend installation skipped (--no-frontend)"
return 0
run_remove() {
info "Starting removal of OSRM instance: '${map_name}'"
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
if [ -f "${COMPOSE_FILE}" ]; then docker compose -f "${COMPOSE_FILE}" down -v || warn "Could not stop containers."; rm -f "${COMPOSE_FILE}"; fi
success "All containers for '${map_name}' have been removed."
if [[ "${do_purge}" == "true" ]]; then
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
if [ "${dry_run}" = true ]; then
warn "[DRY RUN] Would start osrm-frontend via docker compose"
return 0
fi
info "Starting frontend service..."
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans osrm-frontend
success "Frontend started on host port ${frontend_port}"
else info "Removal aborted by user."; fi
}
# -------------------------
# NGPM hints
# -------------------------
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:"
print_summary() {
echo "--- Instance '${map_name}' Summary ---"
echo "Compose File: ${COMPOSE_FILE}"; echo "Map Source: ${MAP_PATH}"; echo "Profiles: ${profiles_csv}"
echo; echo "ACCESS POINTS (Example):"
local idx=0
for profile in "${PROFILE_ARR[@]}"; do
idx=$((idx + 1))
local host_port=$((backend_port + idx - 1))
echo " - Backend (${profile}): http://${backend_domain}:${host_port}${backend_path}/${api_version}/${profile}"
idx=$((idx + 1)); local host_port=$((backend_port + idx - 1))
echo " - Backend (${profile}): http://localhost:${host_port}${backend_path}/${api_version}/${profile}"
echo " (NGPM Target: osrm-routed-${map_name}-${profile}:5000)"
done
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
echo "--------------------------"
}
# -------------------------
# Update logic
# -------------------------
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
# Main Dispatcher
# -------------------------
run_remove() {
info "Remove flow started for component: ${sub_command}"
if [ ! -f "${COMPOSE_FILE}" ]; then
warn "Docker compose file not found at ${COMPOSE_FILE}. Assuming components are already removed."
fi
local services_to_remove=()
case "${sub_command}" in
backend)
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
;;
main() {
parse_args "$@"
prereqs_check; setup_selinux_flag
info "Starting script execution."; info "Command: ${command_name}, Map Instance: ${map_name}"
case "${command_name}" in
install) run_install ;;
update) run_update ;;
remove) run_remove ;;
*) err "Unknown command: ${command_name}"; usage ;;
esac
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."
info "Script execution finished."
}
# -------------------------
# Main dispatcher
# -------------------------
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
main "$@"
\ 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