9 Commits

7 changed files with 288 additions and 107 deletions

View File

@@ -1,22 +0,0 @@
---
name: Lint Code Base
on: [ workflow_call ]
jobs:
build:
name: Lint Code Base
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Lint Code Base
uses: super-linter/super-linter@v5
env:
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -5,11 +5,8 @@ on:
branches: branches:
- main - main
jobs: jobs:
linter:
uses: ./.github/workflows/linter.yml
publish-release: publish-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: linter
steps: steps:
- name: Publish Release - name: Publish Release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1

View File

@@ -5,19 +5,85 @@
![GitHub issues](https://img.shields.io/github/issues/EZSCALE/virtfusion-whmcs-module) ![GitHub issues](https://img.shields.io/github/issues/EZSCALE/virtfusion-whmcs-module)
![GitHub pull requests](https://img.shields.io/github/issues-pr/EZSCALE/virtfusion-whmcs-module) ![GitHub pull requests](https://img.shields.io/github/issues-pr/EZSCALE/virtfusion-whmcs-module)
This module requires VirtFusion v1.7.3 or higher as this is what it's based on. Please refer to the official [documenataion](https://docs.virtfusion.com/integrations/whmcs). This module requires VirtFusion v1.7.3 or higher as this is what it's based on. Please refer to the
official [documentation](https://docs.virtfusion.com/integrations/whmcs).
## Installation ## Installation
1. Download the latest release from the [releases](https://github.com/EZSCALE/virtfusion-whmcs-module/releases) page. 1. Download the latest release from the [releases](https://github.com/EZSCALE/virtfusion-whmcs-module/releases) page.
2. Extract the contents of the archive and upload the modules folder to your WHMCS installation directory. 2. Extract the contents of the archive and upload the modules folder to your WHMCS installation directory.
## :heavy_exclamation_mark: Important Notes :heavy_exclamation_mark:
You must create two custom fields in WHMCS for this module to work. You need to configure the following custom fields on
each product you want to use this module with.
| Field Name | Field Type | Description | Validation | Select Options | Admin Only | Required Field | Show on Order Form | Show on Invoice |
|--------------------------|------------|--------------------------|-------------|----------------|------------|----------------|--------------------|-----------------|
| Initial Operating System | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: |
| Initial SSH Key | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: |
You can run this SQL query to create the custom fields:
```sql
-- Insert records for Initial Operating System if they don't already exist
INSERT INTO tblcustomfields
(type, relid, fieldname, fieldtype, description, fieldoptions, regexpr, adminonly, required, showorder, showinvoice,
sortorder, created_at, updated_at)
SELECT 'product',
id,
'Initial Operating System',
'text',
'',
'',
'',
'',
'',
'on',
'',
0,
UTC_TIMESTAMP(),
UTC_TIMESTAMP()
FROM tblproducts
WHERE servertype = 'VirtFusionDirect'
AND NOT EXISTS (SELECT 1
FROM tblcustomfields
WHERE fieldname = 'Initial Operating System'
AND relid = tblproducts.id);
-- Insert records for Initial SSH Key if they don't already exist
INSERT INTO tblcustomfields
(type, relid, fieldname, fieldtype, description, fieldoptions, regexpr, adminonly, required, showorder, showinvoice,
sortorder, created_at, updated_at)
SELECT 'product',
id,
'Initial SSH Key',
'text',
'',
'',
'',
'',
'',
'on',
'',
0,
UTC_TIMESTAMP(),
UTC_TIMESTAMP()
FROM tblproducts
WHERE servertype = 'VirtFusionDirect'
AND NOT EXISTS (SELECT 1
FROM tblcustomfields
WHERE fieldname = 'Initial SSH Key'
AND relid = tblproducts.id);
```
## What does this module change? ## What does this module change?
This module changes the following things: This module changes the following things:
- Adds configurable options to the product configuration page to allow the user to select the operating system and add - Adds configurable options to the product configuration page to allow the user to select the operating system and add
a ssh key to the initial deployment. an ssh key to the initial deployment.
## TODO ## TODO

View File

@@ -83,9 +83,16 @@ function VirtFusionDirect_updateServerObject(array $params)
return (new ModuleFunctions())->updateServerObject($params); return (new ModuleFunctions())->updateServerObject($params);
} }
/**
* Allows changing of the package of a server
*
* @author https://github.com/BlinkohHost/virtfusion-whmcs-module
* @param array $params
* @return string
*/
function VirtFusionDirect_ChangePackage(array $params) function VirtFusionDirect_ChangePackage(array $params)
{ {
return 'success'; return (new ModuleFunctions())->changePackage($params);
} }
function VirtFusionDirect_AdminServicesTabFields(array $params) function VirtFusionDirect_AdminServicesTabFields(array $params)

View File

@@ -7,14 +7,7 @@ if (!defined("WHMCS")) {
die("This file cannot be accessed directly"); die("This file cannot be accessed directly");
} }
if (!function_exists('add_hook_os_templates')) { add_hook('ClientAreaFooterOutput', 1, function ($vars) {
/**
* @param array $vars
* @return array|null
* @throws JsonException
*/
function add_hook_os_templates(array $vars): ?array
{
if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') { if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') {
return null; return null;
} }
@@ -22,7 +15,7 @@ if (!function_exists('add_hook_os_templates')) {
$cs = new ConfigureService(); $cs = new ConfigureService();
$templates_data = $cs->fetchTemplates( $templates_data = $cs->fetchTemplates(
$cs->fetchPackageId($vars['productinfo']['name']) $cs->fetchPackageByDbId($vars['productinfo']['pid']) ?? $cs->fetchPackageId($vars['productinfo']['name'])
); );
if (empty($templates_data)) { if (empty($templates_data)) {
@@ -44,21 +37,8 @@ if (!function_exists('add_hook_os_templates')) {
return strcmp($a['name'], $b['name']); return strcmp($a['name'], $b['name']);
}); });
$osTemplates = [
'id' => 'os_template',
'optionname' => 'Initial Operating System',
'optiontype' => 1,
'options' => $dropdownOptions,
'selectedvalue' => ''
];
$sshKeys = $cs->getUserSshKeys($vars['loggedinuser']); $sshKeys = $cs->getUserSshKeys($vars['loggedinuser']);
$sshKeysOptions = array_map(function ($sshKey) {
$sshKeysOptions = [
'id' => 'ssh_key',
'optionname' => 'Initial SSH Key',
'optiontype' => 1,
'options' => array_map(function ($sshKey) {
if ($sshKey['enabled'] === false) { if ($sshKey['enabled'] === false) {
return null; return null;
} }
@@ -67,22 +47,77 @@ if (!function_exists('add_hook_os_templates')) {
'id' => $sshKey['id'], 'id' => $sshKey['id'],
'name' => $sshKey['name'] 'name' => $sshKey['name']
]; ];
}, $sshKeys['data'] ?? []), }, $sshKeys['data'] ?? []);
'selectedvalue' => ''
];
$configurableoptions = $vars['configurableoptions']; $osID = array_values(array_filter(array_map(function ($option) {
if ($option['textid'] === 'initialoperatingsystem') {
array_push( return $option['id'];
$configurableoptions,
$osTemplates,
$sshKeysOptions
);
return [
'configurableoptions' => $configurableoptions,
];
} }
}, $vars['customfields'])));
add_hook('ClientAreaPageCart', 1, 'add_hook_os_templates'); $sshID = array_values(array_filter(array_map(function ($option) {
if ($option['textid'] === 'initialsshkey') {
return $option['id'];
} }
}, $vars['customfields'])));
// Construct the JavaScript code
return "
<script>
document.addEventListener('DOMContentLoaded', function() {
var osTemplates = " . json_encode($dropdownOptions) . ";
var sshKeys = " . json_encode($sshKeysOptions) . ";
var osInputField = document.querySelector('[name=\"customfield[" . ($osID[0] ?? null) . "]\"]');
var sshInputField = document.querySelector('[name=\"customfield[" . ($sshID[0] ?? null) . "]\"]');
// Create dropdown options menu, then add it to the DOM then on change, update the regular input.
var osSelect = document.createElement('select');
osSelect.className = 'form-control';
osTemplates.forEach(function(template) {
var option = document.createElement('option');
option.value = template.id;
option.text = template.name;
osSelect.appendChild(option);
});
// Set the default value of the input field to the first option in the dropdown.
osInputField.value = osSelect.options[0].value;
osSelect.addEventListener('change', function() {
osInputField.value = this.value;
console.log(this.value);
});
osInputField.parentNode.insertBefore(osSelect, osInputField.nextSibling);
osInputField.style.display = 'none';
if (sshKeys.length > 0) {
// Create dropdown options menu, then add it to the DOM then on change, update the regular input.
var sshSelect = document.createElement('select');
sshSelect.className = 'form-control';
sshKeys.forEach(function(sshkey) {
var option = document.createElement('option');
option.value = sshkey.id;
option.text = sshkey.name;
sshSelect.appendChild(option);
});
// Set the default value of the input field to the first option in the dropdown.
sshInputField.value = sshSelect.options[0].value;
sshSelect.addEventListener('change', function() {
sshInputField.value = this.value;
});
sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
sshInputField.style.display = 'none';
} else {
sshInputField.style.display = 'none';
}
});
</script>
";
});

View File

@@ -43,13 +43,33 @@ class ConfigureService extends Module
return null; return null;
} }
/**
* @param int $productId
* @return int|null
*/
public function fetchPackageByDbId(int $productId): ?int
{
$product = DB::table('tblproducts')->where('id', $productId)->first();
if (is_null($product)) {
return null;
}
return (int)$product->configoption2;
}
/** /**
* @param int $serverPackageId * @param int $serverPackageId
* @return array|null * @return array|null
* @throws JsonException * @throws JsonException
*/ */
public function fetchTemplates(int $serverPackageId): ?array public function fetchTemplates(?int $serverPackageId): ?array
{ {
if (is_null($serverPackageId)) {
return null;
}
$request = $this->initCurl($this->cp['token']); $request = $this->initCurl($this->cp['token']);
$response = $request->get( $response = $request->get(
@@ -96,4 +116,38 @@ class ConfigureService extends Module
return isset($response['msg']) && $response['msg'] === "ext_relation_id not found" ? null : $response['data']; return isset($response['msg']) && $response['msg'] === "ext_relation_id not found" ? null : $response['data'];
} }
/**
* @param int $id
* @param array $vars
* @return bool
*/
public function initServerBuild(int $id, array $vars): bool
{
$request = $this->initCurl($this->cp['token']);
// Generate a random 8 character hostname
$hostname = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8);
$inputData = [
"operatingSystemId" => $vars['customfields']['Initial Operating System'],
"name" => $hostname,
"sshKeys" => [
$vars['customfields']['Initial SSH Key']
],
'email' => true
];
if (empty($vars['customfields']['Initial SSH Key'])) {
unset($inputData['sshKeys']);
}
$request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData));
$request->post(
sprintf("%s/servers/%d/build", $this->cp['url'], $id)
);
return true;
}
} }

View File

@@ -163,6 +163,10 @@ class ModuleFunctions extends Module
Database::systemOnServerCreate($params['serviceid'], $data); Database::systemOnServerCreate($params['serviceid'], $data);
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data); $this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
// If the server is created successfully, we can initialize the server build.
$cs = new ConfigureService();
$cs->initServerBuild($data->data->id, $params);
/** /**
* *
* Server was created successfully. * Server was created successfully.
@@ -181,6 +185,46 @@ class ModuleFunctions extends Module
} }
} }
// This function was implemented by Zander Scott / awildboop of Blinkoh, LLC
// Please read this function thoroughly before use to ensure security & integrity
/**
* Allows changing of the package of a server
*
* @author https://github.com/BlinkohHost/virtfusion-whmcs-module
* @param $params
* @return string
*/
public function changePackage($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->put($cp['url'] . '/servers/' . $service->server_id . '/package/' . $params['configoption2']);
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 204:
return 'success';
case 404:
return '404 was returned from the web service without the msg property. The service may be currently unavailable.';
case 423:
if (property_exists($data, 'msg')) {
return $data->msg;
}
default:
return 'Update package request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
}
}
return 'Service not found.';
}
/** /**
* *
* TERMINATE SERVER * TERMINATE SERVER