900 lines
16 KiB
Python
900 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Vendors report page.
|
|
|
|
Страница управления базой OUI.
|
|
|
|
Бизнес-логика находится в VendorService.
|
|
Этот модуль отвечает только за HTML-представление.
|
|
"""
|
|
|
|
from html import escape
|
|
|
|
from .statistics import (
|
|
top_vendors,
|
|
)
|
|
|
|
from .utils import (
|
|
format_datetime as format_report_datetime,
|
|
)
|
|
|
|
from .template import (
|
|
page_begin,
|
|
page_end,
|
|
card,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Helpers
|
|
# ============================================================
|
|
|
|
def format_size(size):
|
|
if not size:
|
|
return "-"
|
|
|
|
size = float(size)
|
|
|
|
units = (
|
|
"B",
|
|
"KB",
|
|
"MB",
|
|
"GB",
|
|
)
|
|
|
|
for unit in units:
|
|
|
|
if size < 1024:
|
|
return f"{size:.1f} {unit}"
|
|
|
|
size /= 1024
|
|
|
|
return f"{size:.1f} TB"
|
|
|
|
|
|
def format_value(value):
|
|
if value is None or value == "":
|
|
return "-"
|
|
|
|
return escape(
|
|
str(value)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Database status
|
|
# ============================================================
|
|
|
|
def database_status_block(status):
|
|
if not status["exists"]:
|
|
health = "Not found"
|
|
|
|
elif status["healthy"]:
|
|
health = "Healthy"
|
|
|
|
else:
|
|
health = "Error"
|
|
|
|
html = f"""
|
|
<table>
|
|
|
|
<tr>
|
|
<th>Property</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Database</td>
|
|
<td>{format_value(status["database"])}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Status</td>
|
|
<td>{health}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Total entries</td>
|
|
<td>{status["total"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>MA-L</td>
|
|
<td>{status["ma_l"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>MA-M</td>
|
|
<td>{status["ma_m"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>MA-S</td>
|
|
<td>{status["ma_s"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Schema version</td>
|
|
<td>{format_value(status["schema_version"])}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Last import</td>
|
|
<td>{format_report_datetime(status["last_import"])}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Import status</td>
|
|
<td>{format_value(status["import_status"])}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Size</td>
|
|
<td>{format_size(status["size"])}</td>
|
|
</tr>
|
|
|
|
</table>
|
|
"""
|
|
|
|
return card(
|
|
"🏭 OUI Database",
|
|
html,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Top Vendors
|
|
# ============================================================
|
|
|
|
def top_vendors_block(conn):
|
|
rows = top_vendors(
|
|
conn,
|
|
limit=15
|
|
)
|
|
|
|
html = """
|
|
<table>
|
|
|
|
<tr>
|
|
<th>Vendor</th>
|
|
<th>Devices</th>
|
|
</tr>
|
|
|
|
"""
|
|
|
|
if not rows:
|
|
|
|
html += """
|
|
<tr>
|
|
<td colspan="2">No vendor data.</td>
|
|
</tr>
|
|
|
|
"""
|
|
|
|
else:
|
|
|
|
for vendor, total in rows:
|
|
|
|
if not vendor:
|
|
vendor = "-"
|
|
|
|
html += f"""
|
|
<tr>
|
|
<td>{escape(str(vendor))}</td>
|
|
<td>{total}</td>
|
|
</tr>
|
|
|
|
"""
|
|
|
|
html += """
|
|
</table>
|
|
"""
|
|
|
|
return card(
|
|
"🏭 Top Vendors",
|
|
html,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Vendors page
|
|
# ============================================================
|
|
|
|
def update_block(
|
|
service,
|
|
preview=None,
|
|
token=None,
|
|
job=None
|
|
):
|
|
html = """
|
|
"""
|
|
|
|
sources = service.sources
|
|
mirrors = service.mirrors
|
|
|
|
source_html = """
|
|
<table>
|
|
|
|
<tr>
|
|
<th>Source</th>
|
|
<th>URL</th>
|
|
<th>Mirror</th>
|
|
</tr>
|
|
|
|
"""
|
|
|
|
for name, url in sources.items():
|
|
|
|
mirror = mirrors.get(
|
|
name
|
|
)
|
|
|
|
source_html += f"""
|
|
<tr>
|
|
<td>{escape(str(name))}</td>
|
|
<td>{escape(str(url))}</td>
|
|
<td>{escape(str(mirror)) if mirror else "—"}</td>
|
|
</tr>
|
|
|
|
"""
|
|
|
|
source_html += """
|
|
</table>
|
|
|
|
"""
|
|
|
|
html += """
|
|
<div class="report-grid">
|
|
|
|
"""
|
|
|
|
html += card(
|
|
"🌐 Configured Sources",
|
|
source_html,
|
|
)
|
|
|
|
if preview is None:
|
|
|
|
if job is not None:
|
|
|
|
stage_labels = {
|
|
"starting":
|
|
"Запуск обновления",
|
|
"downloading":
|
|
"Загрузка источника",
|
|
"validating":
|
|
"Проверка источника",
|
|
"fallback":
|
|
"Переход на резервное зеркало",
|
|
"building":
|
|
"Построение базы OUI",
|
|
"analyzing":
|
|
"Анализ изменений",
|
|
"ready":
|
|
"Обновление готово",
|
|
"error":
|
|
"Ошибка обновления"
|
|
}
|
|
|
|
stage = job.get(
|
|
"stage"
|
|
)
|
|
|
|
stage_label = stage_labels.get(
|
|
stage,
|
|
stage or "Неизвестный этап"
|
|
)
|
|
|
|
source = job.get(
|
|
"source"
|
|
)
|
|
|
|
message = job.get(
|
|
"message"
|
|
)
|
|
|
|
error = job.get(
|
|
"error"
|
|
)
|
|
|
|
html += f"""
|
|
<div class="card">
|
|
|
|
<h2>Обновление базы OUI</h2>
|
|
|
|
<p>
|
|
<strong>Этап:</strong>
|
|
<span id="vendor-update-stage">
|
|
{escape(stage_label)}
|
|
</span>
|
|
</p>
|
|
|
|
<p>
|
|
<strong>Источник:</strong>
|
|
<span id="vendor-update-source">
|
|
{escape(str(source)) if source else "—"}
|
|
</span>
|
|
</p>
|
|
|
|
<p>
|
|
<span id="vendor-update-message">
|
|
{escape(str(message)) if message else ""}
|
|
</span>
|
|
</p>
|
|
|
|
<p id="vendor-update-error">
|
|
{escape(str(error)) if error else ""}
|
|
</p>
|
|
|
|
<script>
|
|
(function(){{
|
|
|
|
const jobId = {job["id"]!r};
|
|
|
|
const stageLabels = {{
|
|
"starting": "Запуск обновления",
|
|
"downloading": "Загрузка источника",
|
|
"validating": "Проверка источника",
|
|
"fallback": "Переход на резервное зеркало",
|
|
"building": "Построение базы OUI",
|
|
"analyzing": "Анализ изменений",
|
|
"ready": "Обновление готово",
|
|
"error": "Ошибка обновления"
|
|
}};
|
|
|
|
async function pollVendorUpdate(){{
|
|
|
|
try {{
|
|
|
|
const response = await fetch(
|
|
"/reports/vendors/update/status/"
|
|
+ encodeURIComponent(jobId),
|
|
{{
|
|
cache: "no-store"
|
|
}}
|
|
);
|
|
|
|
if (!response.ok) {{
|
|
return;
|
|
}}
|
|
|
|
const data = await response.json();
|
|
|
|
const stage =
|
|
document.getElementById(
|
|
"vendor-update-stage"
|
|
);
|
|
|
|
const source =
|
|
document.getElementById(
|
|
"vendor-update-source"
|
|
);
|
|
|
|
const message =
|
|
document.getElementById(
|
|
"vendor-update-message"
|
|
);
|
|
|
|
const error =
|
|
document.getElementById(
|
|
"vendor-update-error"
|
|
);
|
|
|
|
if (stage) {{
|
|
stage.textContent =
|
|
stageLabels[data.stage]
|
|
|| data.stage
|
|
|| "Неизвестный этап";
|
|
}}
|
|
|
|
if (source) {{
|
|
source.textContent =
|
|
data.source || "—";
|
|
}}
|
|
|
|
if (message) {{
|
|
message.textContent =
|
|
data.message || "";
|
|
}}
|
|
|
|
if (error) {{
|
|
error.textContent =
|
|
data.error || "";
|
|
}}
|
|
|
|
if (
|
|
data.status === "ready"
|
|
&& data.preview_ready
|
|
) {{
|
|
|
|
window.location.href =
|
|
"/reports/vendors/?job_id="
|
|
+ encodeURIComponent(jobId);
|
|
|
|
return;
|
|
}}
|
|
|
|
if (
|
|
data.status === "error"
|
|
|| data.status === "missing"
|
|
) {{
|
|
return;
|
|
}}
|
|
|
|
}} catch (error) {{
|
|
|
|
console.error(
|
|
"Vendor update polling failed:",
|
|
error
|
|
);
|
|
}}
|
|
|
|
window.setTimeout(
|
|
pollVendorUpdate,
|
|
1000
|
|
);
|
|
}}
|
|
|
|
pollVendorUpdate();
|
|
|
|
}})();
|
|
</script>
|
|
|
|
"""
|
|
|
|
if job.get(
|
|
"status"
|
|
) == "running":
|
|
|
|
html += """
|
|
<p>
|
|
Обновление выполняется. Страница автоматически
|
|
обновит результат после завершения.
|
|
</p>
|
|
"""
|
|
|
|
elif job.get(
|
|
"status"
|
|
) == "error":
|
|
|
|
html += """
|
|
<form
|
|
method="post"
|
|
action="/reports/vendors/update"
|
|
>
|
|
|
|
<div class="vendor-actions">
|
|
|
|
<button type="submit" class="button">
|
|
Повторить обновление
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
"""
|
|
|
|
html += """
|
|
</div>
|
|
|
|
"""
|
|
|
|
else:
|
|
|
|
html += """
|
|
<div class="card">
|
|
|
|
<h2>Обновление базы OUI</h2>
|
|
|
|
<form
|
|
method="post"
|
|
action="/reports/vendors/update"
|
|
>
|
|
|
|
<div class="vendor-actions">
|
|
|
|
<button type="submit" class="button">
|
|
Проверить обновления
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
html += """
|
|
</div>
|
|
|
|
"""
|
|
|
|
return html
|
|
|
|
oui = preview["oui"]
|
|
build = preview["build"]
|
|
ap = preview["ap"]
|
|
|
|
ap_total = (
|
|
ap["unknown_to_vendor"]
|
|
+ ap["vendor_to_vendor"]
|
|
+ ap["vendor_to_unknown"]
|
|
)
|
|
|
|
if oui["identical"]:
|
|
|
|
status_html = """
|
|
<p>
|
|
База OUI не изменилась. Обновление не требуется.
|
|
</p>
|
|
"""
|
|
|
|
elif ap_total == 0:
|
|
|
|
status_html = """
|
|
<p>
|
|
База OUI содержит изменения, но существующие записи
|
|
AP не требуют изменения vendor.
|
|
</p>
|
|
|
|
<p>
|
|
Обновление базы OUI можно применить отдельно.
|
|
Оно обновит справочник для будущих определений vendor.
|
|
</p>
|
|
"""
|
|
|
|
else:
|
|
|
|
status_html = """
|
|
<p>
|
|
База OUI содержит изменения.
|
|
Сначала проверьте изменения в справочнике, затем при
|
|
необходимости выберите синхронизацию vendor у существующих AP.
|
|
</p>
|
|
"""
|
|
|
|
change_html = f"""
|
|
<table>
|
|
|
|
<tr>
|
|
<th>Параметр</th>
|
|
<th>Значение</th>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Текущих записей</td>
|
|
<td>{oui["current"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Записей в новой базе</td>
|
|
<td>{oui["candidate"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Добавлено</td>
|
|
<td>{oui["added"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Изменено</td>
|
|
<td>{oui["changed"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Удалено</td>
|
|
<td>{oui["removed"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Без изменений</td>
|
|
<td>{oui["unchanged"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Импортировано</td>
|
|
<td>{build["imported"]}</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>Пропущено</td>
|
|
<td>{build["skipped"]}</td>
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
"""
|
|
|
|
html += card(
|
|
"🔄 Предварительный просмотр обновления",
|
|
status_html + change_html,
|
|
)
|
|
|
|
html += """
|
|
</div>
|
|
|
|
"""
|
|
|
|
if oui["identical"]:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
html += f"""
|
|
<form
|
|
method="post"
|
|
action="/reports/vendors/apply"
|
|
onsubmit="return confirm('Применить обновление базы OUI?');"
|
|
>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="token"
|
|
value="{escape(token)}"
|
|
>
|
|
|
|
"""
|
|
|
|
if ap_total > 0:
|
|
|
|
html += """
|
|
<div class="vendor-sync">
|
|
|
|
<hr>
|
|
|
|
<h2>
|
|
Синхронизация существующих AP
|
|
</h2>
|
|
|
|
<p>
|
|
Следующие параметры не изменяют саму базу OUI.
|
|
Они определяют, какие изменения vendor применить
|
|
к уже существующим записям WiFi AP.
|
|
</p>
|
|
|
|
<table>
|
|
|
|
<tr>
|
|
<th>Изменение vendor</th>
|
|
<th>Количество</th>
|
|
<th>Применить</th>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>
|
|
<strong>Unknown → Vendor</strong>
|
|
|
|
<details>
|
|
<summary>Что это означает?</summary>
|
|
<p>
|
|
Заполнить поле vendor у существующих AP, для которых
|
|
vendor сейчас неизвестен. Новое значение определяется
|
|
по обновлённой базе OUI.
|
|
</p>
|
|
</details>
|
|
|
|
</td>
|
|
<td>""" + str(ap["unknown_to_vendor"]) + """</td>
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
name="unknown_to_vendor"
|
|
value="1"
|
|
checked
|
|
onchange="updateVendorApplyCount()"
|
|
>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>
|
|
<strong>Vendor → другой Vendor</strong>
|
|
|
|
<details>
|
|
<summary>Что это означает?</summary>
|
|
<p>
|
|
Заменить текущее значение vendor, если обновлённая
|
|
база OUI определяет тот же AP как принадлежащий
|
|
другому vendor.
|
|
</p>
|
|
</details>
|
|
|
|
</td>
|
|
<td>""" + str(ap["vendor_to_vendor"]) + """</td>
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
name="vendor_to_vendor"
|
|
value="1"
|
|
onchange="updateVendorApplyCount()"
|
|
>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td>
|
|
<strong>Vendor → Unknown</strong>
|
|
|
|
<details>
|
|
<summary>Что это означает?</summary>
|
|
<p>
|
|
Удалить текущее значение vendor, если обновлённая
|
|
база OUI больше не позволяет определить vendor
|
|
для данного AP.
|
|
</p>
|
|
</details>
|
|
|
|
</td>
|
|
<td>""" + str(ap["vendor_to_unknown"]) + """</td>
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
name="vendor_to_unknown"
|
|
value="1"
|
|
onchange="updateVendorApplyCount()"
|
|
>
|
|
</td>
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
<p>
|
|
Выбрано изменений AP:
|
|
<strong id="vendor-apply-count">""" + str(
|
|
ap["unknown_to_vendor"]
|
|
) + """</strong>
|
|
</p>
|
|
|
|
<p>
|
|
Синхронизация AP необязательна. Снятые категории
|
|
не будут изменять существующие записи AP.
|
|
</p>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
else:
|
|
|
|
html += """
|
|
<div class="vendor-sync vendor-sync-empty">
|
|
|
|
<hr>
|
|
|
|
<h2>
|
|
Синхронизация существующих AP
|
|
</h2>
|
|
|
|
<p>
|
|
Существующие записи AP не требуют изменения vendor.
|
|
Для этого обновления синхронизация AP недоступна.
|
|
</p>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
html += """
|
|
<div class="vendor-actions">
|
|
|
|
<button type="submit" class="button">
|
|
Применить обновление базы OUI
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
"""
|
|
|
|
if ap_total > 0:
|
|
|
|
html += f"""
|
|
<script>
|
|
function updateVendorApplyCount() {{
|
|
|
|
let total = 0;
|
|
|
|
const fields = [
|
|
[
|
|
"unknown_to_vendor",
|
|
{ap["unknown_to_vendor"]}
|
|
],
|
|
[
|
|
"vendor_to_vendor",
|
|
{ap["vendor_to_vendor"]}
|
|
],
|
|
[
|
|
"vendor_to_unknown",
|
|
{ap["vendor_to_unknown"]}
|
|
]
|
|
];
|
|
|
|
for (const field of fields) {{
|
|
|
|
const checkbox = document.querySelector(
|
|
'input[name="' + field[0] + '"]'
|
|
);
|
|
|
|
if (checkbox && checkbox.checked) {{
|
|
total += field[1];
|
|
}}
|
|
}}
|
|
|
|
document.getElementById(
|
|
"vendor-apply-count"
|
|
).textContent = total;
|
|
}}
|
|
</script>
|
|
|
|
"""
|
|
|
|
return html
|
|
|
|
|
|
def render_vendors_page(
|
|
conn,
|
|
service,
|
|
preview=None,
|
|
token=None,
|
|
message=None,
|
|
context=None,
|
|
job=None
|
|
):
|
|
status = service.status()
|
|
|
|
html = page_begin(
|
|
title="Vendors",
|
|
description=(
|
|
"Vendor database and OUI management"
|
|
),
|
|
active="vendors",
|
|
context=context,
|
|
)
|
|
|
|
if message:
|
|
|
|
html += f"""
|
|
<div class="card">
|
|
|
|
<p>
|
|
{escape(message["text"])}
|
|
</p>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
html += """
|
|
<div class="report-grid">
|
|
|
|
"""
|
|
|
|
html += database_status_block(
|
|
status
|
|
)
|
|
|
|
html += top_vendors_block(
|
|
conn
|
|
)
|
|
|
|
html += """
|
|
</div>
|
|
|
|
"""
|
|
|
|
html += update_block(
|
|
service,
|
|
preview,
|
|
token,
|
|
job
|
|
)
|
|
|
|
html += page_end()
|
|
|
|
return html |