Compare commits
70 commits
template-s
...
develop
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bdc9c413e2 | ||
![]() |
2b16841702 | ||
![]() |
1d6390ddd6 | ||
![]() |
3eb3c7e746 | ||
![]() |
b47adacb95 | ||
![]() |
bc6f9e4c42 | ||
![]() |
3643a717e9 | ||
![]() |
a9214aa820 | ||
![]() |
561aca99df | ||
![]() |
2841c09510 | ||
![]() |
bdaf07a0dc | ||
![]() |
b3b96ee361 | ||
![]() |
4558b31385 | ||
![]() |
98bf2ec95c | ||
![]() |
7957141128 | ||
![]() |
7380d50fc2 | ||
![]() |
c780ca0218 | ||
![]() |
63ee5f2a2b | ||
![]() |
75f81a9b4e | ||
![]() |
f1fda8ba0a | ||
![]() |
1bf95fa2bd | ||
![]() |
a14d754e79 | ||
![]() |
a19a5d5907 | ||
![]() |
a368951705 | ||
![]() |
93b641be86 | ||
![]() |
80672e249e | ||
![]() |
8fe890a5e1 | ||
![]() |
27e9e821b6 | ||
![]() |
31f93264a5 | ||
![]() |
290b1c34ca | ||
![]() |
db40a05daf | ||
![]() |
504db753dc | ||
![]() |
dfeefc0907 | ||
![]() |
31e2ef82d9 | ||
![]() |
903ae10e0d | ||
![]() |
1893eca377 | ||
![]() |
d526ef4b86 | ||
![]() |
68440a78ec | ||
![]() |
0e674742f2 | ||
![]() |
00d903ec1b | ||
![]() |
52e9c05963 | ||
![]() |
93ff72584a | ||
![]() |
e3fec37359 | ||
![]() |
35bc7da3f0 | ||
![]() |
7f44101875 | ||
![]() |
1b1e62cf99 | ||
![]() |
874857e577 | ||
![]() |
5a1aff0f48 | ||
![]() |
ab24c2b177 | ||
![]() |
ca8bdb5ebb | ||
![]() |
570dabccb4 | ||
![]() |
83314c9888 | ||
![]() |
fe78aa8d85 | ||
![]() |
18605091c9 | ||
![]() |
c802a8cb84 | ||
![]() |
620e4b4c6a | ||
![]() |
73f36839c0 | ||
![]() |
137f94a89c | ||
![]() |
727f1449b8 | ||
![]() |
cb737161d9 | ||
![]() |
85cf136cb1 | ||
![]() |
d544501c12 | ||
![]() |
110b623383 | ||
![]() |
20cc82e90a | ||
![]() |
7c678fae4d | ||
![]() |
cec4d437ff | ||
![]() |
4ec316c67c | ||
![]() |
970278ff2c | ||
![]() |
24027698b4 | ||
![]() |
52593e47c7 |
81 changed files with 1433 additions and 798 deletions
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
|
@ -25,8 +25,34 @@ jobs:
|
|||
|
||||
- name: Create Release Archive
|
||||
run: |
|
||||
rm -rf node_modules/ tests/ CODE_OF_CONDUCT.md CONTRIBUTOR_LICENSE_AGREEMENT crowdin.yml docker-compose.ci.yml phpstan.neon phpunit.xml
|
||||
tar -czf panel.tar.gz *
|
||||
# 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"
|
||||
)
|
||||
|
||||
# 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
|
||||
id: extract_changelog
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -18,4 +18,5 @@ yarn-error.log
|
|||
_ide_*.php
|
||||
stats.html
|
||||
.fleet
|
||||
lang/php_*.json
|
||||
lang/php_*.json
|
||||
.phpunit.cache
|
49
CHANGELOG.md
49
CHANGELOG.md
|
@ -4,6 +4,55 @@ 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
|
||||
|
||||
### 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
|
||||
|
|
130
LICENSE.md
130
LICENSE.md
|
@ -1,40 +1,124 @@
|
|||
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.
|
||||
# Convoy Software End User License Agreement (EULA)
|
||||
|
||||
Additional Use Grant
|
||||
**Effective Date:** March 3th, 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.
|
||||
**Last Updated:** March 13th, 2024
|
||||
|
||||
Change License
|
||||
**License Grantor:** Performave
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
Terms
|
||||
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.
|
||||
|
||||
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. License Grant
|
||||
|
||||
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.
|
||||
### 2.1 Personal Use License
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
### 2.2 Enterprise License
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
### 2.3 Non-Profit Organization License
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
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.
|
||||
### 2.4 Partnership Licenses
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Covenants of Licensor
|
||||
### 2.5 Insider License
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
|
|
|
@ -25,6 +25,6 @@ Please [visit this page](https://convoypanel.com/docs/project/about.html#acknowl
|
|||
|
||||
## License
|
||||
|
||||
Convoy is licensed under the Business Source License. Production use of Convoy without an active license from Performave is strictly disallowed.
|
||||
Convoy is licensed under our own proprietary license.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FConvoyPanel%2Fpanel?ref=badge_large)
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace Convoy\Exceptions\Service\Deployment;
|
||||
|
||||
use Convoy\Exceptions\ConvoyException;
|
||||
use Convoy\Exceptions\DisplayException;
|
||||
|
||||
class InvalidTemplateException extends ConvoyException
|
||||
class InvalidTemplateException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Convoy\Exceptions\Service\Network;
|
||||
|
||||
use Convoy\Exceptions\ConvoyException;
|
||||
use Convoy\Exceptions\DisplayException;
|
||||
|
||||
class AddressInUseException extends ConvoyException
|
||||
class AddressInUseException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TooManyBackupsException constructor.
|
||||
|
@ -12,7 +12,7 @@ class AddressInUseException extends ConvoyException
|
|||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Convoy\Exceptions\Service\Node\IsoLibrary;
|
||||
|
||||
use Convoy\Exceptions\ConvoyException;
|
||||
use Convoy\Exceptions\DisplayException;
|
||||
|
||||
class InvalidIsoLinkException extends ConvoyException
|
||||
class InvalidIsoLinkException extends DisplayException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Convoy\Exceptions\Service\Server\Allocation;
|
||||
|
||||
use Convoy\Exceptions\ConvoyException;
|
||||
use Convoy\Exceptions\DisplayException;
|
||||
|
||||
class IsoAlreadyUnmountedException extends ConvoyException
|
||||
class IsoAlreadyUnmountedException extends DisplayException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
namespace Convoy\Exceptions\Service\Server\Allocation;
|
||||
|
||||
use Convoy\Exceptions\ConvoyException;
|
||||
use Convoy\Exceptions\DisplayException;
|
||||
|
||||
class NoAvailableDiskInterfaceException extends ConvoyException
|
||||
class NoAvailableDiskInterfaceException extends DisplayException
|
||||
{
|
||||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?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.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?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.');
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ 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())],
|
||||
|
|
|
@ -2,28 +2,16 @@
|
|||
|
||||
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())
|
||||
|
@ -44,63 +32,4 @@ 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ 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(),
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace Convoy\Http\Requests\Admin\Nodes\Isos;
|
||||
|
||||
use Convoy\Models\ISO;
|
||||
use Convoy\Http\Requests\BaseApiRequest;
|
||||
use Convoy\Models\ISO;
|
||||
|
||||
class UpdateIsoRequest extends BaseApiRequest
|
||||
{
|
||||
|
@ -16,4 +16,5 @@ class UpdateIsoRequest extends BaseApiRequest
|
|||
'hidden' => $rules['hidden'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use Convoy\Http\Requests\BaseApiRequest;
|
|||
use Convoy\Models\Address;
|
||||
use Convoy\Models\Node;
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Rules\EnglishKeyboardCharacters;
|
||||
use Convoy\Rules\Password;
|
||||
use Convoy\Rules\USKeyboardCharacters;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
/**
|
||||
|
@ -23,6 +23,7 @@ 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',
|
||||
|
@ -35,7 +36,7 @@ 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 EnglishKeyboardCharacters()],
|
||||
), new USKeyboardCharacters()],
|
||||
'should_create_server' => 'present|boolean',
|
||||
'template_uuid' => 'required_if:create_server,1|string|exists:templates,uuid',
|
||||
'start_on_completion' => 'present|boolean',
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace Convoy\Http\Requests\Client\Servers\Backups;
|
||||
|
||||
use Convoy\Http\Requests\BaseApiRequest;
|
||||
use Convoy\Models\Backup;
|
||||
use Convoy\Models\Server;
|
||||
|
||||
class DeleteBackupRequest extends BaseApiRequest
|
||||
|
@ -11,8 +10,7 @@ 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', [$backup, $server]);
|
||||
return $this->user()->can('delete', $server);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace Convoy\Http\Requests\Client\Servers\Backups;
|
||||
|
||||
use Convoy\Http\Requests\BaseApiRequest;
|
||||
use Convoy\Models\Backup;
|
||||
use Convoy\Models\Server;
|
||||
|
||||
class RestoreBackupRequest extends BaseApiRequest
|
||||
|
@ -11,8 +10,7 @@ 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', [$backup, $server]);
|
||||
return $this->user()->can('restore', $server);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,18 @@
|
|||
|
||||
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', [Backup::class, $this->parameter('server', Server::class)]);
|
||||
return $this->user()->can('create', $this->parameter('server', Server::class));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
namespace Convoy\Http\Requests\Client\Servers\Settings;
|
||||
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Rules\Password;
|
||||
use Convoy\Models\Template;
|
||||
use Convoy\Http\Requests\BaseApiRequest;
|
||||
use Convoy\Rules\EnglishKeyboardCharacters;
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Models\Template;
|
||||
use Convoy\Rules\Password;
|
||||
use Convoy\Rules\USKeyboardCharacters;
|
||||
|
||||
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 EnglishKeyboardCharacters()],
|
||||
), new USKeyboardCharacters()],
|
||||
'start_on_completion' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,17 +2,15 @@
|
|||
|
||||
namespace Convoy\Http\Requests\Client\Servers\Settings;
|
||||
|
||||
use Convoy\Enums\Server\AuthenticationType;
|
||||
use Convoy\Http\Requests\BaseApiRequest;
|
||||
use Convoy\Models\Server;
|
||||
use Exception;
|
||||
use Convoy\Rules\Password;
|
||||
use Faker\Provider\Base;
|
||||
use Illuminate\Validation\Validator;
|
||||
use Convoy\Rules\USKeyboardCharacters;
|
||||
use Exception;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Illuminate\Validation\Validator;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
use Convoy\Enums\Server\AuthenticationType;
|
||||
use Convoy\Rules\EnglishKeyboardCharacters;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAuthSettingsRequest extends BaseApiRequest
|
||||
{
|
||||
|
@ -26,7 +24,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 EnglishKeyboardCharacters(
|
||||
'password' => ['string', 'min:8', 'max:191', new Password(), new USKeyboardCharacters(
|
||||
), 'exclude_unless:type,password'],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
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'];
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
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 = [
|
||||
|
|
|
@ -13,11 +13,15 @@ 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);
|
||||
|
|
|
@ -49,7 +49,6 @@ class Node extends Model
|
|||
'memory_overallocate' => 'required|integer',
|
||||
'disk' => 'required|integer',
|
||||
'disk_overallocate' => 'required|integer',
|
||||
'template_storage' => ['required', 'string', 'max:191', 'regex:/^\S*$/u'],
|
||||
'vm_storage' => ['required', 'string', 'max:191', 'regex:/^\S*$/u'],
|
||||
'backup_storage' => ['required', 'string', 'max:191', 'regex:/^\S*$/u'],
|
||||
'iso_storage' => ['required', 'string', 'max:191', 'regex:/^\S*$/u'],
|
||||
|
|
|
@ -2,27 +2,18 @@
|
|||
|
||||
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, Backup|string $backup, Server $server): ?bool
|
||||
public function before(User $user, string $ability, 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,11 +38,13 @@ 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'));
|
||||
});
|
||||
|
||||
|
@ -50,11 +52,13 @@ 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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,12 +15,21 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,10 +42,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) {
|
||||
|
|
|
@ -2,20 +2,26 @@
|
|||
|
||||
namespace Convoy\Repositories\Proxmox\Node;
|
||||
|
||||
use Convoy\Models\Node;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Convoy\Data\Helpers\ChecksumData;
|
||||
use Convoy\Data\Node\Storage\IsoData;
|
||||
use Convoy\Data\Node\Storage\FileMetaData;
|
||||
use Convoy\Data\Node\Storage\IsoData;
|
||||
use Convoy\Enums\Node\Storage\ContentType;
|
||||
use Convoy\Repositories\Proxmox\ProxmoxRepository;
|
||||
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
|
||||
use Convoy\Exceptions\Service\Node\IsoLibrary\InvalidIsoLinkException;
|
||||
use Convoy\Models\Node;
|
||||
use Convoy\Repositories\Proxmox\ProxmoxRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
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):\/\//');
|
||||
|
@ -33,12 +39,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);
|
||||
}
|
||||
|
@ -48,28 +54,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()
|
||||
public function getIsos(): DataCollection
|
||||
{
|
||||
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);
|
||||
|
||||
|
@ -86,23 +92,29 @@ 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):\/\//');
|
||||
|
||||
$response = $this->getHttpClient()
|
||||
->withUrlParameters([
|
||||
'node' => $this->node->cluster,
|
||||
])
|
||||
->get('/api2/json/nodes/{node}/query-url-metadata', [
|
||||
'url' => $link,
|
||||
'verify-certificates' => $verifyCertificates,
|
||||
])
|
||||
->json();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
if (Arr::get($response, 'success', 1) !== 1) {
|
||||
throw new InvalidIsoLinkException;
|
||||
throw new InvalidIsoLinkException();
|
||||
}
|
||||
|
||||
$data = $this->getData($response);
|
||||
|
|
|
@ -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 Webmozart\Assert\Assert;
|
||||
use Convoy\Enums\Server\BackupMode;
|
||||
use Convoy\Enums\Server\BackupCompressionType;
|
||||
use Convoy\Repositories\Proxmox\ProxmoxRepository;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
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 ? 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 ? (int)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);
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
namespace Convoy\Repositories\Proxmox\Server;
|
||||
|
||||
use Convoy\Models\Server;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Convoy\Repositories\Proxmox\ProxmoxRepository;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class ProxmoxConfigRepository extends ProxmoxRepository
|
||||
{
|
||||
public function getConfig()
|
||||
public function getConfig(): array
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class ProxmoxServerRepository extends ProxmoxRepository
|
|||
/**
|
||||
* @throws ProxmoxConnectionException
|
||||
*/
|
||||
public function getState(): ServerStateData
|
||||
public function getState()
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
||||
}
|
16
app/Rules/USKeyboardCharacters.php
Normal file
16
app/Rules/USKeyboardCharacters.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
public function delete(Node $node, ISO $iso): void
|
||||
{
|
||||
if (is_null($iso->completed_at)) {
|
||||
throw new BadRequestHttpException(
|
||||
'This ISO cannot be restored at this time: not completed.',
|
||||
'This ISO cannot be deleted at this time: not completed.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,8 +12,9 @@ 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
|
||||
{
|
||||
|
@ -23,9 +24,7 @@ class NetworkService
|
|||
private CloudinitService $cloudinitService,
|
||||
private ProxmoxCloudinitRepository $cloudinitRepository,
|
||||
private ProxmoxConfigRepository $allocationRepository,
|
||||
private ConnectionInterface $connection,
|
||||
)
|
||||
{
|
||||
) {
|
||||
}
|
||||
|
||||
public function deleteIpset(Server $server, string $name)
|
||||
|
@ -41,7 +40,7 @@ class NetworkService
|
|||
return $this->firewallRepository->deleteIpset($name);
|
||||
}
|
||||
|
||||
public function clearIpsets(Server $server)
|
||||
public function clearIpsets(Server $server): void
|
||||
{
|
||||
$this->firewallRepository->setServer($server);
|
||||
|
||||
|
@ -52,7 +51,7 @@ class NetworkService
|
|||
}
|
||||
}
|
||||
|
||||
public function lockIps(Server $server, array $addresses, string $ipsetName)
|
||||
public function lockIps(Server $server, array $addresses, string $ipsetName): void
|
||||
{
|
||||
$this->firewallRepository->setServer($server);
|
||||
|
||||
|
@ -63,7 +62,7 @@ class NetworkService
|
|||
}
|
||||
}
|
||||
|
||||
public function getMacAddresses(Server $server, bool $eloquent = true, bool $proxmox = false)
|
||||
public function getMacAddresses(Server $server, bool $eloquent = true, bool $proxmox = false): MacAddressData
|
||||
{
|
||||
if ($eloquent) {
|
||||
$addresses = $this->getAddresses($server);
|
||||
|
@ -78,7 +77,8 @@ 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)
|
||||
public function syncSettings(Server $server): void
|
||||
{
|
||||
$macAddresses = $this->getMacAddresses($server, true, true);
|
||||
$addresses = $this->getAddresses($server);
|
||||
|
@ -113,7 +113,8 @@ 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([
|
||||
|
@ -130,27 +131,119 @@ class NetworkService
|
|||
);
|
||||
}
|
||||
|
||||
public function updateRateLimit(Server $server, ?int $mebibytes = null)
|
||||
public function updateRateLimit(Server $server, ?int $mebibytes = null): void
|
||||
{
|
||||
$macAddresses = $this->getMacAddresses($server, true, true);
|
||||
$macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox;
|
||||
$rawConfig = $this->allocationRepository->setServer($server)->getConfig();
|
||||
$networkConfig = collect($rawConfig)->where('key', '=', 'net0')->first();
|
||||
|
||||
$payload = "virtio={$macAddress},bridge={$server->node->network},firewall=1";
|
||||
|
||||
if (!is_null($mebibytes)) {
|
||||
$payload .= ',rate=' . $mebibytes;
|
||||
if (is_null($networkConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->allocationRepository->setServer($server)->update(['net0' => $payload]);
|
||||
$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]);
|
||||
}
|
||||
|
||||
public function updateAddresses(Server $server, array $addressIds)
|
||||
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
|
||||
{
|
||||
$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)) {
|
||||
|
|
|
@ -2,21 +2,26 @@
|
|||
|
||||
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,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -25,22 +30,28 @@ 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' => Arr::get($data, 'node_id'),
|
||||
'vmid' => Arr::get($data, 'vmid') ?? random_int(100, 999999999),
|
||||
'node_id' => $nodeId,
|
||||
'vmid' => Arr::get($data, 'vmid') ?? $this->generateUniqueVmId($nodeId),
|
||||
'hostname' => Arr::get($data, 'hostname'),
|
||||
'cpu' => Arr::get($data, 'limits.cpu'),
|
||||
'memory' => Arr::get($data, 'limits.memory'),
|
||||
|
@ -69,15 +80,35 @@ class ServerCreationService
|
|||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique UUID and UUID-Short combo for a 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;
|
||||
}
|
||||
|
||||
public function generateUniqueUuidCombo(): string
|
||||
{
|
||||
$uuid = Str::uuid()->toString();
|
||||
$short = substr($uuid, 0, 8);
|
||||
$attempts = 0;
|
||||
|
||||
if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) {
|
||||
return $this->generateUniqueUuidCombo();
|
||||
while (!$this->repository->isUniqueUuidCombo($uuid, $short)) {
|
||||
$uuid = Str::uuid()->toString();
|
||||
$short = substr($uuid, 0, 8);
|
||||
|
||||
if ($attempts++ > 10) {
|
||||
throw new NoUniqueUuidComboException();
|
||||
}
|
||||
}
|
||||
|
||||
return $uuid;
|
||||
|
|
|
@ -31,7 +31,7 @@ class ServerSuspensionService
|
|||
]);
|
||||
|
||||
try {
|
||||
$this->powerRepository->setServer($server)->send(PowerAction::KILL);
|
||||
$this->powerRepository->setServer($server)->send($isSuspending ? PowerAction::KILL : PowerAction::START);
|
||||
} catch (Exception $exception) {
|
||||
$server->update([
|
||||
'status' => $isSuspending ? null : Status::SUSPENDED->value,
|
||||
|
|
|
@ -23,7 +23,6 @@ class NodeTransformer extends TransformerAbstract
|
|||
'disk' => $node->disk,
|
||||
'disk_overallocate' => $node->disk_overallocate,
|
||||
'disk_allocated' => $node->disk_allocated,
|
||||
'template_storage' => $node->template_storage,
|
||||
'vm_storage' => $node->vm_storage,
|
||||
'backup_storage' => $node->backup_storage,
|
||||
'iso_storage' => $node->iso_storage,
|
||||
|
|
30
database/factories/AddressFactory.php
Normal file
30
database/factories/AddressFactory.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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]),
|
||||
];
|
||||
}
|
||||
}
|
21
database/factories/AddressPoolFactory.php
Normal file
21
database/factories/AddressPoolFactory.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
namespace Database\Factories;
|
||||
|
||||
use Convoy\Models\ISO;
|
||||
use Convoy\Models\Node;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
|
@ -11,11 +10,6 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
|||
*/
|
||||
class ISOFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = ISO::class;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ class NodeFactory extends Factory
|
|||
'memory_overallocate' => 0,
|
||||
'disk' => 137438953472, // 128 gb
|
||||
'disk_overallocate' => 0,
|
||||
'template_storage' => 'local',
|
||||
'vm_storage' => 'local',
|
||||
'backup_storage' => 'local',
|
||||
'iso_storage' => 'local',
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
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;
|
||||
|
@ -32,8 +30,11 @@ class ServerFactory extends Factory
|
|||
'name' => $this->faker->word(),
|
||||
'vmid' => rand(100, 5000),
|
||||
'cpu' => 2,
|
||||
'memory' => 17179869184,
|
||||
'disk' => 17179869184,
|
||||
'memory' => 2048 * 1024 * 1024,
|
||||
'disk' => 20 * 1024 * 1024 * 1024,
|
||||
'backup_limit' => 16,
|
||||
'snapshot_limit' => 16,
|
||||
'bandwidth_limit' => 100 * 1024 * 1024 * 1024,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +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->string('template_storage')->after('disk_overallocate');
|
||||
});
|
||||
|
||||
DB::statement("UPDATE nodes SET template_storage = vm_storage");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
$table->dropColumn('template_storage');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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.',
|
||||
'english_keyboard_characters' => 'The :attribute must contain characters from the English keyboard.',
|
||||
'us_keyboard_characters' => 'The :attribute must contain characters from the US keyboard.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'file' => 'The :attribute must be a 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 {{validation}}',
|
||||
'english_keyboard_characters' => 'Invalid {{validation}}',
|
||||
'hostname' => 'Invalid hostname',
|
||||
'us_keyboard_characters' => 'Invalid US keyboard characters',
|
||||
'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
2
package-lock.json
generated
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "www",
|
||||
"name": "panel",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -14,7 +14,6 @@ interface CreateNodeParameters {
|
|||
memoryOverallocate: number
|
||||
disk: number
|
||||
diskOverallocate: number
|
||||
templateStorage: string
|
||||
vmStorage: string
|
||||
backupStorage: string
|
||||
isoStorage: string
|
||||
|
@ -37,7 +36,6 @@ const createNode = async (data: CreateNodeParameters): Promise<Node> => {
|
|||
memory_overallocate: data.memoryOverallocate,
|
||||
disk: data.disk,
|
||||
disk_overallocate: data.diskOverallocate,
|
||||
template_storage: data.templateStorage,
|
||||
vm_storage: data.vmStorage,
|
||||
backup_storage: data.backupStorage,
|
||||
iso_storage: data.isoStorage,
|
||||
|
|
|
@ -14,12 +14,11 @@ export interface Node {
|
|||
disk: number
|
||||
diskOverallocate: number
|
||||
diskAllocated: number
|
||||
templateStorage: string
|
||||
vmStorage: string
|
||||
backupStorage: string
|
||||
isoStorage: string
|
||||
network: string
|
||||
cotermId: number
|
||||
cotermId: number | null
|
||||
serversCount: number
|
||||
}
|
||||
|
||||
|
@ -37,7 +36,6 @@ export const rawDataToNode = (data: any): Node => ({
|
|||
disk: data.disk,
|
||||
diskOverallocate: data.disk_overallocate,
|
||||
diskAllocated: data.disk_allocated,
|
||||
templateStorage: data.template_storage,
|
||||
vmStorage: data.vm_storage,
|
||||
backupStorage: data.backup_storage,
|
||||
isoStorage: data.iso_storage,
|
||||
|
|
|
@ -14,7 +14,6 @@ interface UpdateNodeParameters {
|
|||
memoryOverallocate: number
|
||||
disk: number
|
||||
diskOverallocate: number
|
||||
templateStorage: string
|
||||
vmStorage: string
|
||||
backupStorage: string
|
||||
isoStorage: string
|
||||
|
@ -37,7 +36,6 @@ const updateNode = async (nodeId: number, payload: UpdateNodeParameters) => {
|
|||
memory_overallocate: payload.memoryOverallocate,
|
||||
disk: payload.disk,
|
||||
disk_overallocate: payload.diskOverallocate,
|
||||
template_storage: payload.templateStorage,
|
||||
vm_storage: payload.vmStorage,
|
||||
backup_storage: payload.backupStorage,
|
||||
iso_storage: payload.isoStorage,
|
||||
|
|
|
@ -43,7 +43,6 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
|
|||
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)),
|
||||
templateStorage: z.string().min(1).max(191),
|
||||
vmStorage: z.string().min(1).max(191),
|
||||
backupStorage: z.string().min(1).max(191),
|
||||
isoStorage: z.string().min(1).max(191),
|
||||
|
@ -65,7 +64,6 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
|
|||
memoryOverallocate: '0',
|
||||
disk: '0',
|
||||
diskOverallocate: '0',
|
||||
templateStorage: '',
|
||||
vmStorage: '',
|
||||
backupStorage: '',
|
||||
isoStorage: '',
|
||||
|
@ -165,11 +163,6 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
|
|||
/>
|
||||
</div>
|
||||
<div className='grid gap-3 grid-cols-3'>
|
||||
<TextInputForm
|
||||
name='templateStorage'
|
||||
label={'Template Storage'}
|
||||
placeholder='local'
|
||||
/>
|
||||
<TextInputForm
|
||||
name='vmStorage'
|
||||
label={t('vm_storage')}
|
||||
|
@ -180,19 +173,17 @@ const CreateNodeModal = ({ open, onClose }: Props) => {
|
|||
label={t('backup_storage')}
|
||||
placeholder='local'
|
||||
/>
|
||||
</div>
|
||||
<div className={'grid gap-3 grid-cols-2'}>
|
||||
<TextInputForm
|
||||
name='isoStorage'
|
||||
label={t('iso_storage')}
|
||||
placeholder='local'
|
||||
/>
|
||||
<TextInputForm
|
||||
name='network'
|
||||
label={tStrings('network')}
|
||||
placeholder='vmbr0'
|
||||
/>
|
||||
</div>
|
||||
<TextInputForm
|
||||
name='network'
|
||||
label={tStrings('network')}
|
||||
placeholder='vmbr0'
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Actions>
|
||||
<Modal.Action type='button' onClick={handleClose}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useFlashKey } from '@/util/useFlash'
|
|||
import usePagination from '@/util/usePagination'
|
||||
import { FormikProvider, useFormik } from 'formik'
|
||||
|
||||
import deleteAddress from '@/api/admin/nodes/addresses/deleteAddress'
|
||||
import deleteAddress from '@/api/admin/addressPools/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(node.id, address.id, syncNetworkConfig)
|
||||
await deleteAddress(address.addressPoolId, address.id)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,6 @@ const NodeAddressesContainer = () => {
|
|||
include: ['server'],
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
|
||||
const rowActions = ({ row }: RowActionsProps<Address>) => {
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
|
@ -119,4 +117,4 @@ const NodeAddressesContainer = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default NodeAddressesContainer
|
||||
export default NodeAddressesContainer
|
||||
|
|
|
@ -39,7 +39,6 @@ const NodeInformationCard = () => {
|
|||
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)),
|
||||
templateStorage: z.string().min(1).max(191),
|
||||
vmStorage: z.string().min(1).max(191),
|
||||
backupStorage: z.string().min(1).max(191),
|
||||
isoStorage: z.string().min(1).max(191),
|
||||
|
@ -61,7 +60,6 @@ const NodeInformationCard = () => {
|
|||
memoryOverallocate: node.memoryOverallocate,
|
||||
disk: node.disk / 1048576,
|
||||
diskOverallocate: node.diskOverallocate,
|
||||
templateStorage: node.templateStorage,
|
||||
vmStorage: node.vmStorage,
|
||||
backupStorage: node.backupStorage,
|
||||
isoStorage: node.isoStorage,
|
||||
|
@ -179,11 +177,6 @@ const NodeInformationCard = () => {
|
|||
/>
|
||||
</div>
|
||||
<div className='grid gap-3 grid-cols-3'>
|
||||
<TextInputForm
|
||||
name='templateStorage'
|
||||
label={'Template Storage'}
|
||||
placeholder='local'
|
||||
/>
|
||||
<TextInputForm
|
||||
name='vmStorage'
|
||||
label={tIndex('vm_storage')}
|
||||
|
@ -194,19 +187,17 @@ const NodeInformationCard = () => {
|
|||
label={tIndex('backup_storage')}
|
||||
placeholder='local'
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-3 grid-cols-2'>
|
||||
<TextInputForm
|
||||
name='isoStorage'
|
||||
label={tIndex('iso_storage')}
|
||||
placeholder='local'
|
||||
/>
|
||||
<TextInputForm
|
||||
name='network'
|
||||
label={tIndex('network')}
|
||||
placeholder='local'
|
||||
/>
|
||||
</div>
|
||||
<TextInputForm
|
||||
name='network'
|
||||
label={tStrings('network')}
|
||||
placeholder='vmbr0'
|
||||
/>
|
||||
</div>
|
||||
</FormCard.Body>
|
||||
<FormCard.Footer>
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { useFlashKey } from '@/util/useFlash'
|
||||
import usePagination from '@/util/usePagination'
|
||||
import {
|
||||
englishKeyboardCharacters,
|
||||
hostname,
|
||||
password,
|
||||
} from '@/util/validation'
|
||||
import { hostname, password, usKeyboardCharacters } from '@/util/validation'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -32,20 +28,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(),
|
||||
|
@ -72,7 +68,7 @@ const CreateServerModal = ({nodeId, userId, open, onClose}: Props) => {
|
|||
z.literal(''),
|
||||
z.preprocess(Number, z.number().min(0)),
|
||||
]),
|
||||
accountPassword: password(englishKeyboardCharacters()).nonempty(),
|
||||
accountPassword: password(usKeyboardCharacters()).nonempty(),
|
||||
shouldCreateServer: z.literal(true),
|
||||
startOnCompletion: z.boolean(),
|
||||
templateUuid: z.string().nonempty(),
|
||||
|
@ -103,7 +99,7 @@ const CreateServerModal = ({nodeId, userId, open, onClose}: Props) => {
|
|||
z.literal(''),
|
||||
z.preprocess(Number, z.number().min(0)),
|
||||
]),
|
||||
accountPassword: password(englishKeyboardCharacters()).optional(),
|
||||
accountPassword: password(usKeyboardCharacters()).optional(),
|
||||
shouldCreateServer: z.literal(false),
|
||||
startOnCompletion: z.boolean(),
|
||||
templateUuid: z.string(),
|
||||
|
@ -163,7 +159,8 @@ 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,
|
||||
|
@ -191,107 +188,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'}
|
||||
<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')}
|
||||
/>
|
||||
<TextInputForm
|
||||
name={'name'}
|
||||
label={tStrings('display_name')}
|
||||
name={'memory'}
|
||||
label={`${tStrings('memory')} (MiB)`}
|
||||
/>
|
||||
{nodeId ? null : <NodesSelectForm/>}
|
||||
{userId ? null : <UsersSelectForm/>}
|
||||
</div>
|
||||
<TextInputForm
|
||||
name={'disk'}
|
||||
label={`${tStrings('disk')} (MiB)`}
|
||||
/>
|
||||
<div className={'grid grid-cols-2 gap-3'}>
|
||||
<TextInputForm
|
||||
name={'vmid'}
|
||||
label={'VMID'}
|
||||
placeholder={
|
||||
t('vmid_placeholder') ??
|
||||
'Leave blank for random VMID'
|
||||
}
|
||||
name={'backupLimit'}
|
||||
label={t('backup_limit')}
|
||||
placeholder={
|
||||
t('limit_placeholder') ??
|
||||
'Leave blank for no limit'
|
||||
}
|
||||
/>
|
||||
<TextInputForm
|
||||
name={'hostname'}
|
||||
label={tStrings('hostname')}
|
||||
name={'bandwidthLimit'}
|
||||
label={`${t('bandwidth_limit')} (MiB)`}
|
||||
placeholder={
|
||||
t('limit_placeholder') ??
|
||||
'Leave blank for no limit'
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
</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
|
||||
|
|
|
@ -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='grid place-items-center mt-5'>
|
||||
<div className='relative 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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ServerContext } from '@/state/server'
|
||||
import { useFlashKey } from '@/util/useFlash'
|
||||
import { englishKeyboardCharacters, password } from '@/util/validation'
|
||||
import { password, usKeyboardCharacters } 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(englishKeyboardCharacters()).nonempty(),
|
||||
accountPassword: password(usKeyboardCharacters()).nonempty(),
|
||||
startOnCompletion: z.boolean(),
|
||||
})
|
||||
|
||||
|
@ -128,4 +128,4 @@ const ReinstallServerCard = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default ReinstallServerCard
|
||||
export default ReinstallServerCard
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ServerContext } from '@/state/server'
|
||||
import { useFlashKey } from '@/util/useFlash'
|
||||
import { englishKeyboardCharacters, password } from '@/util/validation'
|
||||
import { password, usKeyboardCharacters } 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: englishKeyboardCharacters(password()),
|
||||
password: usKeyboardCharacters(password()),
|
||||
})
|
||||
const schema = type === 'password' ? passwordSchema : sshKeysSchema
|
||||
|
||||
|
@ -137,4 +137,4 @@ const AuthenticationCard = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default AuthenticationCard
|
||||
export default AuthenticationCard
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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])$/,
|
||||
|
@ -12,18 +13,15 @@ export const hostname = (string?: ZodString) =>
|
|||
}
|
||||
)
|
||||
|
||||
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 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 password = (string?: ZodString) =>
|
||||
(string ?? z.string()).regex(
|
||||
|
@ -56,4 +54,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)
|
||||
|
|
|
@ -94,8 +94,7 @@ Route::prefix('/nodes')->group(function () {
|
|||
| Endpoint: /api/admin/nodes/{node}/addresses
|
||||
|
|
||||
*/
|
||||
Route::resource('addresses', Admin\Nodes\AddressController::class)
|
||||
->only(['index', 'store', 'update', 'destroy']);
|
||||
Route::get('/addresses', [Admin\Nodes\AddressController::class, 'index']);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -94,8 +94,7 @@ Route::prefix('/nodes')->group(function () {
|
|||
| Endpoint: /api/application/nodes/{node}/addresses
|
||||
|
|
||||
*/
|
||||
Route::apiResource('addresses', Admin\Nodes\AddressController::class)
|
||||
->only(['index', 'store', 'update', 'destroy']);
|
||||
Route::get('/addresses', [Admin\Nodes\AddressController::class, 'index']);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -7,14 +7,18 @@ 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']);
|
||||
|
@ -25,15 +29,26 @@ Route::prefix('/servers/{server}')->middleware([ServerSubject::class, Authentica
|
|||
|
||||
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']);
|
||||
Route::post('/hardware/isos/{iso}/unmount', [Client\Servers\SettingsController::class, 'unmountMedia']);
|
||||
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::get('/network', [Client\Servers\SettingsController::class, 'getNetworkSettings']);
|
||||
Route::put('/network', [Client\Servers\SettingsController::class, 'updateNetworkSettings']);
|
||||
|
|
56
tests/Feature/Controllers/Admin/LocationControllerTest.php
Normal file
56
tests/Feature/Controllers/Admin/LocationControllerTest.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?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();
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
<?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();
|
||||
});
|
122
tests/Feature/Controllers/Admin/Nodes/IsoControllerTest.php
Normal file
122
tests/Feature/Controllers/Admin/Nodes/IsoControllerTest.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?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();
|
||||
});
|
149
tests/Feature/Controllers/Admin/Nodes/NodeControllerTest.php
Normal file
149
tests/Feature/Controllers/Admin/Nodes/NodeControllerTest.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?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();
|
||||
});
|
|
@ -1,117 +1,75 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
use Convoy\Models\Backup;
|
||||
use Convoy\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
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();
|
||||
function testCreateBackup(
|
||||
bool $useSecondUser = false,
|
||||
bool $secondUserIsAdmin = false,
|
||||
): Closure
|
||||
{
|
||||
return function () use ($useSecondUser, $secondUserIsAdmin) {
|
||||
Http::fake([
|
||||
'*' => Http::response(['data' => 'upid'], 200),
|
||||
]);
|
||||
|
||||
[$_, $_, $_, $server] = createServerModel();
|
||||
[$user, $_, $_, $server] = createServerModel();
|
||||
|
||||
$admin = User::factory()->create([
|
||||
'root_admin' => true,
|
||||
]);
|
||||
if ($useSecondUser) {
|
||||
$user = User::factory()->create([
|
||||
'root_admin' => $secondUserIsAdmin,
|
||||
]);
|
||||
}
|
||||
|
||||
$response = $this->actingAs($admin)->postJson("/api/client/servers/{$server->uuid}/backups", [
|
||||
$response = $this->actingAs($user)->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);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
it('can restore backups', function () {
|
||||
Queue::fake();
|
||||
function testRestoreBackups(
|
||||
bool $useSecondUser = false,
|
||||
bool $secondUserIsAdmin = false,
|
||||
): Closure
|
||||
{
|
||||
return function () use ($useSecondUser, $secondUserIsAdmin) {
|
||||
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),
|
||||
|
||||
]);
|
||||
|
||||
[$_, $_, $_, $server] = createServerModel();
|
||||
[$user, $_, $_, $server] = createServerModel();
|
||||
|
||||
$admin = User::factory()->create([
|
||||
'root_admin' => true,
|
||||
]);
|
||||
if ($useSecondUser) {
|
||||
$user = User::factory()->create([
|
||||
'root_admin' => $secondUserIsAdmin,
|
||||
]);
|
||||
}
|
||||
|
||||
$backup = Backup::factory()->create([
|
||||
'is_successful' => true,
|
||||
|
@ -119,23 +77,40 @@ describe('admin', function () {
|
|||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}/restore");
|
||||
$response = $this->actingAs($user)->postJson(
|
||||
"/api/client/servers/{$server->uuid}/backups/{$backup->uuid}/restore",
|
||||
);
|
||||
|
||||
if ($useSecondUser && !$secondUserIsAdmin) {
|
||||
$response->assertNotFound();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response->assertNoContent();
|
||||
|
||||
Queue::assertPushed(MonitorBackupRestorationJob::class);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
it('can delete backups', function () {
|
||||
function testDeleteBackups(
|
||||
bool $useSecondUser = false,
|
||||
bool $secondUserIsAdmin = false,
|
||||
): Closure
|
||||
{
|
||||
return function () use ($useSecondUser, $secondUserIsAdmin) {
|
||||
Http::fake([
|
||||
'*' => Http::response(['data' => 'dummy-upid'], 200),
|
||||
]);
|
||||
|
||||
[$_, $_, $_, $server] = createServerModel();
|
||||
[$user, $_, $_, $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,
|
||||
|
@ -143,9 +118,79 @@ describe('admin', function () {
|
|||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->deleteJson("/api/client/servers/{$server->uuid}/backups/{$backup->uuid}");
|
||||
|
||||
$response = $this->actingAs($user)->deleteJson(
|
||||
"/api/client/servers/{$server->uuid}/backups/{$backup->uuid}",
|
||||
);
|
||||
|
||||
if ($useSecondUser && !$secondUserIsAdmin) {
|
||||
$response->assertNotFound();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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));
|
||||
});
|
|
@ -1,20 +1,34 @@
|
|||
<?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();
|
||||
});
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
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),
|
||||
|
@ -12,66 +10,90 @@ 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),
|
||||
]);
|
||||
|
||||
|
@ -82,14 +104,20 @@ 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),
|
||||
]);
|
||||
|
||||
|
@ -100,7 +128,9 @@ 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();
|
||||
});
|
||||
|
@ -113,7 +143,9 @@ 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);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"success": 1,
|
||||
"data": null
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"data": "UPID:us-southeast:001BE662:04671913:65CF8596:download:virtio-win.iso:root@pam:",
|
||||
"success": 1
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"data": {
|
||||
"filename": "virtio-win.iso",
|
||||
"size": 627519488,
|
||||
"mimetype": "application/octet-stream"
|
||||
},
|
||||
"success": 1
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
<?php
|
||||
|
||||
use Convoy\Models\Node;
|
||||
use Convoy\Models\User;
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Models\Location;
|
||||
use Convoy\Models\Node;
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -20,8 +22,11 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|||
uses(
|
||||
Tests\TestCase::class,
|
||||
DatabaseTransactions::class,
|
||||
// Illuminate\Foundation\Testing\RefreshDatabase::class,
|
||||
)->in('Feature', 'Unit');
|
||||
// Illuminate\Foundation\Testing\RefreshDatabase::class,
|
||||
)->beforeEach(function () {
|
||||
Http::preventStrayRequests();
|
||||
Queue::fake();
|
||||
})->in('Feature', 'Unit');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -34,9 +39,9 @@ uses(
|
|||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
//expect()->extend('toBeOne', function () {
|
||||
// return $this->toBe(1);
|
||||
//});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -49,7 +54,7 @@ expect()->extend('toBeOne', function () {
|
|||
|
|
||||
*/
|
||||
|
||||
function createServerModel()
|
||||
function createServerModel(): array
|
||||
{
|
||||
$location = Location::factory()->create();
|
||||
/** @var User $user */
|
||||
|
|
|
@ -1,14 +1,30 @@
|
|||
<?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([
|
||||
'*' => Http::response(['data' => 'dummy-upid'], 200),
|
||||
'/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)
|
||||
|
||||
]);
|
||||
|
||||
[$_, $_, $node, $server] = createServerModel();
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Convoy\Services\Nodes\ServerUsagesSyncService;
|
||||
|
||||
beforeEach(fn () => Http::preventStrayRequests());
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
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();
|
||||
|
|
Loading…
Add table
Reference in a new issue