package mock import ( "context" "errors" "fmt" "reflect" "strings" "tera-clone/services/tera-api/internal/alarmdelivery" "tera-clone/services/tera-api/internal/networkconfig" ) // ReconcileStartupNetwork checks the owning daemon's committed configuration // and actual interfaces. Pending user changes are never applied at startup. func (s *Store) ReconcileStartupNetwork(ctx context.Context) error { s.mu.Lock() desired, err := networkconfig.Normalize(networkConfigForController(s.runningNetwork)) s.mu.Unlock() if err != nil { return err } status, err := s.networkController.Status(ctx) if err != nil { return err } if status.State == "rollback_failed" || status.State == "pending_confirmation" { return errors.New("network recovery not complete") } if status.Current == nil || !reflect.DeepEqual(desired, *status.Current) || verifyStartupNetworkInterfaces(desired, status) != nil { result, err := s.networkController.Apply(ctx, desired) if err != nil { return err } status, err = s.networkController.Status(ctx) if err == nil { err = verifyStartupNetworkInterfaces(desired, status) } if err != nil { _, rollbackErr := s.networkController.Rollback(ctx, result.TransactionID) return errors.Join(err, rollbackErr) } status, err = s.networkController.Confirm(ctx, result.TransactionID) if err != nil { return err } } return verifyStartupNetworkInterfaces(desired, status) } func verifyStartupNetworkInterfaces(config networkconfig.Config, status networkconfig.Status) error { for _, wanted := range config.Interfaces { found := false for _, actual := range status.Interfaces { if actual.Device != wanted.Device { continue } found = actual.Link != "unknown" if config.Advanced.Mode == networkconfig.AdvancedModeSingle && wanted.AddressMode == networkconfig.AddressModeStatic && wanted.Address != "" { matched := false for _, address := range actual.Addresses { if address == wanted.Address { matched = true } } if !matched { return fmt.Errorf("network address not applied: %s", wanted.Name) } } } if !found { return fmt.Errorf("network interface not observed: %s", wanted.Name) } } return nil } // Policies are consumed directly from the restored running store per request. // Validate enabled policies and credential references without sending alarms or // attempting an external login as a side effect of startup. func (s *Store) VerifyStartupPolicies(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() if err := ctx.Err(); err != nil { return err } for _, err := range []error{validateAlarmDeliveryPolicy(s.runningAlarmDelivery), validateEmailNotification(s.runningEmail), validateRemoteSyslog(s.runningSyslog), validateExternalAuth(s.runningExternalAuth), validateLogRetention(s.runningRetention)} { if err != nil { return err } } if s.runningEmail.Enabled && s.runningEmail.AuthEnabled && !s.secrets.Has(configSecretKey("running", "smtp/password")) { return errors.New("SMTP credential unavailable") } for _, target := range s.runningSNMPTraps { if !target.Enabled { continue } prefix := "snmp/trap/" + strings.ToLower(target.TargetID) if !s.secrets.Has(configSecretKey("running", prefix+"/credential")) { return errors.New("SNMP trap credential unavailable") } if target.PrivacyProtocol != "" && target.PrivacyProtocol != "none" && !s.secrets.Has(configSecretKey("running", prefix+"/privacy")) { return errors.New("SNMP trap privacy credential unavailable") } } if s.runningExternalAuth.Enabled { for kind, servers := range map[string][]externalAuthServerConfig{"radius": s.runningExternalAuth.RadiusServers, "tacacs": s.runningExternalAuth.TACACSServers} { for _, server := range servers { if server.Enabled && !s.secrets.Has(externalAuthSecretKey("running", kind, server.ServerID)) { return errors.New("external authentication credential unavailable") } } } } for _, rule := range s.runningAlarms { if !rule.Enabled { continue } channels, syslog, email, traps := s.availableDeliveryTargetsLocked(rule.Delivery, "startup validation") // Disabled delivery targets are intentionally inactive; enabled targets // must form valid requests before readiness can be reported. if len(channels) == 0 { continue } _, err := alarmdelivery.NormalizeRequest(alarmdelivery.Request{Message: "startup validation", Channels: channels, Syslog: syslog, Email: email, SNMPTraps: traps, RetryCount: s.runningAlarmDelivery.RetryCount, RetryIntervalSeconds: s.runningAlarmDelivery.RetryIntervalSeconds, QueueLimit: min(s.runningAlarmDelivery.PendingQueueLimit, 1000)}) if err != nil { return err } } return nil } func (s *Store) ReconcileStartupLogs(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() if err := ctx.Err(); err != nil { return err } return s.applyJournalRetentionLocked(s.runningRetention) }