commit f2eee63fb790fa29055c79e7a6e0ae0990c7acf3 Author: jerick Date: Mon Feb 2 12:44:10 2026 -0500 first commit diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..b789794 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,173 @@ +pipeline { + agent any + + parameters { + choice( + name: 'PROVISION_TYPE', + choices: ['VM', 'LXC'], + description: 'Select whether to provision a VM or LXC container' + ) + choice( + name: 'TARGET_NODE', + choices: ['homeapp1', 'homeapp2', 'homestrg1', 'homeflux1'], + description: 'Select the Proxmox node to provision on' + ) + string( + name: 'HOSTNAME', + defaultValue: '', + description: 'Hostname for the new VM/LXC (required)' + ) + string( + name: 'CPU_CORES', + defaultValue: '2', + description: 'Number of CPU cores' + ) + string( + name: 'RAM_GB', + defaultValue: '2', + description: 'RAM in GB' + ) + booleanParam( + name: 'INSTALL_DOCKER', + defaultValue: false, + description: 'Install Docker and Docker Compose' + ) + booleanParam( + name: 'INSTALL_NFS', + defaultValue: false, + description: 'Install NFS and mount NFSFolder' + ) + } + + environment { + ANSIBLE_HOST_KEY_CHECKING = 'False' + ANSIBLE_FORCE_COLOR = 'true' + } + + stages { + stage('Validate Parameters') { + steps { + script { + if (!params.HOSTNAME?.trim()) { + error("HOSTNAME is required") + } + if (!params.CPU_CORES.isInteger() || params.CPU_CORES.toInteger() < 1) { + error("CPU_CORES must be a positive integer") + } + if (!params.RAM_GB.isInteger() || params.RAM_GB.toInteger() < 1) { + error("RAM_GB must be a positive integer") + } + } + } + } + + stage('Install Ansible Collections') { + steps { + sh 'ansible-galaxy collection install -r requirements.yml --force' + } + } + + stage('Provision VM/LXC') { + steps { + withCredentials([string(credentialsId: 'proxmox-resource-creator', variable: 'PROXMOX_TOKEN')]) { + script { + // Parse the token: format is user@realm!tokenid=secret + def tokenParts = PROXMOX_TOKEN.split('!') + def apiUser = tokenParts[0] + def tokenIdAndSecret = tokenParts[1].split('=') + def apiTokenId = tokenIdAndSecret[0] + def apiTokenSecret = tokenIdAndSecret[1] + + sh """ + ansible-playbook playbooks/provision.yml \ + -e "proxmox_api_user=${apiUser}" \ + -e "proxmox_api_token_id=${apiTokenId}" \ + -e "proxmox_api_token_secret=${apiTokenSecret}" \ + -e "provision_type=${params.PROVISION_TYPE}" \ + -e "target_node=${params.TARGET_NODE}" \ + -e "vm_hostname=${params.HOSTNAME}" \ + -e "cpu_cores=${params.CPU_CORES}" \ + -e "ram_gb=${params.RAM_GB}" + """ + } + } + } + } + + stage('Wait for Machine to Boot') { + steps { + script { + def targetHost = "${params.HOSTNAME}.lan" + echo "Waiting for ${targetHost} to become available..." + + // Wait up to 3 minutes for the machine to respond to ping + timeout(time: 3, unit: 'MINUTES') { + waitUntil { + def result = sh( + script: "ping -c 1 ${targetHost} > /dev/null 2>&1", + returnStatus: true + ) + return result == 0 + } + } + + // Additional wait for SSH to be ready + sleep(time: 30, unit: 'SECONDS') + } + } + } + + stage('Copy Jenkins SSH Key') { + steps { + script { + def targetHost = "${params.HOSTNAME}.lan" + + // Use sshpass or expect to handle the initial connection + // This assumes the template has a default user that accepts the key + sh """ + sudo -u jenkins ssh-copy-id -i /var/lib/jenkins/.ssh/id_ed25519.pub -o StrictHostKeyChecking=no jenkins@${targetHost} + """ + } + } + } + + stage('Configure Machine') { + steps { + script { + def targetHost = "${params.HOSTNAME}.lan" + + // Create a temporary inventory file with the new host + writeFile file: 'temp_inventory.yml', text: """--- +all: + hosts: + new_host: + ansible_host: ${targetHost} + ansible_user: jenkins + ansible_ssh_private_key_file: /var/lib/jenkins/.ssh/id_ed25519 +""" + + sh """ + ansible-playbook playbooks/configure.yml \ + -i temp_inventory.yml \ + -e "install_docker=${params.INSTALL_DOCKER}" \ + -e "install_nfs=${params.INSTALL_NFS}" + """ + } + } + } + } + + post { + always { + // Clean up temporary inventory file + sh 'rm -f temp_inventory.yml || true' + } + success { + echo "Successfully provisioned ${params.PROVISION_TYPE} '${params.HOSTNAME}' on ${params.TARGET_NODE}" + echo "Machine is accessible at ${params.HOSTNAME}.lan" + } + failure { + echo "Failed to provision ${params.PROVISION_TYPE} '${params.HOSTNAME}'" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..00ee91e --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# Proxmox Template Clone + +An Infrastructure-as-Code automation project for provisioning and configuring virtual machines (VMs) and Linux containers (LXCs) on a Proxmox cluster using Ansible and Jenkins. + +## Overview + +This project automates the complete lifecycle of VM/LXC deployment: + +- Clone VMs or LXCs from pre-built templates +- Configure system resources (CPU, RAM) +- Optionally install Docker and Docker Compose +- Optionally mount NFS shares +- All orchestrated through a Jenkins CI/CD pipeline + +## Project Structure + +``` +proxmox-template-clone/ +├── Jenkinsfile # Jenkins pipeline definition +├── ansible.cfg # Ansible configuration +├── requirements.yml # Galaxy collection dependencies +├── inventory/ +│ └── hosts.yml # Ansible inventory +├── playbooks/ +│ ├── provision.yml # VM/LXC provisioning +│ └── configure.yml # Post-provision configuration +└── roles/ + ├── proxmox_vm/ # VM cloning role + ├── proxmox_lxc/ # LXC cloning role + ├── docker/ # Docker installation role + └── nfs/ # NFS mounting role +``` + +## Requirements + +### Infrastructure + +- **Proxmox cluster** with token-based API authentication +- **Jenkins server** with Ansible installed +- **Pre-built templates:** + - `ubuntu24vm` - Ubuntu 24 VM template + - `ubuntu24lxc` - Ubuntu 24 LXC template +- **Storage:** `local-lvm` backend for VM/LXC disks + +### Jenkins Configuration + +- SSH key pair at `/var/lib/jenkins/.ssh/id_ed25519` +- Proxmox API token stored as Jenkins credential with ID `proxmox-resource-creator` + - Format: `user@realm!tokenid=secret` + +### Ansible Collections + +```yaml +- community.general >= 8.0.0 +- ansible.posix >= 1.5.0 +``` + +Install with: +```bash +ansible-galaxy install -r requirements.yml +``` + +## Usage + +### Jenkins Pipeline Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `PROVISION_TYPE` | Choice | - | `VM` or `LXC` | +| `TARGET_NODE` | Choice | - | Proxmox node (homeapp1, homeapp2, homestrg1, homeflux1) | +| `HOSTNAME` | String | - | Name for the new machine (required) | +| `CPU_CORES` | Integer | 2 | Number of CPU cores | +| `RAM_GB` | Integer | 2 | RAM in gigabytes | +| `INSTALL_DOCKER` | Boolean | false | Install Docker and Docker Compose | +| `INSTALL_NFS` | Boolean | false | Mount NFS share | + +### Pipeline Workflow + +1. **Validate Parameters** - Ensures hostname is provided and resources are valid +2. **Install Collections** - Downloads required Ansible Galaxy collections +3. **Provision** - Clones template and configures VM/LXC resources +4. **Wait for Boot** - Polls target machine until SSH is ready (up to 3 minutes) +5. **Copy SSH Key** - Enables passwordless SSH access for Jenkins +6. **Configure** - Runs system updates, installs Docker/NFS as requested + +## Configuration + +### Network + +- Machines use `.lan` domain (e.g., `hostname.lan`) +- VMs bridge to `vmbr0` using virtio +- LXCs use DHCP on `eth0` + +### NFS (Optional) + +When enabled, mounts: +``` +192.168.0.161:/mnt/share1/NFSFolder → /var/NFSFolder +``` + +### Docker (Optional) + +Installs: +- Docker CE +- Docker CLI +- containerd +- Docker Compose plugin +- Adds jenkins user to docker group + +## Customization + +### Proxmox API Host + +Update the API host in [playbooks/provision.yml](playbooks/provision.yml): +```yaml +proxmox_api_host: "192.168.0.166" +``` + +### Target Nodes + +Modify available nodes in the `Jenkinsfile` parameters section. + +### Templates + +Update template names in the respective role task files: +- [roles/proxmox_vm/tasks/main.yml](roles/proxmox_vm/tasks/main.yml) +- [roles/proxmox_lxc/tasks/main.yml](roles/proxmox_lxc/tasks/main.yml) + +## License + +This project is provided as-is for home lab and educational use. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..39c7a59 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = ./inventory/hosts.yml +host_key_checking = False +retry_files_enabled = False +gathering = smart +fact_caching = memory + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..9d69601 --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,10 @@ +--- +all: + hosts: + localhost: + ansible_connection: local + ansible_python_interpreter: "{{ ansible_playbook_python }}" + children: + proxmox: + hosts: + localhost: diff --git a/playbooks/configure.yml b/playbooks/configure.yml new file mode 100644 index 0000000..727b48c --- /dev/null +++ b/playbooks/configure.yml @@ -0,0 +1,39 @@ +--- +- name: Configure provisioned machine + hosts: new_host + become: true + gather_facts: true + + tasks: + - name: Wait for system to be ready + ansible.builtin.wait_for_connection: + timeout: 300 + + - name: Gather facts after connection + ansible.builtin.setup: + + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Upgrade all packages + ansible.builtin.apt: + upgrade: dist + autoremove: true + + - name: Include Docker role + ansible.builtin.include_role: + name: docker + when: install_docker | default(false) | bool + + - name: Include NFS role + ansible.builtin.include_role: + name: nfs + when: install_nfs | default(false) | bool + + - name: Reboot if required + ansible.builtin.reboot: + reboot_timeout: 300 + when: ansible_facts['distribution'] == 'Ubuntu' + ignore_errors: true diff --git a/playbooks/provision.yml b/playbooks/provision.yml new file mode 100644 index 0000000..970c774 --- /dev/null +++ b/playbooks/provision.yml @@ -0,0 +1,21 @@ +--- +- name: Provision Proxmox VM or LXC + hosts: localhost + gather_facts: false + vars: + proxmox_api_host: "192.168.0.166" + + tasks: + - name: Include VM provisioning role + ansible.builtin.include_role: + name: proxmox_vm + when: provision_type | lower == 'vm' + + - name: Include LXC provisioning role + ansible.builtin.include_role: + name: proxmox_lxc + when: provision_type | lower == 'lxc' + + - name: Display created resource info + ansible.builtin.debug: + msg: "Created {{ provision_type }} '{{ vm_hostname }}' with VMID {{ created_vmid }} on node {{ target_node }}" diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..3834074 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,6 @@ +--- +collections: + - name: community.general + version: ">=8.0.0" + - name: ansible.posix + version: ">=1.5.0" diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..c9afb97 --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Install required packages for Docker + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + update_cache: true + +- name: Create keyrings directory + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + +- name: Add Docker GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + +- name: Get system architecture + ansible.builtin.command: dpkg --print-architecture + register: dpkg_arch + changed_when: false + +- name: Get Ubuntu codename + ansible.builtin.command: lsb_release -cs + register: ubuntu_codename + changed_when: false + +- name: Add Docker repository + ansible.builtin.apt_repository: + repo: "deb [arch={{ dpkg_arch.stdout }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ubuntu_codename.stdout }} stable" + state: present + filename: docker + +- name: Install Docker packages + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: true + +- name: Ensure Docker service is started and enabled + ansible.builtin.systemd: + name: docker + state: started + enabled: true + +- name: Add jenkins user to docker group + ansible.builtin.user: + name: jenkins + groups: docker + append: true + ignore_errors: true diff --git a/roles/nfs/tasks/main.yml b/roles/nfs/tasks/main.yml new file mode 100644 index 0000000..d9c06c7 --- /dev/null +++ b/roles/nfs/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Install NFS client packages + ansible.builtin.apt: + name: + - nfs-common + state: present + update_cache: true + +- name: Create NFS mount directory + ansible.builtin.file: + path: /var/NFSFolder + state: directory + mode: '0755' + +- name: Add NFS mount to fstab + ansible.builtin.lineinfile: + path: /etc/fstab + line: "192.168.0.161:/mnt/share1/NFSFolder /var/NFSFolder nfs auto,nofail,noatime,nolock,intr,tcp,actimeo=1800 0 0" + state: present + create: true + +- name: Mount NFS share + ansible.posix.mount: + path: /var/NFSFolder + src: "192.168.0.161:/mnt/share1/NFSFolder" + fstype: nfs + opts: "auto,nofail,noatime,nolock,intr,tcp,actimeo=1800" + state: mounted diff --git a/roles/proxmox_lxc/tasks/main.yml b/roles/proxmox_lxc/tasks/main.yml new file mode 100644 index 0000000..ad72eb4 --- /dev/null +++ b/roles/proxmox_lxc/tasks/main.yml @@ -0,0 +1,51 @@ +--- +- name: Get next available VMID + community.general.proxmox_next_id: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + register: next_vmid + +- name: Clone LXC from template + community.general.proxmox: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + clone: "ubuntu24lxc" + hostname: "{{ vm_hostname }}" + vmid: "{{ next_vmid.vmid }}" + full: true + storage: "local-lvm" + timeout: 300 + register: cloned_lxc + +- name: Configure LXC resources + community.general.proxmox: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + vmid: "{{ next_vmid.vmid }}" + cores: "{{ cpu_cores }}" + memory: "{{ ram_gb | int * 1024 }}" + netif: + net0: "name=eth0,bridge=vmbr0,ip=dhcp" + state: present + +- name: Start the LXC container + community.general.proxmox: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + vmid: "{{ next_vmid.vmid }}" + state: started + +- name: Set VMID fact for later use + ansible.builtin.set_fact: + created_vmid: "{{ next_vmid.vmid }}" diff --git a/roles/proxmox_vm/tasks/main.yml b/roles/proxmox_vm/tasks/main.yml new file mode 100644 index 0000000..8e754d1 --- /dev/null +++ b/roles/proxmox_vm/tasks/main.yml @@ -0,0 +1,51 @@ +--- +- name: Get next available VMID + community.general.proxmox_next_id: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + register: next_vmid + +- name: Clone VM from template + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + clone: "ubuntu24vm" + name: "{{ vm_hostname }}" + vmid: "{{ next_vmid.vmid }}" + full: true + storage: "local-lvm" + timeout: 300 + register: cloned_vm + +- name: Configure VM resources + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + vmid: "{{ next_vmid.vmid }}" + cores: "{{ cpu_cores }}" + memory: "{{ ram_gb | int * 1024 }}" + net: + net0: "virtio,bridge=vmbr0" + update: true + +- name: Start the VM + community.general.proxmox_kvm: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ target_node }}" + vmid: "{{ next_vmid.vmid }}" + state: started + +- name: Set VMID fact for later use + ansible.builtin.set_fact: + created_vmid: "{{ next_vmid.vmid }}"