Skip to content

Commit 291647a

Browse files
authored
Fix/devcontainer hardening (#17)
* fix: devcontainer script hardening and refactoring - Add process-helpers.sh: shared graceful_kill_pid, graceful_kill_pattern, is_expected_pid helpers used by start-netbox.sh and load-aliases.sh - start-netbox.sh: start RQ worker alongside NetBox, use graceful shutdown, validate PID before killing, guard stale PID files - load-aliases.sh: dynamic PLUGIN_DIR via $(dirname), clean up empty CA bundle env vars that Compose injects, use process-helpers for netbox-stop - setup.sh: fall back to pip when uv unavailable, validate CA bundle before csplit, read superuser credentials via os.environ to avoid shell injection - diagnose.sh: show user ID, DB_NAME/USER, SECRET_KEY prefix, validate PID file content, prefer /proc/net/tcp for port check when ss/netstat absent - welcome.sh: minor improvements * fix: harden devcontainer scripts per code review - start-netbox.sh: guard cd /opt/netbox/netbox with error+exit in main script and all subshells; remove is_expected_pid revalidation before killing tracked PIDs; add comment that simple startup is intentional for dev environment - load-aliases.sh: remove is_expected_pid checks from netbox-stop, netbox-status, rq-status; use env-driven credentials in dev-help URL - diagnose.sh: replace SECRET_KEY fragment echo with set/unset check; add TCP LISTEN state filter to /proc/net/tcp awk check - welcome.sh: align Codespaces fallback domain with start-netbox.sh - setup.sh: wrap csplit with set +e/set -e to capture real exit status; add -- to grep -c to prevent pattern misinterpretation * fix: devcontainer script and CI improvements - diagnose.sh: simplify /proc/net/tcp port check to single awk invocation without redundant grep pipeline - load-aliases.sh: always unset empty CA bundle env vars (never default to system trust store); use uv when available in plugins-install - test.yaml: generate coverage.xml and upload to Codecov via codecov-action
1 parent a066fb5 commit 291647a

7 files changed

Lines changed: 331 additions & 94 deletions

File tree

.devcontainer/scripts/diagnose.sh

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
echo "🔍 DevContainer Startup Diagnostics"
66
echo "=================================="
77

8+
PLUGIN_WS_DIR="${PLUGIN_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
89
echo "📍 Current working directory: $(pwd)"
910
echo "👤 Current user: $(whoami)"
11+
echo "🆔 User ID: $(id)"
1012

1113
echo ""
1214
echo "🐳 Container Environment:"
1315
echo " - NETBOX_VERSION: ${NETBOX_VERSION:-not set}"
1416
echo " - DEBUG: ${DEBUG:-not set}"
17+
echo " - SECRET_KEY: $([ -n "$SECRET_KEY" ] && echo 'set' || echo 'unset')"
1518
echo " - DB_HOST: ${DB_HOST:-not set}"
19+
echo " - DB_NAME: ${DB_NAME:-not set}"
20+
echo " - DB_USER: ${DB_USER:-not set}"
1621
echo " - REDIS_HOST: ${REDIS_HOST:-not set}"
22+
echo " - SUPERUSER_NAME: ${SUPERUSER_NAME:-not set}"
1723

1824
echo ""
1925
echo "🔗 Service Connectivity:"
@@ -23,29 +29,38 @@ echo " - Redis: $(timeout 3 bash -c 'cat < /dev/null > /dev/tcp/redis/6379' 2>/
2329
echo ""
2430
echo "🗂️ File System:"
2531
echo " - NetBox venv: $(test -f /opt/netbox/venv/bin/activate && echo 'Exists' || echo 'Missing')"
26-
echo " - Plugin directory: $(test -d /workspaces/netbox-InterfaceNameRules-plugin && echo 'Exists' || echo 'Missing')"
27-
echo " - Plugin config: $(test -f /workspaces/netbox-InterfaceNameRules-plugin/.devcontainer/config/plugin-config.py && echo 'Found' || echo 'Missing (using defaults)')"
28-
echo " - Extra plugins config: $(test -f /workspaces/netbox-InterfaceNameRules-plugin/.devcontainer/config/extra-plugins.py && echo 'Found' || echo 'Not configured')"
32+
echo " - Plugin directory: $(test -d "$PLUGIN_WS_DIR" && echo 'Exists' || echo 'Missing')"
33+
echo " - Setup script: $(test -f "$PLUGIN_WS_DIR/.devcontainer/scripts/setup.sh" && echo 'Exists' || echo 'Missing')"
34+
echo " - Start script: $(test -f "$PLUGIN_WS_DIR/.devcontainer/scripts/start-netbox.sh" && echo 'Exists' || echo 'Missing')"
35+
echo " - Start script executable: $(test -x "$PLUGIN_WS_DIR/.devcontainer/scripts/start-netbox.sh" && echo 'Yes' || echo 'No')"
36+
echo " - Plugin config: $(test -f "$PLUGIN_WS_DIR/.devcontainer/config/plugin-config.py" && echo 'Found' || echo 'Missing (using defaults)')"
37+
echo " - NetBox config path: /opt/netbox/netbox/netbox/configuration.py"
2938

3039
echo ""
3140
echo "🚀 Process Status:"
3241
if [ -f /tmp/netbox.pid ]; then
3342
PID=$(cat /tmp/netbox.pid)
34-
if kill -0 "$PID" 2>/dev/null; then
43+
if [ -z "$PID" ]; then
44+
echo " - NetBox server: PID file exists but is empty"
45+
elif kill -0 "$PID" 2>/dev/null; then
3546
echo " - NetBox server: Running (PID: $PID)"
3647
else
3748
echo " - NetBox server: PID file exists but process not running"
49+
echo " (PID $PID is dead - NetBox may have crashed)"
3850
fi
3951
else
4052
echo " - NetBox server: Not started"
4153
fi
4254

55+
# Check port listening
4356
echo ""
4457
echo "🌍 Port Check:"
45-
if command -v ss >/dev/null 2>&1; then
46-
echo " - Port 8000: $(ss -tuln 2>/dev/null | grep :8000 >/dev/null && echo 'Listening' || echo 'Not listening')"
47-
elif command -v netstat >/dev/null 2>&1; then
58+
if command -v netstat >/dev/null 2>&1; then
4859
echo " - Port 8000: $(netstat -tuln 2>/dev/null | grep :8000 >/dev/null && echo 'Listening' || echo 'Not listening')"
60+
elif command -v ss >/dev/null 2>&1; then
61+
echo " - Port 8000: $(ss -tuln 2>/dev/null | grep :8000 >/dev/null && echo 'Listening' || echo 'Not listening')"
62+
else
63+
echo " - Port 8000: $(awk 'BEGIN{r="Not listening"} $2 ~ /:1F40$/ && $4 == "0A" {r="Listening"; exit} END{print r}' /proc/net/tcp 2>/dev/null)"
4964
fi
5065

5166
echo ""

.devcontainer/scripts/load-aliases.sh

Lines changed: 149 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,116 +6,212 @@
66

77
export PATH="/opt/netbox/venv/bin:$PATH"
88
export DEBUG="${DEBUG:-True}"
9-
PLUGIN_DIR="/workspaces/netbox-InterfaceNameRules-plugin"
9+
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
1010

11-
netbox-run-bg() {
12-
"$PLUGIN_DIR/.devcontainer/scripts/start-netbox.sh" --background
13-
}
11+
# Unset CA bundle vars when empty (Compose/devcontainer inject "" when the
12+
# host var is unset, which breaks requests/curl).
13+
for _ca_var in REQUESTS_CA_BUNDLE SSL_CERT_FILE CURL_CA_BUNDLE; do
14+
if [ -z "${!_ca_var}" ]; then
15+
unset "$_ca_var"
16+
fi
17+
done
18+
unset _ca_var
1419

15-
netbox-run() {
16-
"$PLUGIN_DIR/.devcontainer/scripts/start-netbox.sh"
17-
}
20+
# Load shared process management helpers
21+
if ! source "$PLUGIN_DIR/.devcontainer/scripts/process-helpers.sh"; then
22+
printf '%s\n' "Failed to load process-helpers.sh" >&2
23+
return 1
24+
fi
25+
26+
netbox-run-bg() { "$PLUGIN_DIR/.devcontainer/scripts/start-netbox.sh" --background; }
27+
netbox-run() { "$PLUGIN_DIR/.devcontainer/scripts/start-netbox.sh"; }
1828

29+
# Robust stop command that kills both tracked and orphaned processes
1930
netbox-stop() {
20-
echo "🛑 Stopping NetBox..."
31+
echo "🛑 Stopping NetBox and RQ workers..."
2132
if [ -f /tmp/netbox.pid ]; then
22-
local pid
23-
pid=$(cat /tmp/netbox.pid 2>/dev/null)
24-
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
25-
kill "$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null
26-
echo " Stopped NetBox (PID: $pid)"
33+
local PID
34+
PID=$(cat /tmp/netbox.pid 2>/dev/null)
35+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
36+
graceful_kill_pid "$PID"
37+
echo " Stopped NetBox (PID: $PID)"
2738
fi
2839
rm -f /tmp/netbox.pid
2940
fi
41+
if [ -f /tmp/rqworker.pid ]; then
42+
local PID
43+
PID=$(cat /tmp/rqworker.pid 2>/dev/null)
44+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
45+
graceful_kill_pid "$PID"
46+
echo " Stopped RQ worker (PID: $PID)"
47+
fi
48+
rm -f /tmp/rqworker.pid
49+
fi
50+
if pgrep -f "python.*rqworker" >/dev/null 2>&1; then
51+
local ORPHAN_COUNT
52+
ORPHAN_COUNT=$(pgrep -cf "python.*rqworker" 2>/dev/null || echo 0)
53+
graceful_kill_pattern "python.*rqworker"
54+
echo " Killed $ORPHAN_COUNT orphaned RQ worker(s)"
55+
fi
3056
if pgrep -f "python.*runserver.*8000" >/dev/null 2>&1; then
31-
pkill -9 -f "python.*runserver.*8000" 2>/dev/null
57+
graceful_kill_pattern "python.*runserver.*8000"
3258
echo " Killed orphaned NetBox server(s)"
3359
fi
3460
echo "✅ All processes stopped"
3561
}
3662

3763
netbox-restart() {
38-
netbox-stop
39-
sleep 1
40-
netbox-run-bg
64+
netbox-stop && sleep 1 && netbox-run-bg
4165
}
4266

4367
netbox-reload() {
44-
cd "$PLUGIN_DIR" && uv pip install -e . && netbox-restart
68+
cd "$PLUGIN_DIR" || return 1
69+
if command -v uv >/dev/null 2>&1; then
70+
uv pip install -e . || return 1
71+
else
72+
pip install -e . || return 1
73+
fi
74+
netbox-restart
4575
}
4676

47-
netbox-logs() {
48-
tail -f /tmp/netbox.log
49-
}
77+
alias netbox-logs="tail -f /tmp/netbox.log"
78+
alias rq-logs="tail -f /tmp/rqworker.log"
5079

5180
netbox-status() {
52-
if [ -f /tmp/netbox.pid ] && kill -0 "$(cat /tmp/netbox.pid)" 2>/dev/null; then
53-
echo "NetBox is running (PID: $(cat /tmp/netbox.pid))"
81+
local PID
82+
if [ -f /tmp/netbox.pid ]; then
83+
PID=$(cat /tmp/netbox.pid 2>/dev/null)
84+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
85+
echo "NetBox is running (PID: $PID)"
86+
else
87+
echo "NetBox is not running"
88+
fi
5489
else
5590
echo "NetBox is not running"
5691
fi
92+
if [ -f /tmp/rqworker.pid ]; then
93+
PID=$(cat /tmp/rqworker.pid 2>/dev/null)
94+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
95+
echo "RQ worker is running (PID: $PID)"
96+
else
97+
echo "RQ worker is not running"
98+
fi
99+
else
100+
echo "RQ worker is not running"
101+
fi
102+
}
103+
104+
rq-status() {
105+
local PID
106+
if [ -f /tmp/rqworker.pid ]; then
107+
PID=$(cat /tmp/rqworker.pid 2>/dev/null)
108+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
109+
echo "RQ worker is running (PID: $PID)"
110+
else
111+
echo "RQ worker is not running"
112+
fi
113+
else
114+
echo "RQ worker is not running"
115+
fi
57116
}
58117

59118
netbox-shell() {
60-
(cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell)
119+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell
61120
}
62121

63122
netbox-test() {
64-
(cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py test netbox_interface_name_rules "$@")
123+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py test netbox_interface_name_rules "$@"
65124
}
66125

67126
netbox-manage() {
68-
(cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py "$@")
127+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py "$@"
69128
}
70129

71130
plugin-install() {
72-
(cd "$PLUGIN_DIR" && uv pip install -e .)
131+
cd "$PLUGIN_DIR" || return 1
132+
if command -v uv >/dev/null 2>&1; then
133+
uv pip install -e .
134+
else
135+
pip install -e .
136+
fi
73137
}
74138

75-
ruff-check() {
76-
(cd "$PLUGIN_DIR" && ruff check .)
139+
plugins-install() {
140+
if [ -f "$PLUGIN_DIR/.devcontainer/extra-requirements.txt" ]; then
141+
source /opt/netbox/venv/bin/activate
142+
if command -v uv >/dev/null 2>&1; then
143+
uv pip install -r "$PLUGIN_DIR/.devcontainer/extra-requirements.txt"
144+
else
145+
pip install -r "$PLUGIN_DIR/.devcontainer/extra-requirements.txt"
146+
fi
147+
else
148+
echo "No .devcontainer/extra-requirements.txt found"
149+
fi
77150
}
78151

79-
ruff-format() {
80-
(cd "$PLUGIN_DIR" && ruff format .)
152+
ruff-check() { cd "$PLUGIN_DIR" && command ruff check .; }
153+
ruff-format() { cd "$PLUGIN_DIR" && command ruff format .; }
154+
ruff-fix() { cd "$PLUGIN_DIR" && command ruff check --fix .; }
155+
156+
diagnose() { "$PLUGIN_DIR/.devcontainer/scripts/diagnose.sh"; }
157+
158+
# RQ job inspection commands
159+
rq-stats() {
160+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py rqstats
81161
}
82162

83-
ruff-fix() {
84-
(cd "$PLUGIN_DIR" && ruff check --fix .)
163+
rq-jobs() {
164+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell -c \
165+
"from django_rq import get_queue; q = get_queue('default'); print(f'Jobs in queue: {len(q)}'); [print(f' {job.id[:8]}: {job.func_name} - {job.get_status()}') for job in q.jobs[:10]]"
85166
}
86167

87-
diagnose() {
88-
"$PLUGIN_DIR/.devcontainer/scripts/diagnose.sh"
168+
rq-failed() {
169+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell -c \
170+
"from django_rq import get_failed_queue; q = get_failed_queue(); print(f'Failed jobs: {len(q)}'); [print(f' {job.id[:8]}: {job.func_name}') for job in q.jobs[:10]]"
89171
}
90172

173+
rq-recent() {
174+
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell -c \
175+
"from core.models import Job; jobs = Job.objects.all().order_by('-created')[:10]; [print(f'{j.id}: {j.name[:50]} - {getattr(j.status, \"value\", j.status)} ({j.user})') for j in jobs]"
176+
}
177+
178+
# Help
91179
dev-help() {
92180
echo "🎯 NetBox Interface Name Rules Plugin Development Commands:"
93181
echo ""
94182
echo "📊 NetBox Server Management:"
95-
echo " netbox-run-bg Start NetBox in background"
96-
echo " netbox-run Start NetBox in foreground"
97-
echo " netbox-stop Stop NetBox"
98-
echo " netbox-restart Restart NetBox"
99-
echo " netbox-reload Reinstall plugin and restart"
100-
echo " netbox-status Check if NetBox is running"
101-
echo " netbox-logs View NetBox server logs"
183+
echo " netbox-run-bg : Start NetBox in background"
184+
echo " netbox-run : Start NetBox in foreground (for debugging)"
185+
echo " netbox-stop : Stop NetBox and RQ worker"
186+
echo " netbox-restart : Restart NetBox and RQ worker"
187+
echo " netbox-reload : Reinstall plugin and restart NetBox"
188+
echo " netbox-status : Check if NetBox and RQ worker are running"
189+
echo " netbox-logs : View NetBox server logs"
190+
echo ""
191+
echo "⚙️ Background Jobs (RQ Worker):"
192+
echo " rq-status : Check if RQ worker is running"
193+
echo " rq-logs : View RQ worker logs"
194+
echo " rq-stats : Show RQ queue statistics"
195+
echo " rq-jobs : List jobs in default queue"
196+
echo " rq-failed : List failed jobs"
197+
echo " rq-recent : Show recent NetBox jobs"
102198
echo ""
103199
echo "🛠️ Development Tools:"
104-
echo " netbox-shell Open NetBox Django shell"
105-
echo " netbox-test Run plugin tests"
106-
echo " netbox-manage CMD Run Django management commands"
107-
echo " plugin-install Reinstall plugin in dev mode"
200+
echo " netbox-shell : Open NetBox Django shell"
201+
echo " netbox-test : Run plugin tests"
202+
echo " netbox-manage : Run Django management commands"
203+
echo " plugin-install : Reinstall plugin in development mode"
108204
echo ""
109205
echo "🧹 Code Quality:"
110-
echo " ruff-check Check code with Ruff"
111-
echo " ruff-format Format code with Ruff"
112-
echo " ruff-fix Auto-fix code issues"
206+
echo " ruff-check : Check code with Ruff"
207+
echo " ruff-format : Format code with Ruff"
208+
echo " ruff-fix : Auto-fix code issues with Ruff"
113209
echo ""
114210
echo "🔎 Diagnostics:"
115-
echo " diagnose Run startup diagnostics"
116-
echo " dev-help Show this help message"
211+
echo " diagnose : Run startup diagnostics"
212+
echo " dev-help : Show this help message"
117213
echo ""
118-
echo "📖 NetBox at: http://localhost:8000 (admin/admin)"
214+
echo "📖 NetBox available at: http://localhost:8000 (${SUPERUSER_NAME:-admin}/${SUPERUSER_PASSWORD:-admin})"
119215
}
120216

121-
echo "Functions loaded! Type 'dev-help' for available commands."
217+
echo "Dev helpers loaded! Try: rq-status, rq-stats, rq-recent, dev-help"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
4+
# Shared process management helpers.
5+
# Sourced by load-aliases.sh and start-netbox.sh.
6+
7+
# Graceful termination: SIGTERM, wait, then SIGKILL if still alive.
8+
graceful_kill_pid() {
9+
local pid="$1"
10+
kill -15 "$pid" 2>/dev/null || true
11+
sleep 2
12+
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
13+
}
14+
15+
graceful_kill_pattern() {
16+
local pattern="$1"
17+
pkill -15 -f "$pattern" 2>/dev/null || true
18+
sleep 2
19+
pgrep -f "$pattern" >/dev/null 2>&1 && pkill -9 -f "$pattern" 2>/dev/null || true
20+
}
21+
22+
# Verify a PID matches the expected process before killing it
23+
is_expected_pid() {
24+
local pid="$1" pattern="$2"
25+
ps -p "$pid" -o args= 2>/dev/null | grep -Eq "$pattern"
26+
}

0 commit comments

Comments
 (0)