Files
network-topology-discovery/cmd/main.go
T
Your Name 2a97f458a9 prod
2026-04-26 22:00:03 +08:00

959 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"network-topology-discovery/internal/config"
"network-topology-discovery/internal/device"
"network-topology-discovery/internal/scanner"
"network-topology-discovery/internal/storage"
"network-topology-discovery/internal/terminal"
"network-topology-discovery/internal/topology"
"network-topology-discovery/pkg/models"
)
// App 应用
type App struct {
config *config.Config
builder *topology.Builder
storage *storage.Storage
topologyStorage *storage.TopologyStorage
tasks map[string]*models.ScanTask
mu sync.RWMutex
httpServer *http.Server
sessions map[string]time.Time // token -> expire time
sessionMu sync.RWMutex
}
// NewApp 创建应用
func NewApp(cfg *config.Config) *App {
// 初始化拓扑存储(管理多个拓扑)
topoStorage, err := storage.NewTopologyStorage("data")
if err != nil {
log.Printf("Warning: failed to initialize topology storage: %v", err)
}
// 初始化设备存储(使用默认文件,兼容旧版)
store, err := storage.NewStorage("devices.json")
if err != nil {
log.Printf("Warning: failed to initialize storage: %v", err)
}
app := &App{
config: cfg,
builder: topology.NewBuilder(),
storage: store,
topologyStorage: topoStorage,
tasks: make(map[string]*models.ScanTask),
sessions: make(map[string]time.Time),
}
// 如果有拓扑存储,切换到第一个拓扑
if topoStorage != nil {
topos, err := topoStorage.GetAllTopologies()
if err == nil && len(topos) > 0 {
topoStorage.SetCurrentTopology(topos[0].ID)
deviceFile := topoStorage.GetDeviceFilePath(topos[0].ID)
app.storage.SetFilePath(deviceFile)
log.Printf("Loaded topology: %s", topos[0].Name)
}
}
// 加载设备到拓扑构建器
if store != nil {
devices, err := store.GetAllDevices()
if err != nil {
log.Printf("Warning: failed to load devices from database: %v", err)
} else {
log.Printf("Loaded %d devices from storage", len(devices))
for _, dev := range devices {
app.builder.AddDevice(dev)
}
}
}
return app
}
// Start 启动应用
func (app *App) Start() error {
// 设置路由
mux := http.NewServeMux()
// 登录API(无需认证)
mux.HandleFunc("/api/login", app.handleLogin)
mux.HandleFunc("/api/logout", app.handleLogout)
// 登录页面(无需认证)
mux.HandleFunc("/login", app.handleLoginPage)
// 认证中间件保护的路由
authMux := http.NewServeMux()
// 静态文件服务
webDir := getWebDir()
if _, err := os.Stat(webDir); err == nil {
authMux.Handle("/", http.FileServer(http.Dir(webDir)))
} else {
log.Printf("警告: web目录不存在,静态文件服务不可用")
authMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<h1>网络拓扑发现系统</h1><p>Web界面文件未找到</p>"))
})
}
// API路由
authMux.HandleFunc("/api/scan", app.handleScan)
authMux.HandleFunc("/api/scan/{id}", app.handleScanProgress)
authMux.HandleFunc("/api/topology", app.handleTopology)
authMux.HandleFunc("/api/devices", app.handleGetDevices)
authMux.HandleFunc("/api/device", app.handleAddDevice)
authMux.HandleFunc("/api/device/{id}", app.handleDeviceDetail)
// 拓扑管理API
authMux.HandleFunc("/api/topologies", app.handleTopologies)
authMux.HandleFunc("/api/topology/switch", app.handleSwitchTopology)
authMux.HandleFunc("/api/topology/{id}", app.handleTopologyDetail)
// SSH终端API
authMux.HandleFunc("/api/terminal", app.handleTerminalConnect)
// 全局设备池API
authMux.HandleFunc("/api/devices/all", app.handleGetAllDevices)
authMux.HandleFunc("/api/topology/{id}/devices", app.handleAddDevicesToTopology)
// 根据认证配置决定是否启用中间件
var handler http.Handler = authMux
if app.config.Auth.Enabled {
handler = app.authMiddleware(authMux)
log.Printf("认证已启用, 用户: %s", app.config.Auth.Username)
} else {
log.Printf("认证未启用")
}
mux.Handle("/", handler)
addr := fmt.Sprintf("%s:%d", app.config.Web.Host, app.config.Web.Port)
app.httpServer = &http.Server{
Addr: addr,
Handler: mux,
}
log.Printf("服务启动在 %s", addr)
return app.httpServer.ListenAndServe()
}
// 生成唯一ID
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
// getWebDir 获取web目录路径
func getWebDir() string {
// 尝试多个可能的路径
possiblePaths := []string{
"web",
filepath.Join("cmd", "web"),
filepath.Join("..", "web"),
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
absPath, _ := filepath.Abs(path)
return absPath
}
}
// 默认返回web
return "web"
}
// 处理扫描请求
func (app *App) handleScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
ScanRange string `json:"scan_range"`
SSHPort int `json:"ssh_port"`
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.SSHPort == 0 {
req.SSHPort = 22
}
// 创建扫描任务
taskID := generateID()
task := &models.ScanTask{
ID: taskID,
Status: "running",
StartTime: time.Now(),
Devices: []models.Device{},
}
app.mu.Lock()
app.tasks[taskID] = task
app.mu.Unlock()
// 确保当前有拓扑
if app.topologyStorage != nil && app.topologyStorage.GetCurrentTopologyID() == "" {
topos, err := app.topologyStorage.GetAllTopologies()
if err == nil && len(topos) > 0 {
app.topologyStorage.SetCurrentTopology(topos[0].ID)
deviceFile := app.topologyStorage.GetDeviceFilePath(topos[0].ID)
app.storage.SetFilePath(deviceFile)
}
}
// 异步执行扫描
go app.runScan(task, req.ScanRange, req.SSHPort, req.Username, req.Password)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"task_id": taskID})
}
// 执行扫描
func (app *App) runScan(task *models.ScanTask, cidr string, sshPort int, username, password string) {
defer func() {
task.EndTime = time.Now()
}()
// 创建扫描器
sc := scanner.NewScanner(app.config.Scanner.Concurrency, time.Duration(app.config.Scanner.Timeout)*time.Second)
// 阶段1: 解析IP范围 (进度 0% -> 10%)
ips, err := sc.ScanRange(cidr)
if err != nil {
task.Status = "failed"
task.ErrorMessage = err.Error()
return
}
task.TotalDevices = len(ips)
task.Progress = 10
log.Printf("[扫描] 阶段1: 解析CIDR %s, 共 %d 个IP", cidr, len(ips))
// 阶段2: 检查存活主机 (进度 10% -> 30%)
aliveHosts := sc.CheckHosts(ips)
task.Progress = 30
log.Printf("[扫描] 阶段2: 发现 %d 个存活主机", len(aliveHosts))
// 阶段3: 检查SSH端口 (进度 30% -> 50%)
sshHosts := sc.CheckSSHHosts(aliveHosts, sshPort)
task.Progress = 50
task.TotalDevices = len(sshHosts)
log.Printf("[扫描] 阶段3: 发现 %d 个SSH主机", len(sshHosts))
// 如果没有SSH主机,直接完成
if len(sshHosts) == 0 {
task.Status = "completed"
task.Progress = 100
log.Printf("[扫描] 未发现SSH主机,扫描完成")
return
}
// 阶段4: 采集设备信息 (进度 50% -> 100%)
var devices []models.Device
for i, ip := range sshHosts {
log.Printf("[扫描] 阶段4: 正在采集设备 %s (%d/%d)", ip, i+1, len(sshHosts))
// 尝试不同设备类型
deviceTypes := []models.DeviceType{
models.DeviceTypeCisco,
models.DeviceTypeHuawei,
models.DeviceTypeH3C,
models.DeviceTypeASA,
models.DeviceTypeLinux,
models.DeviceTypeWindows,
}
var discoveredDevice *models.Device
for _, dtype := range deviceTypes {
dev, err := device.DiscoverDevice(ip, dtype, username, password)
if err == nil && dev.ScanStatus == "success" {
discoveredDevice = dev
break
}
}
if discoveredDevice != nil {
devices = append(devices, *discoveredDevice)
app.builder.AddDevice(*discoveredDevice)
// 保存到数据库
if app.storage != nil {
if err := app.storage.SaveDevice(discoveredDevice); err != nil {
log.Printf("Warning: failed to save device %s to database: %v", ip, err)
}
}
}
// 更新进度: 50% ~ 100%
task.ScannedDevices = i + 1
task.Progress = 50 + (i+1)*50/len(sshHosts)
task.Devices = devices
}
task.Status = "completed"
task.Progress = 100
task.Devices = devices
log.Printf("[扫描] 完成,共发现 %d 台设备", len(devices))
}
// 处理扫描进度查询
func (app *App) handleScanProgress(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
app.mu.RLock()
task, exists := app.tasks[id]
app.mu.RUnlock()
if !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
// 处理拓扑查询
func (app *App) handleTopology(w http.ResponseWriter, r *http.Request) {
graph := app.builder.Build()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(graph)
}
// 处理获取所有设备
func (app *App) handleGetDevices(w http.ResponseWriter, r *http.Request) {
var devices []models.Device
// 优先从存储获取
if app.storage != nil {
var err error
devices, err = app.storage.GetAllDevices()
if err != nil {
log.Printf("Error: failed to get devices from storage: %v", err)
// 降级到 builder获取
devices = app.builder.GetDevices()
}
log.Printf("Returning %d devices from storage", len(devices))
} else {
devices = app.builder.GetDevices()
log.Printf("Returning %d devices from builder", len(devices))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(devices)
}
// 处理添加设备
func (app *App) handleAddDevice(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
IP string `json:"ip"`
Type string `json:"type"`
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
deviceType := models.DeviceType(req.Type)
log.Printf("Adding device: %s (type: %s)", req.IP, req.Type)
dev, err := device.DiscoverDevice(req.IP, deviceType, req.Username, req.Password)
if err != nil {
log.Printf("Failed to discover device %s: %v", req.IP, err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": err.Error()})
return
}
log.Printf("Device discovered: %s, interfaces: %d, neighbors: %d",
dev.IP, len(dev.Interfaces), len(dev.Neighbors))
app.builder.AddDevice(*dev)
// 保存到存储
if app.storage != nil {
if err := app.storage.SaveDevice(dev); err != nil {
log.Printf("Error: failed to save device %s to storage: %v", req.IP, err)
} else {
log.Printf("Device %s saved to storage successfully", req.IP)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(dev)
}
// 处理设备详情查询
func (app *App) handleDeviceDetail(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if r.Method == http.MethodDelete {
// 删除设备
if app.storage == nil {
http.Error(w, "Storage not available", http.StatusInternalServerError)
return
}
if err := app.storage.DeleteDevice(id); err != nil {
log.Printf("Failed to delete device %s: %v", id, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 同时从 builder 中移除
app.builder.RemoveDevice(id)
log.Printf("Deleted device: %s", id)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
}
if r.Method == http.MethodPut {
// 修改设备信息(Hostname、Type
var req struct {
Hostname string `json:"hostname"`
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if app.storage == nil {
http.Error(w, "Storage not available", http.StatusInternalServerError)
return
}
dev, err := app.storage.GetDevice(id)
if err != nil {
http.Error(w, "Device not found", http.StatusNotFound)
return
}
if req.Hostname != "" {
dev.Hostname = req.Hostname
}
if req.Type != "" {
dev.Type = models.DeviceType(req.Type)
}
if err := app.storage.SaveDevice(dev); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 同时更新 builder 中的设备
app.builder.AddDevice(*dev)
log.Printf("Updated device: %s", id)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(dev)
return
}
// GET: 获取设备详情
devices := app.builder.GetDevices()
for _, dev := range devices {
if dev.ID == id || dev.IP == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(dev)
return
}
}
http.Error(w, "Device not found", http.StatusNotFound)
}
// 处理拓扑列表(GET: 获取所有拓扑,POST: 创建新拓扑)
func (app *App) handleTopologies(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
// 获取所有拓扑
if app.topologyStorage == nil {
json.NewEncoder(w).Encode([]models.Topology{})
return
}
topos, err := app.topologyStorage.GetAllTopologies()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 更新每个拓扑的设备数量
currentTopoID := app.topologyStorage.GetCurrentTopologyID()
for i := range topos {
deviceFile := app.topologyStorage.GetDeviceFilePath(topos[i].ID)
store, err := storage.NewStorage(deviceFile)
if err == nil {
devices, err := store.GetAllDevices()
if err == nil {
topos[i].DeviceCount = len(devices)
}
}
// 标记当前拓扑
if topos[i].ID == currentTopoID {
topos[i].Name = topos[i].Name + " (当前)"
}
}
json.NewEncoder(w).Encode(topos)
} else if r.Method == http.MethodPost {
// 创建新拓扑
var req struct {
Name string `json:"name"`
Description string `json:"description"`
ScanRange string `json:"scan_range"`
SSHPort int `json:"ssh_port"`
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
topo, err := app.topologyStorage.CreateTopology(req.Name, req.Description, req.ScanRange, req.SSHPort, req.Username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 自动切换到新拓扑
app.topologyStorage.SetCurrentTopology(topo.ID)
deviceFile := app.topologyStorage.GetDeviceFilePath(topo.ID)
app.storage.SetFilePath(deviceFile)
app.builder.Clear()
log.Printf("Created and switched to new topology: %s", topo.Name)
json.NewEncoder(w).Encode(topo)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// 处理切换拓扑
func (app *App) handleSwitchTopology(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
TopologyID string `json:"topology_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if app.topologyStorage == nil {
http.Error(w, "Topology storage not available", http.StatusInternalServerError)
return
}
// 验证拓扑存在
topo, err := app.topologyStorage.GetTopology(req.TopologyID)
if err != nil {
http.Error(w, "Topology not found", http.StatusNotFound)
return
}
// 切换拓扑
err = app.topologyStorage.SetCurrentTopology(req.TopologyID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 切换设备存储文件
deviceFile := app.topologyStorage.GetDeviceFilePath(req.TopologyID)
err = app.storage.SetFilePath(deviceFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 清空并重新加载拓扑构建器
app.builder.Clear()
devices, err := app.storage.GetAllDevices()
if err == nil {
for _, dev := range devices {
app.builder.AddDevice(dev)
}
}
log.Printf("Switched to topology: %s (%s)", topo.Name, req.TopologyID)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "Topology switched successfully"})
}
// 处理拓扑详情
func (app *App) handleTopologyDetail(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if app.topologyStorage == nil {
http.Error(w, "Topology storage not available", http.StatusInternalServerError)
return
}
topo, err := app.topologyStorage.GetTopology(id)
if err != nil {
http.Error(w, "Topology not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(topo)
}
// 处理SSH终端连接
func (app *App) handleTerminalConnect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
// WebSocket 连接使用 GET,但我们通过 POST 获取凭据后再升级
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// 从查询参数获取设备IP
ip := r.URL.Query().Get("ip")
if ip == "" {
http.Error(w, "ip parameter is required", http.StatusBadRequest)
return
}
port := 22
if p := r.URL.Query().Get("port"); p != "" {
fmt.Sscanf(p, "%d", &port)
}
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
if username == "" || password == "" {
http.Error(w, "username and password are required", http.StatusBadRequest)
return
}
log.Printf("[终端] 正在连接 %s:%d (%s)", ip, port, username)
// 使用 terminal handler 处理 WebSocket 连接
terminal.HandleTerminal(w, r, ip, port, username, password)
}
// ==================== 认证相关 ====================
// authMiddleware 认证中间件
func (app *App) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 cookie 获取 token
cookie, err := r.Cookie("session_token")
if err != nil || !app.isValidSession(cookie.Value) {
// API 请求返回 401,页面请求重定向到登录
if strings.HasPrefix(r.URL.Path, "/api/") {
http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
} else {
http.Redirect(w, r, "/login", http.StatusFound)
}
return
}
next.ServeHTTP(w, r)
})
}
// isValidSession 验证会话是否有效
func (app *App) isValidSession(token string) bool {
if token == "" {
return false
}
app.sessionMu.RLock()
expire, exists := app.sessions[token]
app.sessionMu.RUnlock()
if !exists {
return false
}
if time.Now().After(expire) {
app.sessionMu.Lock()
delete(app.sessions, token)
app.sessionMu.Unlock()
return false
}
return true
}
// generateToken 生成随机 token
func generateToken() string {
b := make([]byte, 32)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
// handleLogin 处理登录
func (app *App) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !app.config.Auth.Enabled {
json.NewEncoder(w).Encode(map[string]interface{}{"success": true})
return
}
if req.Username == app.config.Auth.Username && req.Password == app.config.Auth.Password {
token := generateToken()
expire := time.Now().Add(24 * time.Hour)
app.sessionMu.Lock()
app.sessions[token] = expire
app.sessionMu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: token,
Path: "/",
Expires: expire,
HttpOnly: true,
})
log.Printf("用户 %s 登录成功", req.Username)
json.NewEncoder(w).Encode(map[string]interface{}{"success": true})
} else {
log.Printf("登录失败: 用户名或密码错误 (username=%s)", req.Username)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{"success": false, "message": "用户名或密码错误"})
}
}
// handleLogout 处理登出
func (app *App) handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("session_token"); err == nil {
app.sessionMu.Lock()
delete(app.sessions, cookie.Value)
app.sessionMu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
// handleLoginPage 返回登录页面
func (app *App) handleLoginPage(w http.ResponseWriter, r *http.Request) {
if !app.config.Auth.Enabled {
http.Redirect(w, r, "/", http.StatusFound)
return
}
http.ServeFile(w, r, filepath.Join(getWebDir(), "login.html"))
}
// 获取所有拓扑中的全部设备(全局设备池)
func (app *App) handleGetAllDevices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
deviceMap := make(map[string]models.Device) // 用IP去重
if app.topologyStorage != nil {
topos, err := app.topologyStorage.GetAllTopologies()
if err == nil {
for _, topo := range topos {
deviceFile := app.topologyStorage.GetDeviceFilePath(topo.ID)
store, err := storage.NewStorage(deviceFile)
if err != nil {
continue
}
devices, err := store.GetAllDevices()
if err != nil {
continue
}
for _, dev := range devices {
deviceMap[dev.IP] = dev
}
}
}
}
// 也从旧版 devices.json 加载
if app.storage != nil {
devices, err := app.storage.GetAllDevices()
if err == nil {
for _, dev := range devices {
deviceMap[dev.IP] = dev
}
}
}
devices := make([]models.Device, 0, len(deviceMap))
for _, dev := range deviceMap {
devices = append(devices, dev)
}
log.Printf("[设备池] 返回 %d 个全局设备", len(devices))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(devices)
}
// 向指定拓扑批量添加设备
func (app *App) handleAddDevicesToTopology(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
topoID := r.PathValue("id")
var req struct {
DeviceIDs []string `json:"device_ids"` // IP列表或ID列表
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req.DeviceIDs) == 0 {
http.Error(w, "device_ids is required", http.StatusBadRequest)
return
}
// 获取目标拓扑的存储
if app.topologyStorage == nil {
http.Error(w, "Topology storage not available", http.StatusInternalServerError)
return
}
topo, err := app.topologyStorage.GetTopology(topoID)
if err != nil {
http.Error(w, "Topology not found", http.StatusNotFound)
return
}
deviceFile := app.topologyStorage.GetDeviceFilePath(topoID)
targetStore, err := storage.NewStorage(deviceFile)
if err != nil {
http.Error(w, "Failed to open target storage", http.StatusInternalServerError)
return
}
// 从全局设备池中查找并添加
allDeviceMap := make(map[string]models.Device)
topos, _ := app.topologyStorage.GetAllTopologies()
for _, t := range topos {
df := app.topologyStorage.GetDeviceFilePath(t.ID)
s, err := storage.NewStorage(df)
if err != nil {
continue
}
devs, err := s.GetAllDevices()
if err != nil {
continue
}
for _, d := range devs {
allDeviceMap[d.IP] = d
allDeviceMap[d.ID] = d
}
}
added := 0
for _, idOrIP := range req.DeviceIDs {
if dev, ok := allDeviceMap[idOrIP]; ok {
devCopy := dev // 复制一份
targetStore.SaveDevice(&devCopy)
added++
}
}
// 如果是当前拓扑,刷新builder
if app.topologyStorage.GetCurrentTopologyID() == topoID {
app.builder.Clear()
devices, _ := targetStore.GetAllDevices()
for _, dev := range devices {
app.builder.AddDevice(dev)
}
}
log.Printf("[拓扑] 向 %s 添加了 %d/%d 设备", topo.Name, added, len(req.DeviceIDs))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"added": added,
"total": len(req.DeviceIDs),
"topology": topo.Name,
})
}
func main() {
// 加载配置
configFile := "config.json"
if len(os.Args) > 1 {
configFile = os.Args[1]
}
var cfg *config.Config
if _, err := os.Stat(configFile); err == nil {
cfg, err = config.LoadConfig(configFile)
if err != nil {
log.Printf("加载配置文件失败: %v, 使用默认配置", err)
cfg = config.DefaultConfig()
}
} else {
log.Printf("配置文件不存在, 使用默认配置")
cfg = config.DefaultConfig()
}
// 创建并启动应用
app := NewApp(cfg)
log.Println("网络拓扑发现系统启动...")
if err := app.Start(); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}