--- # Renders the Raspberry Pi's cloud-init first-boot files onto a freshly written # SD card's boot partition: # # make bootfs BOOTFS=/Volumes/bootfs # # The image itself is still built by hand with rpi-imager -- this only writes # the two cloud-init files onto it. Everything it writes comes from the same # templates roles/labelprint uses, so the Pi boots with its Wi-Fi profiles and # rescue access point already in place, and the first deploy has nothing to # correct. # # The rendered files contain the Wi-Fi PSKs in the clear, as any Pi Wi-Fi setup # does. They land on the SD card, never in this repo. - hosts: localhost gather_facts: false connection: local vars_files: - vars/vault.yml - roles/labelprint/defaults/main.yml tasks: - name: check that BOOTFS points at a Raspberry Pi boot partition ansible.builtin.stat: path: "{{ bootfs }}/config.txt" register: labelprint_bootfs_check tags: bootfs - name: refuse to write to anything else ansible.builtin.assert: that: labelprint_bootfs_check.stat.exists fail_msg: >- {{ bootfs }} has no config.txt, so it is not a Raspberry Pi boot partition. Write the image with rpi-imager first, then re-run with BOOTFS pointing at the mounted boot volume. tags: bootfs - name: look for files rpi-imager already wrote ansible.builtin.stat: path: "{{ bootfs }}/{{ item }}" register: labelprint_bootfs_existing loop: - user-data - network-config - cmdline.txt tags: bootfs # Keeps whatever rpi-imager put there, so a bad render can be undone by hand # without reflashing. force:false means the first run's backup is the one # that survives -- a second run must not overwrite it with our own output. - name: back up the files rpi-imager wrote ansible.builtin.copy: src: "{{ bootfs }}/{{ item.item }}" dest: "{{ bootfs }}/{{ item.item }}.rpi-imager.bak" mode: "0644" force: false loop: "{{ labelprint_bootfs_existing.results }}" loop_control: label: "{{ item.item }}" when: item.stat.exists tags: bootfs - name: render the cloud-init files ansible.builtin.template: src: "roles/labelprint/templates/bootfs/{{ item }}.j2" dest: "{{ bootfs }}/{{ item }}" mode: "0644" loop: - user-data - network-config tags: bootfs - name: hash the rendered user-data ansible.builtin.stat: path: "{{ bootfs }}/user-data" checksum_algorithm: sha1 register: labelprint_user_data_stat tags: bootfs - name: stamp the instance id with that hash ansible.builtin.template: src: roles/labelprint/templates/bootfs/meta-data.j2 dest: "{{ bootfs }}/meta-data" mode: "0644" vars: labelprint_user_data_id: "{{ labelprint_user_data_stat.stat.checksum[:12] }}" tags: bootfs # rpi-imager writes `ds=nocloud;i=` onto the kernel command line, and # that id outranks the one in meta-data. Leave it alone and a card that has # booted even once is seen by cloud-init as the same instance forever: it # skips users, write_files and runcmd, silently, and the only symptom is a # Pi that came up with none of this applied. Both places have to agree. - name: pin the same instance id on the kernel command line ansible.builtin.replace: path: "{{ bootfs }}/cmdline.txt" regexp: '(ds=nocloud[^\s]*?);i=[^\s]+' replace: '\1;i={{ labelprint_hostname }}-{{ labelprint_user_data_stat.stat.checksum[:12] }}' tags: bootfs - name: what to do next ansible.builtin.debug: msg: - "Wrote user-data, network-config, meta-data and cmdline.txt to {{ bootfs }}." - "Instance id is {{ labelprint_hostname }}-{{ labelprint_user_data_stat.stat.checksum[:12] }}; the Pi re-applies this config whenever it changes." - "Eject the volume, boot the Pi, then: make deploy-labelprint" tags: bootfs