Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d10228dd | ||
|
|
3212d70db6 | ||
|
|
79a206e95b | ||
|
|
928018285c | ||
|
|
df819f2d40 | ||
|
|
1663668e7c | ||
|
|
914acf3632 | ||
|
|
fbf990d6af | ||
|
|
42cbc6d108 | ||
|
|
d18c723884 | ||
|
|
651abc4880 | ||
|
|
b1052d87c9 | ||
|
|
e21a7fc3e7 | ||
|
|
0056719950 |
32
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
36
README.md
@@ -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!
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 422 KiB |
BIN
modules/addons/pvewhmcs/img/forbidden.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
modules/addons/pvewhmcs/img/logo-stacked.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -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";
|
||||
|
||||