├── .gitignore ├── README.md ├── defaults └── main.yml ├── meta └── main.yml ├── tasks ├── configure.yml ├── download_verify.yml ├── generate_iso.yml ├── main.yml └── upload_kvm.yml └── templates └── user-data.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: Ubuntu Autoinstall 2 | 3 | ### This role will: 4 | * Download and verify (GPG and SHA256) the newest Ubuntu Server 20.04 ISO 5 | * Unpack the ISO and integrate the user-data file for semi-automated installation 6 | * Repack the ISO and (optionally) upload it to [PiKVM](https://pikvm.org/) for futher installation 7 | 8 | ### Special thanks to: 9 | * covertsh for [Ubuntu Autoinstall Generator](https://github.com/covertsh/ubuntu-autoinstall-generator) – this repo is pretty much an Ansible version of their script 10 | 11 | ### Example playbook: 12 | ``` 13 | --- 14 | - hosts: all 15 | gather_facts: yes 16 | become: no 17 | 18 | roles: 19 | - role: notthebee.ubuntu_autoinstall 20 | ``` 21 | 22 | ### Variables 23 | * **boot_drive_serial** – the serial number of the drive where you want to install Ubuntu. You can find it out using `ls /dev/disk/by-id`. Make sure to omit the interface (e.g. **ata-** or **scsi-**). 24 | * **iso_arch** – Architecture of the output ISO file. `amd64` and `arm64` are supported 25 | 26 | 27 | 28 | Other variables are more or less self-explanatory and can be found in defaults/main.yml 29 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | locale: "en-us.UTF-8" 3 | 4 | iso_arch: "amd64" 5 | # Valid values are: amd64, arm64 6 | # ppc64el and s390x haven't been tested yet 7 | 8 | keyboard_layout: "us" 9 | 10 | hostname: "focal-autoinstall" 11 | 12 | password: "ubuntu" 13 | 14 | username: "ubuntu" 15 | 16 | ssh_public_key: "" 17 | 18 | pikvm_address: "pikvm.box" 19 | 20 | pikvm_username: "admin" 21 | 22 | pikvm_password: "admin" 23 | 24 | target_dir: "{{ ansible_env.HOME }}/.local/ansible_ubuntu-autoinstall" 25 | 26 | ubuntu_gpg_key: 843938DF228D22F7B3742BC0D94AA3F0EFE21092 27 | 28 | enable_pikvm: false 29 | 30 | enable_hwe_kernel: false 31 | 32 | enable_swap_file: false 33 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: ubuntu_autoinstall 4 | author: notthebee 5 | description: Generates an Ubuntu 20.04 Server ISO with a user-data template and optionally deploys it to PiKVM 6 | issue_tracker_url: https://github.com/notthebee/ansible-role-cloud-init/issues 7 | license: WTFPL 8 | min_ansible_version: 2.4 9 | platforms: 10 | - name: Ubuntu 11 | versions: 12 | - focal 13 | galaxy_tags: 14 | - system 15 | dependencies: [] 16 | -------------------------------------------------------------------------------- /tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies 3 | become: yes 4 | package: 5 | name: 6 | - xorriso 7 | - gpg 8 | - curl 9 | state: present 10 | when: ansible_os_family != "Darwin" 11 | 12 | - name: Install dependencies (macOS) 13 | package: 14 | name: 15 | - xorriso 16 | - gnupg 17 | - curl 18 | state: present 19 | when: ansible_os_family == "Darwin" 20 | 21 | - name: Create the temporary directory 22 | file: 23 | path: "{{ target_dir }}" 24 | state: directory 25 | -------------------------------------------------------------------------------- /tasks/download_verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download the SHA256 sums 3 | get_url: 4 | url: "https://cdimage.ubuntu.com/ubuntu-server/focal/daily-live/current/SHA256SUMS" 5 | dest: "{{ target_dir }}" 6 | 7 | - name: Get the SHA256 sum for the {{ iso_arch }} ISO 8 | shell: 9 | cmd: "grep {{ iso_arch }}.iso {{ target_dir }}/SHA256SUMS | cut -d ' ' -f1" 10 | changed_when: false 11 | register: sha256sum 12 | 13 | - name: Download the latest Ubuntu Server 20.04 {{ iso_arch }} ISO 14 | get_url: 15 | url: "https://cdimage.ubuntu.com/ubuntu-server/focal/daily-live/current/focal-live-server-{{ iso_arch }}.iso" 16 | dest: "{{ target_dir }}/focal-live-server-{{ iso_arch }}.iso" 17 | checksum: "sha256:{{ sha256sum.stdout }}" 18 | 19 | - name: Download the GPG keys 20 | get_url: 21 | url: "https://cdimage.ubuntu.com/ubuntu-server/focal/daily-live/current/SHA256SUMS.gpg" 22 | dest: "{{ target_dir }}" 23 | 24 | - name: Check if dirmngr is running 25 | shell: 26 | cmd: pgrep dirmngr 27 | failed_when: false 28 | changed_when: false 29 | register: dirmngr_status 30 | 31 | - name: Launch dirmngr if it isn't running 32 | shell: 33 | cmd: "dirmngr --daemon" 34 | when: dirmngr_status.rc != 0 35 | 36 | - name: Import the GPG key 37 | shell: 38 | cmd: "gpg -q --no-default-keyring --keyring '{{ target_dir }}/{{ ubuntu_gpg_key }}.keyring' --keyserver 'hkp://keyserver.ubuntu.com' --recv-keys {{ ubuntu_gpg_key }}" 39 | creates: 40 | - "{{ target_dir }}/{{ ubuntu_gpg_key }}.keyring" 41 | - "{{ target_dir }}/{{ ubuntu_gpg_key }}.keyring~" 42 | ignore_errors: yes 43 | 44 | - name: Verify the GPG key 45 | shell: 46 | cmd: "gpg -q --keyring '{{ target_dir }}/{{ ubuntu_gpg_key }}.keyring' --verify '{{ target_dir }}/SHA256SUMS.gpg' '{{ target_dir }}/SHA256SUMS' 2>/dev/null" 47 | ignore_errors: yes 48 | 49 | - name: Kill dirmngr if we launched it 50 | shell: 51 | cmd: "pkill dirmngr" 52 | when: dirmngr_status.rc != 0 53 | -------------------------------------------------------------------------------- /tasks/generate_iso.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create the extraction directory 3 | file: 4 | path: "{{ target_dir }}/iso" 5 | state: directory 6 | 7 | - name: Extract the ISO 8 | shell: 9 | cmd: "xorriso -osirrox on -indev {{ target_dir }}/focal-live-server-{{ iso_arch }}.iso -extract / {{ target_dir }}/iso" 10 | 11 | - name: Fix extracted ISO mode 12 | file: 13 | path: "{{ target_dir }}/iso" 14 | mode: "u+w" 15 | recurse: yes 16 | follow: no 17 | 18 | - name: Delete the [BOOT] folder 19 | file: 20 | path: "{{ target_dir }}/iso/[BOOT]" 21 | state: absent 22 | 23 | - name: Enable HWE kernel in ISOLinux bootloader 24 | replace: 25 | path: "{{ item }}" 26 | regexp: '/casper/(vmlinuz|initrd)' 27 | replace: '/casper/hwe-\1' 28 | with_items: 29 | - "{{ target_dir }}/iso/isolinux/txt.cfg" 30 | when: iso_arch == 'amd64' and enable_hwe_kernel | default(False) 31 | 32 | - name: Enable HWE kernel in GRUB bootloader 33 | replace: 34 | path: "{{ item }}" 35 | regexp: '/casper/(vmlinuz|initrd)' 36 | replace: '/casper/hwe-\1' 37 | with_items: 38 | - "{{ target_dir }}/iso/boot/grub/grub.cfg" 39 | - "{{ target_dir }}/iso/boot/grub/loopback.cfg" 40 | when: enable_hwe_kernel | default(False) 41 | 42 | - name: Add the autoinstall parameter to the ISOLinux bootloader 43 | replace: 44 | path: "{{ item }}" 45 | regexp: "---$" 46 | replace: " autoinstall ds=nocloud;s=/cdrom/nocloud/ ---" 47 | with_items: 48 | - "{{ target_dir }}/iso/isolinux/txt.cfg" 49 | when: iso_arch == 'amd64' 50 | 51 | - name: Add the autoinstall parameter to the GRUB bootloader 52 | replace: 53 | path: "{{ item }}" 54 | regexp: "---$" 55 | replace: " autoinstall ds=nocloud\\;s=/cdrom/nocloud/ ---" 56 | with_items: 57 | - "{{ target_dir }}/iso/boot/grub/grub.cfg" 58 | - "{{ target_dir }}/iso/boot/grub/loopback.cfg" 59 | 60 | - name: Create the nocloud directory 61 | file: 62 | path: "{{ target_dir }}/iso/nocloud" 63 | state: directory 64 | 65 | - name: Generate and install the user-data file 66 | template: 67 | src: user-data.j2 68 | dest: "{{ target_dir }}/iso/nocloud/user-data" 69 | 70 | - name: Create an empty meta-data file 71 | file: 72 | path: "{{ target_dir }}/iso/nocloud/meta-data" 73 | state: touch 74 | modification_time: preserve 75 | access_time: preserve 76 | 77 | - name: Calculate the new MD5 hashes 78 | stat: 79 | path: "{{ item }}" 80 | checksum_algorithm: md5 81 | with_items: 82 | - "{{ target_dir }}/iso/boot/grub/grub.cfg" 83 | - "{{ target_dir }}/iso/boot/grub/loopback.cfg" 84 | - "{{ target_dir }}/iso/isolinux/txt.cfg" 85 | register: md5sums 86 | 87 | - name: Write the new MD5 hash (grub.cfg) 88 | lineinfile: 89 | line: "{{ md5sums.results[0].stat.checksum }} ./boot/grub/grub.cfg" 90 | search_string: /boot/grub/grub.cfg 91 | path: "{{ target_dir }}/iso/md5sum.txt" 92 | 93 | - name: Write the new MD5 hash (loopback.cfg) 94 | lineinfile: 95 | line: "{{ md5sums.results[1].stat.checksum }} ./boot/grub/loopback.cfg" 96 | search_string: loopback.cfg 97 | path: "{{ target_dir }}/iso/md5sum.txt" 98 | 99 | - name: Write the new MD5 hash (isolinux/txt.cfg) 100 | lineinfile: 101 | line: "{{ md5sums.results[2].stat.checksum }} ./isolinux/txt.cfg" 102 | search_string: isolinux/txt.cfg 103 | path: "{{ target_dir }}/iso/md5sum.txt" 104 | when: iso_arch == 'amd64' 105 | 106 | - name: Repack the ISO (amd64) 107 | shell: 108 | cmd: "cd {{ target_dir }}/iso && xorriso -as mkisofs -quiet -D -r -V ubuntu-autoinstall_{{ iso_arch }} -cache-inodes -J -l -joliet-long -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -eltorito-alt-boot -e boot/grub/efi.img -no-emul-boot -o {{ target_dir }}/ubuntu_autoinstall_{{ iso_arch }}.iso ." 109 | when: iso_arch == 'amd64' 110 | 111 | 112 | - name: Repack the ISO (arm64) 113 | shell: 114 | cmd: "cd {{ target_dir }}/iso && xorriso -as mkisofs -quiet -D -r -V ubuntu-autoinstall_{{ iso_arch }} -cache-inodes -J -joliet-long -no-emul-boot -e boot/grub/efi.img -partition_cyl_align all -append_partition 2 0xef boot/grub/efi.img -no-emul-boot -o {{ target_dir }}/ubuntu_autoinstall_{{ iso_arch }}.iso ." 115 | when: iso_arch == 'arm64' 116 | 117 | 118 | - name: Clean up 119 | file: 120 | path: "{{ item }}" 121 | state: absent 122 | with_items: 123 | - "{{ target_dir }}/iso" 124 | 125 | - name: Done! 126 | debug: 127 | msg: "Done! The ISO file has been generated: {{target_dir}}/ubuntu_autoinstall_{{ iso_arch }}.iso" 128 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure the target system and install dependencies 3 | include_tasks: configure.yml 4 | 5 | - name: Download and verify the ISO 6 | include_tasks: download_verify.yml 7 | 8 | - name: Generate the ISO 9 | include_tasks: generate_iso.yml 10 | 11 | - name: Upload the ISO to the KVM 12 | include_tasks: upload_kvm.yml 13 | when: enable_pikvm | default(False) 14 | -------------------------------------------------------------------------------- /tasks/upload_kvm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get the file size of the ISO 3 | stat: 4 | path: "{{ target_dir }}/ubuntu_autoinstall.iso" 5 | register: iso 6 | 7 | - name: Disconnect the current drive 8 | uri: 9 | url: "http://{{ pikvm_address }}/api/msd/set_connected?connected=0" 10 | method: POST 11 | status_code: [ 400, 200 ] 12 | headers: 13 | X-KVMD-User: "{{ pikvm_username }}" 14 | X-KVMD-Passwd: "{{ pikvm_password }}" 15 | register: response 16 | changed_when: response.json is not search("MsdDisconnectedError") 17 | 18 | - name: Remove the previous ISO 19 | uri: 20 | url: "http://{{ pikvm_address }}/api/msd/remove?image=ubuntu_autoinstall.iso" 21 | status_code: [ 400, 200 ] 22 | method: POST 23 | headers: 24 | X-KVMD-User: "{{ pikvm_username }}" 25 | X-KVMD-Passwd: "{{ pikvm_password }}" 26 | register: response 27 | changed_when: response.json is not search("MsdUnknownImageError") 28 | 29 | - name: Upload the ISO to PiKVM 30 | shell: 31 | cmd: "curl --location --request POST '{{ pikvm_address }}/api/msd/write' --header 'X-KVMD-User: {{ pikvm_username }}' --header 'X-KVMD-Passwd: {{ pikvm_password }}' --form 'image=ubuntu_autoinstall.iso' --form 'size={{ iso.stat.size | int }}' --form 'data=@{{ target_dir }}/ubuntu_autoinstall.iso'" 32 | 33 | - name: Select the ubuntu_autoinstall ISO 34 | uri: 35 | validate_certs: no 36 | url: "http://{{ pikvm_address }}/api/msd/set_params?image=ubuntu_autoinstall.iso" 37 | method: POST 38 | headers: 39 | X-KVMD-User: "{{ pikvm_username }}" 40 | X-KVMD-Passwd: "{{ pikvm_password }}" 41 | 42 | - name: Connect the ISO to the server 43 | uri: 44 | validate_certs: no 45 | url: "http://{{ pikvm_address }}/api/msd/set_connected?connected=true" 46 | method: POST 47 | headers: 48 | X-KVMD-User: "{{ pikvm_username }}" 49 | X-KVMD-Passwd: "{{ pikvm_password }}" -------------------------------------------------------------------------------- /templates/user-data.j2: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | autoinstall: 3 | version: 1 4 | locale: {{ locale }} 5 | keyboard: 6 | layout: {{ keyboard_layout }} 7 | refresh-installer: 8 | update: yes 9 | identity: 10 | hostname: {{ hostname }} 11 | password: {{ password | password_hash('sha512') }} 12 | username: {{ username }} 13 | ssh: 14 | install-server: true 15 | allow-pw: false 16 | authorized-keys: 17 | - {{ ssh_public_key }} 18 | storage: 19 | grub: 20 | reorder_uefi: False 21 | {% if not enable_swap_file %} 22 | swap: 23 | size: 0 24 | {% endif %} 25 | config: 26 | - {ptable: gpt, serial: "{{ boot_drive_serial }}", preserve: false, name: '', grub_device: false, type: disk, id: bootdrive} 27 | 28 | - {device: bootdrive, size: 536870912, wipe: superblock, flag: boot, number: 1, preserve: false, grub_device: true, type: partition, id: efipart} 29 | - {fstype: fat32, volume: efipart, preserve: false, type: format, id: efi} 30 | 31 | - {device: bootdrive, size: -1, wipe: superblock, flag: linux, number: 2, preserve: false, grub_device: false, type: partition, id: rootpart} 32 | - {fstype: ext4, volume: rootpart, preserve: false, type: format, id: root} 33 | 34 | - {device: root, path: /, type: mount, id: rootmount} 35 | - {device: efi, path: /boot/efi, type: mount, id: efimount} 36 | --------------------------------------------------------------------------------