diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 7bb60ae..21bf584 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -7,7 +7,7 @@ import ( ) // Version can be set at build time via ldflags -var Version = "1.2.1" +var Version = "1.3.0" var rootCmd = &cobra.Command{ Use: "go-alived", diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 162cc70..dc50182 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -58,6 +58,7 @@ func runService(cmd *cobra.Command, args []string) { os.Exit(1) } + setupNotifyScripts(vrrpMgr, cfg, log) setupHealthTracking(vrrpMgr, healthMgr, log) healthMgr.StartAll() @@ -100,6 +101,27 @@ func cleanup(log *logger.Logger, vrrpMgr *vrrp.Manager, healthMgr *health.Manage vrrpMgr.StopAll() } +func setupNotifyScripts(vrrpMgr *vrrp.Manager, cfg *config.Config, log *logger.Logger) { + for _, vrrpCfg := range cfg.VRRP { + if vrrpCfg.NotifyMaster == "" && vrrpCfg.NotifyBackup == "" && vrrpCfg.NotifyFault == "" { + continue + } + + inst, ok := vrrpMgr.GetInstance(vrrpCfg.Name) + if !ok { + continue + } + + vrrp.SetupNotify(inst, &vrrp.NotifyConfig{ + Name: vrrpCfg.Name, + NotifyMaster: vrrpCfg.NotifyMaster, + NotifyBackup: vrrpCfg.NotifyBackup, + NotifyFault: vrrpCfg.NotifyFault, + Log: log, + }) + } +} + func setupHealthTracking(vrrpMgr *vrrp.Manager, healthMgr *health.Manager, log *logger.Logger) { instances := vrrpMgr.GetAllInstances() diff --git a/internal/vrrp/notify.go b/internal/vrrp/notify.go new file mode 100644 index 0000000..af58c88 --- /dev/null +++ b/internal/vrrp/notify.go @@ -0,0 +1,89 @@ +package vrrp + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/loveuer/go-alived/pkg/logger" +) + +const notifyTimeout = 60 * time.Second + +// NotifyConfig holds the notify script configuration for a VRRP instance. +type NotifyConfig struct { + Name string + NotifyMaster string + NotifyBackup string + NotifyFault string + Log *logger.Logger +} + +// SetupNotify registers notify scripts as state change callbacks on the instance. +func SetupNotify(inst *Instance, cfg *NotifyConfig) { + if cfg.NotifyMaster != "" { + script := cfg.NotifyMaster + inst.OnMaster(func() { + cfg.Log.Info("[%s] executing notify_master script", cfg.Name) + go runNotifyScript(cfg.Log, cfg.Name, "notify_master", script) + }) + cfg.Log.Info("[%s] registered notify_master script", cfg.Name) + } + + if cfg.NotifyBackup != "" { + script := cfg.NotifyBackup + inst.OnBackup(func() { + cfg.Log.Info("[%s] executing notify_backup script", cfg.Name) + go runNotifyScript(cfg.Log, cfg.Name, "notify_backup", script) + }) + cfg.Log.Info("[%s] registered notify_backup script", cfg.Name) + } + + if cfg.NotifyFault != "" { + script := cfg.NotifyFault + inst.OnFault(func() { + cfg.Log.Info("[%s] executing notify_fault script", cfg.Name) + go runNotifyScript(cfg.Log, cfg.Name, "notify_fault", script) + }) + cfg.Log.Info("[%s] registered notify_fault script", cfg.Name) + } +} + +func runNotifyScript(log *logger.Logger, instName, event, script string) { + ctx, cancel := context.WithTimeout(context.Background(), notifyTimeout) + defer cancel() + + cmd := buildCommand(ctx, script) + cmd.Env = append(os.Environ(), + fmt.Sprintf("GO_ALIVED_INSTANCE=%s", instName), + fmt.Sprintf("GO_ALIVED_EVENT=%s", event), + ) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Error("[%s] %s script failed: %v (output: %s)", + instName, event, err, strings.TrimSpace(string(output))) + return + } + + if len(output) > 0 { + log.Info("[%s] %s script output: %s", + instName, event, strings.TrimSpace(string(output))) + } + log.Info("[%s] %s script completed successfully", instName, event) +} + +func buildCommand(ctx context.Context, script string) *exec.Cmd { + script = strings.TrimSpace(script) + + // If the script is a path to an existing executable file, run it directly + if info, err := os.Stat(script); err == nil && !info.IsDir() { + return exec.CommandContext(ctx, script) + } + + // Otherwise treat as inline shell script + return exec.CommandContext(ctx, "sh", "-c", script) +}