Compare commits

..

4 Commits

Author SHA1 Message Date
patrick 1e3ab88304 fix(proxmox_lxc_provision): use literal module-name keys in module_defaults
The previous commit (d5cf6f6) used 'module_defaults: "{{ _proxmox_module_defaults }}"'
to template the entire defaults dict. Ansible rejects this because
module_defaults keys must be static action/module/group names at parse
time — only values may be templated.

Expose _proxmox_api_args as a flat dict of API parameters in
defaults/main.yml, and in each task file's module_defaults block use
the literal community.proxmox.proxmox and community.proxmox.proxmox_vm_info
keys with templated values pointing at the var.
2026-06-28 13:46:16 -04:00
patrick d5cf6f656e refactor(proxmox_lxc_provision): centralize module_defaults so tasks_from works without setup
Previously the community.proxmox.proxmox / proxmox_vm_info module_defaults
were defined inline on the outer block in main.yml. Invoking individual
task files via 'tasks_from: stop' (or delete/convert/etc.) bypassed
main.yml, leaving the API parameters unset and producing
'missing required arguments: api_host, api_user' errors. The README
worked around this by telling callers to repeat the module_defaults
block at the play level — easy to forget, and duplicated config.

Extract the defaults dict into _proxmox_module_defaults in
defaults/main.yml (using a YAML anchor to share between the two
modules), and wrap every task file that calls a Proxmox module in a
block that references it. Callers only need the proxmox_* connection
vars in scope (typically group_vars/all/) — both 'roles:' and
'tasks_from:' invocations now configure the API consistently.

Files wrapped: check-exists, create, clone, update, start, stop,
delete, convert. wait/post-clone/edit-config don't call Proxmox modules
and are unchanged. main.yml's now-redundant outer module_defaults is
removed.

README updated to drop the 'Using Standalone Tasks' workaround
boilerplate.
2026-06-28 13:42:41 -04:00
patrick 8a9903eb4c fix(system_setup): update include_tasks references from .yaml to .yml
The task files were previously renamed to .yml (commits b2379e5, ce7ec1b)
but main.yml's include_tasks directives still referenced the old .yaml
names, causing role execution to fail with 'Could not find or access'
errors for extra-packages.yaml, user.yaml, and ssh.yaml.
2026-06-28 13:19:58 -04:00
patrick 3b6f59a029 fix(proxmox_lxc_provision): post-clone privilege escalation and SSH key regen var name
The user-module tasks in post-clone.yml were running without become, so
modifying /etc/passwd failed with 'usermod: Permission denied' when the
connection user (e.g. admin) was non-root. Add become: true to both
password tasks. Block-level become is avoided because the known_hosts
task in the same file is delegate_to: localhost and should not sudo on
the controller.

Also fix the variable name passed to the system_setup ssh tasks_from:
post-clone.yml was setting 'regenerate_ssh_keys', but ssh.yml gates on
'regenerate_ssh_host_keys'. The mismatch caused cloned containers to
silently keep the source template's SSH host keys.
2026-06-27 23:19:26 -04:00
13 changed files with 220 additions and 197 deletions
+5 -23
View File
@@ -144,26 +144,17 @@ The role includes idempotency checking. If a container with the specified `lxc_v
### Using Standalone Tasks
When using individual task files via `tasks_from`, you must set `module_defaults` at the play level since the tasks bypass the role's main entry point:
Individual task files (`stop`, `start`, `delete`, `convert`, etc.) can be invoked via `tasks_from` directly — each task file wraps its work in a block with the role's shared `module_defaults`, so the Proxmox API connection is configured automatically as long as the `proxmox_*` connection variables are in scope (typically from `group_vars/all/`).
```yaml
- name: Convert container to a template
hosts: localhost
module_defaults:
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_api_validate_certs }}"
node: "{{ proxmox_node }}"
vars:
lxc_hostname: "{{ lxc_hostname }}"
tasks:
- include_role:
name: proxmox_lxc_provision
tasks_from: convert
vars:
lxc_hostname: my-container
```
### Creating an LXC Container and Converting it to a Template
@@ -185,19 +176,10 @@ When using individual task files via `tasks_from`, you must set `module_defaults
- name: Convert the created container to a template
hosts: localhost
module_defaults:
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_api_validate_certs }}"
node: "{{ proxmox_node }}"
vars:
lxc_hostname: "{{ lxc_hostname }}"
tasks:
- include_role:
name: proxmox_lxc_provision
tasks_from: convert
vars:
lxc_hostname: "{{ lxc_hostname }}"
```
@@ -5,6 +5,17 @@ proxmox_api_validate_certs: false
# Host to delegate pct commands to (use inventory hostname for become_password to work)
proxmox_delegate_host: "{{ proxmox_api_host }}"
# Shared Proxmox API args referenced by each task file's module_defaults block.
# Override the underlying proxmox_* vars (e.g. from group_vars/all/) to customize.
_proxmox_api_args:
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_api_validate_certs }}"
node: "{{ proxmox_node }}"
# LXC defaults
lxc_template: "local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst"
lxc_cores: 4
@@ -1,14 +1,19 @@
---
- name: Query Proxmox for existing LXCs
community.proxmox.proxmox_vm_info:
type: lxc
register: proxmox_lxcs
- name: Check if LXC exists
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Query Proxmox for existing LXCs
community.proxmox.proxmox_vm_info:
type: lxc
register: proxmox_lxcs
- name: Check if LXC already exists
ansible.builtin.set_fact:
lxc_exists: >-
{{
(lxc_vmid is defined and lxc_vmid | int in (proxmox_lxcs.proxmox_vms | map(attribute='vmid') | list))
or
(lxc_hostname is defined and (proxmox_lxcs.proxmox_vms | selectattr('name', 'equalto', lxc_hostname) | list | length > 0))
}}
- name: Check if LXC already exists
ansible.builtin.set_fact:
lxc_exists: >-
{{
(lxc_vmid is defined and lxc_vmid | int in (proxmox_lxcs.proxmox_vms | map(attribute='vmid') | list))
or
(lxc_hostname is defined and (proxmox_lxcs.proxmox_vms | selectattr('name', 'equalto', lxc_hostname) | list | length > 0))
}}
+30 -25
View File
@@ -1,28 +1,33 @@
---
- name: Create a full clone of the container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(0) }}"
clone: "{{ lxc_clone_from }}"
clone_type: "{{ lxc_clone_type }}"
hostname: "{{ lxc_hostname }}"
storage: "{{ lxc_storage }}"
register: clone_result
- name: Clone LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Create a full clone of the container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(0) }}"
clone: "{{ lxc_clone_from }}"
clone_type: "{{ lxc_clone_type }}"
hostname: "{{ lxc_hostname }}"
storage: "{{ lxc_storage }}"
register: clone_result
- name: Add bind mounts via pct
become: yes
ansible.builtin.shell: |
pct set {{ clone_result.vmid | default(lxc_vmid) }} {% for key, value in lxc_mounts.items() %}-{{ key }} {{ value }} {% endfor %}
delegate_to: "{{ proxmox_delegate_host }}"
when: lxc_mounts is defined
- name: Add bind mounts via pct
become: yes
ansible.builtin.shell: |
pct set {{ clone_result.vmid | default(lxc_vmid) }} {% for key, value in lxc_mounts.items() %}-{{ key }} {{ value }} {% endfor %}
delegate_to: "{{ proxmox_delegate_host }}"
when: lxc_mounts is defined
- name: Resize rootfs after clone
ansible.builtin.command:
cmd: "pct resize {{ clone_result.vmid }} rootfs {{ lxc_size }}G"
delegate_to: "{{ proxmox_delegate_host }}"
become: yes
register: resize_result
changed_when: resize_result.rc == 0 and 'already at specified size' not in resize_result.stderr
failed_when:
- resize_result.rc != 0
- "'already at specified size' not in resize_result.stderr"
when: lxc_size is defined
- name: Resize rootfs after clone
ansible.builtin.command:
cmd: "pct resize {{ clone_result.vmid }} rootfs {{ lxc_size }}G"
delegate_to: "{{ proxmox_delegate_host }}"
become: yes
register: resize_result
changed_when: resize_result.rc == 0 and 'already at specified size' not in resize_result.stderr
failed_when:
- resize_result.rc != 0
- "'already at specified size' not in resize_result.stderr"
when: lxc_size is defined
+10 -5
View File
@@ -1,7 +1,12 @@
---
- ansible.builtin.include_tasks: stop.yml
- name: Convert LXC container to template
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- ansible.builtin.include_tasks: stop.yml
- name: Convert container to template
community.proxmox.proxmox:
hostname: "{{ lxc_hostname }}"
state: template
- name: Convert container to template
community.proxmox.proxmox:
hostname: "{{ lxc_hostname }}"
state: template
+28 -23
View File
@@ -1,24 +1,29 @@
---
- name: Create an LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname }}"
password: "{{ lxc_root_password | default(omit) }}"
ostemplate: "{{ lxc_template }}"
cores: "{{ lxc_cores }}"
memory: "{{ lxc_memory }}"
swap: "{{ lxc_swap }}"
disk: "{{ lxc_disk }}"
mounts: "{{ lxc_mounts | default(omit) }}"
netif: >-
{"net0": "name={{ lxc_iface_name }},bridge={{ lxc_bridge }},ip={{ lxc_ipv4 }},gw={{ lxc_gateway }},ip6={{ lxc_ipv6 }}{% if lxc_vlan_tag is defined %},tag={{ lxc_vlan_tag }}{% endif %}"}
pubkey: "{{ lookup('file', lxc_pubkey_file) | default(omit) }}"
onboot: "{{ lxc_onboot | default(false) }}"
startup: "{{ lxc_startup | default(omit) }}"
unprivileged: "{{ lxc_unprivileged | default(true) }}"
features: "{{ lxc_features | default(omit) }}"
timezone: "{{ lxc_timezone | default(omit) }}"
nameserver: "{{ lxc_nameserver | default(omit) }}"
state: present
tags: "{{ lxc_tags | default(omit) }}"
register: lxc_result
- name: Create LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Create an LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname }}"
password: "{{ lxc_root_password | default(omit) }}"
ostemplate: "{{ lxc_template }}"
cores: "{{ lxc_cores }}"
memory: "{{ lxc_memory }}"
swap: "{{ lxc_swap }}"
disk: "{{ lxc_disk }}"
mounts: "{{ lxc_mounts | default(omit) }}"
netif: >-
{"net0": "name={{ lxc_iface_name }},bridge={{ lxc_bridge }},ip={{ lxc_ipv4 }},gw={{ lxc_gateway }},ip6={{ lxc_ipv6 }}{% if lxc_vlan_tag is defined %},tag={{ lxc_vlan_tag }}{% endif %}"}
pubkey: "{{ lookup('file', lxc_pubkey_file) | default(omit) }}"
onboot: "{{ lxc_onboot | default(false) }}"
startup: "{{ lxc_startup | default(omit) }}"
unprivileged: "{{ lxc_unprivileged | default(true) }}"
features: "{{ lxc_features | default(omit) }}"
timezone: "{{ lxc_timezone | default(omit) }}"
nameserver: "{{ lxc_nameserver | default(omit) }}"
state: present
tags: "{{ lxc_tags | default(omit) }}"
register: lxc_result
+15 -10
View File
@@ -1,12 +1,17 @@
---
- ansible.builtin.include_tasks: stop.yml
- name: Delete LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- ansible.builtin.include_tasks: stop.yml
- name: Delete a container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname | default(omit) }}"
state: absent
register: delete_result
failed_when: |
delete_result.failed and
('does not exist' not in delete_result.msg)
- name: Delete a container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname | default(omit) }}"
state: absent
register: delete_result
failed_when: |
delete_result.failed and
('does not exist' not in delete_result.msg)
+44 -56
View File
@@ -1,63 +1,51 @@
---
- name: Proxmox LXC provision
module_defaults:
community.proxmox.proxmox: &proxmox_defaults
api_host: "{{ proxmox_api_host }}"
api_port: "{{ proxmox_api_port }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_api_validate_certs }}"
node: "{{ proxmox_node }}"
community.proxmox.proxmox_vm_info: *proxmox_defaults
- name: Check if container exists
ansible.builtin.include_tasks:
file: check-exists.yml
- name: Skip if container already exists
meta: end_host
when: lxc_exists | bool
- name: Container source must be defined (lxc_clone_from or lxc_template)
ansible.builtin.fail:
msg: "Neither lxc_clone_from or lxc_template are defined"
when: lxc_clone_from is undefined and lxc_template is undefined
- name: Clone container from another container or template, then update
when: lxc_clone_from is defined
block:
- name: Check if container exists
- name: Clone from template
ansible.builtin.include_tasks:
file: check-exists.yml
file: clone.yml
register: clone_result
- name: Skip if container already exists
meta: end_host
when: lxc_exists | bool
- name: Container source must be defined (lxc_clone_from or lxc_template)
ansible.builtin.fail:
msg: "Neither lxc_clone_from or lxc_template are defined"
when: lxc_clone_from is undefined and lxc_template is undefined
- name: Clone container from another container or template, then update
when: lxc_clone_from is defined
block:
- name: Clone from template
ansible.builtin.include_tasks:
file: clone.yml
register: clone_result
- name: Update container
ansible.builtin.include_tasks:
file: update.yml
vars:
lxc_vmid: "{{ clone_result.vmid }}"
register: lxc_result
- name: Create the new container
- name: Update container
ansible.builtin.include_tasks:
file: create.yml
when: lxc_template is defined and lxc_clone_from is undefined
- name: Start the created container and wait for ssh
file: update.yml
vars:
lxc_vmid: "{{ lxc_result.vmid }}"
ansible.builtin.include_tasks:
file: "{{ item }}"
loop:
- start.yml
- wait.yml
when: lxc_start
lxc_vmid: "{{ clone_result.vmid }}"
register: lxc_result
- name: Post clone updates
when: lxc_clone_from is defined
delegate_to: "{{ lxc_hostname }}"
block:
- name: Include post-clone tasks
ansible.builtin.include_tasks:
file: post-clone.yml
- name: Create the new container
ansible.builtin.include_tasks:
file: create.yml
when: lxc_template is defined and lxc_clone_from is undefined
- name: Start the created container and wait for ssh
vars:
lxc_vmid: "{{ lxc_result.vmid }}"
ansible.builtin.include_tasks:
file: "{{ item }}"
loop:
- start.yml
- wait.yml
when: lxc_start
- name: Post clone updates
when: lxc_clone_from is defined
delegate_to: "{{ lxc_hostname }}"
block:
- name: Include post-clone tasks
ansible.builtin.include_tasks:
file: post-clone.yml
@@ -4,6 +4,7 @@
name: root
password: "{{ lxc_root_password | password_hash('sha512') }}"
update_password: always
become: true
when: lxc_root_password is defined
- name: Change user password
@@ -11,6 +12,7 @@
name: "{{ lxc_user_name }}"
password: "{{ lxc_user_password | password_hash('sha512') }}"
update_password: always
become: true
when: lxc_user_password is defined
- name: Regenerate SSH host keys
@@ -18,7 +20,7 @@
name: system_setup
tasks_from: ssh
vars:
regenerate_ssh_keys: true
regenerate_ssh_host_keys: true
- name: Remove previous entry from known hosts
ansible.builtin.known_hosts:
+14 -9
View File
@@ -1,10 +1,15 @@
---
- name: Start the LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_result.vmid }}"
state: started
register: start_result
retries: 3
delay: 5
until: start_result is success
failed_when: start_result.failed and ('already running' not in start_result.msg)
- name: Start LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Start the LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_result.vmid }}"
state: started
register: start_result
retries: 3
delay: 5
until: start_result is success
failed_when: start_result.failed and ('already running' not in start_result.msg)
+15 -10
View File
@@ -1,11 +1,16 @@
---
- name: Stop container if it is running
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname | default(omit) }}"
state: stopped
register: stop_result
failed_when: |-
stop_result.failed and
('not running' not in stop_result.msg) and
('does not exist' not in stop_result.msg)
- name: Stop LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Stop container if it is running
community.proxmox.proxmox:
vmid: "{{ lxc_vmid | default(omit) }}"
hostname: "{{ lxc_hostname | default(omit) }}"
state: stopped
register: stop_result
failed_when: |-
stop_result.failed and
('not running' not in stop_result.msg) and
('does not exist' not in stop_result.msg)
+25 -20
View File
@@ -1,21 +1,26 @@
---
- name: Update an LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid }}"
hostname: "{{ lxc_hostname }}"
password: "{{ lxc_root_password | default(omit) }}"
cores: "{{ lxc_cores }}"
memory: "{{ lxc_memory }}"
swap: "{{ lxc_swap }}"
disk: "{{ lxc_disk }}"
netif: '{"net0": "name={{ lxc_iface_name }},bridge={{ lxc_bridge }},ip={{ lxc_ipv4 }},gw={{ lxc_gateway }},ip6={{ lxc_ipv6 }}{% if lxc_vlan_tag is defined %},tag={{ lxc_vlan_tag }}{% endif %}"}'
pubkey: "{{ lookup('file', lxc_pubkey_file) | default(omit) }}"
onboot: "{{ lxc_onboot | default(false) }}"
startup: "{{ lxc_startup | default(omit) }}"
features: "{{ lxc_features | default(omit) }}"
timezone: "{{ lxc_timezone | default(omit) }}"
nameserver: "{{ lxc_nameserver | default(omit) }}"
state: present
tags: "{{ lxc_tags | default(omit) }}"
update: true
register: lxc_result
- name: Update LXC container
module_defaults:
community.proxmox.proxmox: "{{ _proxmox_api_args }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_api_args }}"
block:
- name: Update an LXC container
community.proxmox.proxmox:
vmid: "{{ lxc_vmid }}"
hostname: "{{ lxc_hostname }}"
password: "{{ lxc_root_password | default(omit) }}"
cores: "{{ lxc_cores }}"
memory: "{{ lxc_memory }}"
swap: "{{ lxc_swap }}"
disk: "{{ lxc_disk }}"
netif: '{"net0": "name={{ lxc_iface_name }},bridge={{ lxc_bridge }},ip={{ lxc_ipv4 }},gw={{ lxc_gateway }},ip6={{ lxc_ipv6 }}{% if lxc_vlan_tag is defined %},tag={{ lxc_vlan_tag }}{% endif %}"}'
pubkey: "{{ lookup('file', lxc_pubkey_file) | default(omit) }}"
onboot: "{{ lxc_onboot | default(false) }}"
startup: "{{ lxc_startup | default(omit) }}"
features: "{{ lxc_features | default(omit) }}"
timezone: "{{ lxc_timezone | default(omit) }}"
nameserver: "{{ lxc_nameserver | default(omit) }}"
state: present
tags: "{{ lxc_tags | default(omit) }}"
update: true
register: lxc_result
+3 -3
View File
@@ -5,10 +5,10 @@
name: system_maintenance
# Install extra packages
- include_tasks: extra-packages.yaml
- include_tasks: extra-packages.yml
# Create a user admin account
- include_tasks: user.yaml
- include_tasks: user.yml
# Harden SSH configuration
- include_tasks: ssh.yaml
- include_tasks: ssh.yml