344 lines
13 KiB
YAML
344 lines
13 KiB
YAML
---
|
|
# playbooks/onboarding/generic_host.yml
|
|
# Bootstrap non-Proxmox hosts for Ansible management
|
|
# Supports both existing production hosts and net-new hosts via onboard_profile.
|
|
|
|
- name: Onboard non-Proxmox host to Ansible management
|
|
hosts: "{{ target_host | default('onboarding_target_undefined') }}"
|
|
gather_facts: false
|
|
vars:
|
|
ansible_user: "{{ onboard_user | default(lab_ansible_user | default('chester')) }}"
|
|
local_ssh_key: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub"
|
|
onboard_profile: "{{ onboarding_profile }}"
|
|
onboard_enable_security_hardening: "{{ onboard_profile == 'new' }}"
|
|
required_packages:
|
|
- python3
|
|
- python3-apt
|
|
- sudo
|
|
- curl
|
|
- git
|
|
- vim
|
|
- htop
|
|
|
|
tasks:
|
|
- name: Validate required runtime variables
|
|
ansible.builtin.assert:
|
|
that:
|
|
- target_host is defined
|
|
- target_host | length > 0
|
|
- onboarding_profile is defined
|
|
- onboarding_profile | length > 0
|
|
- target_host not in ['all', '*', 'ubuntu_lab']
|
|
fail_msg: "Invalid onboarding scope. Use explicit non-broad target_host (for example docker_hosts) and onboarding_profile. Example: -e 'target_host=docker_hosts onboarding_profile=existing'"
|
|
success_msg: "Required runtime variables provided."
|
|
run_once: true
|
|
delegate_to: localhost
|
|
tags: ['connectivity', 'setup']
|
|
|
|
- name: Validate onboarding profile
|
|
ansible.builtin.assert:
|
|
that:
|
|
- onboard_profile in ['new', 'existing']
|
|
fail_msg: "Invalid onboarding_profile='{{ onboard_profile }}'. Use 'new' or 'existing'."
|
|
success_msg: "Onboarding profile '{{ onboard_profile }}' selected."
|
|
tags: ['connectivity', 'setup']
|
|
|
|
# ========================================
|
|
# SECTION 1: Connectivity Test
|
|
# ========================================
|
|
- name: Initial Connectivity Check
|
|
block:
|
|
- name: Test raw connection (no Python required)
|
|
ansible.builtin.raw: echo "Connection successful"
|
|
register: connection_test
|
|
changed_when: false
|
|
|
|
- name: Display connection status
|
|
ansible.builtin.debug:
|
|
msg: "✅ Successfully connected to {{ inventory_hostname }}"
|
|
|
|
rescue:
|
|
- name: Connection failed
|
|
ansible.builtin.fail:
|
|
msg: "❌ Cannot connect to {{ inventory_hostname }}. Check SSH credentials and network connectivity."
|
|
|
|
tags: ['connectivity', 'test']
|
|
|
|
# ========================================
|
|
# SECTION 2: SSH Key Setup
|
|
# ========================================
|
|
- name: Configure SSH Key Authentication
|
|
block:
|
|
- name: Check if .ssh directory exists
|
|
ansible.builtin.raw: test -d ~/.ssh && echo "exists" || echo "missing"
|
|
register: ssh_dir_check
|
|
changed_when: false
|
|
|
|
- name: Create .ssh directory if missing
|
|
ansible.builtin.raw: mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
when: "'missing' in ssh_dir_check.stdout"
|
|
|
|
- name: Read local SSH public key
|
|
ansible.builtin.set_fact:
|
|
ssh_public_key: "{{ lookup('file', local_ssh_key) }}"
|
|
delegate_to: localhost
|
|
|
|
- name: Check if SSH key is already authorized
|
|
ansible.builtin.raw: grep -F "{{ ssh_public_key.split()[1][:30] }}" ~/.ssh/authorized_keys
|
|
register: key_check
|
|
failed_when: false
|
|
changed_when: false
|
|
|
|
- name: Add SSH public key to authorized_keys
|
|
ansible.builtin.raw: |
|
|
echo "{{ ssh_public_key }}" >> ~/.ssh/authorized_keys
|
|
chmod 600 ~/.ssh/authorized_keys
|
|
when: key_check.rc != 0
|
|
|
|
- name: Verify SSH key authentication
|
|
ansible.builtin.ping:
|
|
vars:
|
|
ansible_ssh_pass: ""
|
|
ignore_errors: true
|
|
register: ssh_key_test
|
|
|
|
- name: Display SSH key status
|
|
ansible.builtin.debug:
|
|
msg: "✅ SSH key authentication configured successfully"
|
|
when: ssh_key_test is succeeded
|
|
|
|
tags: ['ssh', 'setup']
|
|
|
|
# ========================================
|
|
# SECTION 3: Python & Prerequisites
|
|
# ========================================
|
|
- name: Install Python and Prerequisites
|
|
block:
|
|
- name: Check if Python3 is installed
|
|
ansible.builtin.raw: which python3 || which python
|
|
register: python_check
|
|
failed_when: false
|
|
changed_when: false
|
|
|
|
- name: Install Python3 if missing (Debian/Ubuntu)
|
|
ansible.builtin.raw: |
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y python3 python3-apt
|
|
when: python_check.rc != 0
|
|
args:
|
|
executable: /bin/bash
|
|
|
|
- name: Gather facts (now that Python is available)
|
|
ansible.builtin.setup:
|
|
|
|
- name: Display system information
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
|
|
- "Hostname: {{ ansible_hostname }}"
|
|
- "Architecture: {{ ansible_architecture }}"
|
|
- "Python: {{ ansible_python_version }}"
|
|
|
|
tags: ['python', 'setup', 'prerequisites']
|
|
|
|
# ========================================
|
|
# SECTION 4: Passwordless Sudo
|
|
# ========================================
|
|
- name: Configure Passwordless Sudo
|
|
become: true
|
|
block:
|
|
- name: Check current sudo configuration
|
|
ansible.builtin.command: sudo -n true
|
|
register: sudo_check
|
|
failed_when: false
|
|
changed_when: false
|
|
become: false
|
|
|
|
- name: Create sudoers.d entry for passwordless sudo
|
|
ansible.builtin.copy:
|
|
content: "{{ ansible_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
|
dest: "/etc/sudoers.d/{{ ansible_user }}"
|
|
mode: '0440'
|
|
owner: root
|
|
group: root
|
|
validate: 'visudo -cf %s'
|
|
when: sudo_check.rc != 0
|
|
|
|
- name: Verify passwordless sudo
|
|
ansible.builtin.command: sudo -n whoami
|
|
register: sudo_verify
|
|
changed_when: false
|
|
become: false
|
|
|
|
- name: Display sudo status
|
|
ansible.builtin.debug:
|
|
msg: "✅ Passwordless sudo configured for {{ ansible_user }}"
|
|
when: sudo_verify.stdout == "root"
|
|
|
|
tags: ['sudo', 'setup']
|
|
|
|
# ========================================
|
|
# SECTION 5: System Packages
|
|
# ========================================
|
|
- name: Install Essential Packages
|
|
become: true
|
|
block:
|
|
- name: Update apt cache
|
|
ansible.builtin.apt:
|
|
update_cache: true
|
|
cache_valid_time: 3600
|
|
when: ansible_os_family == "Debian"
|
|
|
|
- name: Install required packages
|
|
ansible.builtin.apt:
|
|
name: "{{ required_packages }}"
|
|
state: present
|
|
when: ansible_os_family == "Debian"
|
|
|
|
- name: Display installed packages
|
|
ansible.builtin.debug:
|
|
msg: "✅ Essential packages installed"
|
|
|
|
tags: ['packages', 'setup']
|
|
|
|
# ========================================
|
|
# SECTION 6: Basic Security
|
|
# ========================================
|
|
- name: Apply Basic Security Hardening
|
|
become: true
|
|
when: onboard_enable_security_hardening | bool
|
|
block:
|
|
- name: Disable root SSH login
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/ssh/sshd_config
|
|
regexp: '^#?PermitRootLogin'
|
|
line: 'PermitRootLogin no'
|
|
state: present
|
|
validate: 'sshd -t -f %s'
|
|
notify: Restart SSH
|
|
|
|
- name: Disable password authentication (SSH keys only)
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/ssh/sshd_config
|
|
regexp: '^#?PasswordAuthentication'
|
|
line: 'PasswordAuthentication no'
|
|
state: present
|
|
validate: 'sshd -t -f %s'
|
|
notify: Restart SSH
|
|
when: ssh_key_test is succeeded
|
|
|
|
- name: Enable UFW firewall (allow SSH)
|
|
block:
|
|
- name: Check if ufw is installed
|
|
ansible.builtin.command: command -v ufw
|
|
register: ufw_check
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Allow SSH via UFW when available
|
|
community.general.ufw:
|
|
rule: allow
|
|
port: '22'
|
|
proto: tcp
|
|
when: ufw_check.rc == 0
|
|
|
|
- name: Skip UFW configuration when ufw is unavailable
|
|
ansible.builtin.debug:
|
|
msg: "UFW not installed; skipping firewall onboarding step."
|
|
when: ufw_check.rc != 0
|
|
when: ansible_os_family == "Debian"
|
|
|
|
tags: ['security', 'hardening']
|
|
|
|
- name: Security hardening skipped notice
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "⚠️ Security hardening skipped (existing profile)."
|
|
- " Set -e onboarding_profile=new to enforce SSH hardening controls."
|
|
when: not (onboard_enable_security_hardening | bool)
|
|
tags: ['security', 'hardening']
|
|
|
|
# ========================================
|
|
# SECTION 7: Final Validation
|
|
# ========================================
|
|
- name: Validate Onboarding
|
|
block:
|
|
- name: Test passwordless authentication
|
|
ansible.builtin.ping:
|
|
vars:
|
|
ansible_ssh_pass: ""
|
|
ansible_become_pass: ""
|
|
|
|
- name: Test passwordless sudo
|
|
ansible.builtin.command: sudo -n whoami
|
|
register: final_sudo_test
|
|
changed_when: false
|
|
|
|
- name: Gather final system state
|
|
ansible.builtin.setup:
|
|
|
|
- name: Display onboarding summary
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "════════════════════════════════════════════════"
|
|
- "✅ HOST ONBOARDING COMPLETE"
|
|
- "════════════════════════════════════════════════"
|
|
- "Profile: {{ onboard_profile }}"
|
|
- "Host: {{ inventory_hostname }} ({{ ansible_hostname }})"
|
|
- "IP: {{ ansible_default_ipv4.address }}"
|
|
- "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
|
|
- "Python: {{ ansible_python_version }}"
|
|
- "SSH Key Auth: ✅ Enabled"
|
|
- "Passwordless Sudo: ✅ Enabled"
|
|
- "Security Hardening: {{ '✅ Applied' if (onboard_enable_security_hardening | bool) else '⚠️ Skipped' }}"
|
|
- "Ansible User: {{ ansible_user }}"
|
|
- "════════════════════════════════════════════════"
|
|
- "Next Steps:"
|
|
- " • Add host to appropriate inventory groups"
|
|
- " • Run role-specific playbooks"
|
|
- " • Configure host-specific variables"
|
|
- " • (Swarm nodes) Run: ansible-playbook playbooks/docker/bootstrap_swarm.yml --limit {{ inventory_hostname }}"
|
|
- "════════════════════════════════════════════════"
|
|
|
|
tags: ['validate', 'summary']
|
|
|
|
# ========================================
|
|
# SECTION 8: Disk grow (Swarm nodes only)
|
|
# WHY before storage mounts: root filesystem must have space before
|
|
# nfs-common installs or Docker pulls images. Cloud templates ship
|
|
# with a ~2.4G root; Proxmox resizes the virtual disk via qm resize
|
|
# but the in-guest partition and filesystem need explicit expansion.
|
|
# WHY conditional: non-Swarm hosts manage their own disk sizing.
|
|
# ========================================
|
|
- name: Grow root disk partition and filesystem (Swarm nodes only)
|
|
ansible.builtin.include_role:
|
|
name: disk_grow
|
|
apply:
|
|
become: true
|
|
when: inventory_hostname in (groups['swarm_hosts'] | default([]))
|
|
tags: ['disk']
|
|
|
|
# ========================================
|
|
# SECTION 9: Storage mounts (Swarm nodes only)
|
|
# WHY here: NFS mounts must exist before any Swarm stack deploy runs.
|
|
# Wiring this into onboarding ensures a freshly provisioned Swarm VM
|
|
# is storage-ready in a single playbook run with no manual follow-up.
|
|
# WHY conditional: non-Swarm hosts (Proxmox, watchtower, heimdall) do not
|
|
# need /mnt/homelab or /mnt/media; skip silently for those targets.
|
|
# ========================================
|
|
- name: Configure NFS storage mounts (Swarm nodes only)
|
|
ansible.builtin.include_role:
|
|
name: storage_mounts
|
|
apply:
|
|
become: true
|
|
when: inventory_hostname in (groups['swarm_hosts'] | default([]))
|
|
tags: ['storage']
|
|
|
|
handlers:
|
|
- name: Restart SSH
|
|
ansible.builtin.systemd:
|
|
name: "{{ 'ssh' if ansible_os_family == 'Debian' else 'sshd' }}"
|
|
state: restarted
|
|
become: true
|