diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..c1804a7c2 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,39 @@ +x-nilrt-common: &nilrt-common + privileged: true + stdin_open: true + tty: true + restart: unless-stopped + networks: + - nilrt-net + labels: + nilrt.managed: "true" + healthcheck: + test: ["CMD-SHELL", "test -f /var/run/niauth.pid || pgrep -x niauth_daemon"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + entrypoint: + - /bin/bash + - -c + - | + exec /init /bin/bash + +services: + nilrt: + <<: *nilrt-common + image: nilrt-runmode-container:${NILRT_VERSION} + labels: + nilrt.managed: "true" + nilrt.type: runmode + + nilrt-slim: + <<: *nilrt-common + image: nilrt-slim-container:${NILRT_VERSION}-slim + labels: + nilrt.managed: "true" + nilrt.type: slim + +networks: + nilrt-net: + external: true diff --git a/docker/nilrt-ctr.sh b/docker/nilrt-ctr.sh new file mode 100755 index 000000000..308574cfc --- /dev/null +++ b/docker/nilrt-ctr.sh @@ -0,0 +1,493 @@ +#!/bin/bash +# nilrt-ctr.sh — Management CLI for NILRT containers. +# +# Provides discovery, configuration, software management, and monitoring +# for NILRT containers managed by docker-compose.yml. +# +# Usage: +# bash docker/nilrt-ctr.sh [arguments...] +# +# Commands: +# discover|list|find List all managed NILRT containers and their status +# status|info Show detailed system info for a target +# set-feed|feed Set package feed +# install [--feed YYYYQN] Install packages on a target +# remove|uninstall Remove packages from a target +# update|upgrade Update all packages on a target +# rename|hostname Set target hostname +# shell|ssh Open an interactive shell on a target +# scale Scale a service to n instances +# log|logs [lines] Show container logs (default: 50 lines) +# exec|run Execute a command on a target +# +# Targets can be specified by container name, container ID (prefix), or +# index from the discover list (e.g. "1", "2"). +# +# Examples: +# bash docker/nilrt-ctr.sh discover +# bash docker/nilrt-ctr.sh status nilrt-slim-1 +# bash docker/nilrt-ctr.sh scale nilrt-slim 3 +# bash docker/nilrt-ctr.sh update all + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" +DOCKER_CMD="docker" +LABEL_FILTER="label=nilrt.managed=true" + +# Auto-detect NILRT image version from local docker images +if [[ -z "${NILRT_VERSION:-}" ]]; then + NILRT_VERSION=$(docker images --format '{{.Tag}}' nilrt-runmode-container 2>/dev/null \ + | grep -v '^$' | sort -V | tail -n1) + [[ -z "$NILRT_VERSION" ]] && NILRT_VERSION="latest" +fi +export NILRT_VERSION + +COMPOSE_CMD=(docker compose -f "$COMPOSE_FILE") + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +usage() { + cat <<'EOF' +Usage: bash docker/nilrt-ctr.sh [arguments...] + + Management CLI for NILRT containers. + +Commands: + discover|list|find List all managed NILRT containers + status|info Show detailed system info + set-feed|feed Set package feed + install [--feed YYYYQN] Install packages + remove|uninstall Remove packages + update|upgrade [target|all] Update packages (default: all) + rename|hostname Set hostname + shell|ssh Interactive shell + scale Scale service instances + log|logs [lines] Show logs + exec|run Run command on target + +Targets: container name, ID prefix, or index from 'discover' output. +EOF + exit "${1:-0}" +} + +err() { echo -e "${RED}ERROR:${NC} $*" >&2; exit 1; } +warn() { echo -e "${YELLOW}WARNING:${NC} $*" >&2; } +info() { echo -e "${BLUE}==>${NC} $*"; } + +# Wait for opkg lock to be released inside a container. +wait_for_opkg_lock() { + local cid="$1" + local retries=30 + while [[ $retries -gt 0 ]]; do + if $DOCKER_CMD exec "$cid" bash -c 'exec 3>/run/opkg.lock; flock -n 3' 2>/dev/null; then + return 0 + fi + info "Waiting for opkg lock on $(container_name "$cid")..." + sleep 2 + ((retries--)) + done + err "Timed out waiting for opkg lock on $(container_name "$cid")" +} + +# Write feed config to /etc/opkg/base-feeds.conf inside a container. +# opkg reads all *.conf in /etc/opkg/ directly, so this is the only file needed. +set_feed_on_target() { + local cid="$1" feed="$2" + local lv_feed="ni-lv${feed:0:4}" + local name + name=$(container_name "$cid") + info "Setting ${name} feed: ${feed} (LV: ${lv_feed})" + $DOCKER_CMD exec "$cid" bash -c " + feeds_uri=\${NILRT_FEEDS_URI:-http://nickdanger.amer.corp.natinst.com/feeds} + ni_line=\"src/gz ni-software \${feeds_uri}/${feed}/ni-main\" + lv_line=\"src/gz ${lv_feed} \${feeds_uri}/${feed}/${lv_feed}\" + sed -i '/^src\/gz ni-software /d' /etc/opkg/base-feeds.conf 2>/dev/null || true + sed -i '/^src\/gz ni-lv[0-9]/d' /etc/opkg/base-feeds.conf 2>/dev/null || true + echo \"\${ni_line}\" >> /etc/opkg/base-feeds.conf + echo \"\${lv_line}\" >> /etc/opkg/base-feeds.conf + " +} + +# Get all managed container IDs +get_managed_containers() { + $DOCKER_CMD ps -a --filter "$LABEL_FILTER" --format '{{.ID}}' 2>/dev/null +} + +# Get managed containers as a table +get_managed_table() { + $DOCKER_CMD ps -a --filter "$LABEL_FILTER" \ + --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Networks}}' 2>/dev/null +} + +# Resolve a target argument to a container ID. +# Accepts: container name, ID prefix, or numeric index from discover list. +resolve_target() { + local target="$1" + + # If numeric, treat as index from container list + if [[ "$target" =~ ^[0-9]+$ ]]; then + local containers + mapfile -t containers < <(get_managed_containers) + local idx=$((target - 1)) + if [[ $idx -lt 0 || $idx -ge ${#containers[@]} ]]; then + err "Index ${target} out of range. Use 'discover' to see available targets." + fi + echo "${containers[$idx]}" + return + fi + + # Try exact name match + local id + id=$($DOCKER_CMD ps -a --filter "$LABEL_FILTER" --filter "name=^${target}$" --format '{{.ID}}' 2>/dev/null | head -1) + if [[ -n "$id" ]]; then + echo "$id" + return + fi + + # Try name substring match + id=$($DOCKER_CMD ps -a --filter "$LABEL_FILTER" --filter "name=${target}" --format '{{.ID}}' 2>/dev/null | head -1) + if [[ -n "$id" ]]; then + echo "$id" + return + fi + + # Try ID prefix + id=$($DOCKER_CMD ps -a --filter "$LABEL_FILTER" --format '{{.ID}}' 2>/dev/null | grep "^${target}" | head -1) + if [[ -n "$id" ]]; then + echo "$id" + return + fi + + err "Target '${target}' not found. Use 'discover' to list targets." +} + +# Get the container name for a given ID +container_name() { + $DOCKER_CMD inspect --format '{{.Name}}' "$1" 2>/dev/null | sed 's|^/||' +} + +# ---- Commands ---- + +cmd_discover() { + echo -e "${BOLD}NILRT Managed Targets${NC}" + echo "" + + local containers + mapfile -t containers < <(get_managed_containers) + + if [[ ${#containers[@]} -eq 0 ]]; then + warn "No managed containers found. Start with: docker compose -f ${COMPOSE_FILE} up -d" + return + fi + + printf "${BOLD}%-4s %-12s %-30s %-35s %-12s %-18s${NC}\n" \ + "#" "ID" "NAME" "IMAGE" "STATE" "IP" + + local idx=1 + for cid in "${containers[@]}"; do + local name image state ip + name=$($DOCKER_CMD inspect --format '{{.Name}}' "$cid" | sed 's|^/||') + image=$($DOCKER_CMD inspect --format '{{.Config.Image}}' "$cid") + state=$($DOCKER_CMD inspect --format '{{.State.Status}}' "$cid") + # Get IP from nilrt-net if available, else first network + ip=$($DOCKER_CMD inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid" 2>/dev/null) + + local state_color="$NC" + case "$state" in + running) state_color="$GREEN" ;; + exited|dead) state_color="$RED" ;; + *) state_color="$YELLOW" ;; + esac + + printf "%-4s %-12s %-30s %-35s ${state_color}%-12s${NC} %-18s\n" \ + "$idx" "${cid:0:12}" "$name" "$image" "$state" "${ip:-N/A}" + ((idx++)) + done + echo "" + echo -e "Total: $((idx - 1)) target(s)" +} + +cmd_status() { + [[ $# -lt 1 ]] && err "Usage: nilrt-ctr.sh status " + local cid + cid=$(resolve_target "$1") + local name + name=$(container_name "$cid") + + echo -e "${BOLD}System Information: ${CYAN}${name}${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Container metadata + local image state started + image=$($DOCKER_CMD inspect --format '{{.Config.Image}}' "$cid") + state=$($DOCKER_CMD inspect --format '{{.State.Status}}' "$cid") + started=$($DOCKER_CMD inspect --format '{{.State.StartedAt}}' "$cid") + + echo -e "${BOLD}Container:${NC}" + echo " ID: ${cid:0:12}" + echo " Image: ${image}" + echo " State: ${state}" + echo " Started: ${started}" + echo "" + + if [[ "$state" != "running" ]]; then + warn "Container is not running. Start it first." + return + fi + + # Network info + echo -e "${BOLD}Network:${NC}" + local ip mac + ip=$($DOCKER_CMD inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid") + mac=$($DOCKER_CMD inspect --format '{{range .NetworkSettings.Networks}}{{.MacAddress}}{{end}}' "$cid") + local hostname + hostname=$($DOCKER_CMD exec "$cid" hostname 2>/dev/null || echo "N/A") + echo " Hostname: ${hostname}" + echo " IP: ${ip:-N/A}" + echo " MAC: ${mac:-N/A}" + echo "" + + # OS info + echo -e "${BOLD}System:${NC}" + $DOCKER_CMD exec "$cid" bash -c ' + if [[ -f /etc/os-release ]]; then + source /etc/os-release + echo " OS: ${PRETTY_NAME:-$NAME $VERSION}" + fi + if [[ -f /etc/natinst/share/ni-rt.ini ]]; then + model=$(grep -i "^DeviceCode" /etc/natinst/share/ni-rt.ini 2>/dev/null | cut -d= -f2 | tr -d " ") + serial=$(grep -i "^serialnumber" /etc/natinst/share/ni-rt.ini 2>/dev/null | cut -d= -f2 | tr -d " ") + [[ -n "$model" ]] && echo " Model: ${model}" + [[ -n "$serial" ]] && echo " Serial: ${serial}" + fi + echo " Kernel: $(uname -r)" + echo " Arch: $(uname -m)" + if command -v nirtcfg &>/dev/null; then + fw=$(nirtcfg --get section=SystemSettings,token=FirmwareVersion 2>/dev/null || true) + [[ -n "$fw" ]] && echo " Firmware: ${fw}" + fi + : + ' 2>/dev/null || warn "Could not query system info" + echo "" + + # Services + echo -e "${BOLD}Services:${NC}" + $DOCKER_CMD exec "$cid" bash -c ' + check_svc() { + local name="$1" proc="$2" + if pgrep -x "$proc" &>/dev/null; then + printf " %-25s \033[0;32m● running\033[0m\n" "$name" + else + printf " %-25s \033[0;31m○ stopped\033[0m\n" "$name" + fi + } + check_svc "NI Auth Daemon" niauth_daemon + check_svc "System Web Server" SystemWebServer + check_svc "NI mDNS Daemon" nirtmdnsd + check_svc "Avahi Daemon" avahi-daemon + check_svc "D-Bus Daemon" dbus-daemon + check_svc "LabVIEW RT" lvrt-daemon + ' 2>/dev/null || warn "Could not query services" + echo "" + + # Installed NI packages + echo -e "${BOLD}Installed NI Software:${NC}" + $DOCKER_CMD exec "$cid" bash -c ' + if command -v opkg &>/dev/null; then + opkg list-installed 2>/dev/null | grep -i "^ni-" | head -20 + total=$(opkg list-installed 2>/dev/null | grep -ic "^ni-" || echo 0) + if [[ $total -gt 20 ]]; then + echo " ... and $((total - 20)) more" + fi + else + echo " (opkg not available)" + fi + ' 2>/dev/null | sed 's/^/ /' +} + +cmd_set_feed() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh set-feed " + local target="$1" feed="$2" + + if [[ ! "$feed" =~ ^[0-9]{4}Q[1-4]$ ]]; then + err "Feed must be in YYYYQN format (e.g. 2026Q2)" + fi + + local targets=() + if [[ "$target" == "all" ]]; then + mapfile -t targets < <(get_managed_containers) + [[ ${#targets[@]} -eq 0 ]] && err "No managed containers found." + else + targets+=("$(resolve_target "$target")") + fi + + for cid in "${targets[@]}"; do + local state + state=$($DOCKER_CMD inspect --format '{{.State.Status}}' "$cid") + if [[ "$state" != "running" ]]; then + warn "Skipping $(container_name "$cid") (not running)" + continue + fi + set_feed_on_target "$cid" "$feed" + done +} + +cmd_install() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh install [--feed YYYYQN] [package...]" + local target="$1"; shift + local cid + cid=$(resolve_target "$target") + local name + name=$(container_name "$cid") + local feed="" + + # Parse optional --feed flag + if [[ "${1:-}" == "--feed" ]]; then + shift + [[ $# -lt 1 ]] && err "--feed requires a value (e.g. 2026Q2)" + feed="$1"; shift + [[ $# -lt 1 ]] && err "No packages specified after --feed ${feed}" + fi + + if [[ -n "$feed" ]]; then + set_feed_on_target "$cid" "$feed" + fi + + info "Installing on ${name}: $*" + wait_for_opkg_lock "$cid" + $DOCKER_CMD exec "$cid" opkg update + $DOCKER_CMD exec "$cid" opkg install "$@" + info "Installation complete." +} + +cmd_remove() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh remove [package...]" + local target="$1"; shift + local cid + cid=$(resolve_target "$target") + local name + name=$(container_name "$cid") + + info "Removing from ${name}: $*" + wait_for_opkg_lock "$cid" + $DOCKER_CMD exec "$cid" opkg remove "$@" + info "Removal complete." +} + +cmd_update() { + local targets=() + + if [[ $# -eq 0 || "$1" == "all" ]]; then + mapfile -t targets < <(get_managed_containers) + if [[ ${#targets[@]} -eq 0 ]]; then + err "No managed containers found." + fi + else + targets+=("$(resolve_target "$1")") + fi + + for cid in "${targets[@]}"; do + local name + name=$(container_name "$cid") + local state + state=$($DOCKER_CMD inspect --format '{{.State.Status}}' "$cid") + if [[ "$state" != "running" ]]; then + warn "Skipping ${name} (not running)" + continue + fi + info "Updating ${name}..." + wait_for_opkg_lock "$cid" + $DOCKER_CMD exec "$cid" opkg update + $DOCKER_CMD exec "$cid" opkg upgrade + info "${name} updated." + done +} + +cmd_rename() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh rename " + local cid new_hostname="$2" + cid=$(resolve_target "$1") + local name + name=$(container_name "$cid") + + info "Setting hostname of ${name} to '${new_hostname}'..." + $DOCKER_CMD exec "$cid" bash -c " + echo '${new_hostname}' > /etc/hostname + hostname '${new_hostname}' + if command -v nirtcfg &>/dev/null; then + nirtcfg --set section=SystemSettings,token=Host_Name,value='${new_hostname}' 2>/dev/null || true + fi + " + info "Hostname set to '${new_hostname}'. Note: hostname will revert on container restart." +} + +cmd_shell() { + [[ $# -lt 1 ]] && err "Usage: nilrt-ctr.sh shell " + local cid + cid=$(resolve_target "$1") + local name + name=$(container_name "$cid") + + info "Opening shell on ${name}..." + $DOCKER_CMD exec -it "$cid" /bin/bash +} + +cmd_scale() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh scale " + local service="$1" count="$2" + + if ! [[ "$count" =~ ^[0-9]+$ ]]; then + err "Count must be a positive integer." + fi + + info "Scaling ${service} to ${count} instance(s)..." + "${COMPOSE_CMD[@]}" up -d --scale "${service}=${count}" "$service" + info "${service} scaled to ${count}." +} + +cmd_log() { + [[ $# -lt 1 ]] && err "Usage: nilrt-ctr.sh log [lines]" + local cid + cid=$(resolve_target "$1") + local lines="${2:-50}" + + $DOCKER_CMD logs --tail "$lines" "$cid" +} + +cmd_exec() { + [[ $# -lt 2 ]] && err "Usage: nilrt-ctr.sh exec " + local target="$1"; shift + local cid + cid=$(resolve_target "$target") + + $DOCKER_CMD exec "$cid" "$@" +} + +# ---- Main ---- + +[[ $# -eq 0 ]] && usage + +case "$1" in + discover|list|find) shift; cmd_discover "$@" ;; + status|info) shift; cmd_status "$@" ;; + set-feed|feed) shift; cmd_set_feed "$@" ;; + install) shift; cmd_install "$@" ;; + remove|uninstall) shift; cmd_remove "$@" ;; + update|upgrade) shift; cmd_update "$@" ;; + rename|hostname) shift; cmd_rename "$@" ;; + shell|ssh) shift; cmd_shell "$@" ;; + scale) shift; cmd_scale "$@" ;; + log|logs) shift; cmd_log "$@" ;; + exec|run) shift; cmd_exec "$@" ;; + -h|--help|help) usage 0 ;; + *) err "Unknown command: $1. Run 'nilrt-ctr.sh --help' for usage." ;; +esac