Compare commits

..

No commits in common. "develop" and "v4.0.0-rc.6" have entirely different histories.

100 changed files with 1222 additions and 1867 deletions

View file

@ -14,6 +14,7 @@ DB_PORT=3306
DB_DATABASE=convoy
DB_USERNAME=convoy_user
DB_PASSWORD=YzLa2BCBwDGWVkpG
DB_ROOT_PASSWORD=YzLa2BCBwDGWVkpG
CACHE_DRIVER=redis
FILESYSTEM_DISK=local

View file

@ -14,6 +14,7 @@ DB_PORT=3306
DB_DATABASE=convoy
DB_USERNAME=convoy_user
DB_PASSWORD=
DB_ROOT_PASSWORD=
CACHE_DRIVER=redis
FILESYSTEM_DISK=local
@ -22,7 +23,7 @@ SESSION_DRIVER=redis
SESSION_LIFETIME=525600
REDIS_HOST=redis
REDIS_PASSWORD=
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp

View file

@ -76,8 +76,14 @@ body:
attributes:
label: Error Logs
description: |
Check out [this page on our documentation](https://convoypanel.com/docs/project/support.html#collecting-panel-logs) for the log collector utility. You will need to run this on the server
Run the following command to collect logs of your Convoy installation. You will need to run this on the server
hosting your instance of Convoy.
```
wget https://github.com/ConvoyPanel/log-collector/releases/latest/download/log_collector
chmod +x ./log_collector
./log_collector
```
placeholder: "https://paste.frocdn.com/"
- type: checkboxes

View file

@ -2,79 +2,85 @@ name: Release
on:
push:
tags:
- 'v*.*.*'
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 20
node-version: '18.x'
- name: Update Embedded Version String
- name: Create release branch and bump version
env:
REF: ${{ github.ref }}
run: |
BRANCH=release/${REF:10}
git config --local user.email "ci@convoypanel.com"
git config --local user.name "Convoy CI"
git checkout -b $BRANCH
git push -u origin $BRANCH
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
git add config/app.php
git commit -m "bump version for release"
git pull
git push
- name: Build Assets
- name: Build assets
run: |
npm install
npm run build
- name: Create Release Archive
- name: Create release archive
run: |
# Array of files and directories to remove
files_to_remove=(
"node_modules/"
"tests/"
"CODE_OF_CONDUCT.md"
"CONTRIBUTOR_LICENSE_AGREEMENT"
"crowdin.yml"
"docker-compose.ci.yml"
"phpstan.neon"
"phpunit.xml"
"stats.html"
)
# Loop over the files to remove and delete them
rm -rf "${files_to_remove[@]}"
# Array of specific dot files to include
files_to_include=(
".editorconfig"
".env.example"
".gitattributes"
".gitignore"
".prettierignore"
".prettierrc.json"
)
rm -rf node_modules/ tests/ codecov.yml CONTRIBUTOR_LICENSE_AGREEMENT CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml phpstan.neon .env.ci docker-compose.ci.yml .styleci.yml crowdin.yml stats.html
tar -czf panel.tar.gz * .env.example .gitignore .prettierrc.json
# Archive files, using * directly outside the array for proper expansion
tar --exclude=panel.tar.gz -czf panel.tar.gz * "${files_to_include[@]}"
- name: Extract Changelog
- name: Extract changelog
id: extract_changelog
env:
REF: ${{ github.ref }}
run: |
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
echo "version_name=${REF:10}" >> $GITHUB_OUTPUT
echo ::set-output name=version_name::`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`
- name: Create Checksum and Add to Changelog
- name: Create checksum and add to changelog
run: |
SUM=`sha256sum panel.tar.gz`
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo $SUM > checksum.txt
- name: Create Release
uses: softprops/action-gh-release@v1
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: ${{ steps.extract_changelog.outputs.version_name }}
tag_name: ${{ github.ref }}
release_name: ${{ steps.extract_changelog.outputs.version_name }}
body_path: ./RELEASE_CHANGELOG
draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') || contains(github.ref, 'rc') }}
files: |
panel.tar.gz
checksum.txt
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload binary
id: upload-release-archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: panel.tar.gz
asset_name: panel.tar.gz
asset_content_type: application/gzip
- name: Upload checksum
id: upload-release-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksum.txt
asset_name: checksum.txt
asset_content_type: text/plain

View file

@ -10,7 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v2
- name: Create environment file
run: cp .env.ci .env

3
.gitignore vendored
View file

@ -18,5 +18,4 @@ yarn-error.log
_ide_*.php
stats.html
.fleet
lang/php_*.json
.phpunit.cache
lang/php_*.json

View file

@ -1,8 +1,4 @@
# Acknowledgements
## tslib
```
Package: tslib
/******************************************************************************
Copyright (c) Microsoft Corporation.
@ -17,13 +13,10 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
```
## fakerphp/faker
Package: fakerphp/faker
Translations are under the CC-BY-SA-3.0 license.
https://github.com/FakerPHP/Faker
## caniuse-lite
Package: caniuse-lite
https://github.com/browserslist/caniuse-lite

View file

@ -4,103 +4,12 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
## v4.2.4
## v4.0.0-rc.6
### Changes
- Fixed a bug where initiating a backup with no compression fails. #87
## v4.2.3
### Changes
- Updated code for applying rate limits to NIC. Convoy will no longer override settings other than ratelimit, NIC
model (e.g., e1000, vmxnet3, virtio, etc.), and firewall status.
#### From v4.2.2-rc.2
- Fix US keyboard characters validation #80
- Fixed a visual bug on the bandwidth usage card where the text wasn't centered.
#### From v4.2.2-rc.1
- Fix special character support in environment file.
- Added checks in server creation to use unique VMID. #78
- Add error messages instead of generic server error messages. #49
- Scope route model binding by default to prevent unauthorized access of related resources.
- Removed a lot of dead code.
- Added more tests (getting closer to full release! 😁😩).
#### From v4.2.1-rc.1
- Potential fix for disk resize timeout?
## v4.2.2-rc.2
### Changes
- Fix US keyboard characters validation #80
- Fixed a visual bug on the bandwidth usage card where the text wasn't centered.
## v4.2.2-rc.1
### Changes
- Fix special character support in environment file.
- Added checks in server creation to use unique VMID. #78
- Add error messages instead of generic server error messages. #49
- Scope route model binding by default to prevent unauthorized access of related resources.
- Removed a lot of dead code.
- Added more tests (getting closer to full release! 😁😩).
## v4.2.1-rc.1
### Changes
- Potential fix for disk resize timeout?
## v4.2.0-beta
### Changes
- Added server UUID copy to clipboard button in the admin area server table.
- Added ability to toggle TLS verification per node basis.
## v4.1.0-beta
### Changes
- Removed the `DB_ROOT_PASSWORD` variable from the environment file. It is now automatically generated, but we still
aren't planning on using it.
- Added health checks to the Docker compose configuration to prevent the containers from exploding when its dependencies
don't start up in time.
## v4.0.0-beta
If you use Convoy for a production or commercial environment/purpose, please subscribe to a
license [here](https://console.convoypanel.com). It supports my work, and you are also violating the license agreement
if you don't. Your deployment of Convoy may be disabled without warning if you don't adhere to the terms of the license
agreement.
### Changes
- **BREAKING**: Overhauled the IP address management system to add IP pools that can be shared among nodes. #51
- **BREAKING**: Fixed Coterm where it doesn't support multiple nodes #50
- Fixed inability to use special characters for Redis password.
- Fixed error when trying to parse a vm's disk that has no `size` attribute #48
- Fixed typo in the input labels on the node creation modal #42
- Fixed the mobile navigation menu where it won't automatically close when you click on a link #41
- Added ability to copy node and template IDs #40
- Fixed incorrect conversion from mebibytes to bytes of a server's bandwidth limit during manual server creation through
the admin area UI #70
- Fixed missing SSO token creation endpoint
- Fixed bulk importing of IPv6 addresses #66
- Fixed inability to create servers with IP addresses.
- Fixed minor UI bug where addresses in IPAM won't optimistically update after making a change.
- Made the IPAM address table sort by descending.
- Removed API request throttling
- Increased Coterm session token lifetime from 30 seconds to a minute.
- Fixed cloning of VM's to the wrong storage location #64
## v4.0.0-rc.5

View file

@ -1,124 +1,40 @@
# Convoy Software End User License Agreement (EULA)
Business Source License 1.1
License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab.
**Effective Date:** March 3th, 2024
Additional Use Grant
**Last Updated:** March 13th, 2024
The licensee may use this code in production if they have an active subscription from Performave. However, the licensee must remain within the limits defined by the subscription.
**License Grantor:** Performave
Change License
## 1. Acceptance of Terms
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
By installing, copying, downloading, accessing, or otherwise using the Convoy Panel software ("Software"), you agree to
be bound by the terms of this End User License Agreement ("EULA"). If you do not agree to the terms of this EULA, do not
install or use the Software.
Terms
## 2. License Grant
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
### 2.1 Personal Use License
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.
Performave grants you a non-exclusive, non-transferable, free license to download, install, and use the Software for
personal, non-commercial purposes, provided that you comply with all the terms and conditions of this EULA.
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
### 2.2 Enterprise License
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor.
If you wish to use the Software for commercial purposes, including but not limited to production environments, business
operations, or any activity intended for profit, you must subscribe to an Enterprise License. The Enterprise License is
subscription-based, and the fees are based on the number of nodes on which the Software is used. The specific terms,
including the fee structure and the number of nodes allowed, will be determined at the time of the subscription. Each
license permits the use of the Software on the number of nodes paid for and is non-transferable.
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.
### 2.3 Non-Profit Organization License
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.
Non-profit organizations, upon providing proof of 501(c)(3) registration or its equivalent, are granted a non-exclusive,
non-transferable license to use the Software for free. The Software may be used for the organization's operational
purposes, subject to the terms and conditions of this EULA.
This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License).
### 2.4 Partnership Licenses
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
Licenses obtained through partnerships or negotiations with Performave are valid as per the agreements made during such
negotiations. These licenses are subject to the specific terms agreed upon and must also adhere to the general terms and
conditions of this EULA.
MariaDB hereby grants you permission to use this License's text to license your works, and to refer to it using the trademark "Business Source License", as long as you comply with the Covenants of Licensor below.
### 2.5 Insider License
Covenants of Licensor
Performave may grant an Insider License to individuals recruited specifically for testing new versions or features of
the Software. This license includes a waiver of fees associated with the use of the Software during the testing period.
Testers are expected to be available to test the Software as required and provide feedback to Performave. Performave
reserves the right to revoke this license at any time at its discretion, including for lack of participation or if the
tester's needs no longer align with the testing program's objectives.
In consideration of the right to use this License's text and the "Business Source License" name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor:
## 3. Legal Use Requirement
You agree to use the Software only for lawful purposes and in compliance with all applicable laws and regulations. Any
use of the Software for illegal or criminal activities is strictly prohibited. Performave reserves the right to
terminate your license if you engage in any illegal conduct with the Software. In the event of such termination,
Performave isn't obligated to refund any transactions.
## 4. Restrictions
The following restrictions apply to your use of the Software, but these are not all-inclusive. Additional restrictions
may also apply as outlined elsewhere in this EULA or as otherwise determined by Performave:
- You may not modify the Software in a manner that interferes with its licensing mechanism or changes its copyright
information without making substantial other modifications.
- You are permitted to modify the Software for your personal or enterprise use to tailor it to your needs, provided such
modifications do not violate the restrictions stated in this EULA.
- You may not distribute or sublicense modified versions of the Software that violate the terms of this EULA.
- You may not use the Software in any manner that could damage, disable, overburden, or impair any Performave server, or
the network(s) connected to any Performave server, or interfere with any other party's use and enjoyment of the
Software.
### 4.1 Additional Licensing Terms for Modifications and Contributions
Any modifications, enhancements, derivative works of the Software, or any code from the Software that is incorporated into other works by you or any third party are considered part of the Software and subject to the terms and conditions of this EULA. Such modifications, derivative works, or incorporated code must be offered under the same terms and conditions as those set forth in this EULA, including any provisions regarding distribution and sublicensing. You may not alter the terms of this EULA or sublicense any modifications, derivative works, or incorporated code under terms that differ from those specified in this EULA.
## 5. License Enforcement and Digital Rights Management
Performave employs various measures, including Digital Rights Management (DRM), to enforce the terms of this EULA and prevent unauthorized use of the Software. These measures may include, but are not limited to, remotely disabling access to the Software or specific features of the Software for users who are found to be in violation of this EULA. By using the Software, you acknowledge and agree that Performave may, at its sole discretion, implement such measures.
You further agree that Performave shall not be responsible or liable for any loss, damage, or inconvenience you may suffer as a result of such actions taken to enforce this EULA. Your rights under this EULA may be subject to termination and denial of access to the Software without notice if any form of tampering with or circumvention of the DRM or other license enforcement mechanisms is detected.
This section is designed to inform users of the license enforcement practices and to legally protect Performave from liability for actions taken in good faith to protect its intellectual property rights.
## 6. Intellectual Property Rights
The Software is protected by intellectual property laws and treaties. Performave or its suppliers own all title,
copyright, and interest in and to the Software, including any intellectual property rights therein. This EULA grants you
no rights to use such content. All rights not expressly granted are reserved by Performave.
## 7. Termination
This EULA is effective until terminated. Your rights under this EULA will terminate automatically without notice from
Performave if you fail to comply with any term(s) of this EULA. Upon termination, you shall cease all use of the
Software and destroy all copies, full or partial, of the Software.
## 8. Disclaimer of Warranty
The Software is provided "AS IS," with all faults, without warranty of any kind, and Performave hereby disclaims all
warranties and conditions with respect to the Software, either express, implied, or statutory, including, but not
limited to, the implied warranties and/or conditions of merchantability, of satisfactory quality, of fitness for a
particular purpose, of accuracy, of quiet enjoyment, and non-infringement of third-party rights.
## 9. Limitation of Liability
In no event shall Performave be liable for any indirect, incidental, special, consequential, or punitive damages
whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business
information, or any other pecuniary loss) arising out of the use of or inability to use the Software, even if Performave
has been advised of the possibility of such damages.
## 10. Governing Law
This EULA shall be governed by the laws of the jurisdiction in which Performave is located, without reference to
conflict of laws principles.
## 11. Entire Agreement
This EULA constitutes the entire agreement between you and Performave relating to the Software and supersedes all prior
or contemporaneous oral or written communications, proposals, and representations with respect to the Software or any
other subject matter covered by this EULA.
## 12. Amendment
Performave reserves the right to amend this EULA at any time, at its sole discretion, by posting an updated version to
its website or through the Software. Your continued use of the Software following the posting of an updated EULA will
mean that you accept those changes.
1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where "compatible" means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation.
2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text "None".
3. To specify a Change Date.
4. Not to modify this License in any other way.

View file

@ -1,5 +1,5 @@
![Version 4 release announcement banner](https://github.com/ConvoyPanel/panel/assets/37554696/4629321b-7214-4eb1-8cc5-85c89229b5bf)
![Banner Logo](https://imgur.com/oAGZ7fb.png)
![version 3 release banner](https://imgur.com/OEncExI.png)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/convoypanel/panel/tests.yml?branch=develop)
![Discord](https://img.shields.io/discord/746612878261616700?label=Discord&logo=Discord&logoColor=white)
@ -21,10 +21,17 @@ Stop paying hundreds of dollars for unreliable and slow software. Subscribe to a
## Acknowledgements
Please [visit this page](https://convoypanel.com/docs/project/about.html#acknowledgements) on our website to view acknowledgements.
Convoy wouldn't have been possible without these organizations and people
- Advin Servers - provided several development servers (at least $200 of hardware) to help me develop this SaaS
- Kjartann - donated $250 to support Convoy development and encouraged me to keep on working on this panel!
- Pterodactyl Panel - provided the architecture and a lot of backend/boilerplate code for Convoy.
- FastKVM.EU - for reporting bugs and testing the panel
- Just Code Cats - created [Convoy's Blesta module](https://marketplace.blesta.com/#/extensions/179-Convoy%20Module), https://code-cats.com/
- [HeavyNode](https://heavynode.com/) - for helping create [Coterm](https://github.com/ConvoyPanel/coterm)
## License
Convoy is licensed under our own proprietary license.
Convoy is licensed under the Business Source License. Production use of Convoy without an active license from Performave is strictly disallowed.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FConvoyPanel%2Fpanel.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FConvoyPanel%2Fpanel?ref=badge_large)

View file

@ -2,8 +2,8 @@
namespace Convoy\Exceptions\Service\Deployment;
use Convoy\Exceptions\DisplayException;
use Convoy\Exceptions\ConvoyException;
class InvalidTemplateException extends DisplayException
class InvalidTemplateException extends ConvoyException
{
}

View file

@ -2,9 +2,9 @@
namespace Convoy\Exceptions\Service\Network;
use Convoy\Exceptions\DisplayException;
use Convoy\Exceptions\ConvoyException;
class AddressInUseException extends DisplayException
class AddressInUseException extends ConvoyException
{
/**
* TooManyBackupsException constructor.
@ -12,7 +12,7 @@ class AddressInUseException extends DisplayException
public function __construct(int $addressId)
{
parent::__construct(
sprintf('Address %d is currently in use by another server.', $addressId),
sprintf('Address %d is currently in use by another server.', $addressId)
);
}
}

View file

@ -2,9 +2,9 @@
namespace Convoy\Exceptions\Service\Node\IsoLibrary;
use Convoy\Exceptions\DisplayException;
use Convoy\Exceptions\ConvoyException;
class InvalidIsoLinkException extends DisplayException
class InvalidIsoLinkException extends ConvoyException
{
public function __construct()
{

View file

@ -2,9 +2,9 @@
namespace Convoy\Exceptions\Service\Server\Allocation;
use Convoy\Exceptions\DisplayException;
use Convoy\Exceptions\ConvoyException;
class IsoAlreadyUnmountedException extends DisplayException
class IsoAlreadyUnmountedException extends ConvoyException
{
public function __construct()
{

View file

@ -2,14 +2,12 @@
namespace Convoy\Exceptions\Service\Server\Allocation;
use Convoy\Exceptions\DisplayException;
use Convoy\Exceptions\ConvoyException;
class NoAvailableDiskInterfaceException extends DisplayException
class NoAvailableDiskInterfaceException extends ConvoyException
{
public function __construct()
{
parent::__construct(
'There is no available disk interface on the virtual machine to satisfy the request.',
);
parent::__construct('There is no available disk interface on the virtual machine to satisfy the request.');
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace Convoy\Exceptions\Service\Server\Allocation;
use Convoy\Exceptions\DisplayException;
class NoUniqueUuidComboException extends DisplayException
{
public function __construct()
{
parent::__construct('There is no available VMID to use.');
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace Convoy\Exceptions\Service\Server\Allocation;
use Convoy\Exceptions\DisplayException;
class NoUniqueVmidException extends DisplayException
{
public function __construct()
{
parent::__construct('There is no available VMID to use.');
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Convoy\Exceptions\Service\Snapshot;
use Convoy\Exceptions\ConvoyException;
class TooManySnapshotsException extends ConvoyException
{
/**
* TooManyBackupsException constructor.
*/
public function __construct(int $backupLimit)
{
parent::__construct(
sprintf('Cannot create a new snapshot, this server has reached its limit of %d snapshots.', $backupLimit)
);
}
}

View file

@ -18,7 +18,6 @@ class LocationController extends ApiController
{
$locations = QueryBuilder::for(Location::query())
->withCount(['nodes', 'servers'])
->defaultSort('-id')
// @phpstan-ignore-next-line
->allowedFilters(
['short_code', AllowedFilter::custom('*', new FiltersLocationWildcard())],

View file

@ -2,16 +2,28 @@
namespace Convoy\Http\Controllers\Admin\Nodes;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
use Convoy\Http\Controllers\Controller;
use Convoy\Http\Requests\Admin\AddressPools\Addresses\UpdateAddressRequest;
use Convoy\Models\Address;
use Convoy\Models\Filters\FiltersAddressWildcard;
use Convoy\Models\Node;
use Convoy\Services\Servers\NetworkService;
use Convoy\Transformers\Admin\AddressTransformer;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
class AddressController extends Controller
{
public function __construct(
private NetworkService $networkService, private ConnectionInterface $connection,
)
{
}
public function index(Request $request, Node $node)
{
$addresses = QueryBuilder::for($node->addresses())
@ -32,4 +44,63 @@ class AddressController extends Controller
return fractal($addresses, new AddressTransformer())->parseIncludes($request->include)
->respond();
}
public function update(UpdateAddressRequest $request, Node $node, Address $address)
{
$address = $this->connection->transaction(function () use ($request, $address) {
$oldLinkedServer = $address->server;
$address->update($request->validated());
try {
// Detach old server
if ($oldLinkedServer) {
$this->networkService->syncSettings($oldLinkedServer);
}
// Attach new server
if ($address->server) {
$this->networkService->syncSettings($address->server);
}
} catch (ProxmoxConnectionException) {
if ($oldLinkedServer && !$address->server) {
throw new ServiceUnavailableHttpException(
message: "Server {$oldLinkedServer->uuid} failed to sync network settings.",
);
} elseif (!$oldLinkedServer && $address->server) {
throw new ServiceUnavailableHttpException(
message: "Server {$address->server->uuid} failed to sync network settings.",
);
} elseif ($oldLinkedServer && $address->server) {
throw new ServiceUnavailableHttpException(
message: "Servers {$oldLinkedServer->uuid} and {$address->server->uuid} failed to sync network settings.",
);
}
}
return $address;
});
return fractal($address, new AddressTransformer())->parseIncludes($request->include)
->respond();
}
public function destroy(Node $node, Address $address)
{
$this->connection->transaction(function () use ($address) {
$address->delete();
if ($address->server) {
try {
$this->networkService->syncSettings($address->server);
} catch (ProxmoxConnectionException) {
throw new ServiceUnavailableHttpException(
message: "Server {$address->server->uuid} failed to sync network settings.",
);
}
}
});
return $this->returnNoContent();
}
}

View file

@ -3,11 +3,14 @@
namespace Convoy\Http\Controllers\Admin\Nodes;
use Convoy\Http\Controllers\ApiController;
use Convoy\Http\Requests\Admin\Nodes\Settings\UpdateCotermRequest;
use Convoy\Http\Requests\Admin\Nodes\StoreNodeRequest;
use Convoy\Http\Requests\Admin\Nodes\UpdateNodeRequest;
use Convoy\Models\Filters\FiltersNodeWildcard;
use Convoy\Models\Node;
use Convoy\Services\Coterm\CotermTokenCreationService;
use Convoy\Transformers\Admin\NodeTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
@ -15,6 +18,10 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class NodeController extends ApiController
{
public function __construct(private CotermTokenCreationService $cotermTokenCreator)
{
}
public function index(Request $request)
{
$nodes = QueryBuilder::for(Node::query())
@ -59,6 +66,50 @@ class NodeController extends ApiController
return fractal($node, new NodeTransformer())->respond();
}
public function updateCoterm(UpdateCotermRequest $request, Node $node)
{
$payload = $request->validated();
if ($payload['coterm_enabled'] && (empty($node->coterm_token_id) || empty($node->coterm_token))) {
$creds = $this->cotermTokenCreator->handle();
$payload['coterm_token_id'] = $creds['token_id'];
$payload['coterm_token'] = $creds['token'];
}
$node->update($payload);
return new JsonResponse([
'data' => [
'is_enabled' => $node->coterm_enabled,
'is_tls_enabled' => $node->coterm_tls_enabled,
'fqdn' => $node->coterm_fqdn,
'port' => $node->coterm_port,
'token' => isset($creds) ? "{$node->coterm_token_id}|{$node->coterm_token}" : null,
],
]);
}
public function resetCotermToken(Node $node)
{
if (!$node->coterm_enabled) {
throw new AccessDeniedHttpException('Coterm isn\'t enabled on this node.');
}
$creds = $this->cotermTokenCreator->handle();
$node->update([
'coterm_token_id' => $creds['token_id'],
'coterm_token' => $creds['token'],
]);
return new JsonResponse([
'data' => [
'token' => "{$node->coterm_token_id}|{$node->coterm_token}",
],
]);
}
public function destroy(Node $node)
{
$node->loadCount('servers');

View file

@ -17,7 +17,6 @@ class TokenController extends ApiController
{
$tokens = QueryBuilder::for(PersonalAccessToken::query())
->with('tokenable')
->defaultSort('-id')
->where('personal_access_tokens.type', ApiKeyType::APPLICATION->value)
->paginate(min($request->query('per_page', 50), 100))->appends(
$request->query(),

View file

@ -0,0 +1,55 @@
<?php
namespace Convoy\Http\Controllers\Client\Servers;
use Convoy\Http\Controllers\ApiController;
use Convoy\Http\Requests\Client\Servers\Snapshots\SnapshotRequest;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\Server\ProxmoxSnapshotRepository;
use Convoy\Services\Servers\Snapshots\SnapshotCreationService;
use Convoy\Services\Servers\Snapshots\SnapshotDeletionService;
class SnapshotController extends ApiController
{
public function __construct(
protected ProxmoxSnapshotRepository $repository,
protected SnapshotCreationService $creationService,
protected SnapshotDeletionService $deletionService,
)
{
}
public function index(Server $server)
{
$snapshots = $this->repository->setServer($server)->getSnapshots();
return inertia('servers/snapshots/Index', [
'server' => $server,
'snapshots' => $snapshots,
'can_create' => isset($server->snapshot_limit) ? (count(
$snapshots,
) - 1) < $server->snapshot_limit : true,
]);
}
public function store(Server $server, SnapshotRequest $request)
{
$this->creationService->setServer($server)->handle($request->name);
return back();
}
public function destroy(Server $server, SnapshotRequest $request)
{
$this->deletionService->setServer($server)->handle($request->name);
return back();
}
public function rollback(Server $server, SnapshotRequest $request)
{
$this->repository->setServer($server)->restore($request->name);
return back();
}
}

View file

@ -2,8 +2,8 @@
namespace Convoy\Http\Requests\Admin\Nodes\Isos;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\ISO;
use Convoy\Http\Requests\BaseApiRequest;
class UpdateIsoRequest extends BaseApiRequest
{
@ -16,5 +16,4 @@ class UpdateIsoRequest extends BaseApiRequest
'hidden' => $rules['hidden'],
];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Convoy\Http\Requests\Admin\Nodes\Settings;
use Illuminate\Validation\Rule;
use Convoy\Http\Requests\BaseApiRequest;
class UpdateCotermRequest extends BaseApiRequest
{
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'is_enabled' => 'required|boolean',
'is_tls_enabled' => 'required|boolean',
'fqdn' => [
'required_if:is_enabled,1',
Rule::when(!$this->boolean('is_enabled'), ['nullable']),
'string',
'max:191',
],
'port' => 'required|integer',
];
}
public function validated($key = null, $default = null)
{
return [
'coterm_enabled' => $this->input('is_enabled'),
'coterm_tls_enabled' => $this->input('is_tls_enabled'),
'coterm_fqdn' => $this->input('fqdn'),
'coterm_port' => $this->input('port'),
];
}
}

View file

@ -2,12 +2,25 @@
namespace Convoy\Http\Requests\Admin\Nodes;
use Convoy\Models\Node;
use Convoy\Rules\Fqdn;
use Convoy\Models\Node;
use Illuminate\Foundation\Http\FormRequest;
class StoreNodeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$rules = Node::getRules();

View file

@ -2,10 +2,10 @@
namespace Convoy\Http\Requests\Admin\Nodes;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Node;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
use Convoy\Http\Requests\BaseApiRequest;
class UpdateNodeRequest extends BaseApiRequest
{
@ -20,28 +20,20 @@ class UpdateNodeRequest extends BaseApiRequest
];
}
public function withValidator(Validator $validator): void
public function withValidator(Validator $validator)
{
$validator->after(function (Validator $validator) {
$node = $this->parameter('node', Node::class);
// multiply memory by memory_overallocate (which indicates how much you can go over) percentage
$memory = intval($this->input('memory')) * ((intval(
$this->input('memory_overallocate'),
) / 100) + 1);
$disk = intval($this->input('disk')) * ((intval(
$this->input('disk_overallocate'),
) / 100) + 1);
$memory = intval($this->input('memory')) * ((intval($this->input('memory_overallocate')) / 100) + 1);
$disk = intval($this->input('disk')) * ((intval($this->input('disk_overallocate')) / 100) + 1);
if ($memory < $node->memory_allocated) {
$validator->errors()->add(
'memory', 'The memory value is lower than what\'s allocated.',
);
$validator->errors()->add('memory', 'The memory value is lower than what\'s allocated.');
}
if ($disk < $node->disk_allocated) {
$validator->errors()->add(
'disk', 'The disk value is lower than what\'s allocated.',
);
$validator->errors()->add('disk', 'The disk value is lower than what\'s allocated.');
}
});
}

View file

@ -2,19 +2,24 @@
namespace Convoy\Http\Requests\Admin\Servers;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Address;
use Convoy\Models\Node;
use Convoy\Models\Server;
use Convoy\Models\Address;
use Convoy\Rules\Password;
use Convoy\Rules\USKeyboardCharacters;
use Illuminate\Validation\Validator;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Rules\EnglishKeyboardCharacters;
/**
* @property mixed $type
*/
class StoreServerRequest extends BaseApiRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$rules = Server::getRules();
@ -23,7 +28,6 @@ class StoreServerRequest extends BaseApiRequest
'name' => $rules['name'],
'user_id' => $rules['user_id'],
'node_id' => $rules['node_id'],
// TODO: validation should be added for manually setting the vmid
'vmid' => 'present|nullable|numeric|min:100|max:999999999',
'hostname' => $rules['hostname'],
'limits' => 'required|array',
@ -36,14 +40,15 @@ class StoreServerRequest extends BaseApiRequest
'limits.address_ids' => 'sometimes|nullable|array',
'limits.address_ids.*' => 'integer|exists:ip_addresses,id',
'account_password' => ['required_if:should_create_server,1', 'string', 'min:8', 'max:191', new Password(
), new USKeyboardCharacters()],
), new EnglishKeyboardCharacters()],
'should_create_server' => 'present|boolean',
'template_uuid' => 'required_if:create_server,1|string|exists:templates,uuid',
'start_on_completion' => 'present|boolean',
];
}
public function withValidator(Validator $validator): void
// check that all of the address_ids server_id is null
public function withValidator(Validator $validator)
{
$validator->after(function ($validator) {
$addressIds = $this->input('limits.address_ids');
@ -72,15 +77,11 @@ class StoreServerRequest extends BaseApiRequest
$disk = intval($this->input('limits.disk'));
if ($memory > $nodeMemoryLimit || $memory < 0) {
$validator->errors()->add(
'limits.memory', 'The memory value exceeds the node\'s limit.',
);
$validator->errors()->add('limits.memory', 'The memory value exceeds the node\'s limit.');
}
if ($disk > $nodeDiskLimit || $disk < 0) {
$validator->errors()->add(
'limits.disk', 'The disk value exceeds the node\'s limit.',
);
$validator->errors()->add('limits.disk', 'The disk value exceeds the node\'s limit.');
}
});
}

View file

@ -3,6 +3,7 @@
namespace Convoy\Http\Requests\Client\Servers\Backups;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Backup;
use Convoy\Models\Server;
class DeleteBackupRequest extends BaseApiRequest
@ -10,7 +11,8 @@ class DeleteBackupRequest extends BaseApiRequest
public function authorize(): bool
{
$server = $this->parameter('server', Server::class);
$backup = $this->parameter('backup', Backup::class);
return $this->user()->can('delete', $server);
return $this->user()->can('delete', [$backup, $server]);
}
}

View file

@ -3,6 +3,7 @@
namespace Convoy\Http\Requests\Client\Servers\Backups;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Backup;
use Convoy\Models\Server;
class RestoreBackupRequest extends BaseApiRequest
@ -10,7 +11,8 @@ class RestoreBackupRequest extends BaseApiRequest
public function authorize(): bool
{
$server = $this->parameter('server', Server::class);
$backup = $this->parameter('backup', Backup::class);
return $this->user()->can('restore', $server);
return $this->user()->can('restore', [$backup, $server]);
}
}

View file

@ -2,18 +2,19 @@
namespace Convoy\Http\Requests\Client\Servers\Backups;
use Convoy\Enums\Server\BackupCompressionType;
use Convoy\Enums\Server\BackupMode;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Backup;
use Convoy\Enums\Server\BackupMode;
use Convoy\Models\Server;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Foundation\Http\FormRequest;
use Convoy\Enums\Server\BackupCompressionType;
class StoreBackupRequest extends BaseApiRequest
{
public function authorize(): bool
{
return $this->user()->can('create', $this->parameter('server', Server::class));
return $this->user()->can('create', [Backup::class, $this->parameter('server', Server::class)]);
}
public function rules(): array

View file

@ -2,11 +2,11 @@
namespace Convoy\Http\Requests\Client\Servers\Settings;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Server;
use Convoy\Models\Template;
use Convoy\Rules\Password;
use Convoy\Rules\USKeyboardCharacters;
use Convoy\Models\Template;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Rules\EnglishKeyboardCharacters;
class ReinstallServerRequest extends BaseApiRequest
{
@ -28,7 +28,7 @@ class ReinstallServerRequest extends BaseApiRequest
return [
'template_uuid' => 'required|string|exists:templates,uuid',
'account_password' => ['required', 'string', 'min:8', 'max:191', new Password(
), new USKeyboardCharacters()],
), new EnglishKeyboardCharacters()],
'start_on_completion' => 'present|boolean',
];
}

View file

@ -2,15 +2,17 @@
namespace Convoy\Http\Requests\Client\Servers\Settings;
use Convoy\Enums\Server\AuthenticationType;
use Convoy\Http\Requests\BaseApiRequest;
use Convoy\Models\Server;
use Convoy\Rules\Password;
use Convoy\Rules\USKeyboardCharacters;
use Exception;
use Illuminate\Validation\Rules\Enum;
use Convoy\Rules\Password;
use Faker\Provider\Base;
use Illuminate\Validation\Validator;
use Illuminate\Validation\Rules\Enum;
use phpseclib3\Crypt\PublicKeyLoader;
use Convoy\Enums\Server\AuthenticationType;
use Convoy\Rules\EnglishKeyboardCharacters;
use Illuminate\Foundation\Http\FormRequest;
class UpdateAuthSettingsRequest extends BaseApiRequest
{
@ -24,7 +26,7 @@ class UpdateAuthSettingsRequest extends BaseApiRequest
return [
'type' => [new Enum(AuthenticationType::class), 'required'],
'ssh_keys' => ['nullable', 'string', 'exclude_unless:type,ssh_keys'],
'password' => ['string', 'min:8', 'max:191', new Password(), new USKeyboardCharacters(
'password' => ['string', 'min:8', 'max:191', new Password(), new EnglishKeyboardCharacters(
), 'exclude_unless:type,password'],
];
}

View file

@ -0,0 +1,26 @@
<?php
namespace Convoy\Http\Requests\Client\Servers\Snapshots;
use Illuminate\Foundation\Http\FormRequest;
class SnapshotRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required|alpha_dash|max:50',
];
}
}

View file

@ -2,13 +2,10 @@
namespace Convoy\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Address extends Model
{
use HasFactory;
protected $table = 'ip_addresses';
protected $guarded = ['id', 'updated_at', 'created_at'];

View file

@ -2,14 +2,11 @@
namespace Convoy\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class AddressPool extends Model
{
use HasFactory;
protected $guarded = ['id', 'updated_at', 'created_at'];
public static array $validationRules = [

View file

@ -13,15 +13,11 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
*/
class AddressPoolToNode extends Model
{
/**
* The actual name of the table Laravel ORM should query.
*/
protected $table = 'address_pool_to_node';
public $fillable = [
'address_pool_id',
'node_id',
];
public $timestamps = false;
public function addressPool(): BelongsTo
{
return $this->belongsTo(AddressPool::class);

View file

@ -7,13 +7,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Coterm extends Model
{
/**
* The constants for generating Coterm secret keys
*/
public const COTERM_TOKEN_ID_LENGTH = 16;
public const COTERM_TOKEN_LENGTH = 64;
protected $guarded = [
'id',
'created_at',

View file

@ -3,6 +3,7 @@
namespace Convoy\Models;
use Convoy\Casts\MebibytesToAndFromBytes;
use Convoy\Casts\NullableEncrypter;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -13,22 +14,29 @@ class Node extends Model
{
use HasFactory;
/**
* The constants for generating temporary Coterm (a noVNC reverse proxy) session tokens
*/
public const COTERM_TOKEN_ID_LENGTH = 16;
public const COTERM_TOKEN_LENGTH = 64;
/**
* The attributes excluded from the model's JSON form.
*/
protected $hidden = [
'token_id',
'secret',
'token_id', 'secret', 'coterm_token_id', 'coterm_token',
];
/**
* Cast values to correct type.
*/
protected $casts = [
'verify_tls' => 'boolean',
'memory' => MebibytesToAndFromBytes::class,
'disk' => MebibytesToAndFromBytes::class,
'secret' => 'encrypted',
'coterm_enabled' => 'boolean',
'coterm_tls_enabled' => 'boolean',
'coterm_token' => NullableEncrypter::class,
];
/**
@ -40,7 +48,6 @@ class Node extends Model
'location_id' => 'required|integer|exists:locations,id',
'name' => 'required|string|max:191',
'cluster' => 'required|string|max:191',
'verify_tls' => 'sometimes|boolean',
'fqdn' => 'required|string|max:191',
'token_id' => 'required|string|max:191',
'secret' => 'required|string|max:191',

View file

@ -2,18 +2,27 @@
namespace Convoy\Policies;
use Convoy\Models\Backup;
use Convoy\Models\Server;
use Convoy\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class BackupPolicy
{
public function before(User $user, string $ability, Server $server,
): ?bool
public function before(User $user, string $ability, Backup|string $backup, Server $server): ?bool
{
if ($user->root_admin || $user->id === $server->user_id) {
return true;
}
/*
* Stop the user from accessing backups that are not associated with the
* server they are trying to access.
*/
if ($backup !== null && $backup->server_id !== $server->id) {
return false;
}
return null;
}

View file

@ -38,13 +38,11 @@ class RouteServiceProvider extends ServiceProvider
Route::middleware(['auth'])->prefix('/api/client')
->as('client.')
->scopeBindings()
->group(base_path('routes/api-client.php'));
Route::middleware(['auth', AdminAuthenticate::class])
->prefix('/api/admin')
->as('admin.')
->scopeBindings()
->group(base_path('routes/api-admin.php'));
});
@ -52,13 +50,11 @@ class RouteServiceProvider extends ServiceProvider
Route::middleware(['auth:sanctum'])
->prefix('/api/application')
->as('application.')
->scopeBindings()
->group(base_path('routes/api-application.php'));
Route::middleware([CotermAuthenticate::class])
->prefix('/api/coterm')
->as('coterm.')
->scopeBindings()
->group(base_path('routes/api-coterm.php'));
});
});

View file

@ -2,11 +2,11 @@
namespace Convoy\Repositories\Eloquent;
use Convoy\Contracts\Repository\ServerRepositoryInterface;
use Convoy\Exceptions\Repository\RecordNotFoundException;
use Convoy\Models\Server;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Convoy\Exceptions\Repository\RecordNotFoundException;
use Convoy\Contracts\Repository\ServerRepositoryInterface;
class ServerRepository extends EloquentRepository implements ServerRepositoryInterface
{
@ -15,21 +15,12 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
return Server::class;
}
public function isUniqueVmId(int $nodeId, int $vmid): bool
{
return !$this->getBuilder()
->where('vmid', '=', $vmid)
->where('node_id', '=', $nodeId)
->exists();
}
/**
* Check if a given UUID and UUID-Short string are unique to a server.
*/
public function isUniqueUuidCombo(string $uuid, string $short): bool
{
return !$this->getBuilder()->where('uuid', '=', $uuid)->orWhere('uuid_short', '=', $short)
->exists();
return ! $this->getBuilder()->where('uuid', '=', $uuid)->orWhere('uuid_short', '=', $short)->exists();
}
/**
@ -42,10 +33,10 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
try {
/** @var Server $model */
$model = $this->getBuilder()
->where(function (Builder $query) use ($uuid) {
$query->where('uuid_short', $uuid)->orWhere('uuid', $uuid);
})
->firstOrFail($this->getColumns());
->where(function (Builder $query) use ($uuid) {
$query->where('uuid_short', $uuid)->orWhere('uuid', $uuid);
})
->firstOrFail($this->getColumns());
return $model;
} catch (ModelNotFoundException $exception) {

View file

@ -2,26 +2,20 @@
namespace Convoy\Repositories\Proxmox\Node;
use Carbon\CarbonImmutable;
use Convoy\Data\Helpers\ChecksumData;
use Convoy\Data\Node\Storage\FileMetaData;
use Convoy\Data\Node\Storage\IsoData;
use Convoy\Enums\Node\Storage\ContentType;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
use Convoy\Exceptions\Service\Node\IsoLibrary\InvalidIsoLinkException;
use Convoy\Models\Node;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Carbon\CarbonImmutable;
use Illuminate\Support\Arr;
use Spatie\LaravelData\DataCollection;
use Webmozart\Assert\Assert;
use Convoy\Data\Helpers\ChecksumData;
use Convoy\Data\Node\Storage\IsoData;
use Convoy\Data\Node\Storage\FileMetaData;
use Convoy\Enums\Node\Storage\ContentType;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Convoy\Exceptions\Service\Node\IsoLibrary\InvalidIsoLinkException;
class ProxmoxStorageRepository extends ProxmoxRepository
{
public function download(
ContentType $contentType, string $fileName, string $link,
?bool $verifyCertificates = true,
?ChecksumData $checksumData = null,
)
public function download(ContentType $contentType, string $fileName, string $link, ?bool $verifyCertificates = true, ?ChecksumData $checksumData = null)
{
Assert::isInstanceOf($this->node, Node::class);
Assert::regex($link, '/^(http|https):\/\//');
@ -39,12 +33,12 @@ class ProxmoxStorageRepository extends ProxmoxRepository
}
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
])
->post('/api2/json/nodes/{node}/storage/{storage}/download-url', $payload)
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
])
->post('/api2/json/nodes/{node}/storage/{storage}/download-url', $payload)
->json();
return $this->getData($response);
}
@ -54,28 +48,28 @@ class ProxmoxStorageRepository extends ProxmoxRepository
Assert::isInstanceOf($this->node, Node::class);
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
'file' => "{$this->node->iso_storage}:$contentType->value/$fileName",
])
->delete('/api2/json/nodes/{node}/storage/{storage}/content/{file}')
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
'file' => "{$this->node->iso_storage}:$contentType->value/$fileName",
])
->delete('/api2/json/nodes/{node}/storage/{storage}/content/{file}')
->json();
return $this->getData($response);
}
public function getIsos(): DataCollection
public function getIsos()
{
Assert::isInstanceOf($this->node, Node::class);
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
])
->get('/api2/json/nodes/{node}/storage/{storage}/content?content=iso')
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->iso_storage,
])
->get('/api2/json/nodes/{node}/storage/{storage}/content?content=iso')
->json();
$response = $this->getData($response);
@ -92,29 +86,23 @@ class ProxmoxStorageRepository extends ProxmoxRepository
return IsoData::collection($isos);
}
public function getFileMetadata(string $link, bool $verifyCertificates = true): FileMetaData
public function getFileMetadata(string $link, ?bool $verifyCertificates = true): FileMetaData
{
Assert::isInstanceOf($this->node, Node::class);
Assert::regex($link, '/^(http|https):\/\//');
try {
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
])
->get('/api2/json/nodes/{node}/query-url-metadata', [
'url' => $link,
'verify-certificates' => $verifyCertificates,
])
->json();
} catch (ProxmoxConnectionException $e) {
if (str_contains($e->getMessage(), "Can't connect to")) {
throw new InvalidIsoLinkException();
}
}
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
])
->get('/api2/json/nodes/{node}/query-url-metadata', [
'url' => $link,
'verify-certificates' => $verifyCertificates,
])
->json();
if (Arr::get($response, 'success', 1) !== 1) {
throw new InvalidIsoLinkException();
throw new InvalidIsoLinkException;
}
$data = $this->getData($response);

View file

@ -2,15 +2,15 @@
namespace Convoy\Repositories\Proxmox;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
use Convoy\Models\Node;
use Convoy\Models\Server;
use Illuminate\Contracts\Foundation\Application;
use Webmozart\Assert\Assert;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Webmozart\Assert\Assert;
use Illuminate\Contracts\Foundation\Application;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
abstract class ProxmoxRepository
{
@ -56,7 +56,7 @@ abstract class ProxmoxRepository
*
* @return mixed
*/
public function getData(array|string $response): mixed
public function getData(array|string $response)
{
return $response['data'] ?? $response;
}
@ -64,14 +64,12 @@ abstract class ProxmoxRepository
/**
* Return an instance of the Guzzle HTTP Client to be used for requests.
*/
public function getHttpClient(
array $headers = [], array $options = [], bool $shouldAuthorize = true,
): PendingRequest
public function getHttpClient(array $headers = [], array $options = [], bool $shouldAuthorize = true): PendingRequest
{
Assert::isInstanceOf($this->node, Node::class);
return Http::withOptions(array_merge([
'verify' => $this->node->verify_tls,
'verify' => $this->app->environment('production'),
'base_uri' => "https://{$this->node->fqdn}:{$this->node->port}/",
'timeout' => config('convoy.guzzle.timeout'),
'connect_timeout' => config('convoy.guzzle.connect_timeout'),

View file

@ -2,12 +2,12 @@
namespace Convoy\Repositories\Proxmox\Server;
use Convoy\Enums\Server\BackupCompressionType;
use Convoy\Enums\Server\BackupMode;
use Convoy\Models\Backup;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Webmozart\Assert\Assert;
use Convoy\Enums\Server\BackupMode;
use Convoy\Enums\Server\BackupCompressionType;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
class ProxmoxBackupRepository extends ProxmoxRepository
{
@ -16,15 +16,15 @@ class ProxmoxBackupRepository extends ProxmoxRepository
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->backup_storage,
])
->get('/api2/json/nodes/{node}/storage/{storage}/content', [
'content' => 'backup',
'vmid' => $this->server->vmid,
])
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->backup_storage,
])
->get('/api2/json/nodes/{node}/storage/{storage}/content', [
'content' => 'backup',
'vmid' => $this->server->vmid,
])
->json();
return $this->getData($response);
}
@ -43,16 +43,16 @@ class ProxmoxBackupRepository extends ProxmoxRepository
}
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
])
->post('/api2/json/nodes/{node}/vzdump', [
'vmid' => $this->server->vmid,
'storage' => $this->node->backup_storage,
'mode' => $parsedMode,
'compress' => $compressionType === BackupCompressionType::NONE ? (int)false : $compressionType->value,
])
->json();
->withUrlParameters([
'node' => $this->node->cluster,
])
->post('/api2/json/nodes/{node}/vzdump', [
'vmid' => $this->server->vmid,
'storage' => $this->node->backup_storage,
'mode' => $parsedMode,
'compress' => $compressionType === BackupCompressionType::NONE ? false : $compressionType->value,
])
->json();
return $this->getData($response);
}
@ -62,15 +62,15 @@ class ProxmoxBackupRepository extends ProxmoxRepository
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
])
->post('/api2/json/nodes/{node}/qemu', [
'vmid' => $this->server->vmid,
'force' => true,
'archive' => "{$this->node->backup_storage}:backup/{$backup->file_name}",
])
->json();
->withUrlParameters([
'node' => $this->node->cluster,
])
->post('/api2/json/nodes/{node}/qemu', [
'vmid' => $this->server->vmid,
'force' => true,
'archive' => "{$this->node->backup_storage}:backup/{$backup->file_name}",
])
->json();
return $this->getData($response);
}
@ -80,13 +80,13 @@ class ProxmoxBackupRepository extends ProxmoxRepository
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->backup_storage,
'backup' => "{$this->node->backup_storage}:backup/{$backup->file_name}",
])
->delete('/api2/json/nodes/{node}/storage/{storage}/content/{backup}')
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'storage' => $this->node->backup_storage,
'backup' => "{$this->node->backup_storage}:backup/{$backup->file_name}",
])
->delete('/api2/json/nodes/{node}/storage/{storage}/content/{backup}')
->json();
return $this->getData($response);
}

View file

@ -3,12 +3,12 @@
namespace Convoy\Repositories\Proxmox\Server;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Webmozart\Assert\Assert;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
class ProxmoxConfigRepository extends ProxmoxRepository
{
public function getConfig(): array
public function getConfig()
{
Assert::isInstanceOf($this->server, Server::class);

View file

@ -2,10 +2,10 @@
namespace Convoy\Repositories\Proxmox\Server;
use Convoy\Enums\Server\DiskInterface;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Webmozart\Assert\Assert;
use Convoy\Enums\Server\DiskInterface;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
class ProxmoxDiskRepository extends ProxmoxRepository
{
@ -13,18 +13,18 @@ class ProxmoxDiskRepository extends ProxmoxRepository
{
Assert::isInstanceOf($this->server, Server::class);
$kibibytes = floor($bytes / 1024);
$gigabytes = $bytes / 1073741824;
$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'server' => $this->server->vmid,
])
->put('/api2/json/nodes/{node}/qemu/{server}/resize', [
'disk' => $disk->value,
'size' => "{$kibibytes}K",
])
->json();
->withUrlParameters([
'node' => $this->node->cluster,
'server' => $this->server->vmid,
])
->put('/api2/json/nodes/{node}/qemu/{server}/resize', [
'disk' => $disk->value,
'size' => "+{$gigabytes}G",
])
->json();
return $this->getData($response);
}

View file

@ -0,0 +1,21 @@
<?php
namespace Convoy\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class EnglishKeyboardCharacters implements ValidationRule
{
/**
* Determine if the validation rule passes.
*
* @param mixed $value
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!(bool) preg_match('/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{}|;\':",.\/<>?\\ ]*$/', $value)) {
$fail(__('validation.english_keyboard_characters'));
}
}
}

View file

@ -1,16 +0,0 @@
<?php
namespace Convoy\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class USKeyboardCharacters implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!(bool)preg_match('/^[\x20-\x7F]*$/', $value)) {
$fail(__('validation.us_keyboard_characters'));
}
}
}

View file

@ -2,7 +2,7 @@
namespace Convoy\Services\Coterm;
use Convoy\Models\Coterm;
use Convoy\Models\Node;
use Illuminate\Support\Str;
/**
@ -19,8 +19,8 @@ class CotermTokenCreationService
public function handle(): array
{
return [
'token' => Str::random(Coterm::COTERM_TOKEN_LENGTH),
'token_id' => Str::random(Coterm::COTERM_TOKEN_ID_LENGTH),
'token' => Str::random(Node::COTERM_TOKEN_LENGTH),
'token_id' => Str::random(Node::COTERM_TOKEN_ID_LENGTH),
];
}
}

View file

@ -21,7 +21,7 @@ class IsoService
}
public function download(
Node $node, string $name, ?string $fileName, string $link,
Node $node, string $name, ?string $fileName, string $link,
?ChecksumData $checksumData = null, ?bool $hidden = false,
)
{
@ -57,11 +57,11 @@ class IsoService
return $isos->where('file_name', '=', $fileName)->first();
}
public function delete(Node $node, ISO $iso): void
public function delete(Node $node, ISO $iso)
{
if (is_null($iso->completed_at)) {
throw new BadRequestHttpException(
'This ISO cannot be deleted at this time: not completed.',
'This ISO cannot be restored at this time: not completed.',
);
}

View file

@ -12,9 +12,8 @@ use Convoy\Repositories\Eloquent\AddressRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxCloudinitRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxFirewallRepository;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Arr;
use function collect;
use function is_null;
class NetworkService
{
@ -24,7 +23,9 @@ class NetworkService
private CloudinitService $cloudinitService,
private ProxmoxCloudinitRepository $cloudinitRepository,
private ProxmoxConfigRepository $allocationRepository,
) {
private ConnectionInterface $connection,
)
{
}
public function deleteIpset(Server $server, string $name)
@ -40,7 +41,7 @@ class NetworkService
return $this->firewallRepository->deleteIpset($name);
}
public function clearIpsets(Server $server): void
public function clearIpsets(Server $server)
{
$this->firewallRepository->setServer($server);
@ -51,7 +52,7 @@ class NetworkService
}
}
public function lockIps(Server $server, array $addresses, string $ipsetName): void
public function lockIps(Server $server, array $addresses, string $ipsetName)
{
$this->firewallRepository->setServer($server);
@ -62,7 +63,7 @@ class NetworkService
}
}
public function getMacAddresses(Server $server, bool $eloquent = true, bool $proxmox = false): MacAddressData
public function getMacAddresses(Server $server, bool $eloquent = true, bool $proxmox = false)
{
if ($eloquent) {
$addresses = $this->getAddresses($server);
@ -77,8 +78,7 @@ class NetworkService
$proxmoxMacAddress = null;
if (preg_match(
"/\b[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\b/su",
Arr::get($config, 'net0', ''),
$matches,
Arr::get($config, 'net0', ''), $matches,
)) {
$proxmoxMacAddress = $matches[0];
}
@ -102,7 +102,7 @@ class NetworkService
]);
}
public function syncSettings(Server $server): void
public function syncSettings(Server $server)
{
$macAddresses = $this->getMacAddresses($server, true, true);
$addresses = $this->getAddresses($server);
@ -113,8 +113,7 @@ class NetworkService
'ipv6' => $addresses->ipv6->first()?->toArray(),
]));
$this->lockIps(
$server,
array_unique(Arr::flatten($server->addresses()->get(['address'])->toArray())),
$server, array_unique(Arr::flatten($server->addresses()->get(['address'])->toArray())),
'ipfilter-net0',
);
$this->firewallRepository->setServer($server)->updateOptions([
@ -131,119 +130,27 @@ class NetworkService
);
}
public function updateRateLimit(Server $server, ?int $mebibytes = null): void
public function updateRateLimit(Server $server, ?int $mebibytes = null)
{
$macAddresses = $this->getMacAddresses($server, true, true);
$macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox;
$rawConfig = $this->allocationRepository->setServer($server)->getConfig();
$networkConfig = collect($rawConfig)->where('key', '=', 'net0')->first();
if (is_null($networkConfig)) {
return;
$payload = "virtio={$macAddress},bridge={$server->node->network},firewall=1";
if (!is_null($mebibytes)) {
$payload .= ',rate=' . $mebibytes;
}
$parsedConfig = $this->parseConfig($networkConfig['value']);
// List of possible models
$models = ['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', 'e1000e', 'i82551', 'i82557b', 'i82559er', 'ne2k_isa', 'ne2k_pci', 'pcnet', 'rtl8139', 'virtio', 'vmxnet3'];
// Update the model with the new MAC address
$modelFound = false;
foreach ($parsedConfig as $item) {
if (in_array($item->key, $models)) {
$item->value = $macAddress;
$modelFound = true;
break;
}
}
// If no model key exists, add the default model with the MAC address
if (!$modelFound) {
$parsedConfig[] = (object) ['key' => 'virtio', 'value' => $macAddress];
}
// Update or create the bridge value
$bridgeFound = false;
foreach ($parsedConfig as $item) {
if ($item->key === 'bridge') {
$item->value = $server->node->network;
$bridgeFound = true;
break;
}
}
if (!$bridgeFound) {
$parsedConfig[] = (object) ['key' => 'bridge', 'value' => $server->node->network];
}
// Update or create the firewall key
$firewallFound = false;
foreach ($parsedConfig as $item) {
if ($item->key === 'firewall') {
$item->value = 1;
$firewallFound = true;
break;
}
}
if (!$firewallFound) {
$parsedConfig[] = (object) ['key' => 'firewall', 'value' => 1];
}
// Handle the rate limit
if (is_null($mebibytes)) {
// Remove the 'rate' key if $mebibytes is null
$parsedConfig = array_filter($parsedConfig, fn ($item) => $item->key !== 'rate');
} else {
// Add or update the 'rate' key
$rateUpdated = false;
foreach ($parsedConfig as $item) {
if ($item->key === 'rate') {
$item->value = $mebibytes;
$rateUpdated = true;
break;
}
}
if (!$rateUpdated) {
$parsedConfig[] = (object) ['key' => 'rate', 'value' => $mebibytes];
}
}
// Rebuild the configuration string
$newConfig = implode(',', array_map(fn ($item) => "{$item->key}={$item->value}", $parsedConfig));
// Update the Proxmox configuration
$this->allocationRepository->setServer($server)->update(['net0' => $newConfig]);
$this->allocationRepository->setServer($server)->update(['net0' => $payload]);
}
private function parseConfig(string $config): array
{
// Split components by commas
$components = explode(',', $config);
// Array to hold the parsed objects
$parsedObjects = [];
foreach ($components as $component) {
// Split each component into key and value
[$key, $value] = explode('=', $component);
// Create an associative array (or object) for key-value pairs
$parsedObjects[] = (object) ['key' => $key, 'value' => $value];
}
return $parsedObjects;
}
public function updateAddresses(Server $server, array $addressIds): void
public function updateAddresses(Server $server, array $addressIds)
{
$currentAddresses = $server->addresses()->get()->pluck('id')->toArray();
$addressesToAdd = array_diff($addressIds, $currentAddresses);
$addressesToRemove = array_filter(
$currentAddresses,
fn ($id) => !in_array($id, $addressIds),
$currentAddresses, fn ($id) => !in_array($id, $addressIds),
);
if (!empty($addressesToAdd)) {

View file

@ -2,26 +2,21 @@
namespace Convoy\Services\Servers;
use Convoy\Data\Server\Deployments\ServerDeploymentData;
use Convoy\Enums\Server\Status;
use Convoy\Exceptions\Service\Deployment\InvalidTemplateException;
use Convoy\Exceptions\Service\Server\Allocation\NoUniqueUuidComboException;
use Convoy\Exceptions\Service\Server\Allocation\NoUniqueVmidException;
use Convoy\Models\Server;
use Convoy\Models\Template;
use Convoy\Repositories\Eloquent\ServerRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Convoy\Enums\Server\Status;
use Convoy\Repositories\Eloquent\ServerRepository;
use Convoy\Data\Server\Deployments\ServerDeploymentData;
use Convoy\Exceptions\Service\Deployment\InvalidTemplateException;
/**
* Class ServerCreationService
*/
class ServerCreationService
{
public function __construct(
private NetworkService $networkService, private ServerRepository $repository,
private ServerBuildDispatchService $buildDispatchService,
)
public function __construct(private NetworkService $networkService, private ServerRepository $repository, private ServerBuildDispatchService $buildDispatchService)
{
}
@ -30,28 +25,22 @@ class ServerCreationService
$uuid = $this->generateUniqueUuidCombo();
$shouldCreateServer = Arr::get($data, 'should_create_server');
$template = $shouldCreateServer ? Template::where(
'uuid', '=', Arr::get($data, 'template_uuid'),
)->firstOrFail() : null;
$template = $shouldCreateServer ? Template::where('uuid', '=', Arr::get($data, 'template_uuid'))->firstOrFail() : null;
if ($template) {
if ($template->group->node_id !== intval(Arr::get($data, 'node_id'))) {
throw new InvalidTemplateException(
'This template is inaccessible to the specified node',
);
throw new InvalidTemplateException('This template is inaccessible to the specified node');
}
}
$nodeId = Arr::get($data, 'node_id');
$server = Server::create([
'uuid' => $uuid,
'uuid_short' => substr($uuid, 0, 8),
'status' => $shouldCreateServer ? Status::INSTALLING->value : null,
'name' => Arr::get($data, 'name'),
'user_id' => Arr::get($data, 'user_id'),
'node_id' => $nodeId,
'vmid' => Arr::get($data, 'vmid') ?? $this->generateUniqueVmId($nodeId),
'node_id' => Arr::get($data, 'node_id'),
'vmid' => Arr::get($data, 'vmid') ?? random_int(100, 999999999),
'hostname' => Arr::get($data, 'hostname'),
'cpu' => Arr::get($data, 'limits.cpu'),
'memory' => Arr::get($data, 'limits.memory'),
@ -80,35 +69,15 @@ class ServerCreationService
return $server;
}
public function generateUniqueVmId(int $nodeId): int
{
$vmid = random_int(100, 999999999);
$attempts = 0;
while (!$this->repository->isUniqueVmId($nodeId, $vmid)) {
$vmid = random_int(100, 999999999);
if ($attempts++ > 10) {
throw new NoUniqueVmidException();
}
}
return $vmid;
}
/**
* Create a unique UUID and UUID-Short combo for a server.
*/
public function generateUniqueUuidCombo(): string
{
$uuid = Str::uuid()->toString();
$short = substr($uuid, 0, 8);
$attempts = 0;
while (!$this->repository->isUniqueUuidCombo($uuid, $short)) {
$uuid = Str::uuid()->toString();
$short = substr($uuid, 0, 8);
if ($attempts++ > 10) {
throw new NoUniqueUuidComboException();
}
if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) {
return $this->generateUniqueUuidCombo();
}
return $uuid;

View file

@ -31,7 +31,7 @@ class ServerSuspensionService
]);
try {
$this->powerRepository->setServer($server)->send($isSuspending ? PowerAction::KILL : PowerAction::START);
$this->powerRepository->setServer($server)->send(PowerAction::KILL);
} catch (Exception $exception) {
$server->update([
'status' => $isSuspending ? null : Status::SUSPENDED->value,

View file

@ -2,27 +2,28 @@
namespace Convoy\Services\Servers;
use Convoy\Data\Server\Proxmox\Config\DiskData;
use Convoy\Enums\Server\DiskInterface;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxDiskRepository;
use Illuminate\Support\Arr;
use Convoy\Enums\Server\DiskInterface;
use Convoy\Data\Server\Proxmox\Config\DiskData;
use Convoy\Repositories\Proxmox\Server\ProxmoxDiskRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxPowerRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
readonly class SyncBuildService
class SyncBuildService
{
public function __construct(
private AllocationService $allocationService,
private CloudinitService $cloudinitService,
private NetworkService $networkService,
private ServerDetailService $detailService,
private AllocationService $allocationService,
private CloudinitService $cloudinitService,
private NetworkService $networkService,
private ServerDetailService $detailService,
private ProxmoxPowerRepository $powerRepository,
private ProxmoxConfigRepository $allocationRepository,
private ProxmoxDiskRepository $diskRepository,
)
{
private ProxmoxDiskRepository $diskRepository,
) {
}
public function handle(Server $server): void
public function handle(Server $server)
{
$this->allocationRepository->setServer($server);
@ -40,23 +41,16 @@ readonly class SyncBuildService
// find a disk that has a corresponding disk in the deployment
$disksArray = collect($disks->toArray())->pluck('interface')->all();
$bootOrder = array_filter(
collect($bootOrder->filter(fn (DiskData $disk) => !$disk->is_media)->toArray())->pluck(
'interface',
)->toArray(), fn ($disk) => in_array($disk, $disksArray),
);
$bootOrder = array_filter(collect($bootOrder->filter(fn (DiskData $disk) => ! $disk->is_media)->toArray())->pluck('interface')->toArray(), fn ($disk) => in_array($disk, $disksArray));
if (count($bootOrder) > 0) {
/** @var DiskData $disk */
$disk = $disks->where('interface', '=', DiskInterface::from(Arr::first($bootOrder)))
->first();
$disk = $disks->where('interface', '=', DiskInterface::from(Arr::first($bootOrder)))->first();
$diff = $server->disk - $disk->size;
if ($diff > 0) {
$this->diskRepository->setServer($server)->resizeDisk(
$disk->interface, $server->disk,
);
$this->diskRepository->setServer($server)->resizeDisk($disk->interface, $diff);
}
}
}

View file

@ -14,7 +14,6 @@ class NodeTransformer extends TransformerAbstract
'location_id' => $node->location_id,
'name' => $node->name,
'cluster' => $node->cluster,
'verify_tls' => $node->verify_tls,
'fqdn' => $node->fqdn,
'port' => $node->port,
'memory' => $node->memory,
@ -27,8 +26,11 @@ class NodeTransformer extends TransformerAbstract
'backup_storage' => $node->backup_storage,
'iso_storage' => $node->iso_storage,
'network' => $node->network,
'coterm_id' => $node->coterm_id,
'servers_count' => (int)$node->servers_count,
'coterm_enabled' => $node->coterm_enabled,
'coterm_tls_enabled' => $node->coterm_tls_enabled,
'coterm_fqdn' => $node->coterm_fqdn,
'coterm_port' => $node->coterm_port,
'servers_count' => (int) $node->servers_count,
];
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace Database\Factories;
use Convoy\Models\Address;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Address>
*/
class AddressFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$type = $this->faker->randomElement(['ipv4', 'ipv6']);
return [
'type' => $type,
'address' => $type === 'ipv4' ? $this->faker->ipv4 : $this->faker->ipv6,
'cidr' => $this->faker->numberBetween(0, 128),
'gateway' => $type === 'ipv4' ? $this->faker->ipv4 : $this->faker->ipv6,
'mac_address' => $this->faker->randomElement([null, $this->faker->macAddress]),
];
}
}

View file

@ -1,21 +0,0 @@
<?php
namespace Database\Factories;
use Convoy\Models\AddressPool;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class AddressPoolFactory extends Factory
{
protected $model = AddressPool::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use Convoy\Models\ISO;
use Convoy\Models\Node;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -10,6 +11,11 @@ use Illuminate\Database\Eloquent\Factories\Factory;
*/
class ISOFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = ISO::class;
/**

View file

@ -2,19 +2,27 @@
namespace Database\Factories;
use Convoy\Models\Location;
use Convoy\Models\Node;
use Illuminate\Database\Eloquent\Factories\Factory;
class NodeFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Node::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'name' => $this->faker->word(),
'cluster' => 'proxmox',
'verify_tls' => true,
'fqdn' => $this->faker->word(),
'token_id' => $this->faker->word(),
'secret' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password

View file

@ -2,7 +2,9 @@
namespace Database\Factories;
use Convoy\Models\Node;
use Convoy\Models\Server;
use Convoy\Models\User;
use Convoy\Services\Servers\ServerCreationService;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\App;
@ -30,11 +32,8 @@ class ServerFactory extends Factory
'name' => $this->faker->word(),
'vmid' => rand(100, 5000),
'cpu' => 2,
'memory' => 2048 * 1024 * 1024,
'disk' => 20 * 1024 * 1024 * 1024,
'backup_limit' => 16,
'snapshot_limit' => 16,
'bandwidth_limit' => 100 * 1024 * 1024 * 1024,
'memory' => 17179869184,
'disk' => 17179869184,
];
}
}

View file

@ -1,21 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->boolean('verify_tls')->default(true)->after('cluster');
});
}
public function down(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('verify_tls');
});
}
};

View file

@ -1,41 +1,24 @@
services:
workspace:
image: performave/convoy-workspace:latest
tty: true
volumes:
- .:/var/www/
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7.0-alpine
restart: unless-stopped
command: redis-server --save 20 1 --loglevel notice --requirepass ${REDIS_PASSWORD}
expose:
- 6379
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
healthcheck:
test: redis-cli -a $$REDIS_PASSWORD ping | grep PONG
interval: 5s
timeout: 5s
retries: 20
database:
image: mysql:8.0
restart: unless-stopped
volumes:
- ./dockerfiles/mysql/data:/var/lib/mysql/
expose:
- 3306
environment:
MYSQL_RANDOM_ROOT_PASSWORD: true
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: mysqladmin ping -u$$MYSQL_USER -p$$MYSQL_PASSWORD
interval: 5s
timeout: 5s
retries: 20
workspace:
image: performave/convoy-workspace:latest
tty: true
volumes:
- .:/var/www/
redis:
image: redis:7.0-alpine
restart: unless-stopped
command: redis-server --save 20 1 --loglevel notice --requirepass ${REDIS_PASSWORD}
expose:
- 6379
database:
image: mysql:8.0
restart: unless-stopped
volumes:
- ./dockerfiles/mysql/data:/var/lib/mysql/
expose:
- 3306
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}

View file

@ -1,94 +1,78 @@
services:
caddy:
build:
context: ./dockerfiles/caddy
args:
- APP_ENV=$APP_ENV
- APP_URL=$APP_URL
restart: unless-stopped
volumes:
- .:/var/www/
- ./dockerfiles/caddy/data/config:/config
- ./dockerfiles/caddy/data/data:/data
ports:
- 80:80
- 443:443
depends_on:
- php
env_file: .env
php:
build:
context: ./dockerfiles/php
args:
- APP_ENV=$APP_ENV
- PHP_XDEBUG=$PHP_XDEBUG
- PHP_XDEBUG_MODE=$PHP_XDEBUG_MODE
restart: unless-stopped
volumes:
- .:/var/www/
expose:
- 9000
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
extra_hosts:
host.docker.internal: host-gateway
workspace:
build:
context: ./dockerfiles/workspace
args:
- PHP_XDEBUG=$PHP_XDEBUG
- PHP_XDEBUG_MODE=$PHP_XDEBUG_MODE
tty: true
ports:
- 1234:1234
volumes:
- .:/var/www/
extra_hosts:
host.docker.internal: host-gateway
workers:
build:
context: ./dockerfiles/workers
args:
- APP_ENV=$APP_ENV
restart: unless-stopped
volumes:
- .:/var/www/
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7.0-alpine
restart: unless-stopped
command: redis-server --save 60 1 --loglevel notice --requirepass '${REDIS_PASSWORD}'
ports:
- 6379:6379
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
healthcheck:
test: redis-cli -a $$REDIS_PASSWORD ping | grep PONG
interval: 5s
timeout: 5s
retries: 20
database:
image: mysql:8.0
restart: unless-stopped
volumes:
- ./dockerfiles/mysql/data:/var/lib/mysql/
ports:
- 3306:3306
environment:
MYSQL_RANDOM_ROOT_PASSWORD: true
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: mysqladmin ping -u$$MYSQL_USER -p$$MYSQL_PASSWORD
interval: 5s
timeout: 5s
retries: 20
caddy:
build:
context: ./dockerfiles/caddy
args:
- APP_ENV=$APP_ENV
- APP_URL=$APP_URL
restart: unless-stopped
volumes:
- .:/var/www/
- ./dockerfiles/caddy/data/config:/config
- ./dockerfiles/caddy/data/data:/data
ports:
- 80:80
- 443:443
depends_on:
- php
env_file: .env
php:
build:
context: ./dockerfiles/php
args:
- APP_ENV=$APP_ENV
- PHP_XDEBUG=$PHP_XDEBUG
- PHP_XDEBUG_MODE=$PHP_XDEBUG_MODE
restart: unless-stopped
volumes:
- .:/var/www/
expose:
- 9000
depends_on:
- database
- redis
env_file: .env
extra_hosts:
host.docker.internal: host-gateway
workspace:
build:
context: ./dockerfiles/workspace
args:
- PHP_XDEBUG=$PHP_XDEBUG
- PHP_XDEBUG_MODE=$PHP_XDEBUG_MODE
tty: true
ports:
- 1234:1234
volumes:
- .:/var/www/
extra_hosts:
host.docker.internal: host-gateway
redis:
image: redis:7.0-alpine
restart: unless-stopped
command: redis-server --save 60 1 --loglevel notice --requirepass '${REDIS_PASSWORD}'
ports:
- 6379:6379
workers:
build:
context: ./dockerfiles/workers
args:
- APP_ENV=$APP_ENV
restart: unless-stopped
volumes:
- .:/var/www/
depends_on:
- database
- redis
database:
image: mysql:8.0
restart: unless-stopped
volumes:
- ./dockerfiles/mysql/data:/var/lib/mysql/
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}

View file

@ -46,7 +46,7 @@ return [
'doesnt_start_with' => 'The :attribute may not start with one of the following: :values.',
'email' => 'The :attribute must be a valid email address.',
'ends_with' => 'The :attribute must end with one of the following: :values.',
'us_keyboard_characters' => 'The :attribute must contain characters from the US keyboard.',
'english_keyboard_characters' => 'The :attribute must contain characters from the English keyboard.',
'enum' => 'The selected :attribute is invalid.',
'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.',

View file

@ -25,11 +25,11 @@ return [
'datetime' => 'Invalid {{validation}}',
'startsWith' => 'Invalid input: must start with "{{startsWith}}"',
'endsWith' => 'Invalid input: must end with "{{endsWith}}"',
'hostname' => 'Invalid hostname',
'us_keyboard_characters' => 'Invalid US keyboard characters',
'hostname' => 'Invalid {{validation}}',
'english_keyboard_characters' => 'Invalid {{validation}}',
'password' => 'Must contain 8 characters, one uppercase, one lowercase, one number and one special case character',
'ip_address' => 'Invalid IP address',
'mac_address' => 'Invalid MAC address',
'ip_address' => 'Invalid IP Address',
'mac_address' => 'Invalid Mac Address',
],
'too_small' => [
'array' => [

2
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "panel",
"name": "www",
"lockfileVersion": 2,
"requires": true,
"packages": {

View file

@ -0,0 +1,14 @@
import http from '@/api/http'
const deleteAddress = (
nodeId: number,
addressId: number,
syncNetworkConfig: boolean
) =>
http.delete(`/api/admin/nodes/${nodeId}/addresses/${addressId}`, {
params: {
sync_network_config: syncNetworkConfig,
},
})
export default deleteAddress

View file

@ -5,7 +5,6 @@ interface CreateNodeParameters {
locationId: number
name: string
cluster: string
verifyTls: boolean
fqdn: string
tokenId: string
secret: string
@ -27,7 +26,6 @@ const createNode = async (data: CreateNodeParameters): Promise<Node> => {
location_id: data.locationId,
name: data.name,
cluster: data.cluster,
verify_tls: data.verifyTls,
fqdn: data.fqdn,
token_id: data.tokenId,
secret: data.secret,
@ -45,4 +43,4 @@ const createNode = async (data: CreateNodeParameters): Promise<Node> => {
return rawDataToNode(responseData)
}
export default createNode
export default createNode

View file

@ -5,7 +5,6 @@ export interface Node {
locationId: number
name: string
cluster: string
verifyTls: boolean
fqdn: string
port: number
memory: number
@ -18,7 +17,10 @@ export interface Node {
backupStorage: string
isoStorage: string
network: string
cotermId: number | null
cotermEnabled: boolean
cotermTlsEnabled: boolean
cotermFqdn: string | null
cotermPort: number
serversCount: number
}
@ -27,7 +29,6 @@ export const rawDataToNode = (data: any): Node => ({
locationId: data.location_id,
name: data.name,
cluster: data.cluster,
verifyTls: data.verify_tls,
fqdn: data.fqdn,
port: data.port,
memory: data.memory,
@ -40,7 +41,10 @@ export const rawDataToNode = (data: any): Node => ({
backupStorage: data.backup_storage,
isoStorage: data.iso_storage,
network: data.network,
cotermId: data.coterm_id,
cotermEnabled: data.coterm_enabled,
cotermTlsEnabled: data.coterm_tls_enabled,
cotermFqdn: data.coterm_fqdn,
cotermPort: data.coterm_port,
serversCount: data.servers_count,
})
@ -80,4 +84,4 @@ const getNodes = async ({
}
}
export default getNodes
export default getNodes

View file

@ -5,7 +5,6 @@ interface UpdateNodeParameters {
locationId: number
name: string
cluster: string
verifyTls: boolean
fqdn: string
port: number
tokenId?: string | null
@ -27,7 +26,6 @@ const updateNode = async (nodeId: number, payload: UpdateNodeParameters) => {
location_id: payload.locationId,
name: payload.name,
cluster: payload.cluster,
verify_tls: payload.verifyTls,
fqdn: payload.fqdn,
port: payload.port,
token_id: payload.tokenId ? payload.tokenId : undefined,
@ -45,4 +43,4 @@ const updateNode = async (nodeId: number, payload: UpdateNodeParameters) => {
return rawDataToNode(data)
}
export default updateNode
export default updateNode

View file

@ -1,12 +1,12 @@
import useSWR from 'swr'
import getNodes, {NodeResponse, QueryParams} from '@/api/admin/nodes/getNodes'
import getNodes, { NodeResponse, QueryParams } from '@/api/admin/nodes/getNodes'
const useNodesSWR = ({page, query, id, cotermId, ...params}: QueryParams) => {
const useNodesSWR = ({ page, query, id, cotermId, ...params }: QueryParams) => {
return useSWR<NodeResponse>(
['admin:nodes', page, query, id, cotermId],
() => getNodes({page, query, id, cotermId, ...params})
['admin:nodes', page, query, Boolean(id), cotermId],
() => getNodes({ page, query, id, cotermId, ...params })
)
}

View file

@ -1,13 +1,13 @@
import {useFlashKey} from '@/util/useFlash'
import {port} from '@/util/validation'
import {zodResolver} from '@hookform/resolvers/zod'
import {useEffect} from 'react'
import {FormProvider, useForm} from 'react-hook-form'
import {useTranslation} from 'react-i18next'
import {KeyedMutator} from 'swr'
import {z} from 'zod'
import { useFlashKey } from '@/util/useFlash'
import { port } from '@/util/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { KeyedMutator } from 'swr'
import { z } from 'zod'
import {Coterm, CotermResponse} from '@/api/admin/coterms/getCoterms'
import { Coterm, CotermResponse } from '@/api/admin/coterms/getCoterms'
import updateCoterm from '@/api/admin/coterms/updateCoterm'
import useAttachedNodes from '@/api/admin/coterms/useAttachedNodes'
@ -25,12 +25,12 @@ interface Props {
mutate: KeyedMutator<CotermResponse>
}
const EditCotermModal = ({coterm, onClose, mutate}: Props) => {
const {t: tStrings} = useTranslation('strings')
const {clearFlashes, clearAndAddHttpError} = useFlashKey(
`admin.coterms.${coterm?.id}.update`
const EditCotermModal = ({ coterm, onClose, mutate }: Props) => {
const { t: tStrings } = useTranslation('strings')
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
`admin.coterms.${coterm?.id}.update`
)
const {data: attachedNodes} = useAttachedNodes(coterm ? coterm.id : -1, {})
const { data: attachedNodes } = useAttachedNodes(coterm?.id ?? -1, {})
const schema = z.object({
name: z.string().min(1).max(191),
@ -67,7 +67,7 @@ const EditCotermModal = ({coterm, onClose, mutate}: Props) => {
isTlsEnabled: old.isTlsEnabled,
fqdn: old.fqdn,
port: old.port,
nodeIds: attachedNodes ? attachedNodes.items.map(node => node.id.toString()) : [],
nodeIds: attachedNodes?.items.map(node => node.id.toString()) ?? [],
}))
}, [attachedNodes])
@ -81,14 +81,14 @@ const EditCotermModal = ({coterm, onClose, mutate}: Props) => {
clearFlashes()
try {
const updatedCoterm = await updateCoterm(coterm!.id, data)
const updatedCoterm = await updateCoterm(coterm.id, data)
mutate(data => {
if (!data) return data
return {
...data,
items: data.items.map(item =>
item.id === updatedCoterm.id ? updatedCoterm : item
item.id === updatedCoterm.id ? updatedCoterm : item
),
}
}, false)
@ -99,47 +99,47 @@ const EditCotermModal = ({coterm, onClose, mutate}: Props) => {
}
return (
<Modal open={Boolean(coterm)} onClose={handleClose}>
<Modal.Header>
<Modal.Title>Edit {coterm?.name}</Modal.Title>
</Modal.Header>
<Modal open={Boolean(coterm)} onClose={handleClose}>
<Modal.Header>
<Modal.Title>Edit {coterm?.name}</Modal.Title>
</Modal.Header>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(submit)}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={`admin.coterms.${coterm?.id}.update`}
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(submit)}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={`admin.coterms.${coterm?.id}.update`}
/>
<TextInputForm name='name' label={tStrings('name')} />
<div className={'space-y-3'}>
<TextInputForm
name='fqdn'
label={tStrings('fqdn')}
/>
<TextInputForm name='name' label={tStrings('name')}/>
<div className={'space-y-3'}>
<TextInputForm
name='fqdn'
label={tStrings('fqdn')}
/>
<CheckboxForm
name={'isTlsEnabled'}
label={'Is TLS Enabled?'}
/>
</div>
<TextInputForm name='port' label={tStrings('port')}/>
<CotermNodesMultiSelectForm/>
</Modal.Body>
<CheckboxForm
name={'isTlsEnabled'}
label={'Is TLS Enabled?'}
/>
</div>
<TextInputForm name='port' label={tStrings('port')} />
<CotermNodesMultiSelectForm />
</Modal.Body>
<Modal.Actions>
<Modal.Action type='button' onClick={handleClose}>
{tStrings('cancel')}
</Modal.Action>
<Modal.Action
type='submit'
loading={form.formState.isSubmitting}
>
{tStrings('save')}
</Modal.Action>
</Modal.Actions>
</form>
</FormProvider>
</Modal>
<Modal.Actions>
<Modal.Action type='button' onClick={handleClose}>
{tStrings('cancel')}
</Modal.Action>
<Modal.Action
type='submit'
loading={form.formState.isSubmitting}
>
{tStrings('save')}
</Modal.Action>
</Modal.Actions>
</form>
</FormProvider>
</Modal>
)
}

View file

@ -11,12 +11,10 @@ import useNodesSWR from '@/api/admin/nodes/useNodesSWR'
import FlashMessageRender from '@/components/elements/FlashMessageRenderer'
import MessageBox from '@/components/elements/MessageBox'
import Modal from '@/components/elements/Modal'
import CheckboxForm from '@/components/elements/forms/CheckboxForm'
import TextInputForm from '@/components/elements/forms/TextInputForm'
import LocationsSelectForm from '@/components/admin/nodes/LocationsSelectForm'
interface Props {
open: boolean
onClose: () => void
@ -31,22 +29,21 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
const { t } = useTranslation('admin.nodes.index')
const schema = z.object({
name: z.string().min(1).max(191),
name: z.string().max(191).nonempty(),
locationId: z.preprocess(Number, z.number()),
cluster: z.string().min(1).max(191),
verifyTls: z.boolean(),
tokenId: z.string().min(1).max(191),
secret: z.string().min(1).max(191),
fqdn: hostname().min(1).max(191),
cluster: z.string().max(191).nonempty(),
tokenId: z.string().max(191).nonempty(),
secret: z.string().max(191).nonempty(),
fqdn: hostname().max(191).nonempty(),
port: z.preprocess(Number, z.number().int().min(1).max(65535)),
memory: z.preprocess(Number, z.number().int().min(0)),
memoryOverallocate: z.preprocess(Number, z.number().int().min(0)),
disk: z.preprocess(Number, z.number().int().min(0)),
diskOverallocate: z.preprocess(Number, z.number().int().min(0)),
vmStorage: z.string().min(1).max(191),
backupStorage: z.string().min(1).max(191),
isoStorage: z.string().min(1).max(191),
network: z.string().min(1).max(191),
vmStorage: z.string().max(191).nonempty(),
backupStorage: z.string().max(191).nonempty(),
isoStorage: z.string().max(191).nonempty(),
network: z.string().max(191).nonempty(),
})
const form = useForm({
@ -55,7 +52,6 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
name: '',
locationId: '0',
cluster: '',
verifyTls: true,
tokenId: '',
secret: '',
fqdn: '',
@ -136,11 +132,6 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
<TextInputForm name='secret' label={t('secret')} />
</div>
<TextInputForm name='fqdn' label={tStrings('fqdn')} />
<CheckboxForm
name={'verifyTls'}
label='Verify TLS Certificate'
className={'mt-3 relative'}
/>
<TextInputForm name='port' label={tStrings('port')} />
<div className='grid gap-3 grid-cols-2'>
<TextInputForm

View file

@ -2,7 +2,7 @@ import { useFlashKey } from '@/util/useFlash'
import usePagination from '@/util/usePagination'
import { FormikProvider, useFormik } from 'formik'
import deleteAddress from '@/api/admin/addressPools/addresses/deleteAddress'
import deleteAddress from '@/api/admin/nodes/addresses/deleteAddress'
import useAddressesSWR from '@/api/admin/nodes/addresses/useAddressesSWR'
import useNodeSWR from '@/api/admin/nodes/useNodeSWR'
import { Address } from '@/api/server/getServer'
@ -17,23 +17,23 @@ interface Props {
address: Address
}
const DeleteAddressModal = ({open, onClose, address}: Props) => {
const {data: node} = useNodeSWR()
const {clearFlashes, clearAndAddHttpError} = useFlashKey(
`admin.nodes.${node.id}.addresses.${address.id}.delete`
const DeleteAddressModal = ({ open, onClose, address }: Props) => {
const { data: node } = useNodeSWR()
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
`admin.nodes.${node.id}.addresses.${address.id}.delete`
)
const [page] = usePagination()
const {mutate} = useAddressesSWR(node.id, {page, include: ['server']})
const { mutate } = useAddressesSWR(node.id, { page, include: ['server'] })
const form = useFormik({
initialValues: {
syncNetworkConfig: true,
},
onSubmit: async ({syncNetworkConfig}, {setSubmitting}) => {
onSubmit: async ({ syncNetworkConfig }, { setSubmitting }) => {
clearFlashes()
setSubmitting(true)
try {
await deleteAddress(address.addressPoolId, address.id)
await deleteAddress(node.id, address.id, syncNetworkConfig)
mutate(data => {
if (!data) return data
@ -52,34 +52,34 @@ const DeleteAddressModal = ({open, onClose, address}: Props) => {
})
return (
<Modal open={open} onClose={onClose}>
<Modal.Header>
<Modal.Title>Delete Address</Modal.Title>
</Modal.Header>
<FormikProvider value={form}>
<form onSubmit={form.handleSubmit}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={`admin.nodes.${node.id}.addresses.${address.id}.delete`}
/>
<Modal open={open} onClose={onClose}>
<Modal.Header>
<Modal.Title>Delete Address</Modal.Title>
</Modal.Header>
<FormikProvider value={form}>
<form onSubmit={form.handleSubmit}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={`admin.nodes.${node.id}.addresses.${address.id}.delete`}
/>
<Modal.Description>
Are you sure you want to delete this address? This
action cannot be undone.
</Modal.Description>
</Modal.Body>
<Modal.Actions>
<Modal.Action type='button' onClick={onClose}>
Cancel
</Modal.Action>
<Modal.Action type='submit' loading={form.isSubmitting}>
Delete
</Modal.Action>
</Modal.Actions>
</form>
</FormikProvider>
</Modal>
<Modal.Description>
Are you sure you want to delete this address? This
action cannot be undone.
</Modal.Description>
</Modal.Body>
<Modal.Actions>
<Modal.Action type='button' onClick={onClose}>
Cancel
</Modal.Action>
<Modal.Action type='submit' loading={form.isSubmitting}>
Delete
</Modal.Action>
</Modal.Actions>
</form>
</FormikProvider>
</Modal>
)
}

View file

@ -64,6 +64,8 @@ const NodeAddressesContainer = () => {
include: ['server'],
})
console.log(data)
const rowActions = ({ row }: RowActionsProps<Address>) => {
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
@ -117,4 +119,4 @@ const NodeAddressesContainer = () => {
)
}
export default NodeAddressesContainer
export default NodeAddressesContainer

View file

@ -11,7 +11,6 @@ import useNodeSWR from '@/api/admin/nodes/useNodeSWR'
import Button from '@/components/elements/Button'
import FlashMessageRender from '@/components/elements/FlashMessageRenderer'
import FormCard from '@/components/elements/FormCard'
import CheckboxForm from '@/components/elements/forms/CheckboxForm'
import TextInputForm from '@/components/elements/forms/TextInputForm'
import LocationsSelectForm from '@/components/admin/nodes/LocationsSelectForm'
@ -27,22 +26,21 @@ const NodeInformationCard = () => {
const { t: tIndex } = useTranslation('admin.nodes.index')
const schema = z.object({
name: z.string().min(1).max(191),
name: z.string().max(191).nonempty(),
locationId: z.preprocess(Number, z.number()),
cluster: z.string().min(1).max(191),
verifyTls: z.boolean(),
cluster: z.string().max(191).nonempty(),
tokenId: z.string().max(191),
secret: z.string().max(191),
fqdn: hostname().min(1).max(191),
fqdn: hostname().max(191).nonempty(),
port: z.preprocess(Number, z.number().int().min(1).max(65535)),
memory: z.preprocess(Number, z.number().int().min(0)),
memoryOverallocate: z.preprocess(Number, z.number().int().min(0)),
disk: z.preprocess(Number, z.number().int().min(0)),
diskOverallocate: z.preprocess(Number, z.number().int().min(0)),
vmStorage: z.string().min(1).max(191),
backupStorage: z.string().min(1).max(191),
isoStorage: z.string().min(1).max(191),
network: z.string().min(1).max(191),
vmStorage: z.string().max(191).nonempty(),
backupStorage: z.string().max(191).nonempty(),
isoStorage: z.string().max(191).nonempty(),
network: z.string().max(191).nonempty(),
})
const form = useForm({
@ -51,7 +49,6 @@ const NodeInformationCard = () => {
name: node.name,
locationId: node.locationId.toString(),
cluster: node.cluster,
verifyTls: node.verifyTls,
tokenId: '',
secret: '',
fqdn: node.fqdn,
@ -83,7 +80,6 @@ const NodeInformationCard = () => {
name: data.name,
locationId: data.locationId.toString(),
cluster: data.cluster,
verifyTls: data.verifyTls,
tokenId: '',
secret: '',
fqdn: data.fqdn,
@ -125,11 +121,6 @@ const NodeInformationCard = () => {
name='fqdn'
label={tStrings('fqdn')}
/>
<CheckboxForm
name={'verifyTls'}
label='Verify TLS Certificate'
className={'relative'}
/>
<TextInputForm
name='port'
label={tStrings('port')}
@ -218,4 +209,4 @@ const NodeInformationCard = () => {
)
}
export default NodeInformationCard
export default NodeInformationCard

View file

@ -1,6 +1,10 @@
import { useFlashKey } from '@/util/useFlash'
import usePagination from '@/util/usePagination'
import { hostname, password, usKeyboardCharacters } from '@/util/validation'
import {
englishKeyboardCharacters,
hostname,
password,
} from '@/util/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { FormProvider, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
@ -28,20 +32,20 @@ interface Props {
onClose: () => void
}
const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
const CreateServerModal = ({nodeId, userId, open, onClose}: Props) => {
const [page] = usePagination()
const { mutate } = useServersSWR({
const {mutate} = useServersSWR({
nodeId,
userId,
page,
query: '',
include: ['node', 'user'],
})
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
'admin.servers.create'
const {clearFlashes, clearAndAddHttpError} = useFlashKey(
'admin.servers.create'
)
const { t } = useTranslation('admin.servers.index')
const { t: tStrings } = useTranslation('strings')
const {t} = useTranslation('admin.servers.index')
const {t: tStrings} = useTranslation('strings')
const schemaWithCreateVm = z.object({
name: z.string().max(40).nonempty(),
@ -68,7 +72,7 @@ const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
z.literal(''),
z.preprocess(Number, z.number().min(0)),
]),
accountPassword: password(usKeyboardCharacters()).nonempty(),
accountPassword: password(englishKeyboardCharacters()).nonempty(),
shouldCreateServer: z.literal(true),
startOnCompletion: z.boolean(),
templateUuid: z.string().nonempty(),
@ -99,7 +103,7 @@ const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
z.literal(''),
z.preprocess(Number, z.number().min(0)),
]),
accountPassword: password(usKeyboardCharacters()).optional(),
accountPassword: password(englishKeyboardCharacters()).optional(),
shouldCreateServer: z.literal(false),
startOnCompletion: z.boolean(),
templateUuid: z.string(),
@ -159,8 +163,7 @@ const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
disk: disk * 1048576,
snapshots: snapshotLimit !== '' ? snapshotLimit : null,
backups: backupLimit !== '' ? backupLimit : null,
bandwidth:
bandwidthLimit !== '' ? bandwidthLimit * 1048576 : null,
bandwidth: bandwidthLimit !== '' ? bandwidthLimit * 1048576 : null,
addressIds,
},
accountPassword: accountPassword ? accountPassword : null,
@ -188,107 +191,107 @@ const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
}
return (
<Modal open={open} onClose={handleClose}>
<Modal.Header>
<Modal.Title>{t('create_modal.title')}</Modal.Title>
</Modal.Header>
<Modal open={open} onClose={handleClose}>
<Modal.Header>
<Modal.Title>{t('create_modal.title')}</Modal.Title>
</Modal.Header>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(submit)}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={'admin.servers.create'}
/>
<TextInputForm
name={'name'}
label={tStrings('display_name')}
/>
{nodeId ? null : <NodesSelectForm />}
{userId ? null : <UsersSelectForm />}
<TextInputForm
name={'vmid'}
label={'VMID'}
placeholder={
t('vmid_placeholder') ??
'Leave blank for random VMID'
}
/>
<TextInputForm
name={'hostname'}
label={tStrings('hostname')}
/>
<AddressesMultiSelectForm
disabled={watchNodeId === ''}
/>
<div className={'grid grid-cols-2 gap-3'}>
<TextInputForm
name={'cpu'}
label={tStrings('cpu')}
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(submit)}>
<Modal.Body>
<FlashMessageRender
className='mb-5'
byKey={'admin.servers.create'}
/>
<TextInputForm
name={'memory'}
label={`${tStrings('memory')} (MiB)`}
name={'name'}
label={tStrings('display_name')}
/>
</div>
<TextInputForm
name={'disk'}
label={`${tStrings('disk')} (MiB)`}
/>
<div className={'grid grid-cols-2 gap-3'}>
{nodeId ? null : <NodesSelectForm/>}
{userId ? null : <UsersSelectForm/>}
<TextInputForm
name={'backupLimit'}
label={t('backup_limit')}
placeholder={
t('limit_placeholder') ??
'Leave blank for no limit'
}
name={'vmid'}
label={'VMID'}
placeholder={
t('vmid_placeholder') ??
'Leave blank for random VMID'
}
/>
<TextInputForm
name={'bandwidthLimit'}
label={`${t('bandwidth_limit')} (MiB)`}
placeholder={
t('limit_placeholder') ??
'Leave blank for no limit'
}
name={'hostname'}
label={tStrings('hostname')}
/>
</div>
<TextInputForm
name={'accountPassword'}
label={tStrings('system_os_password')}
type={'password'}
/>
<CheckboxForm
name={'shouldCreateServer'}
label={t('should_create_vm')}
className={'mt-3 relative'}
/>
<TemplatesSelectForm
disabled={
!watchShouldCreateServer || watchNodeId === ''
}
/>
<CheckboxForm
name={'startOnCompletion'}
label={t('start_server_after_installing')}
className={'mt-3 relative'}
/>
</Modal.Body>
<Modal.Actions>
<Modal.Action type='button' onClick={handleClose}>
{tStrings('cancel')}
</Modal.Action>
<Modal.Action
type='submit'
loading={form.formState.isSubmitting}
>
{tStrings('create')}
</Modal.Action>
</Modal.Actions>
</form>
</FormProvider>
</Modal>
<AddressesMultiSelectForm
disabled={watchNodeId === ''}
/>
<div className={'grid grid-cols-2 gap-3'}>
<TextInputForm
name={'cpu'}
label={tStrings('cpu')}
/>
<TextInputForm
name={'memory'}
label={`${tStrings('memory')} (MiB)`}
/>
</div>
<TextInputForm
name={'disk'}
label={`${tStrings('disk')} (MiB)`}
/>
<div className={'grid grid-cols-2 gap-3'}>
<TextInputForm
name={'backupLimit'}
label={t('backup_limit')}
placeholder={
t('limit_placeholder') ??
'Leave blank for no limit'
}
/>
<TextInputForm
name={'bandwidthLimit'}
label={`${t('bandwidth_limit')} (MiB)`}
placeholder={
t('limit_placeholder') ??
'Leave blank for no limit'
}
/>
</div>
<TextInputForm
name={'accountPassword'}
label={tStrings('system_os_password')}
type={'password'}
/>
<CheckboxForm
name={'shouldCreateServer'}
label={t('should_create_vm')}
className={'mt-3 relative'}
/>
<TemplatesSelectForm
disabled={
!watchShouldCreateServer || watchNodeId === ''
}
/>
<CheckboxForm
name={'startOnCompletion'}
label={t('start_server_after_installing')}
className={'mt-3 relative'}
/>
</Modal.Body>
<Modal.Actions>
<Modal.Action type='button' onClick={handleClose}>
{tStrings('cancel')}
</Modal.Action>
<Modal.Action
type='submit'
loading={form.formState.isSubmitting}
>
{tStrings('create')}
</Modal.Action>
</Modal.Actions>
</form>
</FormProvider>
</Modal>
)
}
export default CreateServerModal
export default CreateServerModal

View file

@ -5,13 +5,9 @@ import { Link } from 'react-router-dom'
import useServersSWR from '@/api/admin/servers/useServersSWR'
import { ServerBuild } from '@/api/server/getServer'
import Menu from '@/components/elements/Menu'
import Pagination from '@/components/elements/Pagination'
import Spinner from '@/components/elements/Spinner'
import Table, {
Actions,
ColumnArray,
} from '@/components/elements/displays/Table'
import Table, { ColumnArray } from '@/components/elements/displays/Table'
interface Props {
@ -68,35 +64,17 @@ const ServersTable = ({ query, className, nodeId, userId }: Props) => {
},
]
const rowActions = ({ row: server }: RowActionsProps<ServerBuild>) => {
return (
<Actions>
<Menu.Item
onClick={() => navigator.clipboard.writeText(server.uuid)}
>
Copy ID
</Menu.Item>
</Actions>
)
}
return (
<div className={className}>
{!data ? (
<Spinner />
) : (
<Pagination data={data} onPageSelect={setPage}>
{({ items }) => (
<Table
columns={columns}
data={items}
rowActions={rowActions}
/>
)}
{({ items }) => <Table columns={columns} data={items} />}
</Pagination>
)}
</div>
)
}
export default ServersTable
export default ServersTable

View file

@ -182,7 +182,7 @@ const ServerDetailsBlock = () => {
<Card className='flex flex-col justify-between items-center col-span-10 lg:col-span-2'>
<h5 className='h5'>{tStrings('bandwidth_usage')}</h5>
<div className='relative grid place-items-center mt-5'>
<div className='grid place-items-center mt-5'>
<h4 className='absolute text-3xl font-semibold text-foreground'>
{Math.floor(bandwidth.percentage)}
</h4>
@ -227,4 +227,4 @@ const ServerDetailsBlock = () => {
)
}
export default ServerDetailsBlock
export default ServerDetailsBlock

View file

@ -1,6 +1,6 @@
import { ServerContext } from '@/state/server'
import { useFlashKey } from '@/util/useFlash'
import { password, usKeyboardCharacters } from '@/util/validation'
import { englishKeyboardCharacters, password } from '@/util/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
@ -33,7 +33,7 @@ const ReinstallServerCard = () => {
const schema = z.object({
templateUuid: z.string().nonempty(),
accountPassword: password(usKeyboardCharacters()).nonempty(),
accountPassword: password(englishKeyboardCharacters()).nonempty(),
startOnCompletion: z.boolean(),
})
@ -128,4 +128,4 @@ const ReinstallServerCard = () => {
)
}
export default ReinstallServerCard
export default ReinstallServerCard

View file

@ -1,6 +1,6 @@
import { ServerContext } from '@/state/server'
import { useFlashKey } from '@/util/useFlash'
import { password, usKeyboardCharacters } from '@/util/validation'
import { englishKeyboardCharacters, password } from '@/util/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
@ -38,7 +38,7 @@ const AuthenticationCard = () => {
const passwordSchema = z.object({
sshKeys: z.string().optional(),
password: usKeyboardCharacters(password()),
password: englishKeyboardCharacters(password()),
})
const schema = type === 'password' ? passwordSchema : sshKeysSchema
@ -137,4 +137,4 @@ const AuthenticationCard = () => {
)
}
export default AuthenticationCard
export default AuthenticationCard

View file

@ -0,0 +1,38 @@
import { Action, Thunk, action, createContextStore, thunk } from 'easy-peasy'
import isEqual from 'react-fast-compare'
import getNode from '@/api/admin/nodes/getNode'
import { Node } from '@/api/admin/nodes/getNodes'
export interface NodeDataStore {
data?: Node
setNode: Action<NodeDataStore, Node>
getNode: Thunk<NodeDataStore, number>
}
interface NodeStore {
node: NodeDataStore
clearNodeState: Action<NodeStore>
}
const node: NodeDataStore = {
data: undefined,
setNode: action((state, payload) => {
if (!isEqual(payload, state.data)) {
state.data = payload
}
}),
getNode: thunk(async (actions, id) => {
const node = await getNode(id)
actions.setNode(node)
}),
}
export const NodeContext = createContextStore<NodeStore>({
node,
clearNodeState: action(state => {
state.node.data = undefined
}),
})

View file

@ -1,7 +1,6 @@
import { t } from 'i18next'
import { ZodNumber, ZodString, z } from 'zod'
export const hostname = (string?: ZodString) =>
(string ?? z.string()).regex(
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/,
@ -13,15 +12,18 @@ export const hostname = (string?: ZodString) =>
}
)
export const usKeyboardCharacters = (string?: ZodString) =>
(string ?? z.string()).regex(/^[\x20-\x7F]*$/, {
message: t('errors.invalid_string.us_keyboard_characters', {
ns: 'zod',
validation: t('us_keyboard_characters', {
ns: 'strings',
}).toLowerCase(),
})!,
})
export const englishKeyboardCharacters = (string?: ZodString) =>
(string ?? z.string()).regex(
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{}|;':",.\/<>?\\ ]*$/,
{
message: t('errors.invalid_string.english_keyboard_characters', {
ns: 'zod',
validation: t('english_keyboard_characters', {
ns: 'strings',
}).toLowerCase(),
})!,
}
)
export const password = (string?: ZodString) =>
(string ?? z.string()).regex(
@ -54,4 +56,4 @@ export const port = (number?: ZodNumber) =>
(number ?? z.number()).int().min(1).max(65535)
export const vmid = (number?: ZodNumber) =>
(number ?? z.number()).int().min(100).max(999999999)
(number ?? z.number()).int().min(100).max(999999999)

View file

@ -94,7 +94,8 @@ Route::prefix('/nodes')->group(function () {
| Endpoint: /api/admin/nodes/{node}/addresses
|
*/
Route::get('/addresses', [Admin\Nodes\AddressController::class, 'index']);
Route::resource('addresses', Admin\Nodes\AddressController::class)
->only(['index', 'store', 'update', 'destroy']);
/*
|--------------------------------------------------------------------------

View file

@ -94,7 +94,8 @@ Route::prefix('/nodes')->group(function () {
| Endpoint: /api/application/nodes/{node}/addresses
|
*/
Route::get('/addresses', [Admin\Nodes\AddressController::class, 'index']);
Route::apiResource('addresses', Admin\Nodes\AddressController::class)
->only(['index', 'store', 'update', 'destroy']);
/*
|--------------------------------------------------------------------------

View file

@ -7,18 +7,14 @@ use Illuminate\Support\Facades\Route;
Route::get('/servers', [Client\IndexController::class, 'index']);
Route::prefix('/servers/{server}')->middleware(
[ServerSubject::class, AuthenticateServerAccess::class],
)->group(function () {
Route::prefix('/servers/{server}')->middleware([ServerSubject::class, AuthenticateServerAccess::class])->group(function () {
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('servers.show');
Route::get('/details', [Client\Servers\ServerController::class, 'details']);
Route::get('/state', [Client\Servers\ServerController::class, 'getState']);
Route::patch('/state', [Client\Servers\ServerController::class, 'updateState']);
Route::post(
'/create-console-session', [Client\Servers\ServerController::class, 'createConsoleSession'],
);
Route::post('/create-console-session', [Client\Servers\ServerController::class, 'createConsoleSession']);
Route::prefix('/backups')->group(function () {
Route::get('/', [Client\Servers\BackupController::class, 'index']);
@ -29,26 +25,15 @@ Route::prefix('/servers/{server}')->middleware(
Route::prefix('/settings')->group(function () {
Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']);
Route::get(
'/template-groups', [Client\Servers\SettingsController::class, 'getTemplateGroups'],
);
Route::get('/template-groups', [Client\Servers\SettingsController::class, 'getTemplateGroups']);
Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']);
Route::get(
'/hardware/boot-order', [Client\Servers\SettingsController::class, 'getBootOrder'],
);
Route::put(
'/hardware/boot-order', [Client\Servers\SettingsController::class, 'updateBootOrder'],
);
Route::get('/hardware/boot-order', [Client\Servers\SettingsController::class, 'getBootOrder']);
Route::put('/hardware/boot-order', [Client\Servers\SettingsController::class, 'updateBootOrder']);
Route::get('/hardware/isos', [Client\Servers\SettingsController::class, 'getMedia']);
Route::post(
'/hardware/isos/{iso}/mount', [Client\Servers\SettingsController::class, 'mountMedia'],
)->withoutScopedBindings();
Route::post(
'/hardware/isos/{iso}/unmount',
[Client\Servers\SettingsController::class, 'unmountMedia'],
)->withoutScopedBindings();
Route::post('/hardware/isos/{iso}/mount', [Client\Servers\SettingsController::class, 'mountMedia']);
Route::post('/hardware/isos/{iso}/unmount', [Client\Servers\SettingsController::class, 'unmountMedia']);
Route::get('/network', [Client\Servers\SettingsController::class, 'getNetworkSettings']);
Route::put('/network', [Client\Servers\SettingsController::class, 'updateNetworkSettings']);

View file

@ -1,56 +0,0 @@
<?php
use Convoy\Models\Location;
use Convoy\Models\User;
it('can fetch locations', function () {
$user = User::factory()->create([
'root_admin' => true,
]);
$response = $this->actingAs($user)->getJson('/api/admin/locations');
$response->assertOk();
});
it('can create a location', function () {
$user = User::factory()->create([
'root_admin' => true,
]);
$response = $this->actingAs($user)->postJson('/api/admin/locations', [
'name' => 'Test Location',
'short_code' => 'test',
'description' => 'This is a test location.',
]);
$response->assertOk();
});
it('can update a location', function () {
$user = User::factory()->create([
'root_admin' => true,
]);
$location = Location::factory()->create();
$response = $this->actingAs($user)->putJson("/api/admin/locations/{$location->id}", [
'name' => 'Test Location',
'short_code' => 'test',
'description' => 'This is a test location.',
]);
$response->assertOk();
});
it('can delete a location', function () {
$user = User::factory()->create([
'root_admin' => true,
]);
$location = Location::factory()->create();
$response = $this->actingAs($user)->deleteJson("/api/admin/locations/{$location->id}");
$response->assertNoContent();
});

View file

@ -1,28 +0,0 @@
<?php
use Convoy\Models\AddressPool;
use Convoy\Models\AddressPoolToNode;
use Convoy\Models\Location;
use Convoy\Models\Node;
use Convoy\Models\User;
beforeEach(function () {
$this->user = User::factory()->create([
'root_admin' => true,
]);
$this->location = Location::factory()->create();
$this->node = Node::factory()->for($this->location)->create();
$this->pool = AddressPool::factory()->create();
AddressPoolToNode::create([
'address_pool_id' => $this->pool->id,
'node_id' => $this->node->id,
]);
});
it('can fetch addresses', function () {
$response = $this->actingAs($this->user)->getJson(
"/api/admin/nodes/{$this->node->id}/addresses",
);
$response->assertOk();
});

View file

@ -1,122 +0,0 @@
<?php
use Convoy\Jobs\Node\MonitorIsoDownloadJob;
use Convoy\Models\ISO;
use Convoy\Models\Location;
use Convoy\Models\Node;
use Convoy\Models\User;
beforeEach(function () {
$this->user = User::factory()->create([
'root_admin' => true,
]);
$this->location = Location::factory()->create();
$this->node = Node::factory()->for($this->location)->create();
});
it('can fetch ISOs', function () {
$response = $this->actingAs($this->user)->getJson(
"/api/admin/nodes/{$this->node->id}/isos",
);
$response->assertOk();
});
it('can create an ISO', function () {
Http::fake([
'*/download-url' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Node/Storage/DownloadIsoData.json'),
),
200,
),
'*/query-url-metadata*' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Node/Storage/QueryIsoData.json'),
),
200,
),
]);
$response = $this->actingAs($this->user)->postJson(
"/api/admin/nodes/{$this->node->id}/isos",
[
'name' => 'Test ISO',
'file_name' => 'test.iso',
'link' => 'https://example.com/test.iso',
'should_download' => true,
'hidden' => false,
],
);
Queue::assertPushed(MonitorIsoDownloadJob::class);
$response->assertOk();
});
it("can't create an ISO with file_name taken", function () {
ISO::factory()->for($this->node)->create([
'file_name' => 'duplicate.iso',
]);
$response = $this->actingAs($this->user)->postJson(
"/api/admin/nodes/{$this->node->id}/isos",
[
'name' => 'Test ISO',
'file_name' => 'duplicate.iso',
'link' => 'https://example.com/duplicate.iso',
'should_download' => true,
'hidden' => false,
],
);
$response->assertStatus(422);
});
it('can update an ISO', function () {
$iso = ISO::factory()->for($this->node)->create();
$response = $this->actingAs($this->user)->putJson(
"/api/admin/nodes/{$this->node->id}/isos/{$iso->uuid}",
[
'name' => 'Updated ISO',
],
);
$response->assertOk();
});
it('can delete an ISO', function () {
Http::fake([
'*/storage/*/content/*' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Node/Storage/DeleteIsoData.json'),
), 200,
),
]);
$iso = ISO::factory()->for($this->node)->create();
$response = $this->actingAs($this->user)->deleteJson(
"/api/admin/nodes/{$this->node->id}/isos/{$iso->uuid}",
);
$response->assertNoContent();
});
it('can query link', function () {
Http::fake([
'*/query-url-metadata*' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Node/Storage/QueryIsoData.json'),
),
200,
),
]);
$response = $this->actingAs($this->user)->getJson(
'/tools/query-remote-file?link=' . urlencode(
'https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso',
),
);
$response->assertOk();
});

View file

@ -1,149 +0,0 @@
<?php
use Convoy\Models\Location;
use Convoy\Models\Node;
use Convoy\Models\Server;
use Convoy\Models\User;
beforeEach(function () {
$this->user = User::factory()->create([
'root_admin' => true,
]);
$this->location = Location::factory()->create();
$this->node = Node::factory()->for($this->location)->create();
});
it('can fetch nodes', function () {
$response = $this->actingAs($this->user)->getJson('/api/admin/nodes');
$response->assertOk();
});
it('can fetch a node', function () {
$response = $this->actingAs($this->user)->getJson("/api/admin/nodes/{$this->node->id}");
$response->assertOk();
});
it('can create a node', function () {
$response = $this->actingAs($this->user)->postJson('/api/admin/nodes', [
'location_id' => $this->location->id,
'name' => 'Test Node',
'cluster' => 'proxmox',
'fqdn' => 'example.com',
'token_id' => 'test-token',
'secret' => 'test-secret',
'port' => 8006,
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'memory_overallocate' => 0,
'disk' => 512 * 1024 * 1024 * 1024, // 512GB,
'disk_overallocate' => 0,
'vm_storage' => 'local-lvm',
'backup_storage' => 'local-lvm',
'iso_storage' => 'local-lvm',
'network' => 'vmbr0',
]);
$response->assertOk();
});
it('can update a node', function () {
$response = $this->actingAs($this->user)->putJson("/api/admin/nodes/{$this->node->id}", [
'location_id' => $this->location->id,
'name' => 'Test Node',
'cluster' => 'proxmox',
'fqdn' => 'example.com',
'token_id' => 'test-token',
'secret' => 'test-secret',
'port' => 8006,
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'memory_overallocate' => 0,
'disk' => 512 * 1024 * 1024 * 1024, // 512GB,
'disk_overallocate' => 0,
'vm_storage' => 'local-lvm',
'backup_storage' => 'local-lvm',
'iso_storage' => 'local-lvm',
'network' => 'vmbr0',
]);
$response->assertOk();
});
it("can't downsize without over-allocating", function () {
$node = Node::factory()->for($this->location)->create([
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'disk' => 512 * 1024 * 1024 * 1024, // 512GB,
]);
Server::factory()->for($node)->for($this->user)->create([
'memory' => 32 * 1024 * 1024 * 1024, // 32GB,
'disk' => 256 * 1024 * 1024 * 1024, // 256GB,
]);
$response = $this->actingAs($this->user)->putJson("/api/admin/nodes/{$node->id}", [
'location_id' => $this->location->id,
'name' => 'Test Node',
'cluster' => 'proxmox',
'fqdn' => 'example.com',
'token_id' => 'test-token',
'secret' => 'test-secret',
'port' => 8006,
'memory' => 16 * 1024 * 1024 * 1024, // 16GB,
'memory_overallocate' => 0,
'disk' => 128 * 1024 * 1024 * 1024, // 128GB,
'disk_overallocate' => 0,
'vm_storage' => 'local-lvm',
'backup_storage' => 'local-lvm',
'iso_storage' => 'local-lvm',
'network' => 'vmbr0',
]);
$response->assertStatus(422);
});
it('can update node without false positive overallocation', function () {
$node = Node::factory()->for($this->location)->create([
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'disk' => 512 * 1024 * 1024 * 1024, // 512GB,
]);
Server::factory()->for($node)->for($this->user)->create([
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'disk' => 256 * 1024 * 1024 * 1024, // 256GB,
]);
$response = $this->actingAs($this->user)->putJson("/api/admin/nodes/{$node->id}", [
'location_id' => $this->location->id,
'name' => 'New name',
'cluster' => 'proxmox',
'fqdn' => 'example.com',
'token_id' => 'test-token',
'secret' => 'test-secret',
'port' => 8006,
'memory' => 64 * 1024 * 1024 * 1024, // 64GB,
'memory_overallocate' => 0,
'disk' => 512 * 1024 * 1024 * 1024, // 512GB,
'disk_overallocate' => 0,
'vm_storage' => 'local-lvm',
'backup_storage' => 'local-lvm',
'iso_storage' => 'local-lvm',
'network' => 'vmbr0',
]);
$response->assertOk();
});
it('can delete a node', function () {
$response = $this->actingAs($this->user)->deleteJson("/api/admin/nodes/{$this->node->id}");
$response->assertNoContent();
});
it("can't delete a node with servers", function () {
Server::factory()->for($this->node)->for($this->user)->create();
$response = $this->actingAs($this->user)->deleteJson("/api/admin/nodes/{$this->node->id}");
$response->assertForbidden();
});

View file

@ -1,75 +1,117 @@
<?php
use Convoy\Jobs\Server\MonitorBackupJob;
use Convoy\Jobs\Server\MonitorBackupRestorationJob;
use Convoy\Models\Backup;
use Convoy\Models\User;
use Convoy\Models\Backup;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Convoy\Jobs\Server\MonitorBackupJob;
use Convoy\Jobs\Server\MonitorBackupRestorationJob;
function testCreateBackup(
bool $useSecondUser = false,
bool $secondUserIsAdmin = false,
): Closure
{
return function () use ($useSecondUser, $secondUserIsAdmin) {
beforeEach(fn () => Http::preventStrayRequests());
it('can create backups', function () {
Queue::fake();
Http::fake([
'*' => Http::response(['data' => 'upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/backups", [
'name' => 'Test Backup',
'mode' => 'snapshot',
'compression_type' => 'none',
'is_locked' => false,
]);
$response->assertOk()
->assertJsonPath('data.name', 'Test Backup')
->assertJsonPath('data.is_locked', 0);
Queue::assertPushed(MonitorBackupJob::class);
});
it('can restore backups', function () {
Queue::fake();
Http::fake([
'*/status/current' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetStoppedServerStatusData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$backup = Backup::factory()->create([
'is_successful' => true,
'is_locked' => false,
'server_id' => $server->id,
]);
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}/restore");
$response->assertNoContent();
Queue::assertPushed(MonitorBackupRestorationJob::class);
});
it('can delete backups', function () {
Http::fake([
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$backup = Backup::factory()->create([
'is_successful' => true,
'is_locked' => false,
'server_id' => $server->id,
]);
$response = $this->actingAs($user)->deleteJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}");
$response->assertNoContent();
});
describe('admin', function () {
it('can create backups', function () {
Queue::fake();
Http::fake([
'*' => Http::response(['data' => 'upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
[$_, $_, $_, $server] = createServerModel();
if ($useSecondUser) {
$user = User::factory()->create([
'root_admin' => $secondUserIsAdmin,
]);
}
$admin = User::factory()->create([
'root_admin' => true,
]);
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/backups", [
$response = $this->actingAs($admin)->postJson("/api/client/servers/{$server->uuid}/backups", [
'name' => 'Test Backup',
'mode' => 'snapshot',
'compression_type' => 'none',
'is_locked' => false,
],
);
if ($useSecondUser && !$secondUserIsAdmin) {
$response->assertNotFound();
return;
}
]);
$response->assertOk()
->assertJsonPath('data.name', 'Test Backup')
->assertJsonPath('data.is_locked', 0);
Queue::assertPushed(MonitorBackupJob::class);
};
}
});
function testRestoreBackups(
bool $useSecondUser = false,
bool $secondUserIsAdmin = false,
): Closure
{
return function () use ($useSecondUser, $secondUserIsAdmin) {
it('can restore backups', function () {
Queue::fake();
Http::fake([
'*/status/current' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetStoppedServerStatusData.json'),
), 200,
),
'*/status/current' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetStoppedServerStatusData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
[$_, $_, $_, $server] = createServerModel();
if ($useSecondUser) {
$user = User::factory()->create([
'root_admin' => $secondUserIsAdmin,
]);
}
$admin = User::factory()->create([
'root_admin' => true,
]);
$backup = Backup::factory()->create([
'is_successful' => true,
@ -77,40 +119,23 @@ function testRestoreBackups(
'server_id' => $server->id,
]);
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/backups/{$backup->uuid}/restore",
);
if ($useSecondUser && !$secondUserIsAdmin) {
$response->assertNotFound();
return;
}
$response = $this->actingAs($admin)->postJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}/restore");
$response->assertNoContent();
Queue::assertPushed(MonitorBackupRestorationJob::class);
};
}
});
function testDeleteBackups(
bool $useSecondUser = false,
bool $secondUserIsAdmin = false,
): Closure
{
return function () use ($useSecondUser, $secondUserIsAdmin) {
it('can delete backups', function () {
Http::fake([
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
if ($useSecondUser) {
$user = User::factory()->create([
'root_admin' => $secondUserIsAdmin,
]);
}
[$_, $_, $_, $server] = createServerModel();
$admin = User::factory()->create([
'root_admin' => true,
]);
$backup = Backup::factory()->create([
'is_successful' => true,
@ -118,79 +143,9 @@ function testDeleteBackups(
'server_id' => $server->id,
]);
$response = $this->actingAs($user)->deleteJson(
"/api/client/servers/{$server->uuid}/backups/{$backup->uuid}",
);
if ($useSecondUser && !$secondUserIsAdmin) {
$response->assertNotFound();
return;
}
$response = $this->actingAs($admin)->deleteJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}");
$response->assertNoContent();
};
}
it('can create backups', testCreateBackup());
it('can restore backups', testRestoreBackups());
it('can delete backups', testDeleteBackups());
describe('other servers', function () {
beforeEach(function () {
[$_, $_, $_, $server] = createServerModel();
$this->backup = Backup::factory()->for($server)->create();
});
it("can't restore another's backup", function () {
Http::fake([
'*/status/current' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetStoppedServerStatusData.json'),
), 200,
),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/backups/{$this->backup->uuid}/restore",
);
$response->assertNotFound();
});
it("can't delete another's backup", function () {
Http::fake([
'*' => Http::response(['data' => 'upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->deleteJson(
"/api/client/servers/{$server->uuid}/backups/{$this->backup->uuid}",
);
$response->assertNotFound();
});
});
describe('admin', function () {
it('can create backups', testCreateBackup(true, true));
it('can restore backups', testRestoreBackups(true, true));
it('can delete backups', testDeleteBackups(true, true));
});
describe('unauthorized users', function () {
it("can't create backups", testCreateBackup(true));
it("can't restore backups", testRestoreBackups(true));
it("can't delete backups", testDeleteBackups(true));
});

View file

@ -1,34 +1,20 @@
<?php
beforeEach(fn () => Http::preventStrayRequests());
it('can generate noVNC authorization token', function () {
Http::fake([
'*/api2/json/access/users' => Http::response(
file_get_contents(base_path('tests/Fixtures/Repositories/Node/CreateUserData.json')),
200,
),
'*/api2/json/access/roles' => Http::response(
file_get_contents(base_path('tests/Fixtures/Repositories/Node/CreateRoleData.json')),
200,
),
'*/api2/json/access/acl' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/AddUserToServerData.json'),
), 200,
),
'*/api2/json/access/ticket' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Node/CreateUserTicketData.json'),
), 200,
),
'*/api2/json/access/users' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Node/CreateUserData.json')), 200),
'*/api2/json/access/roles' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Node/CreateRoleData.json')), 200),
'*/api2/json/access/acl' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/AddUserToServerData.json')), 200),
'*/api2/json/access/ticket' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Node/CreateUserTicketData.json')), 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/create-console-session", [
'type' => 'novnc',
],
);
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/create-console-session", [
'type' => 'novnc'
]);
$response->assertOk();
});

View file

@ -3,6 +3,8 @@
use Convoy\Models\ISO;
use Illuminate\Support\Facades\Http;
beforeEach(fn () => Http::preventStrayRequests());
it('can rename servers', function () {
Http::fake([
'*' => Http::response(['data' => 'dummy-upid'], 200),
@ -10,90 +12,66 @@ it('can rename servers', function () {
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/settings/rename", [
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/settings/rename", [
'name' => 'advinservers is king',
'hostname' => 'advinservers.com',
],
);
]);
$response->assertOk()
->assertJsonPath('data.name', 'advinservers is king')
->assertJsonPath('data.hostname', 'advinservers.com');
->assertJsonPath('data.name', 'advinservers is king')
->assertJsonPath('data.hostname', 'advinservers.com');
});
it('can change nameservers', function () {
Http::fake([
'*/config' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json'),
), 200,
),
'*/config' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->putJson(
"/api/client/servers/{$server->uuid}/settings/network", [
$response = $this->actingAs($user)->putJson("/api/client/servers/{$server->uuid}/settings/network", [
'nameservers' => [
'1.1.1.1',
'1.0.0.1',
],
],
);
]);
$response->assertOk();
});
it('can fetch sshkeys', function () {
Http::fake([
'*/config' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json'),
), 200,
),
'*/config' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->getJson(
"/api/client/servers/{$server->uuid}/settings/auth",
);
$response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/settings/auth");
$response->assertOk();
});
it('can change server passwords', function () {
Http::fake([
'*/config' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json'),
), 200,
),
'*/config' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$user, $_, $_, $server] = createServerModel();
$response = $this->actingAs($user)->putJson(
"/api/client/servers/{$server->uuid}/settings/auth", [
$response = $this->actingAs($user)->putJson("/api/client/servers/{$server->uuid}/settings/auth", [
'type' => 'password',
'password' => 'Advinservers is king!123',
],
);
]);
$response->assertNoContent();
});
it('can fetch available ISOs', function () {
Http::fake([
'*/config' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json'),
), 200,
),
'*/config' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
@ -104,20 +82,14 @@ it('can fetch available ISOs', function () {
'hidden' => false,
]);
$response = $this->actingAs($user)->getJson(
"/api/client/servers/{$server->uuid}/settings/hardware/isos",
);
$response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/settings/hardware/isos");
$response->assertOk();
});
it('can mount visible ISOs', function () {
Http::fake([
'*/config' => Http::response(
file_get_contents(
base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json'),
), 200,
),
'*/config' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerConfigData.json')), 200),
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
@ -128,9 +100,7 @@ it('can mount visible ISOs', function () {
'hidden' => false,
]);
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/settings/hardware/isos/{$iso->uuid}/mount",
);
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/settings/hardware/isos/{$iso->uuid}/mount");
$response->assertNoContent();
});
@ -143,9 +113,7 @@ it('can\'t mount hidden ISOs as non-admin user', function () {
'hidden' => true,
]);
$response = $this->actingAs($user)->postJson(
"/api/client/servers/{$server->uuid}/settings/hardware/isos/{$iso->uuid}/mount",
);
$response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/settings/hardware/isos/{$iso->uuid}/mount");
$response->assertStatus(403);
});

View file

@ -1,4 +0,0 @@
{
"success": 1,
"data": null
}

View file

@ -1,4 +0,0 @@
{
"data": "UPID:us-southeast:001BE662:04671913:65CF8596:download:virtio-win.iso:root@pam:",
"success": 1
}

View file

@ -1,8 +0,0 @@
{
"data": {
"filename": "virtio-win.iso",
"size": 627519488,
"mimetype": "application/octet-stream"
},
"success": 1
}

View file

@ -1,12 +1,10 @@
<?php
use Convoy\Models\Location;
use Convoy\Models\Node;
use Convoy\Models\Server;
use Convoy\Models\User;
use Convoy\Models\Server;
use Convoy\Models\Location;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
/*
|--------------------------------------------------------------------------
@ -22,11 +20,8 @@ use Illuminate\Support\Facades\Queue;
uses(
Tests\TestCase::class,
DatabaseTransactions::class,
// Illuminate\Foundation\Testing\RefreshDatabase::class,
)->beforeEach(function () {
Http::preventStrayRequests();
Queue::fake();
})->in('Feature', 'Unit');
// Illuminate\Foundation\Testing\RefreshDatabase::class,
)->in('Feature', 'Unit');
/*
|--------------------------------------------------------------------------
@ -39,9 +34,9 @@ uses(
|
*/
//expect()->extend('toBeOne', function () {
// return $this->toBe(1);
//});
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
@ -54,7 +49,7 @@ uses(
|
*/
function createServerModel(): array
function createServerModel()
{
$location = Location::factory()->create();
/** @var User $user */

View file

@ -1,30 +1,14 @@
<?php
use Convoy\Services\Nodes\ServerRateLimitsSyncService;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Convoy\Services\Nodes\ServerRateLimitsSyncService;
beforeEach(fn () => Http::preventStrayRequests());
it('can rate limit servers if over limit', function () {
Http::fake([
'/api2/json/nodes/*/qemu/*/config' => Http::sequence()
->push(
file_get_contents(
base_path(
'tests/Fixtures/Repositories/Server/GetServerConfigData.json',
),
),
200
)
->push(
file_get_contents(
base_path(
'tests/Fixtures/Repositories/Server/GetServerConfigData.json',
),
),
200,
)
->push(['data' => 'dummy-upid'], 200)
'*' => Http::response(['data' => 'dummy-upid'], 200),
]);
[$_, $_, $node, $server] = createServerModel();

View file

@ -1,18 +1,14 @@
<?php
use Carbon\Carbon;
use Convoy\Services\Nodes\ServerUsagesSyncService;
use Illuminate\Support\Facades\Http;
use Convoy\Services\Nodes\ServerUsagesSyncService;
beforeEach(fn () => Http::preventStrayRequests());
it('can sync server usages', function () {
Http::fake([
'*/rrddata*' => Http::response(
file_get_contents(
base_path(
'tests/Fixtures/Repositories/Server/GetServerMetricsData.hourly.average.json',
),
), 200,
),
'*/rrddata*' => Http::response(file_get_contents(base_path('tests/Fixtures/Repositories/Server/GetServerMetricsData.hourly.average.json')), 200),
]);
[$_, $_, $node, $server] = createServerModel();