Dọn log Zabbix khi full disk

Show dung lượng hiện tại: df -h

Check dung lượng các file log: du -sh /var/lib/my*ql/*

Vào MySQL check tiếp: mysql -uroot -p

Dọn log, chỉnh theo số ngày mong muốn, sau đó check lại với df -h: PURGE BINARY LOGS BEFORE NOW() - INTERVAL 1 DAY;

login.php cho wifi marketing Ruckus

 <?php
function h($s) {
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

function cleanParams(array $input): array {
    $clean = [];

    foreach ($input as $key => $value) {
        $key = preg_replace('/^amp;/', '', (string)$key);
        $clean[$key] = $value;
    }

    return $clean;
}

$params = cleanParams($_POST);

// =======================
// CONFIG
// =======================

$defaultNbiHost = 'thay IP controller vào đây';

$nbiUser = 'nbi-acc';
$nbiPass = 'pass của nbi-acc';

// AAA Always Accept nên user/pass này chỉ là dummy
$ueUsername = 'ok';
$uePassword = 'ok';

// Chỉ cho phép gọi đúng vSZ NBI này
$allowedNbiHosts = [
    'thay IP controller vào đây',
];

// =======================
// INPUT FROM vSZ
// =======================

$nbiHost = $params['nbiIP'] ?? $defaultNbiHost;

if (!in_array($nbiHost, $allowedNbiHosts, true)) {
    $nbiHost = $defaultNbiHost;
}

$nbiUrl = 'https://' . $nbiHost . ':9443/portalintf';

$ueIp    = $params['uip'] ?? '';
$ueMac   = $params['client_mac'] ?? '';
$ueProxy = $params['proxy'] ?? '0';

if ($ueIp === '' || $ueMac === '') {
    http_response_code(400);
    echo 'Missing uip or client_mac';
    exit;
}

// =======================
// BUILD PAYLOAD
// =======================

$payload = [
    'Vendor'          => 'ruckus',
    'APIVersion'      => '1.0',
    'RequestCategory' => 'UserOnlineControl',
    'RequestType'     => 'Login',

    'RequestUserName' => $nbiUser,
    'RequestPassword' => $nbiPass,

    'UE-IP'       => $ueIp,
    'UE-MAC'      => $ueMac,
    'UE-Proxy'    => $ueProxy,
    'UE-Username' => $ueUsername,
    'UE-Password' => $uePassword,
];

// =======================
// CALL vSZ NBI
// =======================

$ch = curl_init($nbiUrl);

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_SLASHES),

    // vSZ cert đang expired/self-signed thì để false.
    // Khi thay cert chuẩn có thể bật verify lại.
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,

    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT        => 10,
]);

$response = curl_exec($ch);

$curlNo   = curl_errno($ch);
$curlErr  = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$info     = curl_getinfo($ch);

curl_close($ch);

// =======================
// LOG DEBUG
// =======================

$logPayload = $payload;
$logPayload['RequestPassword'] = '***';

/*
file_put_contents(
    __DIR__ . '/nbi-login.log',
    date('c') .
    ' URL=' . $nbiUrl .
    ' HTTP=' . $httpCode .
    ' CURL_NO=' . $curlNo .
    ' CURL_ERR=' . $curlErr .
    ' INFO=' . json_encode($info, JSON_UNESCAPED_SLASHES) .
    ' PAYLOAD=' . json_encode($logPayload, JSON_UNESCAPED_SLASHES) .
    ' RESPONSE=' . $response .
    PHP_EOL,
    FILE_APPEND
);
*/

// =======================
// CHECK CURL RESULT
// =======================

if ($response === false || $curlNo !== 0 || $curlErr !== '' || $httpCode < 200 || $httpCode >= 300) {
    http_response_code(500);

    echo '<pre>';
    echo "NBI request failed\n";
    echo "HTTP: " . h($httpCode) . "\n";
    echo "CURL_NO: " . h($curlNo) . "\n";
    echo "CURL_ERR: " . h($curlErr) . "\n";
    echo "Response:\n" . h($response) . "\n";
    echo '</pre>';

    exit;
}

$data = json_decode($response, true);

$responseCode = isset($data['ResponseCode']) ? (int)$data['ResponseCode'] : 0;
$replyMessage = $data['ReplyMessage'] ?? '';

// vSZ có thể trả:
// 201 = Login succeeded
// 101 = Client authorized
$successCodes = [101, 201];

if (!in_array($responseCode, $successCodes, true)) {
    http_response_code(500);

    echo '<pre>';
    echo "NBI login not successful\n";
    echo "ResponseCode: " . h($responseCode) . "\n";
    echo "ReplyMessage: " . h($replyMessage) . "\n";
    echo "Full response:\n" . h($response) . "\n";
    echo '</pre>';

    exit;
}

// =======================
// SUCCESS PAGE
// =======================

header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
?>
<!doctype html>
<html lang="vi">
<head>
    <meta charset="utf-8">
    <title>Connected</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        * { box-sizing: border-box; }
        body {
            margin: 0;
            min-height: 100vh;
            font-family: Arial, sans-serif;
            background: #111827;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
        }
        .box {
            width: 90%;
            max-width: 360px;
            background: #ffffff;
            color: #111827;
            padding: 28px;
            border-radius: 16px;
            box-shadow: 0 20px 50px rgba(0,0,0,.35);
        }
        .check {
            width: 64px;
            height: 64px;
            margin: 0 auto 16px;
            border-radius: 50%;
            background: #16a34a;
            color: #ffffff;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 36px;
            font-weight: bold;
        }
        h2 { margin: 0 0 10px; font-size: 24px; }
        p { margin: 0; color: #4b5563; line-height: 1.5; }
        .small { margin-top: 12px; font-size: 12px; color: #6b7280; }
        .dots span {
            animation: bounce 1.2s infinite ease-in-out;
            display: inline-block;
        }
        .dots span:nth-child(2) { animation-delay: 0.2s; }
        .dots span:nth-child(3) { animation-delay: 0.4s; }
        @keyframes bounce {
            0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
            40% { transform: translateY(-5px); opacity: 1; }
        }
        #preload-frame {
            position: absolute;
            width: 1px;
            height: 1px;
            opacity: 0;
            pointer-events: none;
            border: none;
        }
    </style>
</head>
<body>
    <div class="box">
        <div class="check">✓</div>
        <h2>Connected</h2>
        <p>Bạn đã kết nối Internet.</p>
        <div class="small dots">Đang chuyển hướng<span>.</span><span>.</span><span>.</span></div>
    </div>

    <iframe id="preload-frame"></iframe>

<script>
    const TARGET = 'https://tamanhhospital.vn/vu-tru-robot/?utm_campaign=bvta_longbien_wifi-sitelongbien_vutrurobot2026_he-thong-may-moc_traffic&utm_source=wifi-sitelongbien&utm_medium=wifi-marketing&utm_content=robot-traffic&utm_term=mass';
    const MIN_DISPLAY_MS = 4000; // minimum time to show this screen

    const start = Date.now();
    let iframeReady = false;
    let timerDone = false;

    function tryRedirect() {
        if (iframeReady || timerDone) {
            window.location.replace(TARGET);
        }
    }

    // Start preloading in hidden iframe
    const frame = document.getElementById('preload-frame');
    frame.src = TARGET;
    frame.onload = function () {
        iframeReady = true;
        tryRedirect();
    };
    frame.onerror = function () {
        // If iframe fails (e.g. X-Frame-Options blocks it), redirect anyway
        iframeReady = true;
        tryRedirect();
    };

    // Minimum display timer
    setTimeout(function () {
        timerDone = true;
        tryRedirect();
    }, MIN_DISPLAY_MS);
</script>
</body>
</html>
<?php
exit;

index.php cho wifi marketing Ruckus

<?php
function h($s) {
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

function getCleanParams(): array {
    $query = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '';
    $query = html_entity_decode($query, ENT_QUOTES | ENT_HTML5, 'UTF-8');

    $params = [];
    parse_str($query, $params);

    $clean = [];
    foreach ($params as $key => $value) {
        $key = preg_replace('/^amp;/', '', (string)$key);
        $clean[$key] = $value;
    }

    return $clean;
}

$params = getCleanParams();
?>
<!doctype html>
<html lang="vi">
<head>
    <meta charset="utf-8">
    <title>Guest Wi-Fi</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <style>
        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            height: 100vh;
            font-family: Arial, sans-serif;
            background: #111827;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .box {
            width: 90%;
            max-width: 420px;
            background: #ffffff;
            padding: 0;
            border-radius: 16px;
            text-align: center;
            box-shadow: 0 20px 50px rgba(0, 0, 0, .35);
            overflow: hidden;
        }

        .slider {
            position: relative;
            width: 100%;
            aspect-ratio: 9 / 16;
            overflow: hidden;
            background: #e5e7eb;
        }

        .slide {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            object-fit: contain;
            opacity: 0;
            transition: opacity 600ms ease-in-out;
        }

        .slide.active {
            opacity: 1;
        }

        .dots {
            position: absolute;
            left: 0;
            right: 0;
            bottom: 10px;
            display: flex;
            justify-content: center;
            gap: 6px;
        }

        .dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: rgba(255, 255, 255, .55);
        }

        .dot.active {
            background: #ffffff;
        }

        .content {
            padding: 24px;
            background: #111111;
        }

        button {
            width: 100%;
            padding: 14px;
            border: 0;
            border-radius: 10px;
            background: #2563eb;
            color: #ffffff;
            font-size: 17px;
            font-weight: bold;
            cursor: pointer;
        }

        button:hover {
            background: #1d4ed8;
        }

        button:disabled {
            background: #9ca3af;
            cursor: not-allowed;
        }
    </style>
</head>
<body>
    <div class="box">
        <div class="slider">
            <img class="slide active" src="images/1.jpg" alt="Slide 1">
            <img class="slide" src="images/2.jpg" alt="Slide 2">

            <div class="dots">
                <span class="dot active"></span>
                <span class="dot"></span>
            </div>
        </div>

        <div class="content">
            <form method="post" action="login.php" id="connectForm">
                <?php foreach ($params as $key => $value): ?>
                    <input type="hidden" name="<?= h($key) ?>" value="<?= h($value) ?>">
                <?php endforeach; ?>

                <button type="submit" id="connectBtn">Kết nối Internet</button>
            </form>
        </div>
    </div>

    <script>
        const slides = document.querySelectorAll('.slide');
        const dots = document.querySelectorAll('.dot');
        let currentSlide = 0;

        function showSlide(index) {
            slides.forEach(function (slide, i) {
                slide.classList.toggle('active', i === index);
            });
            dots.forEach(function (dot, i) {
                dot.classList.toggle('active', i === index);
            });
        }

        if (slides.length > 1) {
            setInterval(function () {
                currentSlide = (currentSlide + 1) % slides.length;
                showSlide(currentSlide);
            }, 2000);
        }

        document.getElementById('connectForm').addEventListener('submit', function () {
            const btn = document.getElementById('connectBtn');
            btn.innerText = 'Đang kết nối...';
            btn.disabled = true;
        });
    </script>
</body>
</html>

Backup file local sang shard folder

Tạo VBscript: 

Set WshShell = CreateObject("WScript.Shell")

cmd = "robocopy ""Source-path"" ""Target=path"" /E /XO /Z /FFT /R:2 /W:5 /MT:16"

WshShell.Run cmd, 0, True 

Tạo task schdule chạy mỗi 60 phút:

schtasks /Create /TN "ZaloData Robocopy" /SC MINUTE /MO 60 /TR "wscript.exe \"Script-path\DataBackup.vbs\"" /F

Xóa folder có nhiều cấp con - path quá dài

Thực hiện tạo 1 folder trống rồi robocopy kiểu mirror tới folder bị lỗi:

 robocopy C:\Empty "D:\Backup path" /MIR 

Sau đó sẽ xóa được 

Import MAC-IP reservation vào DHCP server

# trước khi chạy: kiểm tra csv dạng UTF-8, check duplicate MAC
# ghi đè lên các bản ghi đã có nếu trùng MAC
# Xóa các reserve có trên DHCP nhưng không có trong file. Không xóa các bản ghi DHCP bình thường
# Ghi log ra file CSV kèm thời gian, action
# ================= CONFIG =================
$ScopeId = "x.x.x.x"
$CsvPath = "C:\bat\Reserve-MAC-IP.csv"
$LogPath = "C:\bat\Dhcp-Reserve-Log.csv"

$rawCsv = Import-Csv -Path $CsvPath -Encoding UTF8

# ================= FUNCTION =================

# Format MAC về aa-bb-cc-dd-ee-ff
function Format-Mac($mac) {
    $mac = $mac -replace "[:-]", ""
    return ($mac.ToLower() -split '(.{2})' | Where-Object { $_ }) -join "-"
}

function Write-Log($mac, $ip, $desc, $action) {

    $time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    $logObj = [PSCustomObject]@{
        "MAC"         = $mac
        "IP"          = $ip
        "Description" = $desc
        "Action"      = $action
        "Time"        = $time
    }

    Write-Host ("[{0}] {1} | {2} | {3} | {4}" -f $time, $action, $mac, $ip, $desc)

    if (!(Test-Path $LogPath)) {
        $logObj | Export-Csv -Path $LogPath -NoTypeInformation -Encoding UTF8
    } else {
        $logObj | Export-Csv -Path $LogPath -NoTypeInformation -Append -Encoding UTF8
    }
}

# ================= NORMALIZE CSV =================
$csvData = $rawCsv | ForEach-Object {
    [PSCustomObject]@{
        MAC         = (Format-Mac $_.'MAC validate')
        IP          = $_.'IP Reserve'
        Description = $_.Description
    }
}

# ================= CHECK DUPLICATE MAC =================
$dupMac = $csvData | Group-Object MAC | Where-Object { $_.Count -gt 1 }

foreach ($d in $dupMac) {
    Write-Host ("WARNING: DUPLICATE MAC {0} ({1} records) -> giữ dòng cuối" -f $d.Name, $d.Count) -ForegroundColor Yellow
}

# ================= REMOVE DUPLICATE (GIỮ DÒNG CUỐI) =================
$csvData = $csvData |
    Group-Object MAC |
    ForEach-Object { $_.Group[-1] }

# ================= GET EXISTING =================
$existingReservations = Get-DhcpServerv4Reservation -ScopeId $ScopeId

$existingByMac = @{}
foreach ($res in $existingReservations) {
    $existingByMac[$res.ClientId.ToLower()] = $res
}

$csvMacSet = @{}

# ================= ADD / OVERWRITE =================
foreach ($row in $csvData) {

    $mac  = $row.MAC
    $ip   = $row.IP
    $desc = $row.Description

    $csvMacSet[$mac.ToLower()] = $true

    if ($existingByMac.ContainsKey($mac.ToLower())) {

        try {
            Remove-DhcpServerv4Reservation -ScopeId $ScopeId -ClientId $mac -Confirm:$false -ErrorAction Stop
            Add-DhcpServerv4Reservation -ScopeId $ScopeId -IPAddress $ip -ClientId $mac -Description $desc -ErrorAction Stop

            Write-Log $mac $ip $desc "Overwrite"
        } catch {
            Write-Host "ERROR Overwrite: $mac" -ForegroundColor Red
        }

    } else {

        try {
            Add-DhcpServerv4Reservation -ScopeId $ScopeId -IPAddress $ip -ClientId $mac -Description $desc -ErrorAction Stop

            Write-Log $mac $ip $desc "Add"
        } catch {
            Write-Host "ERROR Add: $mac" -ForegroundColor Red
        }
    }
}

# ================= DELETE =================
foreach ($res in $existingReservations) {

    $mac = $res.ClientId.ToLower()

    if (-not $csvMacSet.ContainsKey($mac)) {
        try {
            Remove-DhcpServerv4Reservation -ScopeId $ScopeId -ClientId $res.ClientId -Confirm:$false -ErrorAction Stop

            Write-Log $res.ClientId $res.IPAddress $res.Description "Delete"
        } catch {
            Write-Host "ERROR Delete: $mac" -ForegroundColor Red
        }
    }
}

Get cert SSL cho tên miền DDNS

Sử dụng NAS Synology (xpen)

Sử dụng domain free qua ddns

Truy cập domain => lỗi cert

Phương án xử lý:

- Sử dụng ddns của Synology => cần có tài khoản Syno mới tạo đc domain và get cert, xpen thì ko login tài khoản Synology đc

- Sử dụng ddns bên ngoài như noip => domain riêng, quá trình get cert cần thông từ internet tới port 80 trên NAS:

  •     Cần NAT port 80 vào NAS để cấp qua HTTP-01
  •     Bị ISP block port 80 => không thể  dùng HTTP-01, chỉ còn cách dùng DNS-01, nhưng DNS-01 yêu cầu tạo TXT record cho tên miền. Dịch vụ DDNS không cho phép tạo => fail

Hiện chưa có cách gì. Hướng xử lý có thể nghĩ tới: 

- Ôm cục NAS tới mạng khác cho phép mở port 80, chờ ddns update IP mới rồi thực hiện get cert

- Tìm cách login đc tài khoản Synology lên NAS => tạo domain theo Syno

Dựng external captive portal cho hệ thống Ruckus

Cấu hình SSID:
Bật DNS Google hoặc DNS public khác
Authentication type: Hotspot (WISPr)
Hotspot (WISPr) Portal: theo cấu hình portal đã tạo
Authentication server: bật Use Controller as proxy và chọn Always Accept
Tạo tài khoản Northbound Interface nbi-acc để portal xác thực với vSZ
Add IP của MKT-server vào Walled garden

Mở rule:
- tạm đi internet cho Rocky để cài package
- Guest tới MKT-server port http 
- MKT-server tới vSZ port 9443
  

Dựng VM Rocky

Chạy playbook new_server, và:

dnf install -y nginx php php-fpm php-cli php-curl php-json policycoreutils-python-utils firewalld
systemctl enable --now nginx
systemctl enable --now php-fpm
systemctl enable --now firewalld
setsebool -P httpd_can_network_connect 1
firewall-cmd --permanent --add-service=http
firewall-cmd --reload

Lấy serial MSA 1040

 SSH vào thiết bị

Chạy Show configuration

Tìm dòng SKU để lấy serial:

SKU
---
Part Number: E7W00A
Serial Number: xxx
Revision: F
 

Dọn log Zabbix khi full disk

Show dung lượng hiện tại: df -h Check dung lượng các file log: du -sh /var/lib/my*ql/* Vào MySQL check tiếp: mysql -uroot -p Dọn log, chỉnh ...