#!/bin/bash set -euo pipefail # --- UE profile map (index 0..10). 0-index kept for convenience --- # Initialize with "0" to denote no profile chosen. declare -a UE_PROFILE_MAP for i in {0..10}; do UE_PROFILE_MAP[$i]=0; done # ---------------------------[ Random / diurnal Mode ]---------------------------- # UE scaler mode: 'random' (rapid random targets) or 'diurnal' (legacy cosine) UE_SCALER_MODE="${UE_SCALER_MODE:-random}" # Interval (sec) for scaler ticks UE_SCALER_INTERVAL_SEC="${UE_SCALER_INTERVAL_SEC:-30}" # -------------------------------[ Config ]------------------------------------ ARTIFACT_DIR="/media/nakaolab-go/2nd/artifacts" mkdir -p "$ARTIFACT_DIR" POWER_LOG_FILE="${POWER_LOG_FILE:-$ARTIFACT_DIR/power_usage.csv}" CPU_PROXY_LOG_FILE="${CPU_PROXY_LOG_FILE:-$ARTIFACT_DIR/cpu_proxy.csv}" RRC_SINCE_STATE="${RRC_SINCE_STATE:-$ARTIFACT_DIR/.rrc_since}" # xApp (mac stats) container + path inside container STATS_CONTAINER="${STATS_CONTAINER:-xapp-chatgpt}" MAC_STATS_FILE_IN_CONTAINER="${MAC_STATS_FILE_IN_CONTAINER:-/tmp/mac_stats.log}" PDCP_STATS_FILE_IN_CONTAINER="${PDCP_STATS_FILE_IN_CONTAINER:-/tmp/pdcp_stats.log}" # Intervals (seconds) RRC_LOG_INTERVAL_SEC="${RRC_LOG_INTERVAL_SEC:-10}" POWER_LOG_INTERVAL_SEC="${POWER_LOG_INTERVAL_SEC:-5}" CPU_PROXY_INTERVAL_SEC="${CPU_PROXY_INTERVAL_SEC:-5}" # Docker compose helper DC_BIN=(docker compose) dc() { command "${DC_BIN[@]}" "$@"; } dc down -v --remove-orphans # Service names (override if your compose uses different ones) AMF_SERVICE="${AMF_SERVICE:-oai-amf}" UPF_SERVICE="${UPF_SERVICE:-oai-upf}" GNB_SERVICE="${GNB_SERVICE:-oai-gnb}" XAPP_SERVICE="${XAPP_SERVICE:-xapp-chatgpt}" # UE service name pattern (for discovery) UE_NAME_REGEX="${UE_NAME_REGEX:-^oai-nr-ue}" # Test controls TEST_MODE="${TEST_MODE:-0}" # 1=manual profiles allowed (TEST_* honored); 0=auto TEST_UES="${TEST_UES:-}" TEST_PROFILES="${TEST_PROFILES:-}" # profile name TEST_DURATION_SEC="${TEST_DURATION_SEC:-0}" # 0 = run indefinitely # Diurnal scaling controls (normal mode only; ignored if TEST_UE is set) UE_MIN="${UE_MIN:-1}" # Minimum number of UEs to keep connected UE_MAX="${UE_MAX:-10}" UE_PEAK_HOUR="${UE_PEAK_HOUR:-17}" # Local hour with peak load (0..23) UE_NOISE_FRAC="${UE_NOISE_FRAC:-0.15}" # ±fractional noise applied to target # -------------------------[ Power Model (mW) ]-------------------------------- declare -A PWR_MW_PROFILE=( [idle]=60 # screen on, modem camped, no transfers [iot_chatty]=150 # tiny bursts, long sleeps [browsing]=380 # short transfers with think-time [voip]=520 # steady uplink/downlink small packets [video]=850 # continuous DL; 720–1080p [gaming]=950 # bursty DL + UL keepalives [boring]=500 # for single PRB testing [bulk_download]=1100 # sustained UL/DL at MCS ceiling ) # -------------------------------[ Utils ]------------------------------------- ts_now() { date +%s; } ts_iso() { date -Is; } ts_tag() { date +%Y%m%d_%H%M%S; } log() { echo "[$(ts_iso)] $*"; } exec_safe() { local svc="$1"; shift dc exec -T "$svc" bash -lc "$*" } # -----------------------------[ Discovery ]----------------------------------- list_all_services() { dc config --services; } get_all_ue_services() { list_all_services | grep -E "${UE_NAME_REGEX}" || true; } get_core_services() { # everything except UEs, gNB, xApp list_all_services | grep -Ev "(${UE_NAME_REGEX}|^${GNB_SERVICE}$|^${XAPP_SERVICE}$)" || true } get_running_ues() { dc ps --services --filter "status=running" \ | grep -E "${UE_NAME_REGEX}" || true } # -------------------------[ Readiness helpers ]------------------------------- svc_cid() { dc ps -q "$1"; } # container id for a service wait_healthy() { local svc="$1" timeout="${2:-60}" local cid; cid="$(svc_cid "$svc")" if [[ -z "$cid" ]]; then log "wait_healthy: service '$svc' not running." return 1 fi local health health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" if [[ -z "$health" ]]; then log "wait_healthy: no healthcheck for '$svc'; skipping." return 0 fi local start; start="$(ts_now)" while :; do health="$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo "")" if [[ "$health" == "healthy" ]]; then log "$svc is healthy." return 0 fi if (( $(ts_now) - start > timeout )); then log "wait_healthy timeout for '$svc' (last='$health')." return 1 fi sleep 1 done } wait_log() { local svc="$1" pat="$2" timeout="${3:-60}" local cid; cid="$(svc_cid "$svc")" if [[ -z "$cid" ]]; then log "wait_log: service '$svc' not running." return 1 fi local start; start="$(ts_now)" while :; do if dc logs "$svc" --since=2m 2>&1 | grep -Eiq -- "$pat"; then return 0 fi if (( $(ts_now) - start > timeout )); then log "wait_log timeout for '$svc' waiting '$pat'." return 1 fi sleep 1 done } get_container_ip() { local cid; cid="$(svc_cid "$1")" [[ -z "$cid" ]] && return 1 docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid" } # --------------------------[ Staged bring-up ]-------------------------------- start_services_list() { [[ $# -eq 0 ]] && return 0 dc up -d "$@" >/dev/null 2>&1 } start_ue_list() { [[ $# -eq 0 ]] && return 0 for svc in "$@"; do log "Starting: $svc" dc up -d "$svc" >/dev/null 2>&1 done } start_in_stages() { # 1) CORE mapfile -t core_svcs < <(get_core_services) if [[ ${#core_svcs[@]} -gt 0 ]]; then start_services_list "${core_svcs[@]}" else log "No CORE services to start." exit 1 fi # Ensure AMF is ready wait_healthy "${AMF_SERVICE}" 15 wait_healthy "${UPF_SERVICE}" 15 # exec_safe "${UPF_SERVICE}" 'bash -lc " # set -e # sysctl -w net.ipv4.ip_forward=1 # sysctl -w net.ipv4.conf.all.rp_filter=2 # sysctl -w net.ipv4.conf.eth0.rp_filter=2 # sysctl -w net.ipv4.conf.eth1.rp_filter=2 # sysctl -w net.ipv4.conf.tun0.rp_filter=2 # ip rule add pref 100 iif tun0 to 192.168.71.0/24 lookup 201 || true # ip route replace 192.168.71.0/24 dev eth0 table 201 # ip rule add pref 200 iif tun0 lookup 200 || true # ip route replace 12.1.1.0/24 dev tun0 table 200 # ip route replace default via 192.168.72.129 dev eth1 table 200 # # UE -> DN (192.168.72.128/26) via eth1 # iptables -t nat -C POSTROUTING -s 12.1.1.0/24 -d 192.168.72.128/26 -o eth1 -j MASQUERADE 2>/dev/null || \ # iptables -t nat -A POSTROUTING -s 12.1.1.0/24 -d 192.168.72.128/26 -o eth1 -j MASQUERADE # # UE -> 192.168.71.0/24 via eth0 # iptables -t nat -C POSTROUTING -s 12.1.1.0/24 -d 192.168.71.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \ # iptables -t nat -A POSTROUTING -s 12.1.1.0/24 -d 192.168.71.0/24 -o eth0 -j MASQUERADE # "' wait_healthy oai-flexric 15 # 2) gNB start_services_list "${GNB_SERVICE}" wait_log "${GNB_SERVICE}" "Received NGSetupResponse from AMF" 20 wait_log "${GNB_SERVICE}" "Transaction ID E2 SETUP-REQUEST 0 E2 SETUP-RESPONSE 0" 15 # 3) xApp start_services_list "${XAPP_SERVICE}" wait_healthy "${XAPP_SERVICE}" 15 # 4) UE(s) # if [[ -n "${TEST_UE:-}" ]]; then # # Allow multiple UEs in TEST_UE (comma and/or space separated) # IFS=', ' read -r -a _ue_list <<< "${TEST_UE}" # start_services_list "${_ue_list[@]}" # for ue in "${_ue_list[@]}"; do # [[ -z "$ue" ]] && continue # if wait_oai_tun "$ue" 20; then # exec_safe "$ue" 'ip route replace default dev oaitun_ue1' || true # run_traffic_profile "$ue" "random" "0" || true # else # exit 1 # fi # done # else # # Initial diurnal target, then enforce UE count # local target; if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then target="$(calc_target_ue_count)"; else target="$(random_target_ue_count)"; fi # log "Initial diurnal target UE count: ${target}" # ensure_ue_count "$target" # fi } # -------------------------[ Diurnal UE scaling ]------------------------------ calc_target_ue_count() { local total_ues="${UE_MAX:-}" if [[ -z "$total_ues" ]]; then total_ues="$(get_all_ue_services | wc -l | tr -d ' ')" fi local min="${UE_MIN:-1}" if (( total_ues < min )); then total_ues="$min"; fi local H M H="$(date +%H)"; M="$(date +%M)" # Compute target via cosine wave (peak at UE_PEAK_HOUR), then add noise and clamp awk -v min="$min" -v max="$total_ues" -v peak="${UE_PEAK_HOUR:-14}" \ -v noise="${UE_NOISE_FRAC:-0.15}" -v H="$H" -v M="$M" 'BEGIN{ # time-of-day in days [0,1) t = (H*60.0 + M)/1440.0 ph = peak/24.0 norm = (cos(2*3.141592653589793*(t - ph)) + 1)/2 # 0..1 base = min + norm*(max-min) srand(); u = (rand()*2.0 - 1.0) * noise # [-noise, +noise] val = base * (1.0 + u) if (val < min) val = min if (val > max) val = max # round to nearest integer printf("%.0f\n", val) }' } ensure_ue_count() { local target="$1" # Build arrays mapfile -t all_ues < <(get_all_ue_services) mapfile -t running < <(get_running_ues) # Current and desired counts local cur="${#running[@]}" if (( cur == target )); then log "UE scaler: already at target (${target})." return 0 fi # Set helpers for membership tests declare -A IS_RUNNING=() for u in "${running[@]}"; do IS_RUNNING["$u"]=1; done # Determine non-running UEs declare -a not_running=() for u in "${all_ues[@]}"; do [[ -z "${IS_RUNNING[$u]:-}" ]] && not_running+=("$u") done # Start more UEs if needed if (( cur < target )); then local need=$(( target - cur )) log "UE scaler: need +${need} UEs (cur=${cur}, target=${target})." if (( ${#not_running[@]} == 0 )); then log "UE scaler: no available UEs to start." return 0 fi local to_start=() if command -v shuf >/dev/null 2>&1; then mapfile -t to_start < <(printf '%s\n' "${not_running[@]}" | shuf -n "$need" || true) else to_start=("${not_running[@]:0:$need}") fi if (( ${#to_start[@]} > 0 )); then start_ue_list "${to_start[@]}" for ue in "${to_start[@]}"; do if wait_log "$ue" "oaitun_ue1 successfully configured" 5 && exec_safe "$ue" 'ip link show oaitun_ue1 >/dev/null 2>&1'; then exec_safe "$ue" 'ip route replace default dev oaitun_ue1' # run_traffic_profile "$ue" "$(choose_random_profile)" "0" run_traffic_profile "$ue" "boring" else dc down -v "$ue" >/dev/null 2>&1 & fi done fi return 0 fi # Otherwise stop some UEs to shrink local drop=$(( cur - target )) log "UE scaler: need -${drop} UEs (cur=${cur}, target=${target})." local to_stop=() if command -v shuf >/dev/null 2>&1; then mapfile -t to_stop < <(printf '%s\n' "${running[@]}" | shuf -n "$drop" || true) else to_stop=("${running[@]:0:$drop}") fi for ue in "${to_stop[@]}"; do dc down -v "$ue" >/dev/null 2>&1 & done wait } # --------------------------[ UE traffic helpers ]----------------------------- PROFILES=(boring browsing video bulk_download voip iot_chatty gaming) choose_random_profile() { if command -v shuf >/dev/null 2>&1; then shuf -e "${PROFILES[@]}" -n1 else idx=$(( RANDOM % ${#PROFILES[@]} )) echo "${PROFILES[$idx]}" fi } run_traffic_profile() { local ue="$1" profile="$2" # Record selected profile for this UE into UE_PROFILE_MAP if [[ "$ue" =~ ([0-9]+)$ ]]; then __idx="${BASH_REMATCH[1]}" UE_PROFILE_MAP[$__idx]="$profile" fi dc cp /home/nakaolab-go/OAI/openairinterface5g/tools/scripts/traffic_runner.sh "$ue":/tmp/traffic_runner.sh >/dev/null 2>&1 exec_safe "$ue" "bash -lc ' prof=\$1; /tmp/traffic_runner.sh --profile \$prof & ' -- $profile" } # --------------------------[ RRC / MAC scraping ]----------------------------- rotate_stats_capture() { local tag="${1:-}" [[ -z "$tag" ]] && tag="$(ts_tag)" if dc ps --services --filter "status=running" | grep -q "^${STATS_CONTAINER}$"; then log "Rotating stats from ${STATS_CONTAINER}" dc cp "${STATS_CONTAINER}:${MAC_STATS_FILE_IN_CONTAINER}" \ "${ARTIFACT_DIR}/mac_stats_${tag}.log" || true dc cp "${STATS_CONTAINER}:${PDCP_STATS_FILE_IN_CONTAINER}" \ "${ARTIFACT_DIR}/pdcp_stats_${tag}.log" || true # exec_safe "$STATS_CONTAINER" "> '${MAC_STATS_FILE_IN_CONTAINER}'" # exec_safe "$STATS_CONTAINER" "> '${PDCP_STATS_FILE_IN_CONTAINER}'" else log "stats container ${STATS_CONTAINER} not running; skipping rotate" fi } _since_arg_from_file() { local f="$1" local val val="$(tr -d '[]\r\n' < "$f" 2>/dev/null || true)" if [[ -n "$val" ]]; then printf -- '--since=%s' "$val" else printf -- '--since=2m' fi } dump_gnb_logs() { local tag="$1" dc logs "${GNB_SERVICE}" 2>&1 | grep -E "NR_RRC" > "${ARTIFACT_DIR}/gnb_rrc_${tag}.log" dc logs "${GNB_SERVICE}" 2>&1 | grep -E "\[NR_MAC\] I Frame\.Slot|MAC:" > "${ARTIFACT_DIR}/gnb_${tag}.log" } # -----------------------------[ Power / CPU ]--------------------------------- init_power_logs() { if [[ ! -s "$POWER_LOG_FILE" ]]; then echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$POWER_LOG_FILE" fi if [[ ! -s "$CPU_PROXY_LOG_FILE" ]]; then echo "epoch,container,cpu_percent,mem_usage" > "$CPU_PROXY_LOG_FILE" fi } profile_total_mw() { local profile="$1" echo "${PWR_MW_PROFILE[$profile]:-0}" } sample_power_once() { local epoch="$(ts_now)" local dt_s="${POWER_LOG_INTERVAL_SEC}" while IFS= read -r ue; do [[ -z "$ue" ]] && continue # Determine profile without container lock by consulting UE_PROFILE_MAP local picked_profile picked_duration if [[ "$ue" =~ ([0-9]+)$ ]]; then __idx="${BASH_REMATCH[1]}" picked_profile="${UE_PROFILE_MAP[$__idx]}" fi picked_duration="" if [[ -z "$picked_profile" ]]; then if [[ "${TEST_MODE}" -eq 1 ]]; then picked_profile="${TEST_PROFILE}"; else picked_profile="idle"; fi fi while [[ "$picked_profile" == -* ]]; do picked_profile="${picked_profile#-}" done picked_profile="${picked_profile,,}" local p_mw p_mw="$(profile_total_mw "$picked_profile")" : "${p_mw:=0}" local energy_mJ=$(( p_mw * dt_s )) echo "${epoch},${ue},${picked_profile},${p_mw},${dt_s},${energy_mJ}" >> "$POWER_LOG_FILE" done < <(get_running_ues) } # --- CPU proxy: query docker stats by container ID and map back to service name --- cpu_proxy_once() { local epoch="$(ts_now)" # Build list of UE service names (same as container names) declare -a SVC_LIST=() while IFS= read -r svc; do [[ -z "$svc" ]] && continue SVC_LIST+=("$svc") done < <(get_running_ues) # Query docker stats by NAME and key 'seen' by NAME local fmt='{{.Name}},{{.CPUPerc}},{{.MemUsage}}' declare -A SEEN=() while IFS=',' read -r name cpu mem; do [[ -z "$name" ]] && continue name="${name#/}" SEEN["$name"]=1 cpu="${cpu%%%}" # drop trailing '%' echo "${epoch},${name},${cpu},${mem}" >> "$CPU_PROXY_LOG_FILE" done < <(docker stats --no-stream --format "$fmt" "${SVC_LIST[@]}" 2>/dev/null || true) # Any container absent from docker stats output → record zeros once local svc for svc in "${SVC_LIST[@]}"; do if [[ -z "${SEEN[$svc]:-}" ]]; then echo "${epoch},${svc},0,0" >> "$CPU_PROXY_LOG_FILE" fi done } rotate_csv_if_any() { local src="$1" prefix="$2" tag="${3:-}" [[ -z "$tag" ]] && tag="$(ts_tag)" if [[ -s "$src" ]]; then mv "$src" "${ARTIFACT_DIR}/${prefix}_${tag}.csv" || true case "$prefix" in power_usage) echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$src" ;; cpu_proxy) echo "epoch,container,cpu_percent,mem_usage" > "$src" ;; esac fi } finalize_and_rotate_artifacts() { local tag; tag="$(ts_tag)" rotate_stats_capture "$tag" || true dump_gnb_logs "$tag" || true rotate_csv_if_any "$POWER_LOG_FILE" "power_usage" "$tag" rotate_csv_if_any "$CPU_PROXY_LOG_FILE" "cpu_proxy" "$tag" } # -------------------------------[ Signals ]----------------------------------- __SHUTTING_DOWN=0 on_interrupt() { if [[ "${__SHUTTING_DOWN}" -eq 1 ]]; then return; fi __SHUTTING_DOWN=1 log "Signal received (INT/TERM); taking final samples and rotating artifacts…" # One last sample tick sample_power_once || true cpu_proxy_once || true # Rotate artifacts and clean up finalize_and_rotate_artifacts cleanup # 130 is the conventional exit code for SIGINT exit 130 } # --------------------------------[ Cleanup ]---------------------------------- cleanup() { # Default: stop only UEs; keep infra up for inspection log "Cleanup: stopping UE containers only" while IFS= read -r ue; do [[ -z "$ue" ]] && continue dc down -v "$ue" >/dev/null 2>&1 & done < <(dc ps --services | grep -E "${UE_NAME_REGEX}" || true) } # ---------------------------------[ Main ]------------------------------------ random_target_ue_count() { local cur="$(get_running_ues | wc -l | tr -d ' ')" bias=$((RANDOM % 100)) # 0–99 if (( cur <= UE_MIN )); then if (( bias < 70 )); then delta=$(( (RANDOM % 3) + 1 )) # +1..+3 else delta=$(( RANDOM % 7 - 3 )) # -3..+3 fi elif (( cur >= UE_MAX )); then if (( bias < 70 )); then delta=$(( -((RANDOM % 3) + 1) )) # -1..-3 else delta=$(( RANDOM % 7 - 3 )) # -3..+3 fi else # normal delta=$(( RANDOM % 7 - 3 )) # -3..+3 fi local target=$((cur + delta)) if (( target < UE_MIN )); then target="${UE_MIN}" elif (( target > UE_MAX )); then target="${UE_MAX}" fi echo "$target" } main() { trap on_interrupt INT TERM init_power_logs # Staged bring-up with readiness checks for both normal and TEST modes start_in_stages # Kick off traffic workload(s) if [[ "${TEST_MODE}" -eq 1 ]]; then log "TEST UE(S) ENABLED" # Support multiple UEs (comma/space/newline separated) read -r -a _ue_list <<< "$(tr ',\n' ' ' <<< "${TEST_UES}")" read -r -a _ue_profiles <<< "$(tr ',\n' ' ' <<< "${TEST_PROFILES}")" start_ue_list "${_ue_list[@]}" for i in "${!_ue_list[@]}"; do ue="${_ue_list[$i]}" prof="${_ue_profiles[$i]}" [[ -z "$ue" ]] && continue if wait_log "$ue" "oaitun_ue1 successfully configured" 5 && exec_safe "$ue" 'ip link show oaitun_ue1 >/dev/null 2>&1'; then exec_safe "$ue" "ip route replace default dev oaitun_ue1" run_traffic_profile "$ue" "$prof" else exit 1 fi done fi # Optional finite duration for TEST mode if [[ "${TEST_MODE}" -eq 1 && -n "${TEST_UE:-}" && "${TEST_DURATION_SEC:-0}" -gt 0 ]]; then RUN_STOP_TS=$(( $(ts_now) + TEST_DURATION_SEC )) fi # Main sampling & scaling loop local last_rrc="$(( $(ts_now) - RRC_LOG_INTERVAL_SEC ))" local last_power="$(( $(ts_now) - POWER_LOG_INTERVAL_SEC ))" local last_cpu="$(( $(ts_now) - CPU_PROXY_INTERVAL_SEC ))" local last_scale="$(( $(ts_now) - UE_SCALER_INTERVAL_SEC ))" log "Entering main loop (sampling + diurnal scaler)…" while :; do local now; now="$(ts_now)" if (( now - last_power >= POWER_LOG_INTERVAL_SEC )); then sample_power_once || true last_power="$now" fi if (( now - last_cpu >= CPU_PROXY_INTERVAL_SEC )); then cpu_proxy_once || true last_cpu="$now" fi # Re-evaluate diurnal target (normal mode only) if [[ "${TEST_MODE}" -eq 0 ]] && (( now - last_scale >= UE_SCALER_INTERVAL_SEC )); then local target; if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then target="$(calc_target_ue_count)"; else target="$(random_target_ue_count)"; fi log "Scaler tick: target = ${target}" ensure_ue_count "$target" last_scale="$now" fi # TEST duration stop gate if [[ -n "${RUN_STOP_TS:-}" && "$now" -ge "${RUN_STOP_TS}" ]]; then log "TEST run reached duration (${TEST_DURATION_SEC}s); finalizing artifacts…" finalize_and_rotate_artifacts cleanup exit 0 fi sleep 1 done } main "$@"