├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── output ├── http │ └── .gitkeep └── ssh_keys │ └── .gitkeep ├── playbooks ├── post-provisioning.yml ├── pre-provisioning.yml ├── tasks │ ├── enable_cloud-init.yml │ ├── generate_cloud-init_user-data.yml │ ├── generate_user_keys_and_pass.yml │ └── sshd.yml └── templates │ ├── meta-data.j2 │ └── user-data.j2 ├── secrets.auto.pkrvars.hcl.example ├── ubuntu2004.pkr.hcl └── variables.pkr.hcl /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: hashicorp-contrib/setup-packer@v1.0.0 17 | 18 | - name: Packer validate 19 | id: validate 20 | run: packer validate . 21 | 22 | - name: Packer fmt 23 | id: fmt 24 | run: packer fmt -check -diff . 25 | if: success() || failure() 26 | 27 | - name: Comment on Pull Request 28 | uses: actions/github-script@v4.0.2 29 | if: | 30 | github.event_name == 'pull_request' && 31 | (success() || failure()) 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | script: | 35 | const output = `#### Packer test status 36 | | validate | fmt | 37 | |---------------------------------|----------------------------| 38 | |\`${{ steps.validate.outcome }}\`|\`${{ steps.fmt.outcome }}\`| 39 | 40 | *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; 41 | 42 | github.issues.createComment({ 43 | issue_number: context.issue.number, 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | body: output 47 | }) 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.auto.pkrvars.hcl 2 | packer_cache/ 3 | output/http/* 4 | output/ssh_keys/* 5 | !output/*/.gitkeep 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Justin Perdok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # packer-proxmox-ubuntu2004 2 | 3 | [![Github Actions](https://img.shields.io/github/workflow/status/justin-p/packer-proxmox-ubuntu2004/CI?label=Github%20Actions&logo=github&style=flat-square)](https://github.com/justin-p/packer-proxmox-ubuntu2004/actions) 4 | 5 | Packer files to build Ubuntu 20.04 (subiquity-based) images on Proxmox. Ansible is used for 'pre' and 'post' provisioning tasks. 6 | Intended to be used in combination with [this Terraform module.](https://github.com/justin-p/terraform-proxmox-ubuntu2004) 7 | 8 | Pre-provisioning tasks are used to dynamically generated local files such as the cloud-init user-data. This allows you to easily change the username/password used for the initial user created by cloud-init. SSH keys for the initial account are also generated and stored in the `output/ssh_keys` folder. This behavior can be changed by using the [`template_ssh_key_*` variables.](https://github.com/justin-p/packer-proxmox-ubuntu2004/blob/c5254895435cb5e1d219cfc1e78ccc0f69724735/variables.pkr.hcl#L11) 9 | 10 | Post-provisioning tasks currently disable password based authentication in the sshd_config. This is enabled by cloud-init during the provisioning. The current packer proxmox provider does not support key based authentication and needs to connect with ssh during the provisioning [to verify if cloud-init has finished](https://github.com/justin-p/packer-proxmox-ubuntu2004/blob/72153e30393ede40f12b610d4961c9a0f26fa43c/ubuntu2004.pkr.hcl#L55). 11 | 12 | Since the cloud-init template adds a public key to [authorized_keys](https://github.com/justin-p/packer-proxmox-ubuntu2004/blob/d53fdda704347affb6b74668ee2915100efc8a94/playbooks/templates/user-data.j2#L24) file password based authentication is not needed after image creation and thus disabled once packer verifies that cloud-init has finished. 13 | 14 | Another post-provisioning is to ensure the template is 'cloud-init ready'. This is added so Terraform can setup the new networking configuration (for example: correct bridge/vlan + static ip). In order for this to work properly cloud-init must be 'enabled/in unfinished state' when Terraform first boots the cloned image. Besides enabling/placing cloud-init in a unfinished state, files added by subiquity should also be removed in order for cloud-init to manage netplan. This way cloud-init can create a new netplan configuration during the initial boot of the cloned image. 15 | 16 | The idea is to add a provisioner to the Terraform code that disables cloud-init after the deployment. The Terraform code/cloud-init should not manage netplan from that point onward. The [current provider](https://github.com/danitso/terraform-provider-proxmox) I use also [doesn't support this (errors out)](https://github.com/danitso/terraform-provider-proxmox/issues/91) and if enforced sometimes breaks the password of the cloud-init created user. 17 | 18 | Initial code is based on [prior work](https://github.com/aerialls/madalynn-packer) by [Julien Brochet](https://twitter.com/aerialls). [Link to his blog post](https://www.aerialls.io/posts/ubuntu-server-2004-image-packer-subiquity-for-proxmox/). 19 | 20 | ## Usage 21 | 22 | - `git clone https://github.com/justin-p/packer-proxmox-ubuntu2004` 23 | - `cd packer-proxmox-ubuntu2004` 24 | - `cp secrets.auto.pkrvars.hcl.example secrets.auto.pkrvars.hcl` 25 | - Overwrite desired variables in `secrets.auto.pkrvars.hcl`. 26 | See `variables.pkr.hcl` for all variables, most have sane defaults. The `secrets.auto.pkrvars.hcl.example` file includes most variables you want to overwrite. 27 | - `packer build .` 28 | *Ensure the machine you are running packer from can be reached by the guest VM. Packer spins up a HTTP server to transmit the cloud-init template. Using [template_network_bridge](https://github.com/justin-p/packer-proxmox-ubuntu2004/blob/d41c5ba08b2770d3d3753659ad54af0eb75491c9/variables.pkr.hcl#L98) might help you.* 29 | 30 | ## License 31 | 32 | MIT 33 | 34 | ## Author Information 35 | 36 | - Justin Perdok ([@justin-p](https://github.com/justin-p/)) 37 | 38 | ## Contributing 39 | 40 | Feel free to open issues, contribute and submit your Pull Requests. You can also ping me on Twitter ([@JustinPerdok](https://twitter.com/JustinPerdok)). 41 | -------------------------------------------------------------------------------- /output/http/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justin-p/packer-proxmox-ubuntu2004/a5d7028c7073dca15af1b4d8276202cad9b605ec/output/http/.gitkeep -------------------------------------------------------------------------------- /output/ssh_keys/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justin-p/packer-proxmox-ubuntu2004/a5d7028c7073dca15af1b4d8276202cad9b605ec/output/ssh_keys/.gitkeep -------------------------------------------------------------------------------- /playbooks/post-provisioning.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: Run post provisioning tasks 5 | block: 6 | - import_tasks: tasks/sshd.yml 7 | - import_tasks: tasks/enable_cloud-init.yml 8 | become: true 9 | -------------------------------------------------------------------------------- /playbooks/pre-provisioning.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - name: Run pre provisioning tasks 5 | block: 6 | - import_tasks: tasks/generate_user_keys_and_pass.yml 7 | - import_tasks: tasks/generate_cloud-init_user-data.yml 8 | -------------------------------------------------------------------------------- /playbooks/tasks/enable_cloud-init.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # based off https://github.com/mrlesmithjr/packer-templates-revisited/blob/f6c1e69848608a59bb45a3e512d054ef4e807995/playbooks/cloud-init.yml 3 | - name: Ensure cloud-init in installed 4 | ansible.builtin.package: 5 | name: cloud-init 6 | state: present 7 | 8 | - name: Cleaning up ubuntu cloud-init (subiquity) 9 | ansible.builtin.file: 10 | path: "{{ item }}" 11 | state: absent 12 | loop: 13 | - /etc/cloud/cloud.cfg.d/99-installer.cfg 14 | when: 15 | - ansible_distribution == "Ubuntu" 16 | - ansible_distribution_version is version('20.04', '>=') 17 | 18 | - name: Configuring /etc/cloud/cloud.cfg.d/99_pve.cfg 19 | ansible.builtin.copy: 20 | content: | 21 | datasource_list: [ NoCloud, ConfigDrive ] 22 | dest: /etc/cloud/cloud.cfg.d/99_pve.cfg 23 | mode: 0666 24 | 25 | - name: Cleaning up cloud-init 26 | block: 27 | - name: Attempting cloud-init clean 28 | ansible.builtin.command: cloud-init clean -s -l 29 | rescue: 30 | - name: Cleaning up cloud-init manually 31 | ansible.builtin.file: 32 | path: "{{ item }}" 33 | state: absent 34 | loop: 35 | - /var/lib/cloud/instances 36 | - /var/log/cloud-init.log 37 | - /var/log/cloud-init-output.log 38 | 39 | - name: Ensure cloud-init.service is enabled 40 | ansible.builtin.service: 41 | name: cloud-init.service 42 | enabled: true 43 | 44 | - name: Allow cloud-init to set networking 45 | ansible.builtin.file: 46 | path: "{{ item }}" 47 | state: absent 48 | with_items: 49 | - "/etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg" 50 | - "/etc/netplan/00-installer-config.yaml" 51 | -------------------------------------------------------------------------------- /playbooks/tasks/generate_cloud-init_user-data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate cloud-init files 3 | ansible.builtin.template: 4 | src: "{{playbook_dir}}/templates/{{ item }}.j2" 5 | dest: "{{playbook_dir}}/../output/http/{{ item }}" 6 | with_items: 7 | - "user-data" 8 | - "meta-data" 9 | -------------------------------------------------------------------------------- /playbooks/tasks/generate_user_keys_and_pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate SSH Key pair for template in "{{ ssh_folder }}" 3 | community.crypto.openssh_keypair: 4 | path: "{{ ssh_folder }}/{{ ssh_key_name }}" 5 | type: ed25519 6 | comment: initial_template_key_generated_by_packer 7 | 8 | - name: Slurp contents of public key 9 | ansible.builtin.slurp: 10 | src: "{{ ssh_folder }}/{{ ssh_key_name }}.pub" 11 | register: slurped_pubkey 12 | 13 | - name: Decode slurped public key and store as fact 14 | ansible.builtin.set_fact: 15 | pubkey: "{{ slurped_pubkey.content | b64decode | trim }}" 16 | 17 | - name: Generate password hash from password 18 | ansible.builtin.shell: openssl passwd -6 -salt xyz {{password}} 19 | register: gen_hashed_password 20 | -------------------------------------------------------------------------------- /playbooks/tasks/sshd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Disable password login 3 | ansible.builtin.lineinfile: 4 | dest: "/etc/ssh/sshd_config" 5 | regexp: '^(#\s*)?PasswordAuthentication ' 6 | line: 'PasswordAuthentication no' 7 | 8 | - name: Restart sshd 9 | ansible.builtin.service: 10 | name: ssh 11 | state: restarted 12 | -------------------------------------------------------------------------------- /playbooks/templates/meta-data.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justin-p/packer-proxmox-ubuntu2004/a5d7028c7073dca15af1b4d8276202cad9b605ec/playbooks/templates/meta-data.j2 -------------------------------------------------------------------------------- /playbooks/templates/user-data.j2: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | autoinstall: 3 | version: 1 4 | refresh-installer: 5 | update: yes 6 | ssh: 7 | install-server: true 8 | allow-pw: true 9 | packages: 10 | - qemu-guest-agent 11 | storage: 12 | layout: 13 | name: lvm 14 | user-data: 15 | package_upgrade: true 16 | timezone: Europe/Amsterdam 17 | users: 18 | - name: "{{ user }}" 19 | passwd: "{{ gen_hashed_password.stdout }}" 20 | groups: [adm, cdrom, dip, plugdev, lxd, sudo] 21 | lock_passwd: false 22 | sudo: ALL=(ALL) NOPASSWD:ALL 23 | shell: /bin/bash 24 | ssh_authorized_keys: 25 | - "{{ pubkey }}" 26 | -------------------------------------------------------------------------------- /secrets.auto.pkrvars.hcl.example: -------------------------------------------------------------------------------- 1 | proxmox_username = "root@pam" 2 | proxmox_password = "password" 3 | proxmox_url = "https://10.10.10.10:8006/api2/json" 4 | proxmox_insecure_skip_tls_verify = true 5 | template_network_bridge = "vmbr5" 6 | # template_ssh_username = "ubuntu" 7 | # template_ssh_password = "ubuntu" 8 | # template_ssh_key_output_folder = "../../../keys" 9 | # template_ssh_key_name = "id_ed25519_ubuntu_packer" 10 | -------------------------------------------------------------------------------- /ubuntu2004.pkr.hcl: -------------------------------------------------------------------------------- 1 | ## Dummy source that enables us to run the ansible pre-provisioning. 2 | source "null" "ansible-pre-provisioning" { 3 | communicator = "none" 4 | } 5 | 6 | ## Run ansible pre-provisioning playbook. 7 | build { 8 | sources = ["source.null.ansible-pre-provisioning"] 9 | provisioner "ansible" { 10 | playbook_file = "${path.root}/playbooks/pre-provisioning.yml" 11 | extra_arguments = [ 12 | "--extra-vars", 13 | "user='${var.template_ssh_username}' password='${var.template_ssh_password}' ssh_folder='${var.template_ssh_key_output_folder}' ssh_key_name='${var.template_ssh_key_name}'" 14 | ] 15 | } 16 | } 17 | 18 | ## Define proxmox Ubuntu 20.04 template. 19 | source "proxmox" "ubuntu2004" { 20 | username = "${var.proxmox_username}" 21 | password = "${var.proxmox_password}" 22 | proxmox_url = "${var.proxmox_url}" 23 | insecure_skip_tls_verify = "${var.proxmox_insecure_skip_tls_verify}" 24 | node = "${var.proxmox_node}" 25 | 26 | template_name = "${var.template_name}" 27 | template_description = "${var.template_description}" 28 | os = "${var.template_os}" 29 | iso_file = "${var.template_iso_file}" 30 | unmount_iso = "${var.template_unmount_iso}" 31 | memory = "${var.template_memory}" 32 | cores = "${var.template_cores}" 33 | cpu_type = "${var.template_cpu_type}" 34 | scsi_controller = "${var.tempalte_scsi_controller}" 35 | qemu_agent = "${var.template_qemu_agent}" 36 | 37 | vga { 38 | type = "${var.template_vga_type}" 39 | memory = "${var.template_vga_memory}" 40 | } 41 | 42 | network_adapters { 43 | model = "${var.template_network_model}" 44 | bridge = "${var.template_network_bridge}" 45 | } 46 | 47 | disks { 48 | disk_size = "${var.template_disks_disk_size}" 49 | storage_pool = "${var.template_disks_storage_pool}" 50 | storage_pool_type = "${var.template_disks_storage_pool_type}" 51 | type = "${var.template_disks_type}" 52 | } 53 | 54 | http_directory = "output/http" 55 | boot_wait = "5s" 56 | boot_command = [ 57 | "", 58 | "", 59 | "autoinstall ds=nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ ", 60 | "--- " 61 | ] 62 | 63 | ssh_timeout = "20m" 64 | ssh_username = "${var.template_ssh_username}" 65 | ssh_password = "${var.template_ssh_password}" 66 | } 67 | 68 | # Build proxmox Ubuntu 20.04 template and wait for cloud-init to finish. 69 | build { 70 | sources = ["source.proxmox.ubuntu2004"] 71 | 72 | provisioner "shell" { 73 | inline = [ 74 | "echo 'Waiting for cloud-init...'", 75 | "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 1; done" 76 | ] 77 | } 78 | 79 | provisioner "ansible" { 80 | playbook_file = "${path.root}/playbooks/post-provisioning.yml" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /variables.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "template_ssh_username" { 2 | type = string 3 | default = "ubuntu" 4 | } 5 | 6 | variable "template_ssh_password" { 7 | type = string 8 | default = "ubuntu" 9 | } 10 | 11 | variable "template_ssh_key_output_folder" { 12 | type = string 13 | default = "../output/ssh_keys" 14 | } 15 | 16 | variable "template_ssh_key_name" { 17 | type = string 18 | default = "id_ed25519_ubuntu_packer" 19 | } 20 | 21 | variable "proxmox_username" { 22 | type = string 23 | default = "packer@pve" 24 | } 25 | 26 | variable "proxmox_password" { 27 | type = string 28 | default = "toortoor" 29 | } 30 | 31 | variable "proxmox_node" { 32 | type = string 33 | default = "proxmox" 34 | } 35 | 36 | variable "proxmox_url" { 37 | type = string 38 | default = "https://127.0.0.1:8006/api2/json" 39 | } 40 | 41 | variable "proxmox_insecure_skip_tls_verify" { 42 | type = bool 43 | default = false 44 | } 45 | 46 | variable "template_name" { 47 | type = string 48 | default = "ubuntu2004" 49 | } 50 | variable "template_description" { 51 | type = string 52 | default = "Ubuntu 20.04 x86_64 template built with packer" 53 | } 54 | 55 | variable "template_os" { 56 | type = string 57 | default = "l26" 58 | } 59 | 60 | variable "template_iso_file" { 61 | type = string 62 | default = "local:iso/ubuntu-20.04.2-live-server-amd64.iso" 63 | } 64 | 65 | variable "template_unmount_iso" { 66 | type = bool 67 | default = true 68 | } 69 | 70 | variable "template_memory" { 71 | type = string 72 | default = 1024 73 | } 74 | 75 | variable "template_cores" { 76 | type = string 77 | default = 1 78 | } 79 | 80 | variable "template_cpu_type" { 81 | type = string 82 | default = "host" 83 | } 84 | variable "tempalte_scsi_controller" { 85 | type = string 86 | default = "virtio-scsi-pci" 87 | } 88 | variable "template_qemu_agent" { 89 | type = bool 90 | default = true 91 | } 92 | 93 | variable "template_vga_type" { 94 | type = string 95 | default = "qxl" 96 | } 97 | 98 | variable "template_vga_memory" { 99 | type = string 100 | default = 32 101 | } 102 | 103 | variable "template_network_model" { 104 | type = string 105 | default = "virtio" 106 | } 107 | 108 | variable "template_network_bridge" { 109 | type = string 110 | default = "vmbr0" 111 | } 112 | 113 | variable "template_disks_disk_size" { 114 | type = string 115 | default = "10G" 116 | } 117 | 118 | variable "template_disks_storage_pool" { 119 | type = string 120 | default = "local-lvm" 121 | } 122 | 123 | variable "template_disks_storage_pool_type" { 124 | type = string 125 | default = "lvm" 126 | } 127 | 128 | variable "template_disks_type" { 129 | type = string 130 | default = "virtio" 131 | } 132 | --------------------------------------------------------------------------------