mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-09 23:08:36 +02:00
Merge remote-tracking branch 'upstream/main' into boy132/multiple-startup-commands
This commit is contained in:
commit
d17d753c63
@ -3,11 +3,11 @@
|
||||
namespace App\Console\Commands\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Factory as ValidatorFactory;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Exception;
|
||||
|
||||
class BulkPowerActionCommand extends Command
|
||||
@ -19,7 +19,7 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
|
||||
|
||||
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
|
||||
public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void
|
||||
{
|
||||
$action = $this->argument('action');
|
||||
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
|
||||
@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
|
||||
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
|
||||
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed {
|
||||
$bar->clear();
|
||||
|
||||
if (!$server instanceof Server) {
|
||||
@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$powerRepository->setServer($server)->send($action);
|
||||
$serverRepository->setServer($server)->power($action);
|
||||
} catch (Exception $exception) {
|
||||
$this->output->error(trans('command/messages.server.power.action_failed', [
|
||||
'name' => $server->name,
|
||||
|
@ -7,7 +7,7 @@ use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@ -76,7 +76,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
|
||||
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
|
||||
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
try {
|
||||
@ -98,7 +98,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
$serverRepository->setServer($server)->power('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('GSL Token updated')
|
||||
|
@ -6,7 +6,7 @@ use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
@ -59,7 +59,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
|
||||
->preload()
|
||||
->native(false),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
|
||||
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
|
||||
try {
|
||||
$new = $data['image'];
|
||||
$original = $server->image;
|
||||
@ -71,7 +71,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
$serverRepository->setServer($server)->power('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
|
@ -5,7 +5,7 @@ namespace App\Extensions\Features\Schemas;
|
||||
use App\Extensions\Features\FeatureSchemaInterface;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
@ -35,14 +35,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
|
||||
->modalHeading('Minecraft EULA')
|
||||
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
|
||||
->modalSubmitActionLabel('I Accept')
|
||||
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
|
||||
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) {
|
||||
try {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
$serverRepository->setServer($server)->power('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Minecraft EULA accepted')
|
||||
|
@ -10,6 +10,7 @@ use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@ -36,7 +37,7 @@ class CreateApiKey extends CreateRecord
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
|
||||
$data['token'] = str_random(ApiKey::KEY_LENGTH);
|
||||
$data['token'] = Str::random(ApiKey::KEY_LENGTH);
|
||||
$data['user_id'] = auth()->user()->id;
|
||||
$data['key_type'] = ApiKey::TYPE_APPLICATION;
|
||||
|
||||
|
@ -709,8 +709,22 @@ class EditServer extends EditRecord
|
||||
->modalHeading(trans('admin/server.delete_db_heading'))
|
||||
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
|
||||
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
|
||||
->action(function (DatabaseManagementService $databaseManagementService, $record) {
|
||||
$databaseManagementService->delete($record);
|
||||
->action(function (DatabaseManagementService $service, $record) {
|
||||
try {
|
||||
$service->delete($record);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/database.delete_notification', ['database' => $record->database]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(trans('server/database.delete_notification_fail', ['database' => $record->database]))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->persistent()->send();
|
||||
}
|
||||
|
||||
$this->fillForm();
|
||||
})
|
||||
),
|
||||
@ -759,21 +773,22 @@ class EditServer extends EditRecord
|
||||
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
|
||||
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
|
||||
->modalSubmitActionLabel(trans('admin/server.create_database'))
|
||||
->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) {
|
||||
if (empty($data['database'])) {
|
||||
$data['database'] = $randomWordService->word() . random_int(1, 420);
|
||||
}
|
||||
if (empty($data['remote'])) {
|
||||
$data['remote'] = '%';
|
||||
}
|
||||
->action(function (array $data, DatabaseManagementService $service, Server $server) {
|
||||
$data['database'] ??= str_random(12);
|
||||
$data['remote'] ??= '%';
|
||||
|
||||
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
|
||||
|
||||
try {
|
||||
$service->setValidateDatabaseLimit(false)->create($server, $data);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/database.create_notification', ['database' => $data['database']]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.failed_to_create'))
|
||||
->title(trans('server/database.create_notification_fail', ['database' => $data['database']]))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->persistent()->send();
|
||||
|
@ -8,7 +8,7 @@ use App\Filament\Components\Tables\Columns\ServerEntryColumn;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Notifications\Notification;
|
||||
@ -38,11 +38,11 @@ class ListServers extends ListRecords
|
||||
|
||||
public const WARNING_THRESHOLD = 0.7;
|
||||
|
||||
private DaemonPowerRepository $daemonPowerRepository;
|
||||
private DaemonServerRepository $daemonServerRepository;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->daemonPowerRepository = new DaemonPowerRepository();
|
||||
$this->daemonServerRepository = new DaemonServerRepository();
|
||||
}
|
||||
|
||||
/** @return Stack[] */
|
||||
@ -204,7 +204,7 @@ class ListServers extends ListRecords
|
||||
public function powerAction(Server $server, string $action): void
|
||||
{
|
||||
try {
|
||||
$this->daemonPowerRepository->setServer($server)->send($action);
|
||||
$this->daemonServerRepository->setServer($server)->power($action);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/dashboard.power_actions'))
|
||||
|
@ -4,9 +4,10 @@ namespace App\Filament\Components\Forms\Actions;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use Exception;
|
||||
use Filament\Actions\StaticAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
@ -36,10 +37,9 @@ class RotateDatabasePasswordAction extends Action
|
||||
|
||||
$this->requiresConfirmation();
|
||||
|
||||
$this->action(function (DatabasePasswordService $service, Database $database, Set $set) {
|
||||
$this->action(function (DatabaseManagementService $service, Database $database, Set $set) {
|
||||
try {
|
||||
$service->handle($database);
|
||||
|
||||
$service->rotatePassword($database);
|
||||
$database->refresh();
|
||||
|
||||
$set('password', $database->password);
|
||||
@ -57,7 +57,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/databasehost.rotate_error'))
|
||||
->body($exception->getMessage())
|
||||
->body(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) ? $exception->getMessage() : null)
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
|
@ -15,9 +15,11 @@ use App\Traits\Filament\CanCustomizeRelations;
|
||||
use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use App\Traits\Filament\HasLimitBadge;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
@ -122,7 +124,23 @@ class DatabaseResource extends Resource
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
|
||||
DeleteAction::make()
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
->using(function (Database $database, DatabaseManagementService $service) {
|
||||
try {
|
||||
$service->delete($database);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/database.delete_notification', ['database' => $database->database]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('server/database.delete_notification_fail', ['database' => $database->database]))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\CreateAction;
|
||||
@ -15,8 +16,10 @@ use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
@ -62,12 +65,24 @@ class ListDatabases extends ListRecords
|
||||
]),
|
||||
])
|
||||
->action(function ($data, DatabaseManagementService $service) use ($server) {
|
||||
if (empty($data['database'])) {
|
||||
$data['database'] = str_random(12);
|
||||
}
|
||||
$data['database'] = 's'. $server->id . '_' . $data['database'];
|
||||
$data['database'] ??= Str::random(12);
|
||||
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
|
||||
|
||||
$service->create($server, $data);
|
||||
try {
|
||||
$service->create($server, $data);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/database.create_notification', ['database' => $data['database']]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('server/database.create_notification_fail', ['database' => $data['database']]))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
@ -193,10 +193,10 @@ class ListFiles extends ListRecords
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get, File $file) => resolve_path('./' . join_paths($this->path, $get('location') ?? '/', $file->name))),
|
||||
->content(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
$location = $data['location'];
|
||||
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
|
||||
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
@ -353,10 +353,10 @@ class ListFiles extends ListRecords
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
->content(fn (Get $get) => resolve_path(join_paths($this->path, $get('location') ?? ''))),
|
||||
])
|
||||
->action(function (Collection $files, $data) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
$location = $data['location'];
|
||||
|
||||
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
@ -432,11 +432,12 @@ class ListFiles extends ListRecords
|
||||
->modalSubmitActionLabel(trans('server/file.actions.new_file.create'))
|
||||
->action(function ($data) {
|
||||
$path = join_paths($this->path, $data['name']);
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($path, $data['name']))
|
||||
->property('file', $path)
|
||||
->log();
|
||||
} catch (FileExistsException) {
|
||||
AlertBanner::make('file_already_exists')
|
||||
|
@ -4,6 +4,7 @@ namespace App\Helpers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
|
||||
class Utilities
|
||||
@ -14,7 +15,7 @@ class Utilities
|
||||
*/
|
||||
public static function randomStringWithSpecialCharacters(int $length = 16): string
|
||||
{
|
||||
$string = str_random($length);
|
||||
$string = Str::random($length);
|
||||
// Given a random string of characters, randomly loop through the characters and replace some
|
||||
// with special characters to avoid issues with MySQL password requirements on some servers.
|
||||
try {
|
||||
|
@ -6,7 +6,6 @@ use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use App\Models\Database;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Transformers\Api\Application\ServerDatabaseTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
@ -24,7 +23,6 @@ class DatabaseController extends ApplicationApiController
|
||||
*/
|
||||
public function __construct(
|
||||
private DatabaseManagementService $databaseManagementService,
|
||||
private DatabasePasswordService $databasePasswordService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@ -66,7 +64,7 @@ class DatabaseController extends ApplicationApiController
|
||||
*/
|
||||
public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse
|
||||
{
|
||||
$this->databasePasswordService->handle($database);
|
||||
$this->databaseManagementService->rotatePassword($database);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use App\Models\Database;
|
||||
use App\Facades\Activity;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Transformers\Api\Client\DatabaseTransformer;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DeployServerDatabaseService;
|
||||
@ -26,7 +25,6 @@ class DatabaseController extends ClientApiController
|
||||
public function __construct(
|
||||
private DeployServerDatabaseService $deployDatabaseService,
|
||||
private DatabaseManagementService $managementService,
|
||||
private DatabasePasswordService $passwordService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@ -83,7 +81,7 @@ class DatabaseController extends ClientApiController
|
||||
*/
|
||||
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
|
||||
{
|
||||
$this->passwordService->handle($database);
|
||||
$this->managementService->rotatePassword($database);
|
||||
$database->refresh();
|
||||
|
||||
Activity::event('server:database.rotate-password')
|
||||
|
@ -5,9 +5,9 @@ namespace App\Http\Controllers\Api\Client\Servers;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use App\Facades\Activity;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Http\Requests\Api\Client\Servers\SendPowerRequest;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
@ -17,7 +17,7 @@ class PowerController extends ClientApiController
|
||||
/**
|
||||
* PowerController constructor.
|
||||
*/
|
||||
public function __construct(private DaemonPowerRepository $repository)
|
||||
public function __construct(private DaemonServerRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@ -31,7 +31,7 @@ class PowerController extends ClientApiController
|
||||
*/
|
||||
public function index(SendPowerRequest $request, Server $server): Response
|
||||
{
|
||||
$this->repository->setServer($server)->send(
|
||||
$this->repository->setServer($server)->power(
|
||||
$request->input('signal')
|
||||
);
|
||||
|
||||
|
@ -5,11 +5,11 @@ namespace App\Jobs\Schedule;
|
||||
use App\Jobs\Job;
|
||||
use Carbon\CarbonImmutable;
|
||||
use App\Models\Task;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Services\Files\DeleteFilesService;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
@ -31,7 +31,7 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
*/
|
||||
public function handle(
|
||||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository,
|
||||
DaemonServerRepository $serverRepository,
|
||||
DeleteFilesService $deleteFilesService
|
||||
): void {
|
||||
// Do not process a task that is not set to active, unless it's been manually triggered.
|
||||
@ -57,7 +57,7 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
try {
|
||||
switch ($this->task->action) {
|
||||
case Task::ACTION_POWER:
|
||||
$powerRepository->setServer($server)->send($this->task->payload);
|
||||
$serverRepository->setServer($server)->power($this->task->payload);
|
||||
break;
|
||||
case Task::ACTION_COMMAND:
|
||||
$server->send($this->task->payload);
|
||||
|
@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@ -97,71 +98,97 @@ class Database extends Model implements Validatable
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Run the provided statement against the database on a given connection.
|
||||
*/
|
||||
private function run(string $statement): bool
|
||||
private function run(string $statement): void
|
||||
{
|
||||
return $this->host->buildConnection()->statement($statement);
|
||||
$this->host->buildConnection()->statement($statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Create a new database on a given connection.
|
||||
*/
|
||||
public function createDatabase(string $database): bool
|
||||
public function createDatabase(): self
|
||||
{
|
||||
return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database));
|
||||
$this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $this->database));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Create a new database user on a given connection.
|
||||
*/
|
||||
public function createUser(string $username, string $remote, string $password, ?int $max_connections): bool
|
||||
public function createUser(): self
|
||||
{
|
||||
$args = [$username, $remote, $password];
|
||||
$args = [$this->username, $this->remote, $this->password];
|
||||
$command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'';
|
||||
|
||||
if (!empty($max_connections)) {
|
||||
$args[] = $max_connections;
|
||||
if (!empty($this->max_connections)) {
|
||||
$args[] = $this->max_connections;
|
||||
$command .= ' WITH MAX_USER_CONNECTIONS %s';
|
||||
}
|
||||
|
||||
return $this->run(sprintf($command, ...$args));
|
||||
$this->run(sprintf($command, ...$args));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Give a specific user access to a given database.
|
||||
*/
|
||||
public function assignUserToDatabase(string $database, string $username, string $remote): bool
|
||||
public function assignUserToDatabase(): self
|
||||
{
|
||||
return $this->run(sprintf(
|
||||
$this->run(sprintf(
|
||||
'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REFERENCES, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE TEMPORARY TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER ON `%s`.* TO `%s`@`%s`',
|
||||
$database,
|
||||
$username,
|
||||
$remote
|
||||
$this->database,
|
||||
$this->username,
|
||||
$this->remote
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Flush the privileges for a given connection.
|
||||
*/
|
||||
public function flush(): bool
|
||||
public function flushPrivileges(): self
|
||||
{
|
||||
return $this->run('FLUSH PRIVILEGES');
|
||||
$this->run('FLUSH PRIVILEGES');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Drop a given database on a specific connection.
|
||||
*/
|
||||
public function dropDatabase(string $database): bool
|
||||
public function dropDatabase(): self
|
||||
{
|
||||
return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database));
|
||||
$this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $this->database));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PDOException
|
||||
*
|
||||
* Drop a given user on a specific connection.
|
||||
*/
|
||||
public function dropUser(string $username, string $remote): bool
|
||||
public function dropUser(): self
|
||||
{
|
||||
return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote));
|
||||
$this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $this->username, $this->remote));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Daemon;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class DaemonPowerRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Sends a power action to the server instance.
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function send(string $action): Response
|
||||
{
|
||||
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power",
|
||||
['action' => $action],
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
class DaemonServerRepository extends DaemonRepository
|
||||
{
|
||||
@ -149,4 +150,16 @@ class DaemonServerRepository extends DaemonRepository
|
||||
->throw()
|
||||
->json('data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a power action to the server instance.
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function power(string $action): Response
|
||||
{
|
||||
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power",
|
||||
['action' => $action],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Api;
|
||||
|
||||
use App\Models\ApiKey;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class KeyCreationService
|
||||
{
|
||||
@ -33,7 +34,7 @@ class KeyCreationService
|
||||
$data = array_merge($data, [
|
||||
'key_type' => $this->keyType,
|
||||
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
|
||||
'token' => str_random(ApiKey::KEY_LENGTH),
|
||||
'token' => Str::random(ApiKey::KEY_LENGTH),
|
||||
]);
|
||||
|
||||
if ($this->keyType !== ApiKey::TYPE_APPLICATION) {
|
||||
|
@ -10,6 +10,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use App\Exceptions\Repository\DuplicateDatabaseNameException;
|
||||
use App\Exceptions\Service\Database\TooManyDatabasesException;
|
||||
use App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DatabaseManagementService
|
||||
{
|
||||
@ -85,22 +86,18 @@ class DatabaseManagementService
|
||||
|
||||
$data = array_merge($data, [
|
||||
'server_id' => $server->id,
|
||||
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
|
||||
'username' => sprintf('u%d_%s', $server->id, Str::random(10)),
|
||||
'password' => Utilities::randomStringWithSpecialCharacters(24),
|
||||
]);
|
||||
|
||||
return $this->connection->transaction(function () use ($data) {
|
||||
$database = $this->createModel($data);
|
||||
|
||||
$database->createDatabase($database->database);
|
||||
$database->createUser(
|
||||
$database->username,
|
||||
$database->remote,
|
||||
$database->password,
|
||||
$database->max_connections
|
||||
);
|
||||
$database->assignUserToDatabase($database->database, $database->username, $database->remote);
|
||||
$database->flush();
|
||||
$database
|
||||
->createDatabase()
|
||||
->createUser()
|
||||
->assignUserToDatabase()
|
||||
->flushPrivileges();
|
||||
|
||||
Activity::event('server:database.create')
|
||||
->subject($database)
|
||||
@ -114,14 +111,15 @@ class DatabaseManagementService
|
||||
/**
|
||||
* Delete a database from the given host server.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function delete(Database $database): ?bool
|
||||
{
|
||||
return $this->connection->transaction(function () use ($database) {
|
||||
$database->dropDatabase($database->database);
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
$database->flush();
|
||||
$database
|
||||
->dropDatabase()
|
||||
->dropUser()
|
||||
->flushPrivileges();
|
||||
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
@ -132,6 +130,28 @@ class DatabaseManagementService
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a password for a given database.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function rotatePassword(Database $database): void
|
||||
{
|
||||
$password = Utilities::randomStringWithSpecialCharacters(24);
|
||||
|
||||
$this->connection->transaction(function () use ($database, $password) {
|
||||
$database->update([
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$database
|
||||
->dropUser()
|
||||
->createUser()
|
||||
->assignUserToDatabase()
|
||||
->flushPrivileges();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the database if there is not an identical match in the DB. While you can technically
|
||||
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
|
||||
|
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Databases;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Helpers\Utilities;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
|
||||
class DatabasePasswordService
|
||||
{
|
||||
/**
|
||||
* DatabasePasswordService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates a password for a given database.
|
||||
*/
|
||||
public function handle(Database|int $database): void
|
||||
{
|
||||
if (is_int($database)) {
|
||||
$database = Database::query()->findOrFail($database);
|
||||
}
|
||||
|
||||
$password = Utilities::randomStringWithSpecialCharacters(24);
|
||||
|
||||
$this->connection->transaction(function () use ($database, $password) {
|
||||
$database->update([
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
$database->createUser($database->username, $database->remote, $password, $database->max_connections);
|
||||
$database->assignUserToDatabase($database->database, $database->username, $database->remote);
|
||||
$database->flush();
|
||||
});
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Users;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
||||
@ -48,7 +49,7 @@ class ToggleTwoFactorService
|
||||
if ((!$toggleState && !$user->use_totp) || $toggleState) {
|
||||
$user->recoveryTokens()->delete();
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$token = str_random(10);
|
||||
$token = Str::random(10);
|
||||
$user->recoveryTokens()->forceCreate([
|
||||
'token' => password_hash($token, PASSWORD_DEFAULT),
|
||||
]);
|
||||
|
@ -37,7 +37,7 @@ class UserCreationService
|
||||
$this->connection->beginTransaction();
|
||||
if (empty($data['password'])) {
|
||||
$generateResetToken = true;
|
||||
$data['password'] = $this->hasher->make(str_random(30));
|
||||
$data['password'] = $this->hasher->make(Str::random(30));
|
||||
}
|
||||
|
||||
$isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin'];
|
||||
|
@ -52,11 +52,16 @@ if (!function_exists('convert_bytes_to_readable')) {
|
||||
if (!function_exists('join_paths')) {
|
||||
function join_paths(string $base, string ...$paths): string
|
||||
{
|
||||
if ($base === '/') {
|
||||
return str_replace('//', '', implode('/', $paths));
|
||||
$base = trim($base, '/');
|
||||
|
||||
$paths = array_map(fn (string $path) => trim($path, '/'), $paths);
|
||||
$paths = array_filter($paths, fn (string $path) => strlen($path) > 0);
|
||||
|
||||
if (empty($base)) {
|
||||
return implode('/', $paths);
|
||||
}
|
||||
|
||||
return str_replace('//', '', $base . '/' . implode('/', $paths));
|
||||
return $base . '/' . implode('/', $paths);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use App\Contracts\Repository\DaemonKeyRepositoryInterface;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
@ -21,7 +22,7 @@ return new class extends Migration
|
||||
$inserts[] = [
|
||||
'user_id' => $server->owner_id,
|
||||
'server_id' => $server->id,
|
||||
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40),
|
||||
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40),
|
||||
'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(),
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
|
@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use App\Contracts\Repository\DaemonKeyRepositoryInterface;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
@ -19,7 +20,7 @@ return new class extends Migration
|
||||
$inserts[] = [
|
||||
'user_id' => $subuser->user_id,
|
||||
'server_id' => $subuser->server_id,
|
||||
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40),
|
||||
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40),
|
||||
'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(),
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@ -51,7 +52,7 @@ return new class extends Migration
|
||||
DB::transaction(function () {
|
||||
DB::table('service_options')->select(['id', 'tag'])->get()->each(function ($option) {
|
||||
DB::table('service_options')->where('id', $option->id)->update([
|
||||
'tag' => str_random(10),
|
||||
'tag' => Str::random(10),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
@ -19,7 +20,7 @@ return new class extends Migration
|
||||
try {
|
||||
$decrypted = Crypt::decrypt($item->secret);
|
||||
} catch (DecryptException $exception) {
|
||||
$decrypted = str_random(32);
|
||||
$decrypted = Str::random(32);
|
||||
} finally {
|
||||
DB::table('api_keys')->where('id', $item->id)->update([
|
||||
'secret' => $decrypted,
|
||||
@ -66,7 +67,7 @@ return new class extends Migration
|
||||
DB::transaction(function () {
|
||||
DB::table('api_keys')->get()->each(function ($item) {
|
||||
DB::table('api_keys')->where('id', $item->id)->update([
|
||||
'public' => str_random(16),
|
||||
'public' => Str::random(16),
|
||||
'secret' => Crypt::encrypt($item->secret),
|
||||
]);
|
||||
});
|
||||
|
@ -19,4 +19,8 @@ return [
|
||||
'database_host' => 'Database Host',
|
||||
'database_host_select' => 'Select Database Host',
|
||||
'jdbc' => 'JDBC Connection String',
|
||||
'create_notification' => 'Created :database',
|
||||
'create_notification_fail' => 'Failed to create :database',
|
||||
'delete_notification' => 'Deleted :database',
|
||||
'delete_notification_fail' => 'Failed to delete :database',
|
||||
];
|
||||
|
@ -5,7 +5,6 @@ namespace App\Tests\Integration\Api\Client\Server\Database;
|
||||
use App\Models\Subuser;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
@ -33,8 +32,8 @@ class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase
|
||||
$database3 = Database::factory()->create(['server_id' => $server3->id, 'database_host_id' => $host->id]);
|
||||
|
||||
$this
|
||||
->mock($method === 'POST' ? DatabasePasswordService::class : DatabaseManagementService::class)
|
||||
->expects($method === 'POST' ? 'handle' : 'delete')
|
||||
->mock(DatabaseManagementService::class)
|
||||
->expects($method === 'POST' ? 'rotatePassword' : 'delete')
|
||||
->andReturn($method === 'POST' ? 'foo' : null);
|
||||
|
||||
// This is the only valid call for this test, accessing the database for the same
|
||||
|
@ -4,7 +4,7 @@ namespace App\Tests\Integration\Api\Client\Server;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Permission;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
@ -49,8 +49,8 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
|
||||
#[DataProvider('validPowerActionDataProvider')]
|
||||
public function test_action_can_be_sent_to_server(string $action, string $permission): void
|
||||
{
|
||||
$service = \Mockery::mock(DaemonPowerRepository::class);
|
||||
$this->app->instance(DaemonPowerRepository::class, $service);
|
||||
$service = \Mockery::mock(DaemonServerRepository::class);
|
||||
$this->app->instance(DaemonServerRepository::class, $service);
|
||||
|
||||
[$user, $server] = $this->generateTestAccount([$permission]);
|
||||
|
||||
@ -60,7 +60,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
|
||||
}))
|
||||
->andReturnSelf()
|
||||
->getMock()
|
||||
->expects('send')
|
||||
->expects('power')
|
||||
->with(trim($action));
|
||||
|
||||
$this->actingAs($user)
|
||||
|
@ -10,8 +10,8 @@ use App\Models\Server;
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use App\Jobs\Schedule\RunTaskJob;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Tests\Integration\IntegrationTestCase;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
@ -84,13 +84,13 @@ class RunTaskJobTest extends IntegrationTestCase
|
||||
'continue_on_failure' => false,
|
||||
]);
|
||||
|
||||
$mock = \Mockery::mock(DaemonPowerRepository::class);
|
||||
$this->instance(DaemonPowerRepository::class, $mock);
|
||||
$mock = \Mockery::mock(DaemonServerRepository::class);
|
||||
$this->instance(DaemonServerRepository::class, $mock);
|
||||
|
||||
$mock->expects('setServer')->with(\Mockery::on(function ($value) use ($server) {
|
||||
return $value instanceof Server && $value->id === $server->id;
|
||||
}))->andReturnSelf();
|
||||
$mock->expects('send')->with('start');
|
||||
$mock->expects('power')->with('start');
|
||||
|
||||
Bus::dispatchSync(new RunTaskJob($task, $isManualRun));
|
||||
|
||||
@ -117,10 +117,10 @@ class RunTaskJobTest extends IntegrationTestCase
|
||||
'continue_on_failure' => $continueOnFailure,
|
||||
]);
|
||||
|
||||
$mock = \Mockery::mock(DaemonPowerRepository::class);
|
||||
$this->instance(DaemonPowerRepository::class, $mock);
|
||||
$mock = \Mockery::mock(DaemonServerRepository::class);
|
||||
$this->instance(DaemonServerRepository::class, $mock);
|
||||
|
||||
$mock->expects('setServer->send')->andThrow(new ConnectionException());
|
||||
$mock->expects('setServer->power')->andThrow(new ConnectionException());
|
||||
|
||||
if (!$continueOnFailure) {
|
||||
$this->expectException(ConnectionException::class);
|
||||
|
Loading…
x
Reference in New Issue
Block a user