Compare commits

...

4 Commits

15 changed files with 271 additions and 36 deletions

View File

@@ -91,7 +91,7 @@ all:
username: root@pam
password: !vault |
$ANSIBLE_VAULT...
host: pve01
node: pve01
storage: local-lvm
children:
@@ -268,7 +268,7 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
| `url` | string | -- | API host (Proxmox/VMware) |
| `username` | string | -- | API username |
| `password` | string | -- | API password |
| `host` | string | -- | Proxmox node name |
| `node` | string | -- | Target compute node (Proxmox node / VMware ESXi host; mutually exclusive with `cluster` on VMware) |
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
| `datacenter` | string | -- | VMware datacenter |
| `cluster` | string | -- | VMware cluster |

View File

@@ -6,7 +6,7 @@ all:
url: "pve01.example.com"
username: "root@pam"
password: "CHANGE_ME"
host: "pve01"
node: "pve01"
storage: "local-lvm"
boot_iso: "local:iso/archlinux-x86_64.iso"
children:

View File

@@ -110,32 +110,81 @@
ansible.builtin.import_role:
name: system_check
roles:
- role: virtualization
when: system_cfg.type == "virtual"
become: false
vars:
ansible_connection: local
tasks:
- name: Bootstrap pipeline
block:
- name: Record that no pre-existing VM was found
ansible.builtin.set_fact:
_vm_absent_before_bootstrap: true
- role: environment
vars:
ansible_connection: "{{ 'vmware_tools' if hypervisor_type == 'vmware' else 'ssh' }}"
- name: Create virtual machine
when: system_cfg.type == "virtual"
ansible.builtin.include_role:
name: virtualization
public: true
vars:
ansible_connection: local
ansible_become: false
- role: partitioning
vars:
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
- name: Configure environment
ansible.builtin.include_role:
name: environment
public: true
- role: bootstrap
- name: Partition disks
ansible.builtin.include_role:
name: partitioning
public: true
vars:
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
- role: configuration
- name: Install base system
ansible.builtin.include_role:
name: bootstrap
public: true
- role: cis
when: system_cfg.features.cis.enabled | bool
- name: Apply system configuration
ansible.builtin.include_role:
name: configuration
public: true
- role: cleanup
when: system_cfg.type in ["virtual", "physical"]
become: false
- name: Apply CIS hardening
when: system_cfg.features.cis.enabled | bool
ansible.builtin.include_role:
name: cis
public: true
- name: Clean up and finalize
when: system_cfg.type in ["virtual", "physical"]
ansible.builtin.include_role:
name: cleanup
public: true
vars:
ansible_become: false
rescue:
- name: Delete VM on bootstrap failure
when:
- _vm_absent_before_bootstrap | default(false) | bool
- virtualization_vm_created_in_run | default(false) | bool
- system_cfg.type == "virtual"
ansible.builtin.include_role:
name: virtualization
tasks_from: delete
vars:
ansible_connection: local
ansible_become: false
tags:
- rescue_cleanup
- name: Fail host after bootstrap rescue
ansible.builtin.fail:
msg: >-
Bootstrap failed for {{ hostname }}.
{{ 'VM was deleted to allow clean retry.'
if (virtualization_vm_created_in_run | default(false))
else 'VM was not created in this run (kept).' }}
post_tasks:
- name: Set post-reboot connection flags

View File

@@ -13,6 +13,14 @@
| default('')
}}
- name: Bring up network interface
when:
- hypervisor_type == "vmware"
- environment_interface_name | default('') | length > 0
ansible.builtin.command: "ip link set {{ environment_interface_name }} up"
register: environment_link_result
changed_when: environment_link_result.rc == 0
- name: Set IP-Address
when:
- hypervisor_type == "vmware"
@@ -32,13 +40,31 @@
register: environment_gateway_result
changed_when: environment_gateway_result.rc == 0
- name: Configure DNS resolvers
when:
- hypervisor_type == "vmware"
- system_cfg.network.dns.servers | default([]) | length > 0
ansible.builtin.copy:
dest: /etc/resolv.conf
content: |
{% for server in system_cfg.network.dns.servers %}
nameserver {{ server }}
{% endfor %}
{% if system_cfg.network.dns.search | default([]) | length > 0 %}
search {{ system_cfg.network.dns.search | join(' ') }}
{% endif %}
mode: "0644"
- name: Synchronize clock via NTP
ansible.builtin.command: timedatectl set-ntp true
register: environment_ntp_result
changed_when: environment_ntp_result.rc == 0
- name: Configure SSH for root login
when: hypervisor_type == "vmware" and hypervisor_cfg.ssh | bool
when:
- hypervisor_type == "vmware"
- hypervisor_cfg.ssh | default(false) | bool
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
block:
- name: Allow login
ansible.builtin.replace:
@@ -58,7 +84,18 @@
name: sshd
state: reloaded
- name: Set SSH connection for VMware
- name: Switch to SSH connection
ansible.builtin.set_fact:
ansible_connection: ssh
ansible_user: root
ansible_password: ""
ansible_host: "{{ system_cfg.network.ip }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
- name: Reset connection for SSH switchover
ansible.builtin.meta: reset_connection
- name: Verify SSH connectivity
ansible.builtin.wait_for_connection:
timeout: 30
delay: 2

View File

@@ -79,6 +79,13 @@
# bootstrapping RHEL-family distros from the Arch ISO, where the
# host rpm/dnf does not trust target distro GPG keys. Package
# integrity is verified by the target system's own rpm after reboot.
- name: Create RPM macros directory
when: is_rhel | bool
ansible.builtin.file:
path: /etc/rpm
state: directory
mode: "0755"
- name: Relax RPM Sequoia signature policy for RHEL bootstrap
when: is_rhel | bool
ansible.builtin.copy:

View File

@@ -1,6 +1,6 @@
---
- name: Configure work environment
become: "{{ hypervisor_type != 'vmware' }}"
become: "{{ (hypervisor_type | default('none')) != 'vmware' }}"
block:
- name: Detect and validate live environment
ansible.builtin.include_tasks: _detect_live.yml

View File

@@ -46,7 +46,7 @@ hypervisor_defaults:
url: ""
username: ""
password: ""
host: ""
node: ""
storage: ""
datacenter: ""
cluster: ""
@@ -136,10 +136,10 @@ system_defaults:
# All virtual types additionally require network bridge or interfaces.
hypervisor_required_fields:
proxmox:
hypervisor: [url, username, password, host, storage]
hypervisor: [url, username, password, node, storage]
system: [id]
vmware:
hypervisor: [url, username, password, datacenter, cluster, storage]
hypervisor: [url, username, password, datacenter, storage]
system: []
xen:
hypervisor: []

View File

@@ -77,7 +77,12 @@
if (system_raw.mirror | default('') | string | trim | length) > 0
else _mirror_defaults[system_raw.os | default('') | string | lower] | default('')
}}
path: "{{ system_raw.path | default('') | string }}"
path: >-
{{
(system_raw.path | default('') | string)
if (system_raw.path | default('') | string | length > 0)
else (hypervisor_cfg.folder | default('') | string)
}}
packages: >-
{{
(

View File

@@ -32,12 +32,19 @@
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
node: "{{ hypervisor_cfg.node }}"
no_log: true
- name: Normalize system inputs
ansible.builtin.include_tasks: system.yml
- name: Inherit folder from hypervisor when system path is empty
when:
- system_cfg.path | default('') | string | length == 0
- hypervisor_cfg.folder | default('') | string | length > 0
ansible.builtin.set_fact:
system_cfg: "{{ system_cfg | combine({'path': hypervisor_cfg.folder | string}, recursive=True) }}"
- name: Validate variables
ansible.builtin.include_tasks: validation.yml
@@ -85,3 +92,12 @@
when: hypervisor_type == "vmware"
ansible.builtin.set_fact:
ansible_connection: vmware_tools
ansible_vmware_host: "{{ hypervisor_cfg.url }}"
ansible_vmware_port: 443
ansible_vmware_user: "{{ hypervisor_cfg.username }}"
ansible_vmware_password: "{{ hypervisor_cfg.password }}"
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
ansible_vmware_tools_user: root
ansible_vmware_tools_password: "{{ system_cfg.root.password }}"
no_log: true

View File

@@ -48,6 +48,28 @@
}, recursive=True)
}}
- name: Populate primary network fields from first interface (pre-computed)
when:
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
- system_cfg.network.interfaces | default([]) | length > 0
- system_cfg.network.bridge | default('') | string | length == 0
vars:
_primary: "{{ system_cfg.network.interfaces[0] }}"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'network': system_cfg.network | combine({
'bridge': _primary.bridge | default(''),
'vlan': _primary.vlan | default(''),
'ip': _primary.ip | default(''),
'prefix': _primary.prefix | default(''),
'gateway': _primary.gateway | default('')
})
}, recursive=True)
}}
- name: Derive convenience facts from pre-computed system_cfg
when:
- system_cfg is defined

View File

@@ -166,6 +166,23 @@
label: "hypervisor.{{ item }}"
no_log: true
- name: Validate VMware placement (cluster or node required, mutually exclusive)
when:
- system_cfg.type == "virtual"
- hypervisor_type == "vmware"
ansible.builtin.assert:
that:
- >-
(hypervisor_cfg.cluster | default('') | string | length > 0)
or (hypervisor_cfg.node | default('') | string | length > 0)
- >-
(hypervisor_cfg.cluster | default('') | string | length == 0)
or (hypervisor_cfg.node | default('') | string | length == 0)
fail_msg: >-
VMware requires either hypervisor.cluster or hypervisor.node (mutually exclusive).
cluster targets a vSphere cluster; node targets a specific ESXi host.
quiet: true
- name: Validate hypervisor-specific required system fields
when:
- system_cfg.type == "virtual"
@@ -293,8 +310,8 @@
system_disk_mounts: >-
{{
(system_cfg.disks | default([]))
| map(attribute='mount')
| map(attribute='path')
| map(attribute='mount', default={})
| map(attribute='path', default='')
| map('string')
| map('trim')
| reject('equalto', '')

View File

@@ -45,7 +45,7 @@
block:
- name: Query Proxmox for existing VM
community.proxmox.proxmox_vm_info:
node: "{{ hypervisor_cfg.host }}"
node: "{{ hypervisor_cfg.node }}"
vmid: "{{ system_cfg.id }}"
name: "{{ hostname }}"
type: qemu
@@ -66,6 +66,7 @@
- name: Check VM existence in vCenter
when: hypervisor_type == "vmware"
delegate_to: localhost
become: false
module_defaults:
community.vmware.vmware_guest_info:
hostname: "{{ hypervisor_cfg.url }}"
@@ -106,6 +107,7 @@
- name: Check if VM already exists on Xen
when: hypervisor_type == "xen"
delegate_to: localhost
become: false
ansible.builtin.command:
argv:
- xl

View File

@@ -0,0 +1,79 @@
---
- name: Delete VMware VM
when: hypervisor_type == "vmware"
delegate_to: localhost
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
name: "{{ hostname }}"
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
state: absent
force: true
no_log: true
- name: Delete Proxmox VM
when: hypervisor_type == "proxmox"
delegate_to: localhost
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.node }}"
vmid: "{{ system_cfg.id | default(omit, true) }}"
name: "{{ hostname }}"
state: absent
force: true
no_log: true
- name: Destroy libvirt VM
when: hypervisor_type == "libvirt"
delegate_to: localhost
block:
- name: Stop libvirt VM
community.libvirt.virt:
name: "{{ hostname }}"
state: destroyed
uri: "{{ libvirt_uri | default('qemu:///system') }}"
failed_when: false
- name: Undefine libvirt VM
community.libvirt.virt:
name: "{{ hostname }}"
command: undefine
uri: "{{ libvirt_uri | default('qemu:///system') }}"
failed_when: false
- name: Remove libvirt disk images
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ virtualization_libvirt_disks | default([]) }}"
loop_control:
label: "{{ item.path | default('unknown') }}"
- name: Remove libvirt cloud-init disk
ansible.builtin.file:
path: "{{ virtualization_libvirt_cloudinit_path | default('/dev/null') }}"
state: absent
when: virtualization_libvirt_cloudinit_path is defined
- name: Destroy Xen VM
when: hypervisor_type == "xen"
delegate_to: localhost
block:
- name: Stop Xen VM
ansible.builtin.command:
argv:
- xl
- destroy
- "{{ hostname }}"
failed_when: false
- name: Remove Xen VM config
ansible.builtin.file:
path: "/etc/xen/{{ hostname }}.cfg"
state: absent
failed_when: false

View File

@@ -32,7 +32,8 @@
{%- endfor -%}
{{ ns.out }}
community.vmware.vmware_guest:
cluster: "{{ hypervisor_cfg.cluster }}"
cluster: "{{ hypervisor_cfg.cluster if (hypervisor_cfg.node | default('') | length == 0) else omit }}"
esxi_hostname: "{{ hypervisor_cfg.node if (hypervisor_cfg.node | default('') | length > 0) else omit }}"
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
name: "{{ hostname }}"
# Generic guest ID — VMware auto-detects OS post-install

View File

@@ -7,7 +7,7 @@ hypervisor:
url: "pve01.example.com"
username: "root@pam"
password: "CHANGE_ME"
host: "pve01"
node: "pve01"
storage: "local-lvm"
datacenter: "dc01"
cluster: "cluster01"