├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── meta └── main.yml └── tasks └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | yamllint.yaml 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: "2.7" 4 | 5 | # Use the new container infrastructure 6 | sudo: false 7 | 8 | # Install ansible 9 | addons: 10 | apt: 11 | packages: 12 | - python-pip 13 | 14 | install: 15 | # Install ansible 16 | - pip install ansible 17 | 18 | # Check ansible version 19 | - ansible --version 20 | 21 | # Create ansible.cfg with correct roles_path 22 | - printf '[defaults]\nroles_path=../' >ansible.cfg 23 | 24 | script: 25 | # Basic role syntax check 26 | - ansible-playbook tests/test.yml -i tests/inventory --syntax-check 27 | 28 | notifications: 29 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DMunkov 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 | Ansible role to build a new customized CentOS 8 LXC template on Proxmox. 2 | ========= 3 | 4 | **The problem** 5 | 6 | Default CentOS LXC template does not have openssh-server installed, which makes managing these machine difficult. 7 | The solution was found to deploy new LXC from a reference template, modify it, back it up using built-in `vzdump` tool and upload to your storage. More on the manual steps can be found here: 8 | https://www.nathancurry.com/blog/15-customize-lxc-container-template-on-proxmox/ 9 | 10 | **The solution** 11 | 12 | This playbook offers an easy way of creating customized templates. Modify the vars, run the role on your Proxmox host and start using your updated LXC template (`template_name`) 13 | 14 | Requirements 15 | ------------ 16 | 17 | Standard `proxmox` Ansible module requirements: https://docs.ansible.com/ansible/latest/modules/proxmox_module.html 18 | 19 | As of Ansible version 2.9: 20 | * proxmoxer 21 | * python >= 2.7 22 | * requests 23 | 24 | * openssl - for encrypting passwords 25 | 26 | Role Variables 27 | -------------- 28 | 29 | Check example variables file below for comprehensive list of settable variables. 30 | 31 | Dependencies 32 | ------------ 33 | 34 | No dependencies 35 | 36 | Example variables file 37 | ---------------- 38 | `tpl_vars.yml` 39 | 40 | ``` yml 41 | # These variables can be either passed through role vars or hosts/group_vars/host_vars files 42 | pve_node: "pve01" # Your Proxmox node name (eg pve01) 43 | pve_api_user: "root@pam" # Proxmox API user (eg root@pam) 44 | pve_api_password: "password" # API user password (usually same as root) 45 | pve_api_host: "pve01" # Same as Proxmox node name (unless you are into clusters) 46 | 47 | # Reference template must be downloaded prior to running this role 48 | templare_ref: "centos-8-default_20191016_amd64.tar.xz" 49 | # Proxmox storage name for storing container templates 50 | template_ref_storage: "iso" 51 | template_ref_uri: "{{ template_ref_storage }}:vztmpl/{{ templare_ref }}" 52 | 53 | # New template name, should end with .tar.gz 54 | # Passwords can be stored with ansible-vault (more here: https://docs.ansible.com/ansible/latest/user_guide/vault.html) 55 | template_name: "centos-8-ssh-20200604_amd64.tar.gz" 56 | template_storage: "iso" # Proxmox storage name for storing container templates 57 | template_id: "5215" # Any unused Proxmox vmid 58 | template_root_password: "password" # New password for root 59 | template_hostname: "centos8" # Template name 60 | template_new_user: "ansible" # Will create a new administrator account 61 | template_new_user_pass: "password" # Password for this new user account 62 | 63 | # Temporary directory to store vzdump (backup) file for a new template. Local to your Proxmox instance. 64 | template_temp_dir: "~/{{ template_id }}-{{ template_hostname }}" 65 | ``` 66 | 67 | Example Playbook 68 | ---------------- 69 | 70 | ``` yml 71 | - hosts: proxmox01 72 | vars_files: 73 | - tpl_vars.yml 74 | roles: 75 | - dmunkov.customize_pve_lxc_template 76 | ``` 77 | 78 | License 79 | ------- 80 | 81 | [MIT](LICENSE) 82 | 83 | Author Information 84 | ------------------ 85 | 86 | [DMunkov GitHub](https://github.com/DMunkov) 87 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | role_name: customize_pve_lxc_template 3 | author: Dmitrii Munkov 4 | description: Ansible role to build a new customized CentOS 8 LXC template on Proxmox. 5 | license: MIT 6 | min_ansible_version: 2.9 7 | 8 | platforms: 9 | - name: Debian 10 | versions: 11 | - buster 12 | - name: EL 13 | versions: 14 | - 8 15 | 16 | galaxy_tags: 17 | - proxmox 18 | - lxc 19 | - container 20 | - centos 21 | - ssh 22 | - customize 23 | - template 24 | 25 | dependencies: [] 26 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Check if we already have a modified template 2 | shell: "pveam list {{ template_storage }} | grep {{ template_name }}" 3 | register: existing_template 4 | failed_when: existing_template.stdout 5 | changed_when: false # Ansible-lint was yapping 6 | ignore_errors: true 7 | 8 | - name: Abort play if template already exists 9 | fail: 10 | msg: "Template with the name '{{ template_name }}' already exists on storage '{{ template_storage }}'" 11 | when: existing_template.failed_when_result 12 | 13 | - name: Deploy new CT from the reference template 14 | proxmox: 15 | node: "{{ pve_node }}" 16 | api_user: "{{ pve_api_user }}" 17 | api_password: "{{ pve_api_password }}" 18 | api_host: "{{ pve_api_host }}" 19 | vmid: "{{ template_id }}" 20 | password: "{{ template_root_password }}" 21 | hostname: "{{ template_hostname }}" 22 | ostemplate: "{{ template_ref_uri }}" 23 | storage: local-lvm 24 | netif: "{'net0':'name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0'}" 25 | state: present 26 | 27 | - name: Start container 28 | proxmox: 29 | node: "{{ pve_node }}" 30 | api_user: "{{ pve_api_user }}" 31 | api_password: "{{ pve_api_password }}" 32 | api_host: "{{ pve_api_host }}" 33 | vmid: "{{ template_id }}" 34 | state: started 35 | register: ct_start 36 | 37 | - name: Let it settle 38 | pause: 39 | seconds: 5 40 | when: ct_start.changed 41 | 42 | - name: Generate salted password for a template user 43 | command: "openssl passwd -salt 'rhel6usgcb' -6 {{ template_new_user_pass }}" 44 | changed_when: false # Ansible-lint was yapping 45 | delegate_to: localhost 46 | register: salted_passwd 47 | 48 | - name: Make changes to the template 49 | command: "pct exec {{ template_id }} -- {{ item }}" 50 | changed_when: true 51 | register: template_changes 52 | with_items: 53 | - "dnf -y update" 54 | - "dnf -y install openssh-server vim sudo" 55 | - "useradd -m {{ template_new_user }}" 56 | - "usermod -p '{{ salted_passwd.stdout }}' {{ template_new_user }}" 57 | - "usermod -a -G wheel {{ template_new_user }}" 58 | - "bash -c 'echo \"{{ template_new_user }} ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers.d/{{ template_new_user }}'" 59 | - "systemctl enable sshd" 60 | - "systemctl start sshd" 61 | - "dnf clean all" 62 | - "rm -rf /var/cache/dnf" 63 | - "rm -rf /tmp/*" 64 | 65 | - name: Stop container 66 | proxmox: 67 | node: "{{ pve_node }}" 68 | api_user: "{{ pve_api_user }}" 69 | api_password: "{{ pve_api_password }}" 70 | api_host: "{{ pve_api_host }}" 71 | vmid: "{{ template_id }}" 72 | state: stopped 73 | 74 | - name: Create temporary directory to store new template 75 | file: 76 | path: "{{ template_temp_dir }}" 77 | state: directory 78 | 79 | - name: Dump the modified template to temporary folder 80 | command: "vzdump {{ template_id }} --mode stop --compress gzip --dumpdir {{ template_temp_dir }}" 81 | changed_when: true # Ansible-lint was yapping 82 | 83 | - name: Find template backup file 84 | find: 85 | paths: "{{ template_temp_dir }}" 86 | patterns: "*.tar.gz" 87 | register: backup_file 88 | 89 | - name: Copy new template to storage 90 | copy: 91 | dest: "/mnt/pve/{{ template_storage }}/template/cache/{{ template_name }}" 92 | src: "{{ backup_file.files[0].path }}" 93 | remote_src: yes 94 | when: backup_file.matched == 1 # Only when we have one matched file in the directory 95 | 96 | - name: Clean up the temporary directory 97 | file: 98 | path: "{{ template_temp_dir }}" 99 | state: absent 100 | 101 | - name: Delete container 102 | proxmox: 103 | node: "{{ pve_node }}" 104 | api_user: "{{ pve_api_user }}" 105 | api_password: "{{ pve_api_password }}" 106 | api_host: "{{ pve_api_host }}" 107 | vmid: "{{ template_id }}" 108 | state: absent 109 | --------------------------------------------------------------------------------