manager.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. package vrrp
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "sync"
  12. "time"
  13. "github.com/vishvananda/netlink"
  14. )
  15. const (
  16. VRRPStorePath = "/etc/debtik/vrrp.json"
  17. KeepalivedConfPath = "/etc/keepalived/keepalived.conf"
  18. VRRPStatusDir = "/run/debtik/vrrp"
  19. VRRPNotifyScriptPath = "/usr/local/bin/vrrp-notify.sh"
  20. )
  21. type Manager struct {
  22. mu sync.RWMutex
  23. instances map[string]VRRPInstance
  24. storePath string
  25. confPath string
  26. }
  27. func NewManager() *Manager {
  28. m := &Manager{
  29. instances: make(map[string]VRRPInstance),
  30. storePath: VRRPStorePath,
  31. confPath: KeepalivedConfPath,
  32. }
  33. _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
  34. _ = os.MkdirAll(filepath.Dir(m.confPath), 0755)
  35. _ = os.MkdirAll(VRRPStatusDir, 0755)
  36. m.ensureNotifyScript()
  37. m.load()
  38. // Apply config on startup if instances exist
  39. _ = m.applyConfigLocked()
  40. return m
  41. }
  42. func (m *Manager) ensureNotifyScript() {
  43. scriptContent := `#!/bin/bash
  44. STATE="$1"
  45. NAME="$2"
  46. mkdir -p /run/debtik/vrrp
  47. echo "$STATE $(date -Iseconds)" > "/run/debtik/vrrp/${NAME}.state"
  48. `
  49. _ = os.WriteFile(VRRPNotifyScriptPath, []byte(scriptContent), 0755)
  50. }
  51. func (m *Manager) load() {
  52. m.mu.Lock()
  53. defer m.mu.Unlock()
  54. data, err := os.ReadFile(m.storePath)
  55. if err != nil {
  56. return
  57. }
  58. var store VRRPConfigStore
  59. if err := json.Unmarshal(data, &store); err != nil {
  60. return
  61. }
  62. for _, inst := range store.Instances {
  63. m.instances[inst.ID] = inst
  64. }
  65. }
  66. func (m *Manager) saveLocked() error {
  67. store := VRRPConfigStore{
  68. Instances: make([]VRRPInstance, 0, len(m.instances)),
  69. }
  70. for _, inst := range m.instances {
  71. store.Instances = append(store.Instances, inst)
  72. }
  73. data, err := json.MarshalIndent(store, "", " ")
  74. if err != nil {
  75. return err
  76. }
  77. return os.WriteFile(m.storePath, data, 0644)
  78. }
  79. func (m *Manager) List() []VRRPInstance {
  80. m.mu.RLock()
  81. defer m.mu.RUnlock()
  82. isDaemonActive := m.isKeepalivedRunning()
  83. list := make([]VRRPInstance, 0, len(m.instances))
  84. for _, inst := range m.instances {
  85. copyInst := inst
  86. copyInst.VirtualMAC = fmt.Sprintf("00:00:5E:00:01:%02X", copyInst.VRID)
  87. if copyInst.Disabled {
  88. copyInst.State = "DISABLED"
  89. copyInst.Running = false
  90. } else {
  91. copyInst.State, copyInst.Running, copyInst.LastTransition = m.detectInstanceState(copyInst, isDaemonActive)
  92. }
  93. list = append(list, copyInst)
  94. }
  95. sort.Slice(list, func(i, j int) bool {
  96. if list[i].Interface != list[j].Interface {
  97. return list[i].Interface < list[j].Interface
  98. }
  99. return list[i].VRID < list[j].VRID
  100. })
  101. return list
  102. }
  103. func (m *Manager) detectInstanceState(inst VRRPInstance, daemonActive bool) (string, bool, time.Time) {
  104. link, err := netlink.LinkByName(inst.Interface)
  105. if err != nil || link.Attrs().OperState == netlink.OperDown {
  106. return "FAULT", false, time.Time{}
  107. }
  108. // 1. Direct Kernel Check: Does this interface currently hold any of the virtual IPs?
  109. if len(inst.VirtualIPs) > 0 {
  110. addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
  111. if err == nil {
  112. for _, a := range addrs {
  113. for _, vip := range inst.VirtualIPs {
  114. cleanVIP := strings.Split(vip, "/")[0]
  115. if a.IP.String() == cleanVIP {
  116. return "MASTER", true, time.Time{}
  117. }
  118. }
  119. }
  120. }
  121. }
  122. // 2. Check notify state file
  123. stateFile := filepath.Join(VRRPStatusDir, fmt.Sprintf("%s.state", sanitizeName(inst.Name)))
  124. if data, err := os.ReadFile(stateFile); err == nil {
  125. parts := strings.Fields(string(data))
  126. if len(parts) >= 1 {
  127. st := parts[0]
  128. var t time.Time
  129. if len(parts) >= 2 {
  130. t, _ = time.Parse(time.RFC3339, parts[1])
  131. }
  132. if st == "MASTER" {
  133. return "MASTER", true, t
  134. }
  135. if st == "FAULT" {
  136. return "FAULT", false, t
  137. }
  138. if st == "BACKUP" && daemonActive {
  139. return "BACKUP", true, t
  140. }
  141. }
  142. }
  143. if daemonActive {
  144. return "BACKUP", true, time.Time{}
  145. }
  146. return "FAULT", false, time.Time{}
  147. }
  148. func (m *Manager) isKeepalivedRunning() bool {
  149. out, err := exec.Command("systemctl", "is-active", "keepalived").Output()
  150. return err == nil && strings.TrimSpace(string(out)) == "active"
  151. }
  152. func (m *Manager) Get(id string) (VRRPInstance, bool) {
  153. m.mu.RLock()
  154. defer m.mu.RUnlock()
  155. inst, ok := m.instances[id]
  156. if !ok {
  157. return VRRPInstance{}, false
  158. }
  159. inst.VirtualMAC = fmt.Sprintf("00:00:5E:00:01:%02X", inst.VRID)
  160. if inst.Disabled {
  161. inst.State = "DISABLED"
  162. inst.Running = false
  163. } else {
  164. inst.State, inst.Running, inst.LastTransition = m.detectInstanceState(inst, m.isKeepalivedRunning())
  165. }
  166. return inst, true
  167. }
  168. func (m *Manager) Save(inst VRRPInstance) (VRRPInstance, error) {
  169. m.mu.Lock()
  170. defer m.mu.Unlock()
  171. if inst.Name == "" || inst.Interface == "" {
  172. return inst, fmt.Errorf("name and interface are required")
  173. }
  174. if inst.VRID < 1 || inst.VRID > 255 {
  175. return inst, fmt.Errorf("VRID must be between 1 and 255")
  176. }
  177. if inst.Priority < 1 || inst.Priority > 255 {
  178. inst.Priority = 100
  179. }
  180. if inst.Interval < 1 {
  181. inst.Interval = 1
  182. }
  183. if inst.Version != 2 && inst.Version != 3 {
  184. inst.Version = 3
  185. }
  186. if len(inst.VirtualIPs) == 0 {
  187. return inst, fmt.Errorf("at least one Virtual IP (VIP) is required")
  188. }
  189. // Validate VIP formatting
  190. for _, vip := range inst.VirtualIPs {
  191. ipStr := strings.Split(vip, "/")[0]
  192. if net.ParseIP(strings.TrimSpace(ipStr)) == nil {
  193. return inst, fmt.Errorf("invalid virtual IP address: %s", vip)
  194. }
  195. }
  196. if inst.ID == "" {
  197. inst.ID = fmt.Sprintf("vrrp-%s", sanitizeName(inst.Name))
  198. }
  199. m.instances[inst.ID] = inst
  200. if err := m.saveLocked(); err != nil {
  201. return inst, err
  202. }
  203. if err := m.applyConfigLocked(); err != nil {
  204. return inst, err
  205. }
  206. return inst, nil
  207. }
  208. func (m *Manager) Delete(id string) error {
  209. m.mu.Lock()
  210. defer m.mu.Unlock()
  211. inst, ok := m.instances[id]
  212. if !ok {
  213. return fmt.Errorf("VRRP instance not found: %s", id)
  214. }
  215. delete(m.instances, id)
  216. _ = os.Remove(filepath.Join(VRRPStatusDir, fmt.Sprintf("%s.state", sanitizeName(inst.Name))))
  217. if err := m.saveLocked(); err != nil {
  218. return err
  219. }
  220. return m.applyConfigLocked()
  221. }
  222. func (m *Manager) ToggleState(id string, disabled bool) error {
  223. m.mu.Lock()
  224. defer m.mu.Unlock()
  225. inst, ok := m.instances[id]
  226. if !ok {
  227. return fmt.Errorf("VRRP instance not found: %s", id)
  228. }
  229. inst.Disabled = disabled
  230. m.instances[id] = inst
  231. if err := m.saveLocked(); err != nil {
  232. return err
  233. }
  234. return m.applyConfigLocked()
  235. }
  236. func (m *Manager) GetSummary() VRRPSummary {
  237. list := m.List()
  238. s := VRRPSummary{
  239. TotalInstances: len(list),
  240. }
  241. for _, inst := range list {
  242. if inst.State == "MASTER" {
  243. s.MasterInstances++
  244. } else if inst.State == "BACKUP" {
  245. s.BackupInstances++
  246. } else if inst.State == "FAULT" {
  247. s.FaultInstances++
  248. }
  249. }
  250. return s
  251. }
  252. func (m *Manager) applyConfigLocked() error {
  253. m.ensureNotifyScript()
  254. activeCount := 0
  255. for _, inst := range m.instances {
  256. if !inst.Disabled {
  257. activeCount++
  258. }
  259. }
  260. if activeCount == 0 {
  261. _ = exec.Command("systemctl", "stop", "keepalived").Run()
  262. _ = os.Remove(m.confPath)
  263. return nil
  264. }
  265. var sb strings.Builder
  266. sb.WriteString("# ==================================================================\n")
  267. sb.WriteString("# Enterprise VRRP (Virtual Router Redundancy Protocol) Configuration\n")
  268. sb.WriteString("# Managed by DebTik Network Appliance Controller. DO NOT EDIT DIRECTLY.\n")
  269. sb.WriteString("# ==================================================================\n\n")
  270. sb.WriteString("global_defs {\n")
  271. sb.WriteString(" router_id debtik\n")
  272. sb.WriteString(" vrrp_version 3\n")
  273. sb.WriteString(" script_user root\n")
  274. sb.WriteString(" enable_script_security\n")
  275. sb.WriteString("}\n\n")
  276. for _, inst := range m.instances {
  277. if inst.Disabled {
  278. continue
  279. }
  280. instTag := sanitizeName(inst.Name)
  281. sb.WriteString(fmt.Sprintf("# --- VRRP Instance: %s on %s (VRID: %d) ---\n", inst.Name, inst.Interface, inst.VRID))
  282. sb.WriteString(fmt.Sprintf("vrrp_instance %s {\n", instTag))
  283. sb.WriteString(" state BACKUP\n")
  284. sb.WriteString(fmt.Sprintf(" interface %s\n", inst.Interface))
  285. sb.WriteString(fmt.Sprintf(" virtual_router_id %d\n", inst.VRID))
  286. sb.WriteString(fmt.Sprintf(" priority %d\n", inst.Priority))
  287. sb.WriteString(fmt.Sprintf(" advert_int %d\n", inst.Interval))
  288. if inst.Preempt {
  289. sb.WriteString(" preempt\n")
  290. } else {
  291. sb.WriteString(" nopreempt\n")
  292. }
  293. if inst.Version == 2 {
  294. sb.WriteString(" version 2\n")
  295. }
  296. if inst.AuthType == "pass" && inst.AuthPassword != "" {
  297. sb.WriteString(" authentication {\n")
  298. sb.WriteString(" auth_type PASS\n")
  299. sb.WriteString(fmt.Sprintf(" auth_pass %s\n", inst.AuthPassword))
  300. sb.WriteString(" }\n")
  301. }
  302. sb.WriteString(" virtual_ipaddress {\n")
  303. for _, vip := range inst.VirtualIPs {
  304. vipTrim := strings.TrimSpace(vip)
  305. if vipTrim != "" {
  306. if strings.Contains(vipTrim, "/") {
  307. sb.WriteString(fmt.Sprintf(" %s dev %s\n", vipTrim, inst.Interface))
  308. } else {
  309. sb.WriteString(fmt.Sprintf(" %s/32 dev %s\n", vipTrim, inst.Interface))
  310. }
  311. }
  312. }
  313. sb.WriteString(" }\n")
  314. sb.WriteString(fmt.Sprintf(" notify_master \"%s MASTER %s\"\n", VRRPNotifyScriptPath, instTag))
  315. sb.WriteString(fmt.Sprintf(" notify_backup \"%s BACKUP %s\"\n", VRRPNotifyScriptPath, instTag))
  316. sb.WriteString(fmt.Sprintf(" notify_fault \"%s FAULT %s\"\n", VRRPNotifyScriptPath, instTag))
  317. sb.WriteString(fmt.Sprintf(" notify_stop \"%s STOP %s\"\n", VRRPNotifyScriptPath, instTag))
  318. sb.WriteString("}\n\n")
  319. }
  320. if err := os.WriteFile(m.confPath, []byte(sb.String()), 0644); err != nil {
  321. return fmt.Errorf("failed to write keepalived.conf: %w", err)
  322. }
  323. cmd := exec.Command("systemctl", "restart", "keepalived")
  324. if out, err := cmd.CombinedOutput(); err != nil {
  325. return fmt.Errorf("failed to restart keepalived: %s", string(out))
  326. }
  327. return nil
  328. }
  329. // RenameInterface updates VRRP instance interface bindings when an interface is renamed.
  330. func (m *Manager) RenameInterface(oldName, newName string) {
  331. m.mu.Lock()
  332. defer m.mu.Unlock()
  333. changed := false
  334. for id, inst := range m.instances {
  335. if inst.Interface == oldName {
  336. inst.Interface = newName
  337. m.instances[id] = inst
  338. changed = true
  339. }
  340. }
  341. if changed {
  342. _ = m.saveLocked()
  343. _ = m.applyConfigLocked()
  344. }
  345. }
  346. func sanitizeName(n string) string {
  347. var res strings.Builder
  348. for _, r := range n {
  349. if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
  350. res.WriteRune(r)
  351. } else {
  352. res.WriteRune('_')
  353. }
  354. }
  355. s := res.String()
  356. if s == "" {
  357. return "vrrp1"
  358. }
  359. return s
  360. }