package service import ( "context" "fmt" "github.com/spf13/viper" "go.uber.org/zap" "net" "os" "os/exec" "strings" "sync" ) type IpService interface { // AddIp 添加一个IP地址到网络接口并持久化。 AddIp(ctx context.Context, ip string) error // DeleteIp 从网络接口删除一个IP地址并移除持久化配置。 DeleteIp(ctx context.Context, ip string) error } func NewIpService( service *Service, viper *viper.Viper, ) IpService { return &ipService{ Service: service, networkInterface: viper.GetString("ip.network_interface"), ipLabel: viper.GetString("ip.ip_label"), scriptPath: viper.GetString("ip.script_path"), } } type ipService struct { *Service fileMux sync.Mutex networkInterface string ipLabel string scriptPath string } // AddIp 负责添加一个IP地址到指定的网络接口,并将其持久化。 func (s *ipService) AddIp(ctx context.Context, ip string) error { if net.ParseIP(ip) == nil { return fmt.Errorf("无效的IP地址: %s", ip) } fullIp := ip + "/32" addCmdStr := fmt.Sprintf("sudo ip addr add %s dev %s label \"%s\"", fullIp, s.networkInterface, s.ipLabel) s.logger.Info("准备执行添加命令: %s\n", zap.String("addCmdStr", addCmdStr)) // 执行实时添加IP的命令 cmd := exec.CommandContext(ctx, "sudo", "ip", "addr", "add", fullIp, "dev", s.networkInterface, "label", s.ipLabel) output, err := cmd.CombinedOutput() if err != nil { s.logger.Error("执行添加命令失败: %s\n", zap.String("addCmdStr", addCmdStr), zap.Error(err)) return fmt.Errorf("执行添加命令失败 '%s': %w. 输出: %s", addCmdStr, err, string(output)) } // 持久化IP地址到脚本文件 s.logger.Info("尝试将IP持久化到脚本: %s\n", zap.String("scriptPath", s.scriptPath)) if err := s.makePersistent(addCmdStr); err != nil { return fmt.Errorf("持久化IP失败: %w", err) } return nil } // DeleteIp 负责从网络接口删除一个IP地址,并从持久化脚本中移除它。 func (s *ipService) DeleteIp(ctx context.Context, ip string) error { if net.ParseIP(ip) == nil { return fmt.Errorf("无效的IP地址: %s", ip) } fullIp := ip + "/32" // 这是要执行的删除命令 delCmdStr := fmt.Sprintf("sudo ip addr del %s dev %s", fullIp, s.networkInterface) // 这是要在脚本文件中查找并删除的添加命令 addCmdStrInScript := fmt.Sprintf("sudo ip addr add %s dev %s label \"%s\"", fullIp, s.networkInterface, s.ipLabel) s.logger.Info("准备执行删除命令: ", zap.String("delCmdStr", delCmdStr)) // 执行实时删除IP的命令 cmd := exec.CommandContext(ctx, "sudo", "ip", "addr", "del", fullIp, "dev", s.networkInterface) output, err := cmd.CombinedOutput() if err != nil { // 如果错误信息是 "Cannot assign requested address",通常意味着IP已经不存在了。 // 在这种情况下,我们不应该中止,而是应该继续清理脚本。 // 对于其他错误,我们则返回。 if !strings.Contains(string(output), "Cannot assign requested address") { return fmt.Errorf("执行删除命令失败 '%s': %w. 输出: %s", delCmdStr, err, string(output)) } s.logger.Warn("警告: IP %s 已不在网络接口上,继续清理持久化脚本。\n", zap.String("ip", ip)) } else { s.logger.Info("成功从网络接口删除IP地址。") } // 从持久化脚本中移除IP s.logger.Info("尝试从脚本 %s 中移除IP持久化记录\n", zap.String("scriptPath", s.scriptPath)) if err := s.removeFromPersistent(addCmdStrInScript); err != nil { return fmt.Errorf("从持久化脚本中移除IP失败: %w", err) } s.logger.Info("成功从脚本中移除IP持久化记录。") return nil } // makePersistent 将IP添加命令写入到持久化脚本中。 func (s *ipService) makePersistent(cmdToAdd string) error { s.fileMux.Lock() defer s.fileMux.Unlock() content, err := os.ReadFile(s.scriptPath) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("持久化脚本不存在: %s", s.scriptPath) } return fmt.Errorf("读取持久化脚本失败 %s: %w", s.scriptPath, err) } if strings.Contains(string(content), cmdToAdd) { // 如果命令已经在脚本中,那么不需要再次添加。 return nil } lines := strings.Split(string(content), "\n") var newLines []string inserted := false for _, line := range lines { if strings.TrimSpace(line) == "exit 0" && !inserted { newLines = append(newLines, cmdToAdd) inserted = true } newLines = append(newLines, line) } if !inserted { newLines = append(newLines, cmdToAdd) } newContent := strings.Join(newLines, "\n") return os.WriteFile(s.scriptPath, []byte(newContent), 0644) } // removeFromPersistent 从持久化脚本中删除指定的命令。 func (s *ipService) removeFromPersistent(cmdToRemove string) error { s.fileMux.Lock() defer s.fileMux.Unlock() content, err := os.ReadFile(s.scriptPath) if err != nil { if os.IsNotExist(err) { // 如果脚本文件不存在,那么命令肯定也不在里面,可以直接返回成功。 return nil } return fmt.Errorf("读取持久化脚本失败 %s: %w", s.scriptPath, err) } lines := strings.Split(string(content), "\n") var newLines []string found := false for _, line := range lines { // 完全匹配要删除的行,然后跳过它 if line == cmdToRemove { found = true continue } newLines = append(newLines, line) } if !found { // 如果未找到要删除的行,那么命令肯定也不在里面,可以直接返回成功。 return nil } newContent := strings.Join(newLines, "\n") return os.WriteFile(s.scriptPath, []byte(newContent), 0644) }