fredag 27 september 2024

Remote Headless Raspberry Pi OS Lite Setup: No SD Card Reader, No Monitor, No Keyboard

Headless Raspberry Pi OS Flashing Guide

This guide provides a headless method for flashing a Raspberry Pi OS onto a bootable device by running on another another connected device. This approach is especially useful when you don't have direct access to the bootable device from your main computer.

Introduction

The goal of this guide is to help users flash a Raspberry Pi OS to an SSD, SD card, or other bootable device in a headless environment. This method uses another Raspberry Pi to flash the device, configure essential settings, and ensure successful booting from the new OS.

There are certain limitations to consider, such as the inability to boot from a USB when an NVMe SSD is connected. Based on personal experience, you need to flash a microSD card first and boot from it to get the OS onto the SSD.

Additionally, some manual changes may be necessary after flashing to ensure bootability, as the CLI version of the Raspberry Pi Imager doesn’t include customized settings.

For a More Comprehensive Guide

For a more comprehensive guide on headless Raspberry Pi OS flashing, visit

this GitHub repository.

Download and run the script provided, and be sure to read the README for detailed instructions.

Requirements

Before starting, ensure you have the following:

  • A Raspberry Pi that you will use to flash the OS (with SSH access).
  • A bootable device (e.g., NVMe SSD, SD card, or USB drive).
  • Pre-downloaded Raspberry Pi OS image (e.g., raspios_lite_arm64_latest).
  • wget https://downloads.raspberrypi.org/raspios_lite_arm64_latest
  • mv raspios_lite_arm64_latest raspios_lite_arm64_latest.xz
  • Network access to configure Wi-Fi settings on the bootable device.
  • A basic understanding of using the terminal and SSH.

Steps to Flash Raspberry Pi OS

The following YAML-based Ansible playbook automates the process of flashing the Raspberry Pi OS onto a bootable device. The playbook also configures essential settings like SSH, Wi-Fi, and user credentials, ensuring that the newly flashed device is ready to use after boot. Make sure to customize the playbook before running it on your device.

Note: Running this playbook will make changes to your system and the target bootable device. Always double-check your settings and make sure you're targeting the correct device before running the playbook.

Playbook for Flashing OS


---
# Run with sudo ansible-playbook notebook_setup.yml
- name: Set up Raspberry Pi bootable device
  hosts: localhost
  become: yes
  vars:
    bootable_device: /dev/nvme0n1 # Change this to your device path, e.g., /dev/sda, /dev/mmcblk0, etc.
    os_image_path: /home/os_username/raspios_lite_arm64_latest.xz
    wifi_ssid: "WIFI_SSID"
    wifi_password: "WIFI_PASSWORD"
    new_username: "NEW_USERNAME"
    new_password: "NEW_PASSWORD"
    country: "US"
    perform_full_wipe: true
    fixed_wait_time: 60 # Adjust waiting time if needed

  vars_prompt:
    - name: confirmation
      prompt: "Are you sure you want to flash {{ bootable_device }}? All current data on this device will be lost. Type 'yes' to confirm, or 'no' to cancel."
      private: false
      default: ""

  pre_tasks:
    - name: Verify confirmation
      fail:
        msg: "Operation cancelled by the user. Playbook execution aborted."
      when: confirmation != 'yes'

    - name: Check if OS image file exists
      stat:
        path: "{{ os_image_path }}"
      register: os_image_stat

    - name: Fail if OS image doesn't exist
      fail:
        msg: "The specified OS image {{ os_image_path }} does not exist."
      when: not os_image_stat.stat.exists

  tasks:
    - name: Ensure partitions are unmounted
      mount:
        path: "{{ item }}"
        state: unmounted
      loop:
        - /mnt/boot
        - /mnt/rootfs
      ignore_errors: yes

    - name: Clear partition table (optional full wipe)
      command: "dd if=/dev/zero of={{ bootable_device }} bs=512 count=1"
      when: perform_full_wipe | default(false)

    - name: Install required packages
      apt:
        name:
          - rpi-imager
          - parted
        state: present
        update_cache: yes

    - name: Check if bootable device exists
      stat:
        path: "{{ bootable_device }}"
      register: bootable_device_stat

    - name: Fail if bootable device doesn't exist
      fail:
        msg: "The specified bootable device {{ bootable_device }} does not exist."
      when: not bootable_device_stat.stat.exists

    - name: Determine partition suffix
      set_fact:
        partition_suffix: "{{ 'p' if '/dev/mmcblk' in bootable_device or '/dev/loop' in bootable_device or '/dev/nvme' in bootable_device else '' }}"

    - name: Flash new OS to bootable device
      command: >
        rpi-imager --cli {{ os_image_path }} {{ bootable_device }} --enable-writing-system-drives
      register: flash_result
      failed_when: flash_result.rc != 0
      changed_when: flash_result.rc == 0

    - name: Wait for system to settle after flashing
      pause:
        seconds: "{{ fixed_wait_time }}" # Replaces the dynamic partition detection loop

    - name: Inform kernel of partition table changes
      command: "partprobe {{ bootable_device }}"
      ignore_errors: yes

    - name: Ensure partitions are available
      wait_for:
        path: "{{ bootable_device }}{{ partition_suffix }}{{ item }}"
        state: present
        timeout: 30
      loop:
        - "1"
        - "2"
      register: wait_result
      ignore_errors: yes

    - name: Check partition availability results
      fail:
        msg: "Partition {{ bootable_device }}{{ partition_suffix }}{{ item.item }} is not available after 30 seconds."
      when: item.failed
      loop: "{{ wait_result.results }}"

    - name: Debug partition availability
      debug:
        msg: "Partition {{ bootable_device }}{{ partition_suffix }}{{ item.item }} status: {{ item.state }}"
      loop: "{{ wait_result.results }}"

    - name: Fail if partitions are not available
      fail:
        msg: >-
          Partitions are not available after flashing. Please check the device and try again.
      when: wait_result is failed

    - name: Create mount points
      file:
        path: "{{ item }}"
        state: directory
      loop:
        - /mnt/boot
        - /mnt/rootfs

    - name: Mount bootable device partitions
      mount:
        path: "{{ item.path }}"
        src: "{{ item.src }}"
        fstype: "{{ item.fstype }}"
        state: mounted
      loop:
        - path: /mnt/boot
          src: "{{ bootable_device }}{{ partition_suffix }}1"
          fstype: vfat
        - path: /mnt/rootfs
          src: "{{ bootable_device }}{{ partition_suffix }}2"
          fstype: ext4

    # Remaining tasks are unchanged
    - name: Set up Wi-Fi configuration
      copy:
        content: |
          country="{{ country }}"
          ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
          update_config=1

          network={
            ssid="{{ wifi_ssid }}"
            psk="{{ wifi_password }}"
          }
        dest: /mnt/boot/wpa_supplicant.conf

    - name: Enable SSH
      file:
        path: /mnt/boot/ssh
        state: touch

    - name: Ensure SSH password authentication is enabled
      lineinfile:
        path: /mnt/rootfs/etc/ssh/sshd_config
        regexp: "^#?PasswordAuthentication"
        line: PasswordAuthentication yes

    - name: Disable SSH root login
      lineinfile:
        path: /mnt/rootfs/etc/ssh/sshd_config
        regexp: "^#?PermitRootLogin"
        line: PermitRootLogin no

    - name: Check if user exists
      command: "chroot /mnt/rootfs /bin/bash -c 'id -u {{ new_username }}'"
      register: user_exists
      failed_when: false
      changed_when: false

    - name: Remove existing user if present
      command: "chroot /mnt/rootfs /bin/bash -c 'userdel -r {{ new_username }}'"
      when: user_exists.rc == 0

    - name: Create user on the new system
      shell: |
        chroot /mnt/rootfs /bin/bash -c "
        useradd -m -s /bin/bash {{ new_username }} &&
        echo '{{ new_username }}:{{ new_password }}' | chpasswd &&
        usermod -aG sudo {{ new_username }}
        "

    - name: Allow passwordless sudo for the new user
      copy:
        content: "{{ new_username }} ALL=(ALL) NOPASSWD:ALL"
        dest: "/mnt/rootfs/etc/sudoers.d/{{ new_username }}"
        mode: "0440"

    - name: Enable SSH service on the new system
      shell: |
        chroot /mnt/rootfs /bin/bash -c "
        systemctl enable ssh
        "

    - name: Configure network on the new system
      copy:
        content: |
          allow-hotplug eth0
          iface eth0 inet dhcp
          allow-hotplug wlan0
          iface wlan0 inet dhcp
            wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
        dest: /mnt/rootfs/etc/network/interfaces

    - name: Set hostname
      copy:
        content: raspberrypi
        dest: /mnt/rootfs/etc/hostname

    - name: Configure hosts file
      copy:
        content: |
          127.0.0.1 localhost
          127.0.1.1 raspberrypi

          # The following lines are desirable for IPv6 capable hosts
          ::1     localhost ip6-localhost ip6-loopback
          ff02::1 ip6-allnodes
          ff02::2 ip6-allrouters
        dest: /mnt/rootfs/etc/hosts

    - name: Set locale
      copy:
        content: en_US.UTF-8 UTF-8
        dest: /mnt/rootfs/etc/locale.gen

    - name: Generate locale
      command: chroot /mnt/rootfs /bin/bash -c 'locale-gen'

    - name: Set default locale
      copy:
        content: LANG=en_US.UTF-8
        dest: /mnt/rootfs/etc/default/locale

    - name: Set timezone to Eastern Time (US)
      command: >-
        chroot /mnt/rootfs /bin/bash -c 'ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime'

    - name: Unmount partitions
      mount:
        path: "{{ item }}"
        state: unmounted
      loop:
        - /mnt/boot
        - /mnt/rootfs

    - name: Final sync before reboot
      command: sync

    - name: Notify about success
      debug:
        msg: >
          Raspberry Pi bootable device created successfully with OS image {{ os_image_path }} on {{ bootable_device }}.
    - name: Remind user to safely remove device
      debug:
        msg: "Setup complete. You can now safely remove {{ bootable_device }} and boot your Raspberry Pi."


Running the Ansible Playbook

After creating the YAML file with the provided playbook content, follow these steps to run it:

  1. Save the playbook: Save the YAML content to a file named flash_raspberrypi.yml on your Raspberry Pi.
  2. Recommended: Update your RPi in order for installation to succeed:
    sudo apt update && sudo apt upgrade -y
  3. Install Ansible: If you haven't already, install Ansible on your Raspberry Pi:
    sudo apt update
    sudo apt install ansible
  4. Customize the playbook: Before running, make sure to customize the variables in the vars section of the playbook to match your specific requirements:
    • bootable_device: Set this to the correct device path (e.g., /dev/sda, /dev/nvme0n1)
    • os_image_path: Ensure this points to the correct location of your Raspberry Pi OS image
    • wifi_ssid and wifi_password: Update these with your Wi-Fi credentials
    • new_username and new_password: Set these to your desired values for the new user
  5. Run the playbook: Execute the playbook using the following command:
    ansible-playbook flash_raspberrypi.yml
    You may need to run this with sudo if your user doesn't have the necessary permissions:
    sudo ansible-playbook flash_raspberrypi.yml
  6. Monitor the output: Ansible will display the progress of each task in the playbook. Watch for any errors or warnings.
  7. Verify completion: Once the playbook finishes, you should see a success message indicating that the bootable device setup is complete.
  8. Reboot: After the playbook has run successfully, reboot your Raspberry Pi and ensure it boots from the newly flashed device.

Connect to Your New System

When trying to SSH into your new system, you might get an SSH error due to the new OS. To fix this: 1. Remove the old SSH key: ssh-keygen -R your_raspberry_pi_ip_address Replace your_raspberry_pi_ip_address with your Raspberry Pi's actual IP address. 2. You should now be able to SSH into your Raspberry Pi using the username and password you set up earlier. After your first login, it's a good practice to update your system: sudo apt update && sudo apt upgrade -y

Fix the locale


  sudo locale-gen en_US.UTF-8
  # Or for example another country:
  # sudo locale-gen sv_SE.UTF-8
  sudo update-locale LANG=en_US.UTF-8
  
  sudo reboot
  

Conclusion

After following the steps outlined above, your Raspberry Pi OS should be successfully flashed to the bootable device, ready for use. Be sure to configure Wi-Fi settings and enable SSH for easier headless access in the future.