Skip to content

Commit 22f1552

Browse files
committed
Feature: add installer TFTP setup helper
- add interactive tftpd-hpa setup script under scripts - offer local TFTP server setup from install.sh - enforce upload-capable --create server configuration - ensure writable TFTP directory for the daemon user - keep CI-safe behavior and document Ubuntu-first support - 2026-03-18 00:24:48
1 parent d3ad860 commit 22f1552

4 files changed

Lines changed: 299 additions & 0 deletions

File tree

docs/scripts/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Utility scripts located in `scripts/` that support installation, quality control
99
| `init_secrets_key.sh` | Generates the Fernet key and `.env` entries required for encrypted `system.json` secrets. | Run once per repo clone: `./scripts/init_secrets_key.sh` (prompts before overwriting existing keys). |
1010
| `install_py_path.sh` | Ensures the project path is appended to the user’s `PYTHONPATH` for CLI tooling. | `./scripts/install_py_path.sh` (adds exports to `~/.bashrc`). |
1111
| `install_aliases.sh` | Installs convenience shell aliases (e.g., `config-menu`, `pypnm-clean`). | `./scripts/install_aliases.sh` after initial `./install.sh`. |
12+
| `setup_tftp_server.sh` | Inspects a local `tftpd-hpa` install and enforces upload-capable config (`--create`) plus a writable TFTP root. | `./scripts/setup_tftp_server.sh` after installing `tftpd-hpa`; applies Debian/Ubuntu defaults directly and exits cleanly on other Linux layouts. |
1213
| `update_env.sh` | Activates the local venv and exports common dev env vars. | `./scripts/update_env.sh .env` (or with your venv path). |
1314

1415
## Build, update, and quality control

docs/system/pnm-file-retrieval/tftp.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ interactive `pypnm config-menu` helper. In this scenario, `localhost` is selecte
55
the TFTP host, which means the TFTP server and PyPNM are running on the same box.
66
PyPNM will still use the TFTP protocol to download PNM files for analysis.
77

8+
During a normal interactive `./install.sh` run, PyPNM now also offers a local
9+
`tftpd-hpa` setup helper:
10+
11+
```bash
12+
./scripts/setup_tftp_server.sh
13+
```
14+
15+
That helper checks whether `tftpd-hpa` is installed and, when present, rewrites
16+
`/etc/default/tftpd-hpa` to keep upload-capable options such as `--create`,
17+
ensures the configured `TFTP_DIRECTORY` exists, and verifies that the daemon
18+
user can write to it before restarting the service.
19+
20+
The helper is CI-safe:
21+
22+
- `install.sh` only offers it in interactive non-CI runs
23+
- the helper itself exits immediately when `CI` or `GITHUB_ACTIONS` is set
24+
25+
It is also Linux-safe across other distros:
26+
27+
- on Debian/Ubuntu, it manages `/etc/default/tftpd-hpa`
28+
- on other Linux systems, it detects whether `tftpd-hpa` is present
29+
- if the distro does not use the Debian defaults file layout, it exits cleanly
30+
with a manual-configuration message instead of forcing Debian-specific paths
31+
832
The `remote_dir` is the directory on the TFTP server where PNM files are stored
933
and served. Leaving it empty (`""`) uses the TFTP server's default root
1034
(often something like `/srv/tftp`, depending on your server configuration).

install.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ Options:
6868
• CI/GITHUB_ACTIONS are not set.
6969
In CI environments, the option is acknowledged but skipped.
7070
71+
During interactive local installs, install.sh also offers to inspect and
72+
remediate a local tftpd-hpa server for upload-capable TFTP service by
73+
running:
74+
75+
./scripts/setup_tftp_server.sh
76+
77+
On Debian/Ubuntu, this helper updates
78+
/etc/default/tftpd-hpa directly. On other Linux distros,
79+
it performs detection and exits cleanly when the distro uses
80+
a different tftpd-hpa layout.
81+
7182
venv_dir Optional virtual environment directory name. Defaults to ".env".
7283
7384
--help, -h Show this help message and exit.
@@ -112,6 +123,10 @@ After installation, you can also configure how PyPNM retrieves PNM files
112123
(local/TFTP/FTP/SCP/SFTP/HTTP/HTTPS) manually by running:
113124
114125
./tools/pnm/pnm_file_retrieval_setup.py
126+
127+
And you can inspect or remediate a local tftpd-hpa server manually with:
128+
129+
./scripts/setup_tftp_server.sh
115130
EOF
116131
}
117132

@@ -653,6 +668,35 @@ run_pnm_setup_if_possible() {
653668
fi
654669
}
655670

671+
run_tftp_server_setup_offer_if_possible() {
672+
if [[ ! -t 0 || -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]; then
673+
echo "ℹ️ Skipping local TFTP server setup offer (non-interactive or CI environment)."
674+
echo " You can run it later with:"
675+
echo " ./scripts/setup_tftp_server.sh"
676+
return
677+
fi
678+
679+
if [[ ! -x "${PROJECT_ROOT}/scripts/setup_tftp_server.sh" ]]; then
680+
echo "ℹ️ scripts/setup_tftp_server.sh is missing or not executable."
681+
echo " Run it later once available:"
682+
echo " ./scripts/setup_tftp_server.sh"
683+
return
684+
fi
685+
686+
echo
687+
read -r -p "Offer local tftpd-hpa upload setup now? [y/N]: " setup_tftp_answer || true
688+
case "${setup_tftp_answer:-N}" in
689+
y|Y|yes|YES)
690+
"${PROJECT_ROOT}/scripts/setup_tftp_server.sh"
691+
;;
692+
*)
693+
echo "ℹ️ Skipping local TFTP server setup."
694+
echo " You can run it later with:"
695+
echo " ./scripts/setup_tftp_server.sh"
696+
;;
697+
esac
698+
}
699+
656700
run_pnm_alias_installer_if_available() {
657701
if [[ -x "${PROJECT_ROOT}/scripts/install_aliases.sh" ]]; then
658702
echo "🔗 Installing PyPNM shell aliases (e.g., config-menu)…"
@@ -744,6 +788,8 @@ else
744788
echo " ./tools/pnm/pnm_file_retrieval_setup.py"
745789
fi
746790

791+
run_tftp_server_setup_offer_if_possible
792+
747793
ensure_pypnm_shared_group
748794
ensure_tmp_pypnm_permissions
749795
run_pnm_alias_installer_if_available

scripts/setup_tftp_server.sh

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
TFTPD_DEFAULTS_FILE="/etc/default/tftpd-hpa"
5+
DEFAULT_TFTP_USER="tftp"
6+
DEFAULT_TFTP_DIR="/srv/tftp"
7+
DEFAULT_TFTP_ADDRESS=":69"
8+
DEFAULT_TFTP_OPTIONS="--secure --create"
9+
10+
prompt_yes_no() {
11+
local prompt="${1}"
12+
local default_answer="${2:-N}"
13+
local answer=""
14+
15+
read -r -p "${prompt} " answer || true
16+
if [[ -z "${answer}" ]]; then
17+
answer="${default_answer}"
18+
fi
19+
20+
case "${answer}" in
21+
y|Y|yes|YES)
22+
return 0
23+
;;
24+
*)
25+
return 1
26+
;;
27+
esac
28+
}
29+
30+
run_with_privilege_if_needed() {
31+
if [[ "$(id -u)" -eq 0 ]]; then
32+
"$@"
33+
return $?
34+
fi
35+
if command -v sudo >/dev/null 2>&1; then
36+
sudo "$@"
37+
return $?
38+
fi
39+
echo "Privilege escalation is required for: $*"
40+
return 1
41+
}
42+
43+
is_tftpd_hpa_installed() {
44+
if command -v systemctl >/dev/null 2>&1; then
45+
if systemctl list-unit-files 2>/dev/null | grep -q '^tftpd-hpa\.service'; then
46+
return 0
47+
fi
48+
fi
49+
50+
if command -v dpkg-query >/dev/null 2>&1; then
51+
if dpkg-query -W -f='${Status}\n' tftpd-hpa 2>/dev/null | grep -q '^install ok installed$'; then
52+
return 0
53+
fi
54+
fi
55+
56+
return 1
57+
}
58+
59+
read_tftp_setting() {
60+
local key="${1}"
61+
local default_value="${2}"
62+
63+
if [[ ! -f "${TFTPD_DEFAULTS_FILE}" ]]; then
64+
printf '%s\n' "${default_value}"
65+
return
66+
fi
67+
68+
local line=""
69+
line="$(grep -E "^${key}=" "${TFTPD_DEFAULTS_FILE}" 2>/dev/null | tail -n 1 || true)"
70+
if [[ -z "${line}" ]]; then
71+
printf '%s\n' "${default_value}"
72+
return
73+
fi
74+
75+
line="${line#*=}"
76+
line="${line%\"}"
77+
line="${line#\"}"
78+
printf '%s\n' "${line}"
79+
}
80+
81+
normalize_tftp_options() {
82+
local raw_options="${1}"
83+
local normalized=""
84+
local token=""
85+
86+
for token in ${raw_options}; do
87+
if [[ "${token}" == "--create" ]]; then
88+
continue
89+
fi
90+
normalized="${normalized} ${token}"
91+
done
92+
93+
normalized="${normalized} --create"
94+
normalized="$(printf '%s\n' "${normalized}" | xargs)"
95+
96+
if [[ -z "${normalized}" ]]; then
97+
normalized="${DEFAULT_TFTP_OPTIONS}"
98+
fi
99+
100+
printf '%s\n' "${normalized}"
101+
}
102+
103+
write_tftpd_defaults() {
104+
local tftp_user="${1}"
105+
local tftp_dir="${2}"
106+
local tftp_address="${3}"
107+
local tftp_options="${4}"
108+
local tmp_file=""
109+
110+
tmp_file="$(mktemp)"
111+
cat > "${tmp_file}" <<EOF
112+
# /etc/default/tftpd-hpa
113+
114+
TFTP_USERNAME="${tftp_user}"
115+
TFTP_DIRECTORY="${tftp_dir}"
116+
TFTP_ADDRESS="${tftp_address}"
117+
TFTP_OPTIONS="${tftp_options}"
118+
EOF
119+
120+
run_with_privilege_if_needed install -m 0644 "${tmp_file}" "${TFTPD_DEFAULTS_FILE}"
121+
rm -f "${tmp_file}"
122+
}
123+
124+
ensure_tftp_directory() {
125+
local tftp_user="${1}"
126+
local tftp_dir="${2}"
127+
local tftp_group="${tftp_user}"
128+
129+
if id -u "${tftp_user}" >/dev/null 2>&1; then
130+
tftp_group="$(id -gn "${tftp_user}")"
131+
else
132+
echo "User '${tftp_user}' does not exist; cannot verify writable TFTP directory."
133+
return 1
134+
fi
135+
136+
run_with_privilege_if_needed mkdir -p "${tftp_dir}"
137+
run_with_privilege_if_needed chown "${tftp_user}:${tftp_group}" "${tftp_dir}"
138+
run_with_privilege_if_needed chmod 0775 "${tftp_dir}"
139+
140+
if [[ "$(id -u)" -eq 0 ]]; then
141+
runuser -u "${tftp_user}" -- test -w "${tftp_dir}"
142+
elif command -v sudo >/dev/null 2>&1; then
143+
sudo -u "${tftp_user}" test -w "${tftp_dir}"
144+
else
145+
test -w "${tftp_dir}"
146+
fi
147+
}
148+
149+
restart_tftpd_hpa() {
150+
if ! command -v systemctl >/dev/null 2>&1; then
151+
echo "systemctl not found; skipping tftpd-hpa restart."
152+
return
153+
fi
154+
155+
run_with_privilege_if_needed systemctl enable tftpd-hpa
156+
run_with_privilege_if_needed systemctl restart tftpd-hpa
157+
}
158+
159+
main() {
160+
if [[ ! -t 0 ]]; then
161+
echo "TFTP setup requires an interactive terminal."
162+
exit 0
163+
fi
164+
165+
if [[ "$(uname -s)" != "Linux" ]]; then
166+
echo "Local tftpd-hpa setup is only supported on Linux hosts."
167+
exit 0
168+
fi
169+
170+
if [[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]; then
171+
echo "CI environment detected; skipping interactive tftpd-hpa setup."
172+
exit 0
173+
fi
174+
175+
if ! is_tftpd_hpa_installed; then
176+
echo "tftpd-hpa is not installed on this host."
177+
echo "Install it with: sudo apt-get install -y tftpd-hpa"
178+
echo "Then rerun: ./scripts/setup_tftp_server.sh"
179+
exit 0
180+
fi
181+
182+
if [[ ! -f "${TFTPD_DEFAULTS_FILE}" ]]; then
183+
echo "Detected tftpd-hpa, but ${TFTPD_DEFAULTS_FILE} is not present."
184+
echo "This helper currently manages Debian/Ubuntu-style tftpd-hpa defaults."
185+
echo "On this distro, adjust your tftpd-hpa service configuration manually and rerun PyPNM setup."
186+
exit 0
187+
fi
188+
189+
local tftp_user=""
190+
local tftp_dir=""
191+
local tftp_address=""
192+
local tftp_options=""
193+
194+
tftp_user="$(read_tftp_setting "TFTP_USERNAME" "${DEFAULT_TFTP_USER}")"
195+
tftp_dir="$(read_tftp_setting "TFTP_DIRECTORY" "${DEFAULT_TFTP_DIR}")"
196+
tftp_address="$(read_tftp_setting "TFTP_ADDRESS" "${DEFAULT_TFTP_ADDRESS}")"
197+
tftp_options="$(read_tftp_setting "TFTP_OPTIONS" "${DEFAULT_TFTP_OPTIONS}")"
198+
tftp_options="$(normalize_tftp_options "${tftp_options}")"
199+
200+
echo "Current tftpd-hpa configuration:"
201+
echo " defaults file : ${TFTPD_DEFAULTS_FILE}"
202+
echo " TFTP_USERNAME : ${tftp_user}"
203+
echo " TFTP_DIRECTORY: ${tftp_dir}"
204+
echo " TFTP_ADDRESS : ${tftp_address}"
205+
echo " TFTP_OPTIONS : ${tftp_options}"
206+
207+
if ! prompt_yes_no "Apply upload-capable tftpd-hpa settings now? [y/N]" "N"; then
208+
echo "Skipping local TFTP server setup."
209+
exit 0
210+
fi
211+
212+
write_tftpd_defaults "${tftp_user}" "${tftp_dir}" "${tftp_address}" "${tftp_options}"
213+
214+
if ensure_tftp_directory "${tftp_user}" "${tftp_dir}"; then
215+
echo "Verified writable TFTP directory: ${tftp_dir}"
216+
else
217+
echo "Unable to verify TFTP directory writability for ${tftp_user} at ${tftp_dir}."
218+
exit 1
219+
fi
220+
221+
restart_tftpd_hpa
222+
223+
echo "Local tftpd-hpa setup complete."
224+
echo " TFTP_DIRECTORY: ${tftp_dir}"
225+
echo " TFTP_OPTIONS : ${tftp_options}"
226+
}
227+
228+
main "$@"

0 commit comments

Comments
 (0)