first commit

This commit is contained in:
2026-02-02 12:44:10 -05:00
commit f2eee63fb7
11 changed files with 583 additions and 0 deletions

173
Jenkinsfile vendored Normal file
View File

@@ -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}'"
}
}
}

131
README.md Normal file
View File

@@ -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.

10
ansible.cfg Normal file
View File

@@ -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

10
inventory/hosts.yml Normal file
View File

@@ -0,0 +1,10 @@
---
all:
hosts:
localhost:
ansible_connection: local
ansible_python_interpreter: "{{ ansible_playbook_python }}"
children:
proxmox:
hosts:
localhost:

39
playbooks/configure.yml Normal file
View File

@@ -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

21
playbooks/provision.yml Normal file
View File

@@ -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 }}"

6
requirements.yml Normal file
View File

@@ -0,0 +1,6 @@
---
collections:
- name: community.general
version: ">=8.0.0"
- name: ansible.posix
version: ">=1.5.0"

View File

@@ -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

28
roles/nfs/tasks/main.yml Normal file
View File

@@ -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

View File

@@ -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 }}"

View File

@@ -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 }}"