Initial commit device-management
tests / ci (push) Has been cancelled

This commit is contained in:
Wian Drs
2026-08-25 09:42:00 +07:00
commit 1e80be180e
1159 changed files with 149545 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
<?php
namespace App\Jobs;
use App\Models\Device;
use App\Models\TenantDeviceSetting;
use App\Network\Clients\Hisfocus\HisfocusSshClient;
use App\Network\Clients\Hisfocus\HisfocusWebClient;
use App\Network\Clients\Hsgq\HsgqSshClient;
use App\Network\Clients\Hsgq\HsgqTelnetClient;
use App\Network\Clients\Hsgq\HsgqWebClient;
use App\Network\Clients\RouterOs\RouterOsApiClient;
use App\Network\Clients\Zte\ZteSshClient;
use App\Network\Drivers\Hisfocus\HisfocusOltDriver;
use App\Network\Drivers\Hsgq\HsgqOltDriver;
use App\Network\Drivers\Mikrotik\MikrotikDriver;
use App\Network\Drivers\Zte\ZteC3xxDriver;
use App\Support\TenantContext;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class SyncDeviceBaseCredential implements ShouldQueue
{
use Queueable;
public int $tries = 1;
public int $timeout = 55;
public function __construct(
public readonly int $tenantId,
public readonly int $deviceId,
public readonly ?string $oldUsername = null,
) {}
public function handle(TenantContext $context): void
{
$context->set($this->tenantId);
setPermissionsTeamId($this->tenantId);
try {
$device = Device::with(['vendor', 'model', 'credentials'])->find($this->deviceId);
$setting = TenantDeviceSetting::where('tenant_id', $this->tenantId)->first();
if (! $device || ! $setting) {
return;
}
$device->update(['base_sync_status' => 'processing', 'base_sync_attempted_at' => now(), 'base_sync_error_code' => null]);
if ($device->vendor?->slug === 'zte' && $device->hasConnection('ssh') && preg_match('/C3(?:00|20)/i', $device->model?->name ?? '')) {
$this->syncZte($device, $setting);
return;
}
if ($device->vendor?->slug === 'hsgq' && ($device->hasConnection('ssh') || $device->hasConnection('telnet'))) {
$this->syncHsgq($device, $setting);
return;
}
if ($device->vendor?->slug === 'hisfocus' && ($device->hasConnection('ssh') || $device->hasConnection('telnet'))) {
$this->syncHisfocus($device, $setting);
return;
}
if ($device->vendor?->slug !== 'mikrotik' || ! $device->hasConnection('routeros_api')) {
$device->update(['base_sync_status' => 'unsupported', 'base_sync_error_code' => 'DRIVER_NOT_AVAILABLE', 'base_sync_message' => "Driver rotasi Base User untuk {$device->vendor?->name} / {$device->connection_type} belum tersedia.", 'activation_status' => 'failed', 'activation_message' => 'DRIVER_NOT_AVAILABLE']);
return;
}
$credential = $device->credentials->where('is_active', true)->sortByDesc(fn ($item) => $item->name === 'Base User RADIQ' ? 2 : (int) $item->is_master)->first();
if (! $credential) {
$device->update(['base_sync_status' => 'pending', 'base_sync_error_code' => 'CREDENTIAL_REQUIRED', 'base_sync_message' => 'Menunggu credential aktif untuk masuk ke perangkat.']);
return;
}
$driver = new MikrotikDriver($this->client($device, $credential->username, $credential->password, $setting));
$driver->provisionBaseAccess($setting->base_username, $setting->base_password);
$newDriver = new MikrotikDriver($this->client($device, $setting->base_username, $setting->base_password, $setting));
$newDriver->testConnection();
$previousUsername = $this->oldUsername ?? ($credential->name === 'Base User RADIQ' ? $credential->username : null);
if ($previousUsername && $previousUsername !== $setting->base_username) {
$newDriver->deleteUser($previousUsername);
}
$device->credentials()->update(['is_master' => false]);
$device->credentials()->updateOrCreate(['name' => 'Base User RADIQ'], [
'tenant_id' => $device->tenant_id, 'username' => $setting->base_username,
'password' => $setting->base_password, 'connection_type' => $device->connection_type,
'privilege_type' => 'master', 'is_master' => true, 'is_active' => true,
'last_verified_at' => now(),
]);
$device->update(['base_sync_status' => 'synced', 'base_sync_error_code' => null, 'base_sync_message' => 'Base User berhasil diperbarui pada perangkat.', 'base_synced_at' => now(), 'status' => 'online', 'last_seen_at' => now()]);
} catch (Throwable $exception) {
report($exception);
$code = str($exception->getMessage())->before(':')->limit(64)->toString() ?: 'UNKNOWN_ERROR';
$pending = in_array($code, ['DEVICE_UNREACHABLE', 'CONNECTION_TIMEOUT', 'CONNECTION_FAILED'], true);
Device::whereKey($this->deviceId)->update([
'base_sync_status' => $pending ? 'pending' : 'failed',
'base_sync_error_code' => $code,
'base_sync_message' => $pending ? 'Perangkat belum terhubung; scheduler akan mencoba kembali.' : 'Rotasi Base User gagal dan perlu diperiksa.',
'status' => $pending ? 'offline' : 'unknown',
'activation_status' => $pending ? 'pending' : 'failed',
'activation_message' => $code,
]);
} finally {
$context->clear();
setPermissionsTeamId(null);
}
}
private function client(Device $device, string $username, string $password, TenantDeviceSetting $setting): RouterOsApiClient
{
return new RouterOsApiClient($device->management_address, $device->portFor('routeros_api'), $username, $password, $setting->connection_timeout, $setting->use_tls, $setting->verify_tls);
}
private function syncZte(Device $device, TenantDeviceSetting $setting): void
{
$credential = $device->credentials->where('is_active', true)->sortByDesc(fn ($item) => $item->name === 'Base User RADIQ' ? 2 : (int) $item->is_master)->first();
if (! $credential) {
$device->update(['base_sync_status' => 'pending', 'base_sync_error_code' => 'CREDENTIAL_REQUIRED', 'base_sync_message' => 'Menunggu credential login dan enable ZTE.', 'activation_status' => 'failed', 'activation_message' => 'CREDENTIAL_REQUIRED']);
return;
}
$driver = new ZteC3xxDriver(new ZteSshClient($device->management_address, $device->portFor('ssh'), $setting->connection_timeout));
$driver->provisionBaseAccess($credential->username, $credential->password, $credential->enable_password, $setting->base_username, $setting->base_password);
$driver->testLogin($setting->base_username, $setting->base_password, $credential->enable_password);
$previousUsername = $this->oldUsername ?? ($credential->name === 'Base User RADIQ' ? $credential->username : null);
if ($previousUsername && $previousUsername !== $setting->base_username) {
$driver->deleteUser($setting->base_username, $setting->base_password, $credential->enable_password, $previousUsername);
}
$device->credentials()->update(['is_master' => false]);
$device->credentials()->updateOrCreate(['name' => 'Base User RADIQ'], [
'tenant_id' => $device->tenant_id, 'username' => $setting->base_username,
'password' => $setting->base_password, 'enable_password' => $credential->enable_password,
'connection_type' => 'ssh', 'privilege_type' => 'master',
'is_master' => true, 'is_active' => true, 'last_verified_at' => now(),
]);
$device->update(['base_sync_status' => 'synced', 'base_sync_error_code' => null, 'base_sync_message' => 'Base User privilege 15 berhasil diperbarui pada ZTE.', 'base_synced_at' => now(), 'status' => 'online', 'last_seen_at' => now(), 'activation_status' => 'active', 'activation_message' => 'Base User ZTE privilege 15 berhasil dibuat dan diverifikasi.', 'activated_at' => now()]);
}
public function failed(?Throwable $exception): void
{
Device::withoutGlobalScope('tenant')->whereKey($this->deviceId)->update([
'base_sync_status' => 'failed',
'base_sync_error_code' => 'JOB_TIMEOUT',
'base_sync_message' => 'Proses koneksi perangkat melewati batas waktu queue.',
'activation_status' => 'failed',
'activation_message' => 'JOB_TIMEOUT',
]);
}
private function syncHsgq(Device $device, TenantDeviceSetting $setting): void
{
$credential = $device->credentials->where('is_active', true)->sortByDesc(fn ($item) => $item->name === 'HSGQ Root' ? 2 : (int) $item->is_master)->first();
if (! $credential || $credential->username !== 'root') {
$device->update(['base_sync_status' => 'pending', 'base_sync_error_code' => 'ROOT_CREDENTIAL_REQUIRED', 'base_sync_message' => 'HSGQ membutuhkan credential root aktif.', 'activation_status' => 'failed', 'activation_message' => 'ROOT_CREDENTIAL_REQUIRED']);
return;
}
$webClient = new HsgqWebClient($device->management_address, $setting->connection_timeout);
$hsgqPrimary = $device->hasConnection('ssh')
? new HsgqSshClient($device->management_address, $device->portFor('ssh'), $setting->connection_timeout)
: new HsgqTelnetClient($device->management_address, $device->portFor('telnet'), $setting->connection_timeout);
$driver = new HsgqOltDriver(
$hsgqPrimary,
$device->hasConnection('ssh') && $device->hasConnection('telnet') ? new HsgqTelnetClient($device->management_address, $device->portFor('telnet'), $setting->connection_timeout) : null,
$webClient,
);
$rootPassword = $credential->password;
if ($device->remove_legacy_users_on_activation && $rootPassword !== $setting->base_password) {
$driver->rotateRootPassword($rootPassword, $setting->base_password, $credential->enable_password);
$rootPassword = $setting->base_password;
} else {
$driver->testLogin('root', $rootPassword, $credential->enable_password);
}
$device->credentials()->update(['is_master' => false]);
$device->credentials()->updateOrCreate(['name' => 'HSGQ Root'], [
'tenant_id' => $device->tenant_id, 'username' => 'root', 'password' => $rootPassword,
'enable_password' => $credential->enable_password,
'connection_type' => 'ssh', 'privilege_type' => 'master', 'is_master' => true,
'is_active' => true, 'last_verified_at' => now(),
]);
$message = $device->remove_legacy_users_on_activation
? 'Password root HSGQ telah disinkronkan dengan Base Password tenant.'
: 'Credential root awal dipertahankan sesuai opsi perangkat.';
$webClient->login('root', $rootPassword);
$facts = $webClient->boardInfo();
$device->update([
'serial_number' => $facts['sn'] ?? $device->serial_number,
'firmware_version' => $facts['fw_ver'] ?? $device->firmware_version,
'software_version' => $facts['sys_ver'] ?? $device->software_version,
'device_facts' => array_merge($device->device_facts ?? [], $facts, ['management_transports' => ['ssh', 'telnet', 'webgui']]),
'base_sync_status' => 'synced', 'base_sync_error_code' => null, 'base_sync_message' => $message,
'base_synced_at' => now(), 'status' => 'online', 'last_seen_at' => now(),
'activation_status' => 'active', 'activation_message' => $message, 'activated_at' => now(),
]);
}
private function syncHisfocus(Device $device, TenantDeviceSetting $setting): void
{
$credential = $device->credentials->where('is_active', true)->sortByDesc('is_master')->first();
if (! $credential) {
$device->update(['base_sync_status' => 'pending', 'base_sync_error_code' => 'CREDENTIAL_REQUIRED', 'base_sync_message' => 'Menunggu credential awal Hisfocus.', 'activation_status' => 'failed', 'activation_message' => 'CREDENTIAL_REQUIRED']);
return;
}
$client = $device->hasConnection('telnet')
? new HsgqTelnetClient($device->management_address, $device->portFor('telnet'), $setting->connection_timeout)
: new HisfocusSshClient($device->management_address, $device->portFor('ssh'), $setting->connection_timeout);
$driver = new HisfocusOltDriver($client);
$facts = $driver->probe($credential->username, $credential->password, $credential->enable_password);
$driver->provisionBaseAccess($credential->username, $credential->password, $credential->enable_password, $setting->base_username, $setting->base_password);
$webPort = $device->portFor('web_http');
if ($webPort) {
$web = new HisfocusWebClient($device->management_address, $webPort, $setting->connection_timeout);
$webUsers = $web->users($credential->username, $credential->password);
if (! isset($webUsers[$setting->base_username])) {
$web->addUser($credential->username, $credential->password, $setting->base_username, $setting->base_password, 'Administrator');
} elseif (! $web->canLogin($setting->base_username, $setting->base_password)) {
$web->deleteUser($credential->username, $credential->password, $setting->base_username);
$web->addUser($credential->username, $credential->password, $setting->base_username, $setting->base_password, 'Administrator');
}
if (! $web->canLogin($setting->base_username, $setting->base_password)) {
throw new \RuntimeException('HISFOCUS_WEB_LOGIN_FAILED: Base User belum dapat login ke WebGUI.');
}
}
if ($device->remove_legacy_users_on_activation && $credential->username !== $setting->base_username) {
$driver->deleteUser($setting->base_username, $setting->base_password, $credential->enable_password, $credential->username);
}
$device->credentials()->update(['is_master' => false]);
$device->credentials()->updateOrCreate(['name' => 'Base User RADIQ'], [
'tenant_id' => $device->tenant_id, 'username' => $setting->base_username,
'password' => $setting->base_password, 'enable_password' => $credential->enable_password ?: $credential->password,
'connection_type' => $device->connection_type, 'privilege_type' => 'master',
'is_master' => true, 'is_active' => true, 'last_verified_at' => now(),
]);
$message = $device->remove_legacy_users_on_activation
? 'Base User Hisfocus dibuat dan user login awal dihapus.'
: 'Base User Hisfocus dibuat; user login awal dipertahankan.';
$device->update([
'serial_number' => $facts['serial_number'] ?? $device->serial_number,
'firmware_version' => $facts['firmware_version'] ?? $device->firmware_version,
'device_facts' => array_merge($device->device_facts ?? [], $facts, ['user_management' => 'multi_user', 'roles' => ['administrator', 'operator', 'guest'], 'web_management_port' => $webPort, 'user_stores' => $webPort ? ['cli', 'webgui'] : ['cli']]),
'base_sync_status' => 'synced', 'base_sync_error_code' => null, 'base_sync_message' => $message,
'base_synced_at' => now(), 'status' => 'online', 'last_seen_at' => now(),
'activation_status' => 'active', 'activation_message' => $message, 'activated_at' => now(),
]);
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
namespace App\Jobs;
use App\Models\DeviceUserAssignment;
use App\Models\TenantDeviceSetting;
use App\Network\Clients\Hisfocus\HisfocusSshClient;
use App\Network\Clients\Hisfocus\HisfocusWebClient;
use App\Network\Clients\Hsgq\HsgqSshClient;
use App\Network\Clients\Hsgq\HsgqTelnetClient;
use App\Network\Clients\Hsgq\HsgqWebClient;
use App\Network\Clients\RouterOs\RouterOsApiClient;
use App\Network\Clients\Zte\ZteSshClient;
use App\Network\Drivers\Hisfocus\HisfocusOltDriver;
use App\Network\Drivers\Hsgq\HsgqOltDriver;
use App\Network\Drivers\Mikrotik\MikrotikDriver;
use App\Network\Drivers\Zte\ZteC3xxDriver;
use App\Support\TenantContext;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class SyncDeviceUser implements ShouldQueue
{
use Queueable;
public int $tries = 1;
public function __construct(public readonly int $tenantId, public readonly int $assignmentId) {}
public function handle(TenantContext $context): void
{
$context->set($this->tenantId);
setPermissionsTeamId($this->tenantId);
try {
$assignment = DeviceUserAssignment::with(['device.vendor', 'device.credentials', 'accessUser'])->find($this->assignmentId);
if (! $assignment || $assignment->tenant_id !== $this->tenantId) {
return;
}
$mikrotik = $assignment->device->vendor->slug === 'mikrotik' && $assignment->device->hasConnection('routeros_api');
$zte = $assignment->device->vendor->slug === 'zte' && $assignment->device->hasConnection('ssh');
$hsgq = $assignment->device->vendor->slug === 'hsgq' && ($assignment->device->hasConnection('ssh') || $assignment->device->hasConnection('telnet'));
$hisfocus = $assignment->device->vendor->slug === 'hisfocus' && ($assignment->device->hasConnection('ssh') || $assignment->device->hasConnection('telnet'));
if (! $mikrotik && ! $zte && ! $hsgq && ! $hisfocus) {
$assignment->update(['sync_status' => 'unsupported', 'error_code' => 'DRIVER_NOT_AVAILABLE', 'message' => 'Driver user perangkat belum tersedia untuk vendor ini.']);
return;
}
$setting = TenantDeviceSetting::where('tenant_id', $this->tenantId)->first();
if (! $setting) {
$assignment->update(['sync_status' => 'pending', 'error_code' => 'BASE_SETTING_REQUIRED', 'message' => 'Menunggu Base User perangkat dikonfigurasi.']);
return;
}
$credential = $assignment->device->credentials->where('is_active', true)->sortByDesc('is_master')->first();
if (! $credential) {
$assignment->update(['sync_status' => 'pending', 'error_code' => 'CREDENTIAL_REQUIRED', 'message' => 'Menunggu credential aktif perangkat.']);
return;
}
$assignment->update(['sync_status' => 'processing', 'attempts' => $assignment->attempts + 1, 'last_attempted_at' => now(), 'error_code' => null, 'message' => null]);
$mikrotikDriver = $mikrotik
? new MikrotikDriver(new RouterOsApiClient($assignment->device->management_address, $assignment->device->portFor('routeros_api'), $credential->username, $credential->password, $setting->connection_timeout, $setting->use_tls, $setting->verify_tls))
: null;
$zteDriver = $zte
? new ZteC3xxDriver(new ZteSshClient($assignment->device->management_address, $assignment->device->portFor('ssh'), $setting->connection_timeout))
: null;
$hsgqDriver = $hsgq
? new HsgqOltDriver(
$assignment->device->hasConnection('ssh')
? new HsgqSshClient($assignment->device->management_address, $assignment->device->portFor('ssh'), $setting->connection_timeout)
: new HsgqTelnetClient($assignment->device->management_address, $assignment->device->portFor('telnet'), $setting->connection_timeout),
$assignment->device->hasConnection('ssh') && $assignment->device->hasConnection('telnet')
? new HsgqTelnetClient($assignment->device->management_address, $assignment->device->portFor('telnet'), $setting->connection_timeout)
: null,
new HsgqWebClient($assignment->device->management_address, $setting->connection_timeout),
)
: null;
$hisfocusDriver = $hisfocus
? new HisfocusOltDriver($assignment->device->hasConnection('telnet')
? new HsgqTelnetClient($assignment->device->management_address, $assignment->device->portFor('telnet'), $setting->connection_timeout)
: new HisfocusSshClient($assignment->device->management_address, $assignment->device->portFor('ssh'), $setting->connection_timeout))
: null;
$hisfocusWeb = $hisfocus
&& $assignment->device->hasConnection('web_http') ? new HisfocusWebClient(
$assignment->device->management_address,
$assignment->device->portFor('web_http'),
$setting->connection_timeout,
)
: null;
if ($assignment->desired_operation === 'delete') {
if ($mikrotik) {
$mikrotikDriver->deleteUser($assignment->remote_username ?: $assignment->accessUser->username);
} elseif ($zte) {
$zteDriver->deleteAccessUser($credential->username, $credential->password, $credential->enable_password, $assignment->remote_username ?: $assignment->accessUser->username);
} elseif ($hsgq) {
$hsgqDriver->deleteUser($credential->password, $credential->enable_password, $assignment->remote_username ?: $assignment->accessUser->username);
} else {
$hisfocusDriver->deleteUser($credential->username, $credential->password, $credential->enable_password, $assignment->remote_username ?: $assignment->accessUser->username);
$webUsername = $assignment->remote_username ?: $assignment->accessUser->username;
if ($hisfocusWeb && isset($hisfocusWeb->users($credential->username, $credential->password)[$webUsername])) {
$hisfocusWeb->deleteUser($credential->username, $credential->password, $webUsername);
}
}
$accessUser = $assignment->accessUser;
$assignment->delete();
if ($accessUser->trashed() && ! $accessUser->assignments()->exists()) {
$accessUser->forceDelete();
}
return;
} else {
if ($assignment->remote_username && $assignment->remote_username !== $assignment->accessUser->username) {
if ($mikrotik) {
$mikrotikDriver->deleteUser($assignment->remote_username);
} elseif ($zte) {
$zteDriver->deleteAccessUser($credential->username, $credential->password, $credential->enable_password, $assignment->remote_username);
} elseif ($hsgq) {
$hsgqDriver->deleteUser($credential->password, $credential->enable_password, $assignment->remote_username);
} else {
$hisfocusDriver->deleteUser($credential->username, $credential->password, $credential->enable_password, $assignment->remote_username);
if ($hisfocusWeb && isset($hisfocusWeb->users($credential->username, $credential->password)[$assignment->remote_username])) {
$hisfocusWeb->deleteUser($credential->username, $credential->password, $assignment->remote_username);
}
}
}
if ($mikrotik) {
$result = $mikrotikDriver->syncUser($assignment->accessUser->username, $assignment->accessUser->password, $assignment->group_name, $assignment->accessUser->is_enabled);
} elseif ($zte) {
$result = $zteDriver->syncUser($credential->username, $credential->password, $credential->enable_password, $assignment->accessUser->username, $assignment->accessUser->password, $assignment->group_name, $assignment->accessUser->is_enabled);
} elseif ($hsgq) {
$result = $hsgqDriver->syncUser($credential->password, $credential->enable_password, $assignment->accessUser->username, $assignment->accessUser->password, $assignment->group_name, $assignment->accessUser->is_enabled);
} else {
$result = $hisfocusDriver->syncUser($credential->username, $credential->password, $credential->enable_password, $assignment->accessUser->username, $assignment->accessUser->password, $assignment->group_name, $assignment->accessUser->is_enabled);
if ($hisfocusWeb) {
$webRoles = ['RADIQ-NOC' => 'Administrator', 'RADIQ-WRITE' => 'Operator', 'RADIQ-READ' => 'Guest'];
$webUsers = $hisfocusWeb->users($credential->username, $credential->password);
if (isset($webUsers[$assignment->accessUser->username])) {
$hisfocusWeb->deleteUser($credential->username, $credential->password, $assignment->accessUser->username);
}
if ($assignment->accessUser->is_enabled) {
$hisfocusWeb->addUser($credential->username, $credential->password, $assignment->accessUser->username, $assignment->accessUser->password, $webRoles[$assignment->group_name]);
}
}
}
}
$assignment->update(['sync_status' => 'synced', 'remote_id' => $result['remote_id'] ?? $assignment->remote_id, 'remote_username' => $assignment->accessUser->username, 'last_synced_at' => now(), 'error_code' => null, 'message' => 'Sinkronisasi berhasil.']);
$assignment->device->update(['status' => 'online', 'last_seen_at' => now()]);
} catch (Throwable $exception) {
report($exception);
$code = str($exception->getMessage())->before(':')->limit(64)->toString();
$pending = in_array($code, ['DEVICE_UNREACHABLE', 'CONNECTION_TIMEOUT', 'CREDENTIAL_REQUIRED'], true);
DeviceUserAssignment::whereKey($this->assignmentId)->update(['sync_status' => $pending ? 'pending' : 'failed', 'error_code' => $code ?: 'UNKNOWN_ERROR', 'message' => $pending ? 'Perangkat belum dapat dijangkau; akan dicoba kembali.' : 'Sinkronisasi gagal tanpa membuka data sensitif.']);
if ($pending) {
DeviceUserAssignment::whereKey($this->assignmentId)->first()?->device()->update(['status' => 'offline']);
}
} finally {
$context->clear();
setPermissionsTeamId(null);
}
}
}