Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: SSH/SFTP Connections #8

Merged
merged 33 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4f764d3
refactor: Implented initial connection
lewislarsen Jul 21, 2024
22a5685
refactor: Wip
lewislarsen Jul 21, 2024
1c16e9e
refactor: Wip
lewislarsen Jul 22, 2024
cc36e6e
Merge branch 'main' into feat/redo_remote_server_connecting
lewislarsen Jul 23, 2024
6e78509
fix: Increased connection timeout
lewislarsen Jul 23, 2024
5b1e719
refactor: Wip
lewislarsen Jul 23, 2024
ae756aa
style: automated changes to code style
lewislarsen Jul 23, 2024
a2e96d7
ci: ignoring commit change in git blame
lewislarsen Jul 23, 2024
9c4bafe
refactor: Moved traits to concerns
lewislarsen Jul 23, 2024
2349425
refactor: Wip
lewislarsen Jul 23, 2024
1c1c927
refactor: Wip
lewislarsen Jul 23, 2024
cca789c
refactor: Wip
lewislarsen Jul 23, 2024
dc4da56
style: automated changes to code style
lewislarsen Jul 23, 2024
508ae56
ci: ignoring commit change in git blame
lewislarsen Jul 23, 2024
8d490a0
refactor: Wip
lewislarsen Jul 23, 2024
04aeccd
refactor: Wip key removal
lewislarsen Jul 23, 2024
8e43a02
style: automated changes to code style
lewislarsen Jul 23, 2024
54356af
ci: ignoring commit change in git blame
lewislarsen Jul 23, 2024
3613f9e
refactor: Wip
lewislarsen Jul 23, 2024
604364b
Merge branch 'feat/redo_remote_server_connecting' of github.com:vangu…
lewislarsen Jul 23, 2024
b373065
wip
lewislarsen Jul 24, 2024
92fead4
style: automated changes to code style
lewislarsen Jul 24, 2024
2a93afd
ci: ignoring commit change in git blame
lewislarsen Jul 24, 2024
8f3b7a9
refactor: Wip
lewislarsen Jul 25, 2024
bf4cebf
refactor: Add logging
lewislarsen Jul 25, 2024
a2aeba0
style: automated changes to code style
lewislarsen Jul 25, 2024
159df31
ci: ignoring commit change in git blame
lewislarsen Jul 25, 2024
65f445d
fix: Path construction
lewislarsen Jul 25, 2024
4fb73e8
Merge remote-tracking branch 'origin/feat/redo_remote_server_connecti…
lewislarsen Jul 25, 2024
01a0e2a
style: automated changes to code style
lewislarsen Jul 25, 2024
f7c01f1
ci: ignoring commit change in git blame
lewislarsen Jul 25, 2024
8f98f8d
fix: Updated ci
lewislarsen Jul 25, 2024
da653e4
refactor: Wip
lewislarsen Jul 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ cfebd7374679440f4f65ae9a9834e468661884ac
804ea8cdef7c8fd0147edd1a51395c496f4277f4
3ed4621268fc5a9310b57d4768c72451d2472522
2840bdb93070016aecea2b31f9473c697b834bb2
ae756aa79581fcb25834604f29eff9e9bbe5abc4
dc4da567a2683bac3e0f4ae3384b3f03d2acbdde
8e43a02d7dc3a0cb5fe04301f31d5f77e1755391
92fead4d79fc6f51a20ad953a7e96ff2e8c5daa1
a2aeba000de332321193ca3261420fa02ab6bf48
01a0e2a1aef33b18be5e886745c577d9be510402
1 change: 1 addition & 0 deletions .github/workflows/pest-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
run: |
cp .env.example.ci .env
php artisan key:generate
php artisan vanguard:generate-ssh-key
- name: Cache Laravel application
uses: actions/cache@v4
Expand Down
145 changes: 91 additions & 54 deletions app/Actions/RemoteServer/CheckRemoteServerConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,110 +5,147 @@
namespace App\Actions\RemoteServer;

use App\Events\RemoteServerConnectivityStatusChanged;
use App\Facades\ServerConnection;
use App\Models\RemoteServer;
use App\Support\ServerConnection\Exceptions\ConnectionException;
use Exception;
use Illuminate\Support\Facades\Log;
use phpseclib3\Crypt\Common\PrivateKey;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Net\SSH2;
use RuntimeException;

/**
* Manages remote server connection checks
*
* This class is responsible for verifying the connectivity status of remote servers.
* It can perform checks using either a server ID or connection details provided directly.
*/
class CheckRemoteServerConnection
{
/**
* @return array<string, mixed>
* Check connection status of a remote server by its ID
*
* @throws Exception
* Retrieves the server by ID and initiates a connection check.
*
* @param int $remoteServerId The ID of the remote server to check
* @return array{status: string, connectivity_status: string, message?: string, error?: string} Connection check results
*
* @throws Exception If the server cannot be found or connection fails unexpectedly
*/
public function byRemoteServerId(int $remoteServerId): array
{
$remoteServer = RemoteServer::findOrFail($remoteServerId);

Log::debug('[Server Connection Check] Beginning connection check of server.', ['id' => $remoteServer->id]);

return $this->checkServerConnection([
'host' => $remoteServer->ip_address,
'port' => $remoteServer->port,
'username' => $remoteServer->username,
], $remoteServer);
return $this->checkServerConnection($remoteServer);
}

/**
* @param array<string, mixed> $remoteServerConnectionDetails
* @return array<string, mixed>
* Check connection status using provided server connection details
*
* Creates a temporary RemoteServer instance and initiates a connection check.
*
* @throws Exception
* @param array<string, mixed> $remoteServerConnectionDetails Connection details including host, port, and username
* @return array{status: string, connectivity_status: string, message?: string, error?: string} Connection check results
*
* @throws Exception If required connection details are missing or connection fails unexpectedly
*/
public function byRemoteServerConnectionDetails(array $remoteServerConnectionDetails): array
{
Log::debug('[Server Connection Check] Beginning connection check of server by connection details.', ['connection_details' => $remoteServerConnectionDetails]);

return $this->checkServerConnection($remoteServerConnectionDetails);
if (! isset($remoteServerConnectionDetails['host'], $remoteServerConnectionDetails['port'], $remoteServerConnectionDetails['username'])) {
throw new RuntimeException('Missing required data to check server connection. Ensure host, port, and username are provided.');
}

$remoteServer = new RemoteServer([
'ip_address' => $remoteServerConnectionDetails['host'],
'port' => $remoteServerConnectionDetails['port'],
'username' => $remoteServerConnectionDetails['username'],
]);

return $this->checkServerConnection($remoteServer);
}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
* Perform the actual server connection check
*
* Attempts to establish an SSH connection to the server and updates its status accordingly.
*
* @param RemoteServer $remoteServer The server to check
* @return array{status: string, connectivity_status: string, message?: string, error?: string} Connection check results
*
* @throws Exception
* @throws Exception If connection fails unexpectedly
*/
private function checkServerConnection(array $data, ?RemoteServer $remoteServer = null): array
private function checkServerConnection(RemoteServer $remoteServer): array
{
if (! array_key_exists('host', $data) || ! array_key_exists('port', $data) || ! array_key_exists('username', $data)) {
throw new RuntimeException('Missing required data to check server connection. Ensure host, port, and username are provided.');
}

try {
/** @var PrivateKey $key */
$key = PublicKeyLoader::load(get_ssh_private_key(), config('app.ssh.passphrase'));
$connection = ServerConnection::connectFromModel($remoteServer)
->establish();

$ssh2 = new SSH2($data['host'], $data['port']);
if ($connection->connected()) {
$this->updateServerStatus($remoteServer, RemoteServer::STATUS_ONLINE);

// Attempt to login
if (! $ssh2->login($data['username'], $key)) {
Log::debug('[Server Connection Check] Failed to connect to remote server', ['error' => $ssh2->getLastError()]);

$remoteServer?->update([
'connectivity_status' => RemoteServer::STATUS_OFFLINE,
]);
Log::debug('[Server Connection Check] Successfully connected to remote server');

return [
'error' => $ssh2->getLastError(),
'status' => 'error',
'status' => 'success',
'connectivity_status' => RemoteServer::STATUS_ONLINE,
'message' => 'Successfully connected to remote server',
];
}

$remoteServer?->update([
'connectivity_status' => RemoteServer::STATUS_ONLINE,
]);
// If we reach here, the connection was established but not explicitly connected
$this->updateServerStatus($remoteServer, RemoteServer::STATUS_OFFLINE);

Log::debug('[Server Connection Check] Successfully connected to remote server');

if ($remoteServer instanceof RemoteServer) {
Log::debug('[Server Connection Check] Dispatching RemoteServerConnectivityStatusChanged event (result: ' . $remoteServer->getAttribute('connectivity_status') . ')', ['remote_server' => $remoteServer]);
RemoteServerConnectivityStatusChanged::dispatch($remoteServer, $remoteServer->getAttribute('connectivity_status'));
}
Log::debug('[Server Connection Check] Connection established but not explicitly connected');

return [
'status' => 'success',
'status' => 'error',
'connectivity_status' => RemoteServer::STATUS_OFFLINE,
'message' => 'Connection established but not explicitly connected',
'error' => 'Connection not fully established',
];

} catch (Exception $exception) {
Log::error('[Server Connection Check] Failed to connect to remote server', ['error' => $exception->getMessage()]);

$remoteServer?->update([
'connectivity_status' => RemoteServer::STATUS_OFFLINE,
} catch (ConnectionException $exception) {
Log::info('[Server Connection Check] Unable to connect to remote server (offline)', [
'error' => $exception->getMessage(),
'server_id' => $remoteServer->getAttribute('id'),
]);

if ($remoteServer instanceof RemoteServer) {
Log::debug('[Server Connection Check] Dispatching RemoteServerConnectivityStatusChanged event (result: ' . $remoteServer->getAttribute('connectivity_status') . ')', ['remote_server' => $remoteServer]);
RemoteServerConnectivityStatusChanged::dispatch($remoteServer, $remoteServer->getAttribute('connectivity_status'));
}
$this->updateServerStatus($remoteServer, RemoteServer::STATUS_OFFLINE);

return [
'error' => $exception->getMessage(),
'status' => 'error',
'connectivity_status' => RemoteServer::STATUS_OFFLINE,
'message' => 'Server is offline or unreachable',
'error' => $exception->getMessage(),
];
} finally {
if (isset($connection)) {
$connection->disconnect();
}
}
}

/**
* Update the connectivity status of a remote server
*
* Updates the server's status in the database and dispatches a status change event.
*
* @param RemoteServer $remoteServer The server to update
* @param string $status The new connectivity status
*/
private function updateServerStatus(RemoteServer $remoteServer, string $status): void
{
if (! $remoteServer->exists()) {
return;
}

$remoteServer->update([
'connectivity_status' => $status,
]);

Log::debug('[Server Connection Check] Dispatching RemoteServerConnectivityStatusChanged event (result: ' . $status . ')', ['remote_server' => $remoteServer]);
RemoteServerConnectivityStatusChanged::dispatch($remoteServer, $status);
}
}
55 changes: 55 additions & 0 deletions app/Facades/ServerConnection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace App\Facades;

use App\Models\RemoteServer;
use App\Support\ServerConnection\Fakes\ServerConnectionFake;
use App\Support\ServerConnection\PendingConnection;
use App\Support\ServerConnection\ServerConnectionManager;
use Illuminate\Support\Facades\Facade;

/**
* Facade for ServerConnectionManager.
*
* This facade provides a static interface to the ServerConnectionManager,
* allowing for easy access to server connection functionality throughout the application.
*
* @method static PendingConnection connect(string $host = '', int $port = 22, string $username = 'root')
* @method static PendingConnection connectFromModel(RemoteServer $server)
* @method static ServerConnectionFake fake()
* @method static void assertConnected()
* @method static void assertDisconnected()
* @method static void assertNotConnected()
* @method static void assertCommandRan(?string $command = null)
* @method static void assertAnyCommandRan()
* @method static void assertNoCommandsRan()
* @method static void assertFileUploaded(string $localPath, string $remotePath)
* @method static void assertFileDownloaded(string $remotePath, string $localPath)
* @method static void assertOutput(string $output)
* @method static void assertConnectionAttempted(array $connectionDetails)
* @method static void defaultPrivateKey(string $path)
* @method static void defaultPassphrase(string $passphrase)
* @method static ServerConnectionFake shouldConnect()
* @method static ServerConnectionFake shouldNotConnect()
* @method static string getPrivateKeyContent(string $path)
* @method static string getDefaultPrivateKey()
* @method static string getDefaultPassphrase()
* @method static string getDefaultPrivateKeyPath()
* @method static string getDefaultPublicKeyPath()
* @method static string getDefaultPublicKey()
* @method static string getPublicKeyContent(string $path)
*
* @see ServerConnectionManager
*/
class ServerConnection extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return ServerConnectionManager::class;
}
}
37 changes: 30 additions & 7 deletions app/Jobs/CheckRemoteServerConnectionJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
namespace App\Jobs;

use App\Actions\RemoteServer\CheckRemoteServerConnection;
use Exception;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

/**
* Job to check the connection status of a remote server
*
* This job is responsible for initiating a connection check to a specific remote server.
* It can be dispatched to the queue for asynchronous processing.
*/
class CheckRemoteServerConnectionJob implements ShouldQueue
{
use Batchable;
Expand All @@ -20,14 +28,29 @@ class CheckRemoteServerConnectionJob implements ShouldQueue
use Queueable;
use SerializesModels;

public function __construct(public int $remoteServerId)
{
//
}
/**
* Create a new job instance
*
* @param int $remoteServerId The ID of the remote server to check
*/
public function __construct(public readonly int $remoteServerId) {}

public function handle(): void
/**
* Execute the job
*
* Performs the connection check for the specified remote server and logs the result.
*
* @param CheckRemoteServerConnection $checkRemoteServerConnection The action to perform the connection check
*
* @throws Exception
*/
public function handle(CheckRemoteServerConnection $checkRemoteServerConnection): void
{
$checkRemoteServerConnection = new CheckRemoteServerConnection;
$checkRemoteServerConnection->byRemoteServerId($this->remoteServerId);
$result = $checkRemoteServerConnection->byRemoteServerId($this->remoteServerId);

Log::info('[Server Connection Check Job] Completed', [
'server_id' => $this->remoteServerId,
'result' => $result,
]);
}
}
12 changes: 9 additions & 3 deletions app/Livewire/RemoteServers/CreateRemoteServerForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,22 @@ public function usingServerProvider(string $provider): void
private function connectionAttempt(): bool
{
$checkRemoteServerConnection = new CheckRemoteServerConnection;

$response = $checkRemoteServerConnection->byRemoteServerConnectionDetails([
'host' => $this->host,
'port' => $this->port,
'username' => $this->username,
]);

if ($response['status'] === 'error') {
$this->connectionError = $response['error'];
$status = $response['status'] ?? 'error';
$error = $response['error'] ?? $response['message'] ?? 'Unknown error occurred';

if ($status === 'error') {
$this->connectionError = $error;

return false;
}

return $response['status'] === 'success';
return true;
}
}
Loading
Loading