Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Migrate Proxmox template upload API from Python to Node.js
Replaces Python scripts proxmox_upload.py and proxmox_utils.py with Node.js equivalents proxmox_upload.js and proxmox_utils.js for uploading LXC templates to Proxmox. Adds package.json for dependencies and updates CI workflow to trigger on changes in the packer directory. Minor improvements to Ansible provisioning and Packer variable files.
  • Loading branch information
Carter Myers committed Oct 31, 2025
commit ff632499ab2fafdff3d5f84fbfc92946f097959c
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
name: Build and Upload Templates
on:
push:
# Only trigger on pushes that touch files in this packer tree
paths:
- 'container-creation/intern-phxdc-pve1/packer/**'
schedule:
- cron: "0 4 * * *" # Nightly at 4 AM
workflow_dispatch:

jobs:

build:
runs-on: ubuntu-latest
env:
Expand Down
71 changes: 71 additions & 0 deletions container-creation/intern-phxdc-pve1/packer/api/proxmox_upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
// api/proxmox_upload.js
const path = require('path');
const fs = require('fs');
const { getNodes, getStorages, uploadTemplate, chooseDefaultStorage } = require('./proxmox_utils');

function parseArgs() {
const argv = process.argv.slice(2);
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--file' && argv[i + 1]) {
out.file = argv[i + 1];
i++;
}
}
return out;
}

async function main() {
const args = parseArgs();
if (!args.file) {
console.error('Error: --file is required');
process.exit(1);
}

const filepath = args.file;
if (!fs.existsSync(filepath)) {
console.error(`Error: file not found: ${filepath}`);
process.exit(1);
}

console.log(`Starting template upload for: ${filepath}`);

try {
const nodes = await getNodes();
if (!nodes || !nodes.length) {
console.error('Error: No Proxmox nodes found.');
process.exit(1);
}

console.log(`Found nodes: ${nodes.join(', ')}`);

for (const node of nodes) {
console.log(`--- Processing Node: ${node} ---`);
const storagesList = await getStorages(node);
const storage = chooseDefaultStorage(storagesList);
if (!storage) {
console.warn(`Warning: No suitable storage found on node ${node}. Skipping.`);
continue;
}

console.log(`Uploading to ${node}:${storage}...`);
try {
const result = await uploadTemplate(node, storage, filepath);
console.log(`Successfully uploaded to ${node}:${storage}. Task: ${JSON.stringify(result.data || result)}`);
} catch (e) {
console.error(`Error uploading to ${node}:${storage}: ${e.message || e}`);
}
}
} catch (e) {
console.error(`An unexpected error occurred: ${e.message || e}`);
process.exit(1);
}

console.log('Template upload process finished.');
}

if (require.main === module) {
main();
}
48 changes: 0 additions & 48 deletions container-creation/intern-phxdc-pve1/packer/api/proxmox_upload.py

This file was deleted.

71 changes: 71 additions & 0 deletions container-creation/intern-phxdc-pve1/packer/api/proxmox_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// api/proxmox_utils.js
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
const https = require('https');

const API_URL = process.env.PROXMOX_API_URL;
const TOKEN_ID = process.env.PROXMOX_TOKEN_ID;
const TOKEN_SECRET = process.env.PROXMOX_TOKEN_SECRET;

if (!API_URL || !TOKEN_ID || !TOKEN_SECRET) {
// Do not throw; allow functions to be imported but fail loudly when used.
// Console a warning to help debugging.
console.warn('Warning: PROXMOX_API_URL, PROXMOX_TOKEN_ID or PROXMOX_TOKEN_SECRET is not set. Requests will fail.');
}

const HEADERS = {
Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}`,
};

// Accept self-signed / insecure if needed (mirrors requests verify=False)
const httpsAgent = new https.Agent({ rejectUnauthorized: false });

async function getNodes() {
const resp = await axios.get(`${API_URL}/nodes`, { headers: HEADERS, httpsAgent });
const data = resp.data && resp.data.data ? resp.data.data : [];
return data.map(n => n.node);
}

async function getStorages(node) {
const resp = await axios.get(`${API_URL}/nodes/${encodeURIComponent(node)}/storage`, { headers: HEADERS, httpsAgent });
const data = resp.data && resp.data.data ? resp.data.data : [];
return data.map(s => s.storage);
}

async function uploadTemplate(node, storage, filepath) {
const basename = path.basename(filepath);
const form = new FormData();

// Append file stream
form.append('content', fs.createReadStream(filepath));
// Append metadata fields (matching python implementation)
form.append('content', 'vztmpl');
form.append('filename', basename);

const headers = Object.assign({}, HEADERS, form.getHeaders());

const resp = await axios.post(
`${API_URL}/nodes/${encodeURIComponent(node)}/storage/${encodeURIComponent(storage)}/upload`,
form,
{ headers, httpsAgent, maxContentLength: Infinity, maxBodyLength: Infinity }
);

return resp.data;
}

function chooseDefaultStorage(storages) {
if (!Array.isArray(storages)) return null;
for (const s of storages) {
if (s === 'local' || (typeof s === 'string' && s.toLowerCase().includes('local'))) return s;
}
return storages.length ? storages[0] : null;
}

module.exports = {
getNodes,
getStorages,
uploadTemplate,
chooseDefaultStorage,
};
49 changes: 0 additions & 49 deletions container-creation/intern-phxdc-pve1/packer/api/proxmox_utils.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ build {

provisioner "shell" {
inline = [
"set -eux", # Good practice from your rocky file
"set -eux",
"mkdir -p /tmp/output",
"cd /tmp/rootfs",
# Use the variables to build the filename
Expand Down
16 changes: 16 additions & 0 deletions container-creation/intern-phxdc-pve1/packer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "intern-phxdc-pve1-packer-api",
"version": "0.0.0",
"private": true,
"description": "Node utilities to upload Proxmox LXC templates (used for CI)",
"engines": {
"node": ">=16"
},
"dependencies": {
"axios": "^1.6.0",
"form-data": "^4.0.0"
},
"scripts": {
"upload": "node api/proxmox_upload.js"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,15 @@
- ca-certificates
- nano
- git
- gpg
state: present

- name: Set up /etc/motd branding
ansible.builtin.copy:
dest: /etc/motd
content: |
This will be replaced by logic to deploy container information.
This container image was built automatically via Packer and Ansible.

- name: Configure system DNS (common)
ansible.builtin.copy:
dest: /etc/resolv.conf
content: |
nameserver 1.1.1.1
nameserver 8.8.8.8
when: ansible_os_family in ['Debian', 'RedHat']

- name: Copy pown.sh (disabled until first boot)
ansible.builtin.copy:
src: files/pown.sh
dest: /usr/local/bin/pown.sh
mode: '0755'

- name: Copy Wazuh placeholder registration script
ansible.builtin.copy:
dest: /etc/wazuh-agent/init_disabled
content: |
#!/bin/bash
# Registration disabled in build context; runs on first boot only.
mode: '0755'

- name: Clean temporary or build-specific files
ansible.builtin.file:
path: "{{ item }}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
rocky_release = "9"
template_name = "rocky9-lxc"
rootfs_dir = "/tmp/rocky9-rootfs"
hostname = "template-rocky9"