Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
86 changes: 51 additions & 35 deletions create-a-container/routers/containers.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,15 @@ router.get('/new', requireAuth, async (req, res) => {
})
});

// Get datastores for this node
const datastores = await client.datastores(node.name, 'vztmpl', true);

// Iterate over each datastore and get its contents
for (const datastore of datastores) {
const contents = await client.storageContents(node.name, datastore.storage, 'vztmpl');

// Add templates from this storage
for (const item of contents) {
templates.push({
volid: item.volid,
name: item.volid.split('/').pop(), // Extract filename from volid
size: item.size,
node: node.name,
storage: datastore.storage
});
}
const lxcTemplates = await client.getLxcTemplates(node.name);

for (const lxc of lxcTemplates) {
templates.push({
vmid: lxc.vmid,
name: lxc.name,
status: lxc.status,
node: node.name
});
}
}

Expand Down Expand Up @@ -217,36 +209,45 @@ router.post('/', async (req, res) => {

// TODO: build the container async in a Job
try {
// clone the template
const { hostname, template, services } = req.body;
const [ nodeName, ostemplate ] = template.split(',');
const [ nodeName, templateVmid ] = template.split(',');
const node = await Node.findOne({ where: { name: nodeName, siteId } });
const client = new ProxmoxApi(node.apiUrl, node.tokenId, node.secret, {
httpsAgent: new https.Agent({
rejectUnauthorized: node.tlsVerify !== false
})
});
const vmid = await client.nextId();
const upid = await client.createLxc(node.name, {
ostemplate,
vmid,
cores: 4,
features: 'nesting=1', // allow nested containers
const upid = await client.cloneLxc(node.name, parseInt(templateVmid, 10), vmid, {
hostname,
memory: 4096, // 4GB RAM
description: `Cloned from template ${templateVmid}`,
full: 1
});

// wait for the task to complete
while (true) {
const status = await client.taskStatus(node.name, upid);
if (status.status === 'stopped') break;
}

// Configure the cloned container
await client.updateLxcConfig(node.name, vmid, {
cores: 4,
features: 'nesting=1',
memory: 4096,
net0: 'name=eth0,ip=dhcp,bridge=vmbr0',
rootfs: `${ostemplate.split(':')[0]}:50`, // 50GB root disk on the template's storage
searchdomain: site.internalDomain, // use the site's search domain
searchdomain: site.internalDomain,
swap: 0,
onboot: 1, // start the container automatically on node boot
start: 1, // start the container immediately after creation
onboot: 1,
tags: req.session.user,
unprivileged: 1
});

// Start the container
const startUpid = await client.startLxc(node.name, vmid);

// wait for the task to complete
// wait for the start task to complete
while (true) {
const status = await client.taskStatus(node.name, upid);
const status = await client.taskStatus(node.name, startUpid);
if (status.status === 'stopped') break;
}

Expand Down Expand Up @@ -355,9 +356,24 @@ router.post('/', async (req, res) => {

return res.redirect(`/sites/${siteId}/containers`);
} catch (err) {
console.log(err);
console.log(err.response?.data?.errors);
throw err;
console.error('Error creating container:', err);

// Handle axios errors with detailed messages
let errorMessage = 'Failed to create container: ';
if (err.response?.data) {
if (err.response.data.errors) {
errorMessage += JSON.stringify(err.response.data.errors);
} else if (err.response.data.message) {
errorMessage += err.response.data.message;
} else {
errorMessage += err.message;
}
} else {
errorMessage += err.message;
}

req.flash('error', errorMessage);
return res.redirect(`/sites/${siteId}/containers/new`);
}
});

Expand Down
63 changes: 63 additions & 0 deletions create-a-container/utils/proxmox-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,69 @@ class ProxmoxApi {

return response.data;
}

/**
* Get LXC template containers on a node
* @param {string} node - The node name
* @returns {Promise<Array>} - Array of LXC templates
*/
async getLxcTemplates(node) {
const response = await axios.get(
`${this.baseUrl}/api2/json/nodes/${node}/lxc`,
this.options
);
return response.data.data.filter(lxc => lxc.template === 1);
}

/**
* Clone an LXC container from a template
* @param {string} node - The node name
* @param {number} vmid - The template container VMID to clone from
* @param {number} newid - The new container VMID
* @param {object} options - Additional options (hostname, description, storage, etc.)
* @returns {Promise<string>} - The task UPID
*/
async cloneLxc(node, vmid, newid, options = {}) {
const response = await axios.post(
`${this.baseUrl}/api2/json/nodes/${node}/lxc/${vmid}/clone`,
{
newid,
...options
},
this.options
);
return response.data.data;
}

/**
* Update LXC container configuration
* @param {string} node - The node name
* @param {number} vmid - The container VMID
* @param {object} config - Configuration options to update
* @returns {Promise<void>}
*/
async updateLxcConfig(node, vmid, config) {
await axios.put(
`${this.baseUrl}/api2/json/nodes/${node}/lxc/${vmid}/config`,
config,
this.options
);
}

/**
* Start an LXC container
* @param {string} node - The node name
* @param {number} vmid - The container VMID
* @returns {Promise<string>} - The task UPID
*/
async startLxc(node, vmid) {
const response = await axios.post(
`${this.baseUrl}/api2/json/nodes/${node}/lxc/${vmid}/status/start`,
{},
this.options
);
return response.data.data;
}
}

module.exports = ProxmoxApi;
2 changes: 1 addition & 1 deletion create-a-container/views/containers/form.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const breadcrumbLabel = isEdit ? 'Edit' : 'New';
<option value="">Select a template...</option>
<% if (typeof templates !== 'undefined' && templates && templates.length > 0) { %>
<% templates.forEach(template => { %>
<option value="<%= template.node %>,<%= template.volid %>">
<option value="<%= template.node %>,<%= template.vmid %>">
<%= template.name %> (<%= template.node %>)
</option>
<% }) %>
Expand Down
Loading