16762c14ac
Made-with: Cursor
273 lines
12 KiB
Markdown
273 lines
12 KiB
Markdown
# nettopo-go
|
||
|
||
Минимальный старт проекта сетового инвентаризатора и топологии на Go без Docker.
|
||
|
||
## Что уже есть
|
||
|
||
- `GET /health` - проверка состояния API.
|
||
- `POST /api/scans` - создание задания скана.
|
||
- `GET /api/scans` - список сканов.
|
||
- `GET /api/scans/{id}` - просмотр созданного задания.
|
||
- `GET /api/scans/diff?from=&to=` - сравнение двух сканов (hosts/links).
|
||
- `GET /api/scans/{id}/hosts` - результаты проверки хостов по скану.
|
||
- `GET /api/scans/{id}/ports` - результаты проверки TCP-портов по скану.
|
||
- `GET /api/scans/{id}/snmp` - результаты SNMP v2c (`sysName`, `sysDescr`, `sysObjectID`).
|
||
- `GET /api/scans/{id}/lldp` - LLDP соседи (если устройство отдает LLDP-MIB по SNMP).
|
||
- `GET /api/scans/{id}/links` - базовые связи из LLDP с попыткой сопоставления по `remote_sys_name`.
|
||
- `GET /api/scans/{id}/topology` - граф `nodes/edges` для визуализации.
|
||
- `GET /` — UI для просмотра топологии (страница встроена в бинарник через `embed`, каталог `internal/webui/`; **не зависит** от `WorkingDirectory` и папки `web/` рядом с процессом).
|
||
- Валидация CIDR и exclude IP.
|
||
- Два backend-хранилища: in-memory и PostgreSQL.
|
||
- После создания scan запускается фоновый discovery с обновлением статуса и прогресса.
|
||
- Отдельный чек-лист ручной приемки: `MANUAL_TEST_CHECKLIST.md`.
|
||
- План продолжения на завтра: `NEXT_STEPS.md`.
|
||
|
||
## Требования
|
||
|
||
- Go 1.22+ (достаточно пакета `golang-go` из Ubuntu 24.04). Зависимость `pgx` зафиксирована на **v5.7.4**, потому что **v5.7.5+** объявляет `go 1.23` и не соберётся на Go 1.22 из apt. При желании можно поднять toolchain до **Go 1.23+** и снова обновить `pgx`.
|
||
|
||
Проверка версии:
|
||
|
||
```bash
|
||
go version
|
||
```
|
||
|
||
## Зависимости Go (`go.mod`, `go.sum`)
|
||
|
||
В репозитории закоммичен **`go.sum`**: в нём зафиксированы контрольные суммы модулей из `go.mod`. Это нужно, чтобы на новом клоне (например на сервере после `git pull`) команда `go build` не завершалась ошибкой вида `missing go.sum entry for module ...`. Исходники зависимостей в git не копируются — при сборке модули по-прежнему загружаются через прокси модулей (`proxy.golang.org` или ваш `GOPROXY`), если не используете `vendor`.
|
||
|
||
После изменения зависимостей выполните **`go mod tidy`** и закоммитьте обновлённые `go.mod` и `go.sum`. Проверка целостности: **`go mod verify`**.
|
||
|
||
Если на машине **нет доступа в интернет** во время сборки, можно закоммитить каталог **`vendor/`** (команда `go mod vendor`) и собирать так: **`go build -mod=vendor -o nettopo-server ./cmd/server`**.
|
||
|
||
## Запуск локально
|
||
|
||
1. Перейдите в проект:
|
||
|
||
```bash
|
||
cd nettopo-go
|
||
```
|
||
|
||
2. Скопируйте переменные окружения:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
3. Запустите API:
|
||
|
||
```bash
|
||
HTTP_ADDR=:8080 go run ./cmd/server
|
||
```
|
||
|
||
UI открывается по адресу:
|
||
|
||
```bash
|
||
http://localhost:8080/
|
||
```
|
||
|
||
Что умеет UI сейчас:
|
||
|
||
- загрузить topology по `scan_id`;
|
||
- автоматически подставить последний scan (`Последний scan`);
|
||
- сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`.
|
||
- выбрать scan из выпадающего списка последних 20;
|
||
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
|
||
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
|
||
- экспортировать topology и diff в JSON-файлы кнопками из UI.
|
||
- создать scan по CIDR из UI и дождаться завершения кнопкой "Ждать завершения".
|
||
|
||
По умолчанию используется `STORE_BACKEND=memory`.
|
||
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
|
||
|
||
## Логика одного scan (что делает программа)
|
||
|
||
Один запуск scan — это **одна фоновая задача** (`runner`), без отдельных фаз «только ping» / «только LLDP» в API.
|
||
|
||
1. По полю `cidrs` строится список IP (с учётом `exclude_ips`).
|
||
2. Параллельно (ограничение `max_parallel_hosts`) для каждого IP:
|
||
- **ping** (ICMP через системную утилиту `ping`);
|
||
- если хост «вверх» и включён port-scan — проверка **TCP-портов** из `options.ports`;
|
||
- если хост «вверх» и `SNMP_ENABLED=true` — **SNMP v2c** (sysName, sysDescr, sysObjectID) и сразу **обход LLDP-MIB** (соседи, локальный/удалённый порт и т.д.). Это не отдельная кнопка: LLDP выполняется в том же проходе, что и базовый SNMP.
|
||
3. Статус задачи: `queued` → `running` (поле `progress` 0…100, `stats.hosts_total` / `stats.hosts_up`) → `done` или `failed`.
|
||
4. Результаты читаются через API: `/hosts`, `/ports`, `/snmp`, `/lldp`, агрегированный граф — `/topology`.
|
||
|
||
В веб-UI после **«Создать scan и ждать»** опрашивается статус, показывается расшифровка в строке состояния, по завершении обновляется таблица **найденных устройств** (ping + SNMP) и строится граф по кнопке **«Загрузить»** (или автоматически после ожидания).
|
||
|
||
### LLDP пустой на коммутаторе (например Ubiquiti EdgeSwitch)
|
||
|
||
Опрос LLDP идёт по стандартному **LLDP-MIB** (`1.0.8802.1.1.2.1.4.1.1.*`) после успешного SNMP **Get** системной группы. Раньше при **GET-BULK** часть прошивок отвечала слишком долго или с ошибкой — обход обрывался, в БД не попадало ни строки («No LLDP data» в UI). Сейчас для фазы LLDP увеличен таймаут (не меньше **3 c** на запрос), снижен размер «пачки» **MaxRepetitions**, добавлен **повтор при ошибке через SNMP Walk (GetNext)**. В логах сервера ищите строки `lldp snmp BulkWalkAll` и `fallback Walk`. Проверьте с хоста nettopo: community (**`SNMP_COMMUNITY` / `public`**), доступ UDP **161** с этого сервера и что на свиче включён **SNMP v2c** (не только LLDP в L2, но и ответ агента SNMP).
|
||
|
||
## PostgreSQL без Docker
|
||
|
||
### Вариант A: macOS (Homebrew)
|
||
|
||
```bash
|
||
brew install postgresql@16
|
||
brew services start postgresql@16
|
||
createdb nettopo
|
||
createuser nettopo
|
||
```
|
||
|
||
```bash
|
||
psql -d postgres -c "alter user nettopo with password 'nettopo';"
|
||
psql -d postgres -c "grant all privileges on database nettopo to nettopo;"
|
||
```
|
||
|
||
### Вариант B: Ubuntu/Debian
|
||
|
||
```bash
|
||
sudo apt update
|
||
sudo apt install -y postgresql postgresql-contrib
|
||
sudo -u postgres psql -c "create user nettopo with password 'nettopo';"
|
||
sudo -u postgres psql -c "create database nettopo owner nettopo;"
|
||
```
|
||
|
||
### Вариант C: Windows
|
||
|
||
- Установить PostgreSQL через официальный installer.
|
||
- Создать БД `nettopo` и пользователя `nettopo` (через pgAdmin или `psql`).
|
||
|
||
### Запуск API с PostgreSQL
|
||
|
||
```bash
|
||
export STORE_BACKEND=postgres
|
||
export DB_DSN='postgres://nettopo:nettopo@localhost:5432/nettopo?sslmode=disable'
|
||
export RUN_MIGRATIONS=true
|
||
go run ./cmd/server
|
||
```
|
||
|
||
При первом запуске автоматически применится миграция `scan_jobs`.
|
||
|
||
## Быстрая проверка API
|
||
|
||
Проверка health:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/health
|
||
```
|
||
|
||
Создать scan:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:8080/api/scans \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"name":"Office scan",
|
||
"cidrs":["192.168.1.0/24"],
|
||
"exclude_ips":["192.168.1.1"],
|
||
"options":{
|
||
"ping_timeout_ms":700,
|
||
"ping_retries":1,
|
||
"max_parallel_hosts":128,
|
||
"port_scan_enabled":true,
|
||
"ports":[22,80,443,161]
|
||
}
|
||
}'
|
||
```
|
||
|
||
Получить scan по id:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>
|
||
```
|
||
|
||
Получить список scan jobs:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans?limit=20
|
||
```
|
||
|
||
Сравнить два scan:
|
||
|
||
```bash
|
||
curl -s "http://localhost:8080/api/scans/diff?from=<scan_id_1>&to=<scan_id_2>"
|
||
```
|
||
|
||
В ответе у scan есть:
|
||
|
||
- `status`: `queued`, `running`, `done`, `failed`
|
||
- `progress`: 0..100
|
||
- `stats.hosts_total`, `stats.hosts_up`
|
||
|
||
Получить результаты по хостам:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/hosts?limit=200
|
||
```
|
||
|
||
Получить результаты по портам:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/ports?limit=500
|
||
```
|
||
|
||
Получить SNMP результаты:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/snmp?limit=500
|
||
```
|
||
|
||
Получить LLDP результаты:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/lldp?limit=1000
|
||
```
|
||
|
||
Получить связи:
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/links?limit=1000
|
||
```
|
||
|
||
Получить topology (nodes/edges):
|
||
|
||
```bash
|
||
curl -s http://localhost:8080/api/scans/<scan_id>/topology?limit=2000
|
||
```
|
||
|
||
## Запуск как сервис на Linux (systemd)
|
||
|
||
1. Собрать бинарник:
|
||
|
||
```bash
|
||
go build -o nettopo-server ./cmd/server
|
||
```
|
||
|
||
2. Скопировать unit-файл:
|
||
|
||
```bash
|
||
sudo cp deploy/nettopo-go.service /etc/systemd/system/
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now nettopo-go
|
||
```
|
||
|
||
3. Проверить статус:
|
||
|
||
```bash
|
||
systemctl status nettopo-go
|
||
```
|
||
|
||
### Логи (systemd)
|
||
|
||
Сервер пишет в **стандартный лог** пакета `log` (stderr): старт, выбранное хранилище, фатальные ошибки БД. Просмотр:
|
||
|
||
```bash
|
||
journalctl -u nettopo-go -f
|
||
journalctl -u nettopo-go -n 100 --no-pager
|
||
```
|
||
|
||
Отдельного файла логов приложение **не создаёт**; при необходимости перенаправьте вывод unit-а в файл через `StandardOutput=` / `StandardError=` в override.
|
||
|
||
### Если в браузере `404 page not found` на `/`
|
||
|
||
Раньше UI отдавался из каталога `web/` относительно **текущей рабочей директории** процесса; при `WorkingDirectory=/opt/nettopo-go` без этой папки корень сайта давал 404. Сейчас UI **вшит в бинарник** — после обновления до актуального коммита пересоберите сервер (`go build ...`) и перезапустите сервис. Проверка API: `curl -s http://127.0.0.1:8080/health` должен вернуть JSON `{"status":"ok",...}`.
|
||
|
||
## Размещение за IIS на Windows
|
||
|
||
- Запускать `nettopo-server.exe` как Windows Service.
|
||
- На IIS настроить Reverse Proxy к `http://localhost:8080`.
|
||
- Требуются URL Rewrite + ARR.
|
||
|
||
Шаблон `web.config` лежит в `deploy/web.config.example`.
|