update port remote change
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Action\ChangeRemotePortJob;
|
||||
use App\Models\ModuleSnapshot;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Order;
|
||||
@@ -600,85 +601,44 @@ class ServiceController extends Controller
|
||||
/**
|
||||
* Change Remote Desktop port (Windows only)
|
||||
*/
|
||||
public function changeRemotePort(Request $request, vCenter $serviceVCenter)
|
||||
public function changeRemotePort(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'),
|
||||
], 401);
|
||||
}
|
||||
|
||||
$validated = FacadesValidator::make($request->all(), [
|
||||
'product_id' => 'required|string|max:255',
|
||||
'port' => 'required|integer|min:1|max:65535',
|
||||
]);
|
||||
|
||||
if ($validated->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Dữ liệu không hợp lệ'),
|
||||
'errors' => $validated->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$productId = $this->sanitizeInput($request->product_id);
|
||||
$newPort = (int) $request->port;
|
||||
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'os.osGroup')
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$product) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$osFamily = strtoupper($product->os->osGroup->name ?? '');
|
||||
if (!str_contains($osFamily, 'WINDOWS')) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Chỉ hỗ trợ Windows'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter ?? null;
|
||||
if (!$vcenter) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Cấu hình vCenter không hợp lệ'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
if (!$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không thể kết nối đến vCenter'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
// NOTE: Implement the actual change RDP port over WinRM/PowerShell here.
|
||||
// Placeholder success until service method is available.
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => __('Yêu cầu đổi port RDP đã được ghi nhận'),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Lỗi changeRemotePort', [
|
||||
'product_id' => $request->product_id ?? null,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Có lỗi xảy ra khi đổi port'),
|
||||
], 500);
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return response()->json(['error' => true, 'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')], 401);
|
||||
}
|
||||
|
||||
$validated = Validator::make($request->all(), [
|
||||
'product_id' => 'required|string|max:255',
|
||||
'new_port' => 'required|string|min:4|max:128',
|
||||
'password' => 'required|string|min:8|max:128',
|
||||
]);
|
||||
if ($validated->fails()) {
|
||||
return response()->json(['error' => true, 'message' => __('Dữ liệu không hợp lệ'), 'errors' => $validated->errors()], 422);
|
||||
}
|
||||
|
||||
$productId = $this->sanitizeInput($request->product_id);
|
||||
$newPort = (string) $request->new_port;
|
||||
|
||||
// load & ownership check nhanh (không cần toàn bộ nặng nề vì Job sẽ retry)
|
||||
$product = ModuleVmware::where('vmware_id', $productId)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')], 404);
|
||||
}
|
||||
|
||||
// Dispatch job với delay 5s
|
||||
ChangeRemotePortJob::dispatch(
|
||||
productId: $productId,
|
||||
newPort: $newPort,
|
||||
password: (string) $request->password,
|
||||
userId: $user->id,
|
||||
email: $user->email
|
||||
)->delay(now()->addSeconds(5));
|
||||
|
||||
// trả accepted để UI biết đang xử lý nền
|
||||
return redirect()->back()->with('success', 'Đã gửi yêu cầu thay đổi cổng Remote Desktop. Quá trình sẽ mất 3-5 phút, vui lòng chờ đợi.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -425,6 +425,76 @@ PS;
|
||||
return "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
protected function CommandChangePort(string $cmd, bool $expectJson = false)
|
||||
{
|
||||
$auth = $this->LoginAuth();
|
||||
if (!$auth)
|
||||
return "Login_Fail";
|
||||
|
||||
$esc = fn(string $v) => str_replace("'", "''", $v);
|
||||
|
||||
$server = $esc($this->apiUri ?? '');
|
||||
$user = $esc($this->username ?? '');
|
||||
$pass = $esc($this->password ?? '');
|
||||
|
||||
// PowerShell script template
|
||||
$psTemplate = <<<'PS'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$InformationPreference = 'SilentlyContinue'
|
||||
$WarningPreference = 'SilentlyContinue'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
Import-Module VMware.PowerCLI -ErrorAction Stop
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
|
||||
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
|
||||
|
||||
& {
|
||||
<CMD>
|
||||
}
|
||||
PS;
|
||||
|
||||
$psScript = strtr($psTemplate, [
|
||||
'<SERVER>' => $server,
|
||||
'<USER>' => $user,
|
||||
'<PASS>' => $pass,
|
||||
'<CMD>' => $cmd,
|
||||
]);
|
||||
|
||||
// Encode UTF-16LE cho -EncodedCommand
|
||||
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
|
||||
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
|
||||
|
||||
try {
|
||||
$stream = ssh2_exec($auth, $action);
|
||||
if (!$stream)
|
||||
return "ssh2_exec failed";
|
||||
$err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
|
||||
stream_set_blocking($stream, true);
|
||||
stream_set_blocking($err, true);
|
||||
|
||||
$out = stream_get_contents($stream) ?: '';
|
||||
$errOut = stream_get_contents($err) ?: '';
|
||||
|
||||
if ($expectJson) {
|
||||
// CẮT JSON cuối: {...} hoặc [...]
|
||||
$trimmed = trim($out);
|
||||
// đơn giản: bắt khối { ... } hoặc [ ... ] ở cuối chuỗi
|
||||
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
// không tìm thấy → trả nguyên STDOUT (để caller log raw)
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
// Trường hợp không kỳ vọng JSON: giữ nguyên hành vi cũ (append STDERR)
|
||||
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
|
||||
} catch (\Throwable $e) {
|
||||
return "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string
|
||||
{
|
||||
// File ps1 chạy được ngoài PowerShell: bạn đã test ok
|
||||
@@ -461,6 +531,60 @@ PS;
|
||||
return $data ?: $raw; // nếu chưa parse được, trả raw để debug
|
||||
}
|
||||
|
||||
|
||||
public function changeRDPPortWindows(string $vmName, string $guestUser, string $guestPassword, string $newPort): array|string
|
||||
{
|
||||
$psFile = "C:\\CLISave\\AssignIP\\change-rdp-port.ps1";
|
||||
$q = fn(string $v) => str_replace("'", "''", $v);
|
||||
|
||||
// Dùng -VMName (string). Alias -VMNames cũng ok, nhưng mình set đúng tên tham số.
|
||||
$args = " -VMName '" . $q($vmName) . "'";
|
||||
$args .= " -GuestUser '" . $q($guestUser) . "'";
|
||||
$args .= " -GuestPassword '" . $q($guestPassword) . "'"; // khớp tên tham số trong script 1-VM
|
||||
$args .= " -NewPort " . (int) $newPort;
|
||||
|
||||
$cmd = "& '$psFile'$args";
|
||||
|
||||
// Nếu đã có phiên bản CommandRSP($cmd, bool $expectJson) → bật expectJson để không trộn STDERR
|
||||
$raw = method_exists($this, 'CommandRSP')
|
||||
? ($this->CommandChangePort($cmd, true) ?? '')
|
||||
: '';
|
||||
|
||||
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
|
||||
return "Login_Fail";
|
||||
}
|
||||
$raw = trim($raw);
|
||||
|
||||
// CẮT LẤY KHỐI JSON CUỐI (object hoặc array) nếu có noise
|
||||
if (!preg_match('~(\{(?:[^{}]|(?1))*\}|\[(?:[^\[\]]|(?0))*\])\s*$~s', $raw, $m)) {
|
||||
// fallback đơn giản hơn
|
||||
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $raw, $m)) {
|
||||
$raw = $m[1];
|
||||
}
|
||||
} else {
|
||||
$raw = $m[1];
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'JSON parsing failed',
|
||||
'raw_output' => $raw,
|
||||
'json_error' => json_last_error_msg()
|
||||
];
|
||||
}
|
||||
|
||||
// Script 1-VM trả về 1 object, nhưng nếu lỡ trả mảng thì lấy phần tử đầu
|
||||
if (isset($data[0])) {
|
||||
$data = $data[0];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getAllNetwork()
|
||||
{
|
||||
$check = $this->checkApiExits();
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Action;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Mail\ChangePortRemote;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Str;
|
||||
|
||||
class ChangeRemotePortJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $timeout = 180;
|
||||
|
||||
public function __construct(
|
||||
public string $productId,
|
||||
public string $newPort,
|
||||
public string $password,
|
||||
public int $userId,
|
||||
public string $email
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(vCenter $serviceVCenter): void
|
||||
{
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'os.osGroup')
|
||||
->where('vmware_id', $this->productId)
|
||||
->where('customer_id', $this->userId)
|
||||
->firstOrFail();
|
||||
|
||||
$osFamily = (string) ($product->os->osGroup->name ?? '');
|
||||
$isWindows = stripos($osFamily, 'windows') !== false;
|
||||
if (!$isWindows) {
|
||||
throw new \RuntimeException('Only Windows is supported by this reset job.');
|
||||
}
|
||||
|
||||
$vmName = $product->ipAddress->vm_name
|
||||
?? $product->name
|
||||
?? $product->hostname
|
||||
?? null;
|
||||
|
||||
if (!$vmName && preg_match('/^vm-\d+$/i', (string) $product->vmware_id)) {
|
||||
$rawName = trim($serviceVCenter->runPsCommand("(Get-VM -Id '" . addslashes($product->vmware_id) . "').Name"));
|
||||
if (($p = strpos($rawName, '[STDERR]')) !== false) {
|
||||
$rawName = trim(substr($rawName, 0, $p));
|
||||
}
|
||||
$vmName = trim(preg_replace('/\s+/', ' ', $rawName));
|
||||
}
|
||||
if (!$vmName) {
|
||||
throw new \RuntimeException('Cannot resolve VM Name on vCenter.');
|
||||
}
|
||||
|
||||
$guestUser = 'Administrator';
|
||||
|
||||
$result = $serviceVCenter->changeRDPPortWindows(
|
||||
vmName: $vmName,
|
||||
guestUser: 'Administrator',
|
||||
guestPassword: $this->password,
|
||||
newPort: $this->newPort,
|
||||
);
|
||||
|
||||
if ($result === 'Login_Fail' || $result === false || $result === null) {
|
||||
throw new \RuntimeException('SSH runner login failed.');
|
||||
}
|
||||
if (is_string($result)) {
|
||||
// Script trả non-JSON → bắn lỗi kèm raw để debug
|
||||
throw new \RuntimeException('Script returned non-JSON output: ' . $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chuẩn hoá $row:
|
||||
* - Script 1-VM: trả 1 object: ['vm'=>.., 'status'=>.., 'message'=>.., 'newPort'=>..]
|
||||
* - (Phòng hờ) Nếu lỡ trả array nhiều item, chọn item có 'vm' trùng $vmName, không thì lấy phần tử đầu.
|
||||
*/
|
||||
$row = null;
|
||||
|
||||
if (is_array($result)) {
|
||||
// case: object đơn
|
||||
if (isset($result['vm']) && isset($result['status'])) {
|
||||
$row = $result;
|
||||
} else {
|
||||
// case: mảng các object
|
||||
// Tìm theo vmName
|
||||
foreach ($result as $it) {
|
||||
if (is_array($it) && (($it['vm'] ?? null) === $vmName)) {
|
||||
$row = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// fallback: phần tử đầu tiên nếu chưa có
|
||||
if ($row === null && isset($result[0]) && is_array($result[0])) {
|
||||
$row = $result[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($row) || empty($row)) {
|
||||
Log::error('ChangeRemotePortJob: invalid script response shape', ['vm' => $vmName, 'raw' => $result]);
|
||||
throw new \RuntimeException('Invalid script response.');
|
||||
}
|
||||
|
||||
// Đọc trường returned
|
||||
$status = strtolower((string) ($row['status'] ?? ''));
|
||||
$message = (string) ($row['message'] ?? '');
|
||||
$newPort = (string) ($row['newPort'] ?? $this->newPort);
|
||||
|
||||
// Xét thành công: status === 'ok'
|
||||
$ok = ($status === 'ok');
|
||||
|
||||
Log::info('ChangeRemotePortJob: script result', [
|
||||
'vm' => $vmName,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'newPort' => $newPort,
|
||||
'raw' => $row,
|
||||
]);
|
||||
|
||||
if (!$ok) {
|
||||
// Nếu muốn cho phép 'warning' pass, có thể đổi điều kiện ở trên hoặc thêm nhánh dưới:
|
||||
// if ($status === 'warning' && str_contains(strtolower($message), 'rdp port is now')) { $ok = true; }
|
||||
throw new \RuntimeException($message !== '' ? $message : ('Change RDP port failed with status: ' . $status));
|
||||
}
|
||||
|
||||
// Gửi mail thông báo
|
||||
Mail::to($this->email)->send(
|
||||
new ChangePortRemote(vmName: $vmName, newPort: (string) $newPort)
|
||||
);
|
||||
|
||||
Log::info('ChangeRemotePortJob: success', ['vm' => $vmName, 'newPort' => $newPort]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ChangePortRemote extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public string $vmName,
|
||||
public string $newPort
|
||||
) {}
|
||||
|
||||
public function build(): ChangePortRemote
|
||||
{
|
||||
return $this->subject('Port VPS của bạn đã được thay đổi')
|
||||
->view('emails.vm_port_change');
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,15 @@ class ModuleVmware extends Model
|
||||
'customer_id',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = ['password' => 'hashed'];
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Customer::class);
|
||||
}
|
||||
public function ipAddress()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\ModuleIpAddress::class,'module_ip_address_id','id');
|
||||
return $this->belongsTo(\App\Models\ModuleIpAddress::class, 'module_ip_address_id', 'id');
|
||||
}
|
||||
public function os()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Xin chào,</p>
|
||||
<p>Remote Desktop VPS <strong>{{ $vmName }}</strong> đã được đặt lại thành công.</p>
|
||||
<p>Port mới: <code>{{ $newPort }}</code></p>
|
||||
<p>Vui lòng đăng nhập và đổi lại mật khẩu theo ý bạn.</p>
|
||||
<p>Trân trọng,</p>
|
||||
@@ -7,24 +7,31 @@
|
||||
<flux:text class="mt-2">{{ __('Nhập port RDP mới cho máy ảo Windows (mặc định 3389).') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.change.remote.port') }}">
|
||||
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()"
|
||||
method="POST" action="{{ route('service.change.remote.port') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Port mới') }}</flux:label>
|
||||
<flux:input name="port" type="number" min="1" max="65535" placeholder="3389" required />
|
||||
<flux:description>{{ __('Chỉ áp dụng cho Windows. Kết nối đến sẽ cần dùng port này.') }}</flux:description>
|
||||
<flux:input name="new_port" type="number" min="1" max="65535" placeholder="3389" required />
|
||||
<flux:description>{{ __('Chỉ áp dụng cho Windows. Kết nối đến sẽ cần dùng port này.') }}
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Mật khẩu') }}</flux:label>
|
||||
<flux:input name="password" type="password" placeholder="Nhập mật khẩu" required />
|
||||
<flux:description>Vì lý do bảo mật, vui lòng xác thực mật khẩu VPS
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<div class="flex gap-2 justify-end mt-6">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">{{ __('Hủy') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">{{ __('Đồng ý') }}</flux:button>
|
||||
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">
|
||||
{{ __('Đồng ý') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user