14 Commits

Author SHA1 Message Date
Luke S Thompson
c3d10228dd v1.3.3: Nodes Tidy; RRD Migration Tool 2026-02-05 18:33:41 +11:00
Luke S Thompson
3212d70db6 proxmox-rrd-migration-tool (#188)
- mention tool in GUI error to end-user

- README.md section with more info
2026-01-29 09:10:13 +11:00
Luke S Thompson
79a206e95b Admin > Nodes tab: Tidy-up 2026-01-14 17:47:26 +11:00
Luke S Thompson
928018285c Commit 400, honouring HTTP Status Dogs
- if no auth, image and logo display

- logo onto Support/Config panes

- improve plan add/edit wording

- Disk Cache, explicit "none"

- link to reference docs x2

- clarify re: data xfer cap

- clarify re: IPv6 "prefix"

- consistent icon sizing

- remove <pre></pre>

- improve ver. display

- "add" WS 2025
2026-01-10 13:47:29 +11:00
Luke S Thompson
df819f2d40 Fully remove Bobby Tables (#160)
- and link to the latest release directly
2026-01-09 21:28:37 +11:00
Luke S Thompson
1663668e7c If auth NOK, ensure CSS renders (#177) 2026-01-09 17:01:34 +11:00
Luke S Thompson
914acf3632 v1.3.2: TPL Node; VNC Resolve; Tidy-up 2026-01-09 16:28:13 +11:00
Luke S Thompson
fbf990d6af TPL_Node_QEMU/LXC (#185) & VNC Node Search (#183)
- new custom fields for TPL_Node_QEMU/LXC

- use above field, if exists, for API; else fallback

- VNC guest resolution now via root access

- VNC itself remains via VM.Console
2026-01-09 15:37:15 +11:00
Luke S Thompson
42cbc6d108 /cluster/nextid no-param (#185)
- previously, sent param which will not give nextid

- now trying sans-param to get nextid instead

- then if no bueno, attempt with vmid param
2026-01-08 10:28:52 +11:00
Luke S Thompson
d18c723884 v1.3.1: Relativity & Nodes 2025-12-12 23:33:32 +11:00
Chris
651abc4880 (Clusters) Resolve Node for VM/CT (#178)
* Resolve Proxmox node for each VM/CT via /cluster/resources instead of assuming first node

* Refactor guest node retrieval in pvewhmcs.php

* Fix issues from PR review

* fix indentation

* more indentation fixes

* Format comma spacing

* Check if `$guest` is null before performing actions.

* Handle errors in account termination

* Load js and PNGs with relative paths

This helps in setups where WHMCS is installed in a subfolder e.g. `/clientarea/`x

* fix docblock and property typo

* Bump version and add contributors

---------

Co-authored-by: hliasa <hliasant@gmail.com>
2025-12-12 23:27:34 +11:00
Luke S Thompson
b1052d87c9 Support PVE 9.x.x (re: #162)
Tested working OK on pve-manager 8.4.14.
Tested working OK on pve-manager 9.1.2.

Module version 1.3.0 on both (updated RRD).
2025-12-04 16:52:43 +11:00
Luke S Thompson
e21a7fc3e7 #166 KVM to QEMU (except SQL)
Plan Type still in DB as 'kvm' then swapped for API usage. Need to convert this to 'qemu' in future.
2025-12-03 21:10:41 +11:00
Luke S Thompson
0056719950 KVM -> QEMU (Client Area logo) 2025-12-03 19:44:49 +11:00
14 changed files with 635 additions and 474 deletions

View File

@@ -1,6 +1,38 @@
# Changelog
All notable changes to Proxmox VE for WHMCS will be documented in this file.
## [1.3.3] - 2026-02-05 - _"Tidy Nodes & RRD"_
### 💅 Polish
- Nodes tab: Improve presentation of data points
- Client Area, RRD error: Mention migration tool (#188)
## [1.3.2] - 2026-01-10 - _"VNC, Cleaning, etc"_
### 🚀 Feature
- Custom Fields: TPL_Node_QEMU/LXC (Template Storage Node)
- Template Node: Honour Fields if set, else fallback (#186)
### 💅 Polish
- QEMU CPU: Add AMD EPYC-Milan-v2 processor model
- QEMU CPU: Link to Admin Guide for CPU comparisons
- Spacing: Clean-up all files to space concatenations
- Update Available: Links directly to the latest release
- Naming: $srv -> $pve; $res -> $resource; $v -> $guest_type
### 🐛 Bug Fix
- VNC: Resolve node as root, then connect VNC as limited user (#183)
- nextid: No param, so we get nextid; then declare as required (#185)
- CSS: If node authentication fails, close div so render is OK (#177)
## [1.3.1] - 2025-12-12 - _"Relativity & Nodes"_
### 💅 Polish
- Clusters: Requests are made to the node hosting Guest (#16)
### 🐛 Bug Fix
- Client Area: Images load in sub-dir installs (relative src)
## [1.3.0] - 2025-12-03 - _"RRD: Clients & Admins"_
### 🚀 Feature

View File

@@ -10,6 +10,8 @@ This document seeks to say "cheers", "many thanks" & "love your work" to the peo
- [@nodespacehosting](https://github.com/nodespacehosting)
- [@WaldperlachFabi](https://github.com/WaldperlachFabi)
- [@is7Qin](https://github.com/is7Qin)
- [@chrismfz](https://github.com/chrismfz)
- [@hliasa](https://github.com/hliasa)
## Why not make it even better?

View File

@@ -26,6 +26,14 @@ https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/
<img alt="Client Area GUI showing management of a powered-on Guest, after the Statistics action has been clicked resulting in the graphs at the bottom." src="_images/zVMclientGUI.png">
> [!IMPORTANT]
> Nodes must be using RRD `PVE-$TYPE-9.0` format. You can use `proxmox-rrd-migration-tool` to migrate.
>
> Old RRD Dir for VMs: `/var/lib/rrdcached/db/pve2-vm/` <br>
> New RRD Dir for VMs: `/var/lib/rrdcached/db/pve-vm-9.0/`
>
> You can research more online, and <a href="https://www.mail-archive.com/pve-devel@lists.proxmox.com/msg29223.html" target="_blank">here is part of a patch series</a> showing the new logic.
**Admin Area GUI - PVE Nodes:**
<img alt="Admin Area GUI for the Module, showing the Nodes tab that you land on upon opening the Module." src="_images/zClusterNodes.png">
@@ -50,7 +58,7 @@ https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/
- **(PHP)** v8.x.x (latest stable version)
- **(PHP)** max_execution_time = 300
- **(Proxmox)** 2 users (API & VNC)
- **(Proxmox)** VE v8.x.x (current)
- **(Proxmox)** VE v8.4/v9 (latest)
Please note specific VNC & Network requirements below - read 100% of the README.md. :-)
@@ -215,25 +223,31 @@ This needs configuring for each `WHMCS Admin > Products & Services` entry.
Firstly, create the Template VM in PVE. You need its unique PVE ID.
Use that ID in the Custom Field `KVMTemplate`, as in `ID|Name`.
Secondly, use that ID in the Custom Field `KVMTemplate`, as in `ID|Name`.
> **Note**: `ID` is the Unique ID that your Template VM has in PVE.<br>
> **Note**: `Name` is what will be displayed to your Clients in WHMCS.
Thirdly, add another Custom Field `TPL_Node_QEMU` with the node short name.
### VM Option 2: QEMU, WHMCS Plan + PVE ISO
Firstly, create the Plan in WHMCS Module. Then too in WHMCS Config > Services.
> Under the Service, you need to add a Custom Field `ISO` with the full location.<br>
> This ISO must be located on the PVE Host, and not on the WHMCS installation side.
> This ISO must be located on all PVE Nodes, and not on the WHMCS installation side.
### CT Option 1: LXC, PVE Template File
Firstly, store the Template in PVE. You need its storage, folder & File Name.
> Use that prefixed file name in the Custom Field `Template`, as in:<br>
Secondly, use that prefixed file name in the Custom Field `Template`.
> Here is the syntax for that field, including display name:<br>
> `local:vztmpl/ubuntu-99.99-standard_amd64.tar.gz|Ubuntu 99`
Thirdly, add another Custom Field `TPL_Node_LXC` with the node short name.
### VM/CT Import/Associate Existing Guest
You can associate an existing PVE Guest through the WHMCS Module too, like this:
@@ -265,6 +279,14 @@ Create a 2nd Custom Field `Password` for the Container's root user on all CT Ser
### Updating to a newer release!
> [!WARNING]
> There are 2x states that new Proxmox VE for WHMCS releases typically go through.
>
> 1. Module shows Update Available, but GitHub repo does NOT have a published release.<br>
> In this state, it is ready for testing - but we do not recommend deploying to prod.
> 2. Module shows Update Available, and GitHub repo DOES have a published release.<br>
> In this state, it is tested and considered ready for production usage.
1. Download the new version
2. Upload it over the top (FTP)
3. Login to WHMCS Admin
@@ -273,14 +295,14 @@ Create a 2nd Custom Field `Password` for the Container's root user on all CT Ser
> **Logging in _should_ trigger the self-upgrade procedure for the SQL database.**
>
> (**Beta in v1.2.x:** for now, verify yourself that updates were successful)
> (**Beta Feature:** For now, verify yourself that updates were successful)
### SQL: Keeping your DB up-to-date
> [!IMPORTANT]
> Since v1.3.0, logging into WHMCS Admin & opening the module should run any needed SQL Ops.
> Since v1.3.x, logging into WHMCS Admin & opening the module should run any needed SQL Ops.
>
> v1.2.9 & below, consult the **_docs/UPDATE-SQL.md** file, open your SQL DB & run statements.
> v1.2.x & below, consult the **_docs/UPDATE-SQL.md** file, open your SQL DB & run statements.
Then you're done with each update!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -171,7 +171,7 @@ class PVE2_API {
private function action ($action_path, $http_method, $put_post_parameters = null) {
// Check if we have a prefixed / on the path, if not add one.
if (substr($action_path, 0, 1) != "/") {
$action_path = "/".$action_path;
$action_path = "/" . $action_path;
}
if (!$this->check_login_ticket()) {
@@ -225,7 +225,7 @@ class PVE2_API {
curl_setopt($prox_ch, CURLOPT_HEADER, true);
curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=".$this->login_ticket['ticket']);
curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=" . $this->login_ticket['ticket']);
curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false);

File diff suppressed because it is too large Load Diff

View File

@@ -287,20 +287,20 @@
<div class="pve-status-section">
{* VM Type & OS Icons *}
<div class="pve-vm-icons">
<img src="/modules/servers/pvewhmcs/img/{$vm_config['vtype']}.png" alt="{$vm_config['vtype']}" title="Type: {$vm_config['vtype']}"/>
<img src="/modules/servers/pvewhmcs/img/os/{$vm_config['ostype']}.png" alt="{$vm_config['ostype']}" title="OS: {$vm_config['ostype']}"/>
<img src="./modules/servers/pvewhmcs/img/{$vm_config['vtype']}.png" alt="{$vm_config['vtype']}" title="Type: {$vm_config['vtype']}"/>
<img src="./modules/servers/pvewhmcs/img/os/{$vm_config['ostype']}.png" alt="{$vm_config['ostype']}" title="OS: {$vm_config['ostype']}"/>
</div>
{* Status Badge *}
<div class="pve-status-badge">
<img src="/modules/servers/pvewhmcs/img/{$vm_status['status']}.png" alt="{$vm_status['status']}"/>
<img src="./modules/servers/pvewhmcs/img/{$vm_status['status']}.png" alt="{$vm_status['status']}"/>
<span class="status-text">{$vm_status['status']}</span>
<span class="uptime-text">Up {$vm_status['uptime']}</span>
</div>
{* Resource Gauges *}
<div class="pve-gauges">
<script src="/modules/servers/pvewhmcs/js/CircularLoader.js"></script>
<script src="./modules/servers/pvewhmcs/js/CircularLoader.js"></script>
<div class="pve-gauge-item">
<div id="c1" class="circle" data-percent="{$vm_status['cpu']}"></div>
<strong>CPU</strong>
@@ -441,7 +441,7 @@
</tr>
{/if}
<tr>
<td><span class="spec-label">Operating System</span></td>
<td><span class="spec-label">Kernel</span> <span class="spec-sublabel">(OS)</span></td>
<td><span class="spec-value">{$vm_config['ostype']}</span></td>
</tr>
</table>
@@ -495,7 +495,7 @@
{else}
<div class="pve-alert-warning">
<i class="fa fa-exclamation-triangle"></i>
Stats Error: RRD Unavailable. Ask Support to upgrade guest RRD Data from 2.x to 9.0 format on their Node/s!
Stats Error: RRD Unavailable. Ask Support to upgrade/migrate RRD Data using: <code>proxmox-rrd-migration-tool</code>
</div>
{/if}
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,6 +1,6 @@
<?php
/*
/*
Proxmox VE for WHMCS - Addon/Server Modules for WHMCS (& PVE)
https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/
File: /modules/servers/pvewhmcs/pvewhmcs.php (PVE Work)
@@ -19,7 +19,7 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// DEP: Proxmox API Class - make sure we can access via PVE via API
@@ -76,24 +76,6 @@ function pvewhmcs_ConfigOptions() {
$ippools[$ippool->id] = $ippool->title ;
}
/*
$proxmox = new PVE2_API($server->ipaddress, $server->username, "pam", pvewhmcs_get_whmcs_server_password($server->password));
if ($proxmox->login()) {
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$storage_contents = $proxmox->get('/nodes/'.$first_node.'/storage/local/content') ;
foreach ($storage_contents as $storage_content) {
if ($storage_content['content']=='vztmpl') {
$templates[$storage_content['volid']]=explode('.',explode('/',$storage_content['volid'])[1])[0] ;
}
}
}
*/
// OPTIONS FOR THE QEMU/LXC PACKAGE; ties WHMCS PRODUCT to MODULE PLAN/POOL
// Ref: https://developers.whmcs.com/provisioning-modules/config-options/
// SQL/Param: configoption1 configoption2
@@ -146,25 +128,29 @@ function pvewhmcs_CreateAccount($params) {
// Get the starting VMID from the config options
$vmid = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('start_vmid');
////////////////////////
// CREATE IF QEMU/KVM //
////////////////////////
////////////////////
// CREATE IF QEMU //
////////////////////
if (!empty($params['customfields']['KVMTemplate'])) {
// KVM TEMPLATE - CREATION LOGIC
// QEMU TEMPLATE - CREATION LOGIC
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword);
if ($proxmox->login()) {
// Get first node name.
// Get template node: prefer TPL_Node_QEMU custom field, fallback to first node
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
if (!empty($params['customfields']['TPL_Node_QEMU'])) {
$template_node = $params['customfields']['TPL_Node_QEMU'];
} else {
$template_node = $nodes[0];
}
unset($nodes);
// Find the next available VMID by checking if the VMID exists either for QEMU or LXC
$vmid = pvewhmcs_find_next_available_vmid($proxmox, $first_node, $vmid);
$vmid = pvewhmcs_find_next_available_vmid($proxmox, $template_node, $vmid);
$vm_settings['newid'] = $vmid;
$vm_settings['name'] = "vps" . $params["serviceid"] . "-cus" . $params['clientsdetails']['userid'];
$vm_settings['full'] = true;
// KVM TEMPLATE - Conduct the VM CLONE from Template to Machine
$logrequest = '/nodes/' . $first_node . '/qemu/' . $params['customfields']['KVMTemplate'] . '/clone' . $vm_settings;
$response = $proxmox->post('/nodes/' . $first_node . '/qemu/' . $params['customfields']['KVMTemplate'] . '/clone', $vm_settings);
// QEMU TEMPLATE - Conduct the VM CLONE from Template to Machine
$logrequest = '/nodes/' . $template_node . '/qemu/' . $params['customfields']['KVMTemplate'] . '/clone' . $vm_settings;
$response = $proxmox->post('/nodes/' . $template_node . '/qemu/' . $params['customfields']['KVMTemplate'] . '/clone', $vm_settings);
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
@@ -187,7 +173,7 @@ function pvewhmcs_CreateAccount($params) {
for ($i = 0; $i < $max_retries; $i++) {
// Check task status
$task_status = $proxmox->get('/nodes/' . $first_node . '/tasks/' . $upid . '/status');
$task_status = $proxmox->get('/nodes/' . $template_node . '/tasks/' . $upid . '/status');
if (isset($task_status['status']) && $task_status['status'] === 'stopped') {
// Task is completed, now check exit status
@@ -233,7 +219,7 @@ function pvewhmcs_CreateAccount($params) {
$cloned_tweaks['cpu'] = $plan->cpuemu;
$cloned_tweaks['kvm'] = $plan->kvm;
$cloned_tweaks['onboot'] = $plan->onboot;
$amendment = $proxmox->post('/nodes/' . $first_node . '/qemu/' . $vm_settings['newid'] . '/config', $cloned_tweaks);
$amendment = $proxmox->post('/nodes/' . $template_node . '/qemu/' . $vm_settings['newid'] . '/config', $cloned_tweaks);
return true;
} else {
throw new Exception("Proxmox Error: Failed to initiate clone. Response: " . json_encode($response));
@@ -371,24 +357,28 @@ function pvewhmcs_CreateAccount($params) {
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword);
if ($proxmox->login()) {
// Get first node name.
// Get template node: prefer TPL_Node_LXC custom field for LXC, fallback to first node
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
if ($plan->vmtype != 'kvm' && !empty($params['customfields']['TPL_Node_LXC'])) {
$template_node = $params['customfields']['TPL_Node_LXC'];
} else {
$template_node = $nodes[0];
}
unset($nodes);
// Find the next available VMID by checking if the VMID exists either for QEMU or LXC
$vmid = pvewhmcs_find_next_available_vmid($proxmox, $first_node, $vmid);
$vmid = pvewhmcs_find_next_available_vmid($proxmox, $template_node, $vmid);
$vm_settings['vmid'] = $vmid;
if ($plan->vmtype == 'kvm') {
$v = 'qemu';
$guest_type = 'qemu';
} else {
$v = 'lxc';
$guest_type = 'lxc';
}
// ACTION - Fire the attempt to create
$logrequest = '/nodes/' . $first_node . '/' . $v . $vm_settings;
$response = $proxmox->post('/nodes/' . $first_node . '/' . $v, $vm_settings);
$logrequest = '/nodes/' . $template_node . '/' . $guest_type . $vm_settings;
$response = $proxmox->post('/nodes/' . $template_node . '/' . $guest_type, $vm_settings);
// DEBUG - Log the request parameters after it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
@@ -411,7 +401,7 @@ function pvewhmcs_CreateAccount($params) {
for ($i = 0; $i < $max_retries; $i++) {
// Check task status
$task_status = $proxmox->get('/nodes/' . $first_node . '/tasks/' . $upid . '/status');
$task_status = $proxmox->get('/nodes/' . $template_node . '/tasks/' . $upid . '/status');
if (isset($task_status['status']) && $task_status['status'] === 'stopped') {
// Task is completed, now check exit status
@@ -441,7 +431,7 @@ function pvewhmcs_CreateAccount($params) {
'id' => $params['serviceid'],
'vmid' => $vmid,
'user_id' => $params['clientsdetails']['userid'],
'vtype' => $v,
'vtype' => $guest_type,
'ipaddress' => $ip->ipaddress,
'subnetmask' => $ip->mask,
'gateway' => $ip->gateway,
@@ -473,49 +463,71 @@ function pvewhmcs_CreateAccount($params) {
}
/**
* Find the next free VMID cluster-wide by probing /cluster/nextid.
* Returns the first VMID >= $start_vmid for which /cluster/nextid?vmid=X returns X.
* Find the next available VMID in the Proxmox cluster.
*
* @param PVE2_API $proxmox Proxmox API client
* @param string $node Ignored (VMIDs are cluster-wide)
* This function first tries to use Proxmox's /cluster/nextid endpoint directly,
* which is the most reliable method. If the returned VMID is below the configured
* start_vmid, it will probe for an available VMID starting from start_vmid.
*
* @param PVE2_API $proxmox Proxmox API client (logged in)
* @param string $node Ignored (VMIDs are cluster-wide)
* @param int $start_vmid Starting VMID from Module config
* @return int
* @throws Exception on unexpected API errors or if cap exceeded
* @return int The next available VMID
* @throws Exception on unexpected API errors or if no free VMID found
*/
function pvewhmcs_find_next_available_vmid($proxmox, $node, $start_vmid) {
$max_attempts = 1000; // Safety cap to avoid infinite loops
$vmid = (int) $start_vmid; // Starting with configured VMID
$start_vmid = (int) $start_vmid;
for ($i = 0; $i < $max_attempts; $i++, $vmid++) {
try {
// Proxmox API: is this VMID vacant?
// If vacant, servers echo it back in 'data'.
$resp = $proxmox->get('/cluster/nextid', ['vmid' => $vmid]);
// First, try to get the cluster's next available VMID directly
try {
$resp = $proxmox->get('/cluster/nextid');
$data = (is_array($resp) && array_key_exists('data', $resp)) ? $resp['data'] : $resp;
$cluster_next = (int) $data;
// Different PHP clients return either ['data' => '123'] or just '123'
$data = (is_array($resp) && array_key_exists('data', $resp)) ? $resp['data'] : $resp;
// If cluster's next VMID is >= our start, use it directly
if ($cluster_next >= $start_vmid) {
return $cluster_next;
}
} catch (\Throwable $e) {
// If /cluster/nextid fails entirely, fall through to the probe method
}
if ((int) $data === $vmid) {
return $vmid; // confirmed vacant
}
// If cluster's next VMID is below our start_vmid, or the call failed,
// we need to probe starting from start_vmid
$max_attempts = 1000;
$vmid = $start_vmid;
// If the API returns a *different* number here, we ignore it and keep probing
continue;
for ($i = 0; $i < $max_attempts; $i++, $vmid++) {
try {
// Ask Proxmox if this specific VMID is available
// If available, it returns the same VMID; if not, it throws an error
$resp = $proxmox->get('/cluster/nextid', ['vmid' => $vmid]);
$data = (is_array($resp) && array_key_exists('data', $resp)) ? $resp['data'] : $resp;
} catch (\Throwable $e) {
$msg = strtolower($e->getMessage());
// Proxmox confirmed this VMID is available
if ((int) $data === $vmid) {
return $vmid;
}
// Occupied case looks like: "400 Parameter verification failed. vmid: VM 106 already exists"
if (strpos($msg, 'already exists') !== false || strpos($msg, 'parameter verification failed') !== false) {
continue; // try the next VMID
}
// If API returns a different number, that's unexpected but try next
continue;
// Any other error is unexpected; surface it.
throw $e;
}
}
} catch (\Throwable $e) {
$msg = strtolower($e->getMessage());
throw new Exception("Unable to find a free VMID starting at {$start_vmid} after {$max_attempts} attempts");
// VMID is occupied - these are expected errors, try next VMID
if (strpos($msg, 'already exists') !== false ||
strpos($msg, 'parameter verification failed') !== false ||
strpos($msg, 'vm ') !== false) {
continue;
}
// Any other error is unexpected; surface it
throw $e;
}
}
throw new Exception("Unable to find a free VMID starting at {$start_vmid} after {$max_attempts} attempts");
}
// PVE API FUNCTION, ADMIN: Test Connection with Proxmox node
@@ -560,16 +572,20 @@ function pvewhmcs_SuspendAccount(array $params) {
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword);
if ($proxmox->login()) {
// Get first node name & prepare
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest=Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// Log and fire request
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
logModuleCall(
@@ -597,16 +613,17 @@ function pvewhmcs_UnsuspendAccount(array $params) {
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword);
if ($proxmox->login()) {
// Get first node name & prepare
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest=Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// Log and fire request
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start');
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
logModuleCall(
@@ -634,53 +651,59 @@ function pvewhmcs_TerminateAccount(array $params) {
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword);
if ($proxmox->login()){
// Get first node name
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
// Find virtual machine type
$guest=Capsule::table('mod_pvewhmcs_vms')->where('id', '=', $params['serviceid'])->get()[0];
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id', '=', $params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// Stop the service if it is not already stopped
$guest_specific = $proxmox->get('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/current');
$guest_specific = $proxmox->get('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/current');
if ($guest_specific['status'] != 'stopped') {
$proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop' , $pve_cmdparam);
$proxmox->post('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop', $pve_cmdparam);
sleep(30);
}
if ($proxmox->delete('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid,array('skiplock'=>1))) {
$delete_response = $proxmox->delete('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid,array('skiplock'=>1));
if ($delete_response) {
// Delete entry from module table once service terminated in PVE
Capsule::table('mod_pvewhmcs_vms')->where('id', '=', $params['serviceid'])->delete();
return "success";
} else {
$response_message = isset($delete_response['errors']) ? json_encode($delete_response['errors']) : "Unknown Error, consider using Debug Mode.";
return "Error terminating account: {$response_message}";
}
} else {
return "Error terminating account. Couldn't login to PVE.";
}
$response_message = json_encode($proxmox['data']['errors']);
return "Error performing action. " . $response_message;
}
// GENERAL CLASS: WHMCS Decrypter
class pvewhmcs_hash_encryption {
/**
* Hashed value of the user provided encryption key
* @var string
* @var string
**/
var $hash_key;
/**
* String length of hashed values using the current algorithm
* @var int
* @var int
**/
var $hash_lenth;
var $hash_length;
/**
* Switch base64 enconding on / off
* @var bool true = use base64, false = binary output / input
* @var bool true = use base64, false = binary output / input
**/
var $base64;
/**
* Secret value added to randomize output and protect the user provided key
* @var string Change this value to add more randomness to your encryption
* @var string Change this value to add more randomness to your encryption
**/
var $salt = 'Change this to any secret value you like. "d41d8cd98f00b204e9800998ecf8427e" might be a good example.';
@@ -688,8 +711,8 @@ class pvewhmcs_hash_encryption {
* Constructor method
*
* Used to set key for encryption and decryption.
* @param string $key Your secret key used for encryption and decryption
* @param boold $base64 Enable base64 en- / decoding
* @param string $key Your secret key used for encryption and decryption
* @param bool $base64 Enable base64 en- / decoding
* @return mixed
*/
function pvewhmcs_hash_encryption($key, $base64 = true) {
@@ -708,8 +731,8 @@ class pvewhmcs_hash_encryption {
/**
* Method used for encryption
* @param string $string Message to be encrypted
* @return string Encrypted message
* @param string $string Message to be encrypted
* @return string Encrypted message
*/
function encrypt($string) {
$iv = $this->_generate_iv();
@@ -744,8 +767,8 @@ class pvewhmcs_hash_encryption {
/**
* Method used for decryption
* @param string $string Message to be decrypted
* @return string Decrypted message
* @param string $string Message to be decrypted
* @return string Decrypted message
*/
function decrypt($string) {
// Apply base64 decoding if necessary
@@ -789,9 +812,9 @@ class pvewhmcs_hash_encryption {
* Currently support for md5 and sha1 is provided. In theory even crc32 could be used
* but I don't recommend this.
*
* @access private
* @param string $string Message to hashed
* @return string Hash value of input message
* @access private
* @param string $string Message to hashed
* @return string Hash value of input message
*/
function _hash($string) {
// Use sha1() if possible, php versions >= 4.3.0 and 5
@@ -817,10 +840,10 @@ class pvewhmcs_hash_encryption {
* is the basis of encryption. The encrypted IV will be added to the encrypted message
* to make decryption possible. The transmitted IV will be encoded using the user provided key.
*
* @todo Add more random sources.
* @access private
* @see function pvewhmcs_hash_encryption
* @return string Binary pseudo random string
* @todo Add more random sources.
* @access private
* @see function pvewhmcs_hash_encryption
* @return string Binary pseudo random string
**/
function _generate_iv() {
// Initialize pseudo random generator
@@ -841,9 +864,9 @@ class pvewhmcs_hash_encryption {
*
* This method converts any given hexadecimal number between 00 and ff to the corresponding ASCII char
*
* @access private
* @param string Hexadecimal number between 00 and ff
* @return string Character representation of input value
* @access private
* @param string Hexadecimal number between 00 and ff
* @return string Character representation of input value
**/
function _hex2chr($num) {
return chr(hexdec($num));
@@ -854,10 +877,10 @@ class pvewhmcs_hash_encryption {
function pvewhmcs_get_whmcs_server_password($enc_pass){
global $cc_encryption_hash;
// Include WHMCS database configuration file
include_once(dirname(dirname(dirname(dirname(__FILE__)))).'/configuration.php');
include_once(dirname(dirname(dirname(dirname(__FILE__)))) . '/configuration.php');
$key1 = md5 (md5 ($cc_encryption_hash));
$key2 = md5 ($cc_encryption_hash);
$key = $key1.$key2;
$key = $key1 . $key2;
$hasher = new pvewhmcs_hash_encryption($key);
return $hasher->decrypt($enc_pass);
}
@@ -889,21 +912,21 @@ function pvewhmcs_ClientAreaCustomButtonArray() {
/**
* Fetch RRD statistics from Proxmox with graceful error handling.
*
*
* Proxmox RRD schema changed in PVE 9 from pve2-{type} to pve-{type}-9.0.
* The ds parameter names (cpu, mem, netin, netout, diskread, diskwrite) remain valid
* across both old and new schemas - verified in pve-cluster/src/pmxcfs/status.c.
*
*
* RRD data may be unavailable when:
* - VM/CT was just created (RRD takes ~60s to populate)
* - RRD schema migration is incomplete on the PVE host
* - RRD files are corrupted or missing
*
*
* Refs:
* - Issue #162: https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/issues/162
* - PVE RRD schema: https://github.com/proxmox/pve-cluster/blob/master/src/pmxcfs/status.c
* - Schema change: https://www.mail-archive.com/pve-devel@lists.proxmox.com/msg28317.html
*
*
* @param PVE2_API $proxmox The Proxmox API client instance
* @param string $node The Proxmox node name
* @param string $vtype Guest type: 'qemu' or 'lxc'
@@ -965,16 +988,18 @@ function pvewhmcs_ClientArea($params) {
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
if ($proxmox->login()) {
//$proxmox->setCookie();
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
// Where node lives ?
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
throw new Exception(
"PVEWHMCS Error: Unable to determine node for VMID {$guest->vmid} (Service #{$params['serviceid']})."
);
}
# Get and set VM variables
$vm_config = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/config') ;
$vm_config = $proxmox->get('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/config');
$cluster_resources = $proxmox->get('/cluster/resources');
$vm_status = null;
// DEBUG - Log the /cluster/resources and /config for the VM/CT, if enabled
$cluster_encoded = json_encode($cluster_resources);
$vmspecs_encoded = json_encode($vm_config);
@@ -1013,10 +1038,10 @@ function pvewhmcs_ClientArea($params) {
if ($guest->vtype == 'lxc') {
// Check on swap before setting graph value
$ct_specific = $proxmox->get('/nodes/'.$first_node.'/lxc/'.$guest->vmid.'/status/current');
$ct_specific = $proxmox->get('/nodes/' . $guest_node . '/lxc/' . $guest->vmid . '/status/current');
if ($ct_specific['maxswap'] != 0) {
$vm_status['swapusepercent'] = intval($ct_specific['swap'] * 100 / $ct_specific['maxswap']);
}
}
} else {
// Fall back to 0% usage to satisfy chart requirement
$vm_status['swapusepercent'] = 0;
@@ -1033,28 +1058,28 @@ function pvewhmcs_ClientArea($params) {
// ----------------------------------------------------------------
// CPU usage statistics (day/week/month/year)
$vm_statistics['cpu']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'cpu');
$vm_statistics['cpu']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'cpu');
$vm_statistics['cpu']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'cpu');
$vm_statistics['cpu']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'cpu');
$vm_statistics['cpu']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'year', 'cpu');
$vm_statistics['cpu']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'month', 'cpu');
$vm_statistics['cpu']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'week', 'cpu');
$vm_statistics['cpu']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'day', 'cpu');
// Memory usage statistics (day/week/month/year)
$vm_statistics['mem']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'mem');
$vm_statistics['mem']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'mem');
$vm_statistics['mem']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'mem');
$vm_statistics['mem']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'mem');
$vm_statistics['mem']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'year', 'mem');
$vm_statistics['mem']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'month', 'mem');
$vm_statistics['mem']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'week', 'mem');
$vm_statistics['mem']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'day', 'mem');
// Network I/O statistics (day/week/month/year)
$vm_statistics['netinout']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'netin,netout');
$vm_statistics['netinout']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'netin,netout');
$vm_statistics['netinout']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'netin,netout');
$vm_statistics['netinout']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'netin,netout');
$vm_statistics['netinout']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'year', 'netin,netout');
$vm_statistics['netinout']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'month', 'netin,netout');
$vm_statistics['netinout']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'week', 'netin,netout');
$vm_statistics['netinout']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'day', 'netin,netout');
// Disk I/O statistics (day/week/month/year)
$vm_statistics['diskrw']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'diskread,diskwrite');
$vm_statistics['diskrw']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'diskread,diskwrite');
$vm_statistics['diskrw']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'diskread,diskwrite');
$vm_statistics['diskrw']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'diskread,diskwrite');
$vm_statistics['diskrw']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'year', 'diskread,diskwrite');
$vm_statistics['diskrw']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'month', 'diskread,diskwrite');
$vm_statistics['diskrw']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'week', 'diskread,diskwrite');
$vm_statistics['diskrw']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $guest_node, $guest->vtype, $guest->vmid, 'day', 'diskread,diskwrite');
$vm_config['vtype'] = $guest->vtype ;
$vm_config['ipv4'] = $guest->ipaddress ;
@@ -1064,7 +1089,7 @@ function pvewhmcs_ClientArea($params) {
$vm_config['v6prefix'] = $guest->v6prefix ;
}
else {
echo '<center><strong>Error: Unable to gather data from Hypervisor.<br>Please contact Tech Support!</strong></center>';
echo '<center><strong>Error: Unable to gather data from Hypervisor.<br>Please contact Tech Support!</strong></center>';
exit;
}
@@ -1092,29 +1117,39 @@ function pvewhmcs_noVNC($params) {
throw new Exception("PVEWHMCS Error: VNC Secret in Module Config either not set or not long enough. Recommend 20+ characters for security.");
}
// Get login credentials then make the Proxmox connection attempt.
// Get server credentials and find guest node (VNC user lacks VM.Audit permission for /cluster/resources)
$serverip = $params["serverip"];
$serverusername = 'vnc';
$serverpassword = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('vnc_secret');
$proxmox_server = new PVE2_API($serverip, $params["serverusername"], "pam", $params["serverpassword"]);
if (!$proxmox_server->login()) {
return 'Failed to prepare noVNC. Unable to connect to server.';
}
$proxmox = new PVE2_API($serverip, $serverusername, "pve", $serverpassword);
// Early prep work - find guest and node using server credentials
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox_server, $guest, $params['serviceid']);
if (empty($guest_node)) {
return 'Failed to prepare noVNC. Unable to determine node.';
}
// Now use VNC credentials for the actual VNC proxy request (restricted permissions)
$vncusername = 'vnc';
$vncpassword = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('vnc_secret');
$proxmox = new PVE2_API($serverip, $vncusername, "pve", $vncpassword);
if ($proxmox->login()) {
// Get first node name
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
// Early prep work
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$vm_vncproxy = $proxmox->post('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/vncproxy', array( 'websocket' => '1' )) ;
$vm_vncproxy = $proxmox->post('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncproxy', array('websocket' => '1'));
// Get both tickets prepared
$pveticket = $proxmox->getTicket();
$vncticket = $vm_vncproxy['ticket'];
// $path should only contain the actual path without any query parameters
$path = 'api2/json/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncwebsocket?port=' . $vm_vncproxy['port'] . '&vncticket=' . urlencode($vncticket);
$path = 'api2/json/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncwebsocket?port=' . $vm_vncproxy['port'] . '&vncticket=' . urlencode($vncticket);
// Construct the noVNC Router URL with the path already prepared now
$url = '/modules/servers/pvewhmcs/novnc_router.php?host=' . $serverip . '&pveticket=' . urlencode($pveticket) . '&path=' . urlencode($path) . '&vncticket=' . urlencode($vncticket);
// Build and deliver the noVNC Router hyperlink for access
$vncreply = '<center style="background-color: green;"><strong style="color: white;">Console (noVNC) successfully prepared!<br><a href="'.$url.'" target="_blanK" style="color: Khaki;"><u>Click here to launch noVNC.</u></a></strong></center>' ;
$vncreply = '<center style="background-color: green;"><strong style="color: white;">Console (noVNC) successfully prepared!<br><a href="' . $url . '" target="_blanK" style="color: Khaki;"><u>Click here to launch noVNC.</u></a></strong></center>';
return $vncreply;
} else {
$vncreply = 'Failed to prepare noVNC. Please contact Technical Support.';
@@ -1129,29 +1164,39 @@ function pvewhmcs_SPICE($params) {
throw new Exception("PVEWHMCS Error: VNC Secret in Module Config either not set or not long enough. Recommend 20+ characters for security.");
}
// Get login credentials then make the Proxmox connection attempt.
// Get server credentials and find guest node (VNC user lacks VM.Audit permission for /cluster/resources)
$serverip = $params["serverip"];
$serverusername = 'vnc';
$serverpassword = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('vnc_secret');
$proxmox_server = new PVE2_API($serverip, $params["serverusername"], "pam", $params["serverpassword"]);
if (!$proxmox_server->login()) {
return 'Failed to prepare SPICE. Unable to connect to server.';
}
$proxmox = new PVE2_API($serverip, $serverusername, "pve", $serverpassword);
// Early prep work - find guest and node using server credentials
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox_server, $guest, $params['serviceid']);
if (empty($guest_node)) {
return 'Failed to prepare SPICE. Unable to determine node.';
}
// Now use VNC credentials for the actual SPICE proxy request (restricted permissions)
$vncusername = 'vnc';
$vncpassword = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('vnc_secret');
$proxmox = new PVE2_API($serverip, $vncusername, "pve", $vncpassword);
if ($proxmox->login()) {
// Get first node name
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
// Early prep work
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$vm_vncproxy = $proxmox->post('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/vncproxy', array( 'websocket' => '1' )) ;
$vm_vncproxy = $proxmox->post('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncproxy', array('websocket' => '1'));
// Get both tickets prepared
$pveticket = $proxmox->getTicket();
$vncticket = $vm_vncproxy['ticket'];
// $path should only contain the actual path without any query parameters
$path = 'api2/json/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncwebsocket?port=' . $vm_vncproxy['port'] . '&vncticket=' . urlencode($vncticket);
$path = 'api2/json/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/vncwebsocket?port=' . $vm_vncproxy['port'] . '&vncticket=' . urlencode($vncticket);
// Construct the SPICE Router URL with the path already prepared now
$url = '/modules/servers/pvewhmcs/spice_router.php?host=' . $serverip . '&pveticket=' . urlencode($pveticket) . '&path=' . urlencode($path) . '&vncticket=' . urlencode($vncticket);
// Build and deliver the SPICE Router hyperlink for access
$vncreply = '<center style="background-color: green;"><strong>Console (SPICE) successfully prepared.<br><a href="'.$url.'" target="_blanK" style="color: Khaki;"><u>Click here</u></a> to launch SPICE.</strong></center>' ;
$vncreply = '<center style="background-color: green;"><strong>Console (SPICE) successfully prepared.<br><a href="' . $url . '" target="_blanK" style="color: Khaki;"><u>Click here</u></a> to launch SPICE.</strong></center>';
return $vncreply;
} else {
$vncreply = 'Failed to prepare SPICE. Please contact Technical Support.';
@@ -1171,16 +1216,20 @@ function pvewhmcs_vmStart($params) {
'password2' => $pveserver->password,
);
$serverpassword = localAPI('DecryptPassword', $api_data);
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
if ($proxmox->login()) {
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
@@ -1213,26 +1262,31 @@ function pvewhmcs_vmReboot($params) {
'password2' => $pveserver->password,
);
$serverpassword = localAPI('DecryptPassword', $api_data);
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
if ($proxmox->login()) {
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// Check status before doing anything
$guest_specific = $proxmox->get('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/current');
if ($guest_specific['status'] = 'stopped') {
$guest_specific = $proxmox->get('/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/current');
if ($guest_specific['status'] == 'stopped') {
// START if Stopped
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/start';
$response = $proxmox->post($logrequest, $pve_cmdparam);
} else {
// REBOOT if Started
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/reboot';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/reboot' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/reboot';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
logModuleCall(
@@ -1265,18 +1319,23 @@ function pvewhmcs_vmShutdown($params) {
'password2' => $pveserver->password,
);
$serverpassword = localAPI('DecryptPassword', $api_data);
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
if ($proxmox->login()) {
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// $pve_cmdparam['timeout'] = '60';
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/shutdown';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/shutdown' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/shutdown';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
logModuleCall(
@@ -1308,18 +1367,23 @@ function pvewhmcs_vmStop($params) {
'password2' => $pveserver->password,
);
$serverpassword = localAPI('DecryptPassword', $api_data);
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
if ($proxmox->login()) {
# Get first node name.
$nodes = $proxmox->get_node_list();
$first_node = $nodes[0];
unset($nodes);
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
$guest = Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->first();
if ($guest === null) {
return "Error performing action. Unable to find guest linked to Service ID ({$params['serviceid']})";
}
$guest_node = pvewhmcs_find_guest_node($proxmox, $guest, $params['serviceid']);
if (empty($guest_node)) {
return "Error performing action. Unable to determine node for VMID {$guest->vmid}.";
}
$pve_cmdparam = array();
// $pve_cmdparam['timeout'] = '60';
$logrequest = '/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop';
$response = $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop' , $pve_cmdparam);
$logrequest = '/nodes/' . $guest_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop';
$response = $proxmox->post($logrequest, $pve_cmdparam);
}
// DEBUG - Log the request parameters before it's fired
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
logModuleCall(
@@ -1339,6 +1403,46 @@ function pvewhmcs_vmStop($params) {
}
}
/**
* Locate the Proxmox node that hosts a given VM/CT.
*
* @param PVE2_API $proxmox
* @param object $guest Row from mod_pvewhmcs_vms (expects ->vmid, ->vtype)
* @param int $serviceId WHMCS service ID (compatibility)
* @return string|null
*/
function pvewhmcs_find_guest_node(PVE2_API $proxmox, $guest, $serviceId)
{
// 1) Where guest lives?
$cluster_resources = $proxmox->get('/cluster/resources');
if (is_array($cluster_resources)) {
foreach ($cluster_resources as $res) {
if (!isset($res['type'], $res['vmid'], $res['node'])) {
continue;
}
// match vmid + type
if ($res['vmid'] == $guest->vmid && $res['type'] === $guest->vtype) {
return $res['node'];
}
// Legacy fallback (<1.2.9): vmid == serviceid
if ($res['vmid'] == $serviceId && $res['type'] === $guest->vtype) {
return $res['node'];
}
}
}
// 2) Fallback old behavior
$nodes = $proxmox->get_node_list();
if (is_array($nodes) && !empty($nodes)) {
return $nodes[0];
}
return null;
}
// CLIENT AREA: REFRESH TO CHECK STATUS ON-CLICK
function pvewhmcs_vmCheck($params) {
return "success";

View File

@@ -1 +1 @@
1.3.0
1.3.3