Skip to content

Comments

fix: Add sudo access for systemd service status on DietPi/Orange Pi#19

Open
mverteuil wants to merge 40 commits intomainfrom
feature/diet-orange-crush
Open

fix: Add sudo access for systemd service status on DietPi/Orange Pi#19
mverteuil wants to merge 40 commits intomainfrom
feature/diet-orange-crush

Conversation

@mverteuil
Copy link
Owner

Summary

  • Fix service status detection showing "unknown" for all services on DietPi/Orange Pi platforms
  • Add sudo access for systemctl commands to resolve D-Bus permission issues
  • Create sudoers configuration during installation

Problem

On DietPi/Orange Pi, the birdnetpi user cannot access systemd D-Bus without sudo privileges, resulting in:

  • All services showing "Unknown" status in the web UI
  • Error: "Failed to connect to bus: No such file or directory"
  • SSH testing confirmed sudo systemctl is-active works but plain systemctl doesn't for birdnetpi user

Solution

  1. Service Strategies: Added sudo prefix to systemctl commands in EmbeddedSystemdStrategy

    • get_service_status(): Added sudo to systemctl is-active
    • get_service_details(): Added sudo to systemctl show
    • reboot_system(): Already had sudo
  2. Installation: Created /etc/sudoers.d/birdnetpi-systemctl with NOPASSWD access

    • Grants limited sudo access for service management commands
    • Uses wildcards to allow flags like --no-pager
    • Includes permissions for birdnetpi services, caddy, redis, and reboot
  3. Tests: Updated test expectations to match new command format with sudo

Verification

Tested on DietPi running on Orange Pi Zero 2W:

  • All 8 services (birdnetpi-* + caddy + redis) now show correct status
  • Services display PIDs and uptimes correctly
  • All tests and linters pass

Test Plan

  • Unit tests pass (uv run pytest tests/birdnetpi/system/test_service_strategies.py)
  • All linters pass (uv run pre-commit run)
  • Manually verified on DietPi/Orange Pi device
  • Confirmed all services show correct status in web UI

Files Changed

  • src/birdnetpi/system/service_strategies.py: Added sudo to systemctl commands
  • install/install.sh: Create sudoers file during installation
  • tests/birdnetpi/system/test_service_strategies.py: Updated test expectations

Related Work

This completes the DietPi/Orange Pi support stack:

  • Config file permissions ✓
  • Orange Pi Zero 2W detection ✓
  • UV package cache in tmpfs ✓
  • Caddy timeout/fallback ✓
  • Service status detection ✓ (this PR)

Root cause: DietPi automation scripts only execute when
AUTO_SETUP_INSTALL_SOFTWARE=1 is set. Without this, scripts
are created but never run, leaving no install.sh after first boot.

Changes:
- Add AUTO_SETUP_INSTALL_SOFTWARE=1 to dietpi.txt config
- Create Automation_Custom_PreScript.sh (before partition cleanup)
- Create Automation_Custom_Script.sh (after cleanup, fallback)
- Enhanced logging to /var/log/birdnetpi_automation.log
- Add BIRDNETPI_README.txt with troubleshooting guide
- Update summary messages for all preservation methods

Implements quadruple redundancy for install.sh preservation:
1. AUTO_SETUP_CUSTOM_SCRIPT_EXEC (traditional)
2. preserve_installer.sh (custom script)
3. Automation_Custom_PreScript.sh (key fix - before cleanup)
4. Automation_Custom_Script.sh (fallback - after cleanup)

Fixes install.sh not appearing on DietPi + Orange Pi 5 Pro
where DIETPISETUP partition is deleted after first boot.
The DIETPISETUP partition is deleted after DietPi first boot, removing
install.sh. While automation scripts attempt to preserve it, they run
during first boot when the partition may already be gone.

Solution: Also copy install.sh directly to /root on the rootfs partition
during SD card flashing. This ensures install.sh persists regardless of
when automation scripts execute.

- Mount rootfs partition (partition 2) during configure_dietpi_boot
- Copy install.sh to rootfs:/root/install.sh
- Keeps automation scripts as fallback for macOS users (ext4 limitations)
- Linux users get install.sh directly on persistent storage
macOS can write to ext4 filesystems using anylinuxfs. Update the
configure_dietpi_boot function to attempt mounting the rootfs
partition on macOS using diskutil.

- Try to mount partition 2 (rootfs) using diskutil mount
- anylinuxfs automatically handles ext4 filesystem access
- Falls back to boot partition + automation scripts if mount fails
- Unmounts rootfs after copying install.sh

This ensures install.sh is written directly to persistent storage
on macOS, not just relying on first-boot preservation scripts.
Fixed two issues with macOS rootfs mounting:
1. DietPi uses partition 3 for rootfs (1=BIOS, 2=DIETPISETUP, 3=ROOTFS)
2. /root directory doesn't exist on fresh rootfs - create it with mkdir -p

Changes:
- Changed from disk4s2 to disk4s3 for rootfs partition on macOS
- Added 'sudo mkdir -p /root' before copying install.sh
- Applied same fix to Linux path for consistency
Added debug output to show:
- All available partitions on the device
- Which partition we're trying to mount
- Error message when mount fails

This will help diagnose why rootfs partition mounting is failing
on macOS.
Discovered via debug output that DietPi Orange Pi 5 Pro uses:
- Partition 1: Linux Filesystem (rootfs) - 1.3 GB
- Partition 2: DIETPISETUP (FAT32) - 1.0 MB

Changed from disk4s3 to disk4s1 for macOS rootfs mounting.
This matches the actual partition layout on the SD card.
Implemented proper anylinuxfs integration for DietPi similar to Armbian:
- Check if anylinuxfs is installed
- Unmount any existing anylinuxfs mounts
- Run 'sudo anylinuxfs disk4s1 -w false' to mount rootfs
- Wait for mount point to appear in /Volumes
- Copy install.sh to /root on the mounted rootfs
- Unmount with 'sudo anylinuxfs unmount' when done

This should finally enable rootfs mounting on macOS with anylinuxfs.
anylinuxfs creates a mount using the partition name (e.g., disk4s1) rather
than a filesystem label. The previous code looked for specific mount names
like "dietpi_root", "DIETPI", or "Linux", which didn't exist.

Now captures the list of volumes before running anylinuxfs, then finds the
new volume by comparing before/after and checking for Linux filesystem
markers (/etc, /root, /usr). This correctly detects the mount regardless
of what name anylinuxfs uses.
When copying to ext4 filesystem via anylinuxfs, macOS cp tries to copy
extended attributes which are not supported on ext4. This causes:
"could not copy extended attributes: Operation not permitted"

Using dd instead bypasses extended attributes entirely and just copies
the raw file content. Also improved anylinuxfs unmount error handling
to catch CalledProcessError in addition to TimeoutExpired.
DietPi and Orange Pi systems don't have spi/gpio groups by default like
Raspberry Pi OS does. This was causing usermod errors:
  usermod: group 'spi' does not exist
  usermod: group 'gpio' does not exist

Now create these groups if they don't exist before adding the birdnetpi
user to them. Uses getent to check group existence safely.
…guration

- Copy birdnetpi_config.txt to rootfs partition (persists after DIETPISETUP deletion)
- Add os_key and device_key to config file for unattended setup
- Update install.sh to source config from /root, /boot/firmware, or /boot
- Clean up temp config file after flashing
- Initialize config_file variable to fix pyright warning
- Add orange_pi_0w2 to Armbian and DietPi device lists
- Remove orange_pi_5 from both OS image configurations
- Keep Orange Pi 5 Plus and Pro variants
- Update Armbian URL: orangepizero2w
- Update DietPi URL: DietPi_OrangePiZero2W-ARMv8-Bookworm.img.xz
- Update docstring example to use orange_pi_5_pro
- Add BIRDNETPI_OS_KEY and BIRDNETPI_DEVICE_KEY to environment exports
- Enables setup_system.py to use pre-configuration when config file is missing
- Fixes issue where OS/device prompts appeared despite config file being created
- Works around DietPi deleting DIETPISETUP partition after first boot
- Add orange_pi_0w2 to DEVICE_PROPERTIES with SPI capability
- Fix DEVICE_PROPERTIES keys to match actual device keys (use underscores)
- Add Allwinner H618 SPI overlay (spi-spidev) for Orange Pi Zero 2W
- Update SPI configuration to handle Orange Pi Zero 2W specifically
- Improve error handling when SPI overlay is not configured
- Add chip description to SPI enabled messages (Allwinner H618 vs RK3588)
The config file needs to export os_key and device_key so they're
available to install.sh when it sources the file. Without export,
the variables are set in the config file's scope but not passed
to the parent shell.
- Remove PI_IMAGES dictionary (was only referenced by dead functions)
- Remove select_pi_version() function (never called)
- Remove download_image() function (never called, superseded by download_image_new)
- Update FlasherWizardApp instantiation to use only OS_PROPERTIES argument

The TUI wizard (flasher_tui.py) now builds os_images internally from the
devices module, so flash_sdcard.py no longer needs to pass it.

Total cleanup: 212 lines removed
The copy_birdnetpi_config function was still writing shell script format
to birdnetpi_config.txt instead of JSON format to birdnetpi_config.json.

This meant WiFi settings and git branch were not being persisted to the
boot configuration.

Changes:
- Rewrite copy_birdnetpi_config to output JSON format
- Add wifi_ssid, wifi_password, wifi_auth to saved config
- Change output filename from birdnetpi_config.txt to birdnetpi_config.json
- Use json.dumps() instead of building shell script lines
- Update docstring to reflect JSON format
The install.sh script was still looking for birdnetpi_config.txt and
trying to source it as a shell script, but we now write JSON format
to birdnetpi_config.json.

Changes:
- Update config file paths from .txt to .json
- Parse JSON using python3 instead of sourcing shell script
- Extract repo_url and branch fields from JSON
- Gracefully handle missing python3 or parse errors
- Update comment to reference .json format
The DietPi preservation scripts embedded in flash_sdcard.py were still
referencing birdnetpi_config.txt instead of the new .json format.

This caused fresh flashes to write .txt files instead of .json files,
which broke the JSON parsing in install.sh and setup_system.py.

Changes:
- Update first preservation script to copy birdnetpi_config.json
- Update second preservation script to copy birdnetpi_config.json
- Update direct rootfs copy to use birdnetpi_config.json
- Update all echo messages to reference .json files

All three code paths that preserve config for DietPi now use .json format.
Instead of trying to parse JSON before Python is installed, we now:
1. Read install.sh during flash
2. Substitute the REPO_URL and BRANCH defaults with configured values
3. Write the modified install.sh to boot partition

This ensures the branch setting is used even before Python is available,
eliminating the need for JSON parsing in early boot stages.

Simplifies install.sh by removing the JSON parsing logic that ran before
Python was installed (which always failed silently).
The root cause was that copy_installer_script() correctly substituted
the branch/repo defaults in install.sh and copied it to the boot partition,
but then the DietPi rootfs copy code was copying the ORIGINAL install.sh
again, overwriting the modified one.

Now copy_installer_script() returns the path to the modified temp file,
and the rootfs copy uses that instead of reading the original file again.
Remove --quiet flags from uv lock and uv sync commands to provide
feedback during the dependency installation process, which can take
several minutes on slower devices.
The birdnetpi user cannot read /root/birdnetpi_config.json due to
directory permissions (700). Copy the config file to /opt/birdnetpi/
during installation where the birdnetpi user has access.

Changes:
- install.sh: Copy config from /root to /opt/birdnetpi after cloning
- setup_system.py: Check /opt/birdnetpi first for config file
The orange_pi_0w2 device key was not in the supported devices list,
causing a KeyError during setup. Add it with proper display name.
Configure UV_CACHE_DIR=/var/cache/uv to cache downloaded packages.
This speeds up retries when installation fails and avoids re-downloading
packages that were already fetched successfully.
- Cache packages in /dev/shm (RAM) instead of SD card
- Speeds up retries without wearing SD card
- Clean up cache at end to free RAM for runtime
Fix service status detection showing "unknown" for all services on
DietPi/Orange Pi platforms by adding sudo to systemctl commands.

On DietPi, the birdnetpi user cannot access systemd D-Bus without
sudo privileges, resulting in "Failed to connect to bus" errors.

Changes:
- Add sudo prefix to systemctl commands in EmbeddedSystemdStrategy
- Create /etc/sudoers.d/birdnetpi-systemctl during installation
- Grant NOPASSWD sudo access for service management commands
- Update test expectations to match new command format

The sudoers file uses wildcards to allow flags like --no-pager and
includes permissions for birdnetpi services, caddy, redis, and reboot.
Add support for enabling SPI on DietPi/Armbian (Orange Pi) platforms
by detecting and configuring /boot/armbianEnv.txt.

Changes:
- Check for existing SPI devices first (skip if already enabled)
- Support Raspberry Pi OS via /boot/firmware/config.txt (existing)
- Support DietPi/Armbian via /boot/armbianEnv.txt (new)
  - Add spi-spidev to overlays parameter
  - Handle both new and existing overlays configurations
- Provide warning if platform cannot be detected

This ensures e-paper HAT detection works on all supported platforms.
DietPi uses /boot/dietpiEnv.txt instead of /boot/armbianEnv.txt
on some platforms. Update SPI detection to check for both files.

This fixes SPI enablement on Orange Pi 5 Pro running DietPi.
On RK3588-based boards like Orange Pi 5 Pro, the platform comes with
pre-configured SPI overlays (e.g., rk3588-spi4-m0-cs1-spidev). The
installer was incorrectly adding the generic spi-spidev overlay on
top of these, which could cause conflicts.

Changes:
- Detect any existing SPI overlay (platform-specific or generic)
- If SPI overlay exists, only verify/add param_spidev_spi_bus=0
- Don't add spi-spidev if platform-specific overlay already present
- Only add spi-spidev on systems without any SPI overlay configured

This ensures we respect platform-specific device tree configurations
while still enabling SPI on boards that need the generic overlay.
When SPI overlay and param_spidev_spi_bus are both already configured,
no reboot is needed since nothing was changed. Only set SPI_ENABLED=true
(which triggers reboot) when we actually modify the boot configuration.

This prevents unnecessary reboots when re-running the installer on a
system that already has SPI properly configured.
- Detect RK3588 overlays with incorrect chip prefix
- Replace with correct format (spi4-m2-cs0-spidev without prefix)
- DietPi automatically prepends overlay_prefix, so overlay names should not include it
- Add param_spidev_max_freq=100000000 for RK3588 platforms
- Only trigger reboot when configuration is actually modified

Tested on Orange Pi 5 Pro - /dev/spidev4.0 now appears correctly after reboot
- Use spi4-m2-cs0-spidev overlay (M2-CS0 variant is verified working)
- Remove chip prefix from overlay names (DietPi auto-prepends from overlay_prefix)
- Add param_spidev_max_freq=100000000 for RK3588 platforms (required)
- Also fixed ROCK 5B overlay format for consistency

This eliminates the need for the installer to reboot when SPI is enabled
since the SD card boots with correct SPI configuration from the start.
The configure_dietpi_boot() function now downloads the Waveshare ePaper
library to the boot partition when SPI is enabled, matching the behavior
of configure_boot_partition_new() for Raspberry Pi OS.

This eliminates the massive git clone during install.sh which was failing
due to the repository containing 20k+ STM32 firmware files. The flasher
uses sparse-checkout to download only the Python subdirectory (~6MB).

The install.sh script can then use the pre-downloaded library from
/boot/firmware/waveshare-epd (or /boot/waveshare-epd on DietPi).
Created download_waveshare_library() helper to eliminate code duplication
between configure_dietpi_boot() and configure_boot_partition_new().

The helper function:
- Uses sparse-checkout to download only Python subdirectory (~6MB)
- Avoids cloning 20k+ STM32 firmware files from the full repository
- Provides clear documentation and consistent behavior across platforms

This reduces the code from ~130 lines (65 lines × 2) to ~70 lines (65 lines
helper + 2 simple calls), improving maintainability.
- Copy only lib/ directory and setup.py (essential for installation)
- Skip pic/ (example images, several MB) and examples/ (test scripts)
- Prevents 'No space left on device' error on ~200MB boot partitions
- Update docstring and comments to reflect space-saving approach
The previous implementation had several issues preventing the Waveshare
library from being installed from the pre-downloaded tarball:

1. Boot partition mount location was hardcoded to /boot/firmware, but DietPi
   uses different mount points and deletes DIETPISETUP after first boot
2. The 'uv lock' command was regenerating the lockfile by accessing Git,
   even with the patched pyproject.toml, causing /dev/shm to run out of space
3. Tarball was only on boot partition which gets deleted on DietPi first boot

Changes:
- **flash_sdcard.py**: Copy Waveshare tarball to rootfs:/root/ for persistence
- **install.sh**: Check multiple locations for tarball (boot partition variants + rootfs)
- **install.sh**: Remove 'uv lock' command - let 'uv sync' handle the local path
- **install.sh**: Add fallback message when tarball not found

This ensures the compressed Waveshare library is available even after the
DIETPISETUP partition is deleted, preventing the 20,805-file git clone
that exhausts /dev/shm memory.
/dev/shm is often limited to 512MB on many systems, which is insufficient
for uv's cache when building packages. This causes 'No space left on device'
errors even for small packages.

Switch to /tmp which is typically larger and still avoids excessive SD card
writes on most systems (often tmpfs-backed or similar).
- Add patch_waveshare_orangepi.py to automatically add Orange Pi support
  to Waveshare library during installation
- Replace build-essential with minimal dependencies (gcc, libc6-dev)
  to reduce installation size (removes perl, make, g++, dpkg-dev)
- Add libportaudio2 and libsndfile1 for audio dependencies
- Fix pyproject.toml patching using heredoc to avoid DietPi su wrapper
  quote mangling issues
- OrangePi class uses lgpio with gpiochip4 and SPI bus 4
- Detects Orange Pi via /proc/device-tree/model
- Idempotent patching (can run multiple times safely)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant