fix: improve firewall lifecycle and sync (#13809)

This commit is contained in:
ssongliu
2026-09-15 10:04:53 +08:00
committed by GitHub
parent 75da53e374
commit 005f240fb7
23 changed files with 499 additions and 99 deletions

View File

@@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
// @Summary Operate firewall
// @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request"
// @Success 200
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/operate [post]
@@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
return
}
if err := firewallService.OperateFirewall(request); err != nil {
result, err := firewallService.QueueFirewallOperation(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
// @Tags Firewall

View File

@@ -18,6 +18,7 @@ type FirewallSubsystemStatus struct {
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
}
@@ -27,6 +28,11 @@ type FirewallLifecycleOperation struct {
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallLifecycleOperationResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued"`
}
type FirewallBackendOption struct {
Name string `json:"name"`
Installed bool `json:"installed"`

View File

@@ -9,11 +9,13 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
@@ -55,10 +57,16 @@ type firewallRuleRuntimeResolver interface {
var firewallRuleMutationMu sync.Mutex
var (
firewallLifecycleTaskMu sync.Mutex
firewallLifecycleTaskID string
firewallLifecycleRequest dto.FirewallLifecycleOperation
)
type IFirewallService interface {
UpdatePanelPort(context.Context, uint, uint) error
LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error)
OperateFirewall(request dto.FirewallLifecycleOperation) error
QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error)
OperateFilterChain(request dto.FilterChainOperation) error
QueueFilterChainInitialization(request dto.FilterChainOperation) (dto.FilterChainOperationResponse, error)
Reset(context.Context, dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error)
@@ -103,6 +111,7 @@ func newFirewallService() *FirewallService {
func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystemStatus, error) {
status := dto.FirewallSubsystemStatus{Version: "-", Name: "-", Backend: "-"}
status.LifecycleTaskID = currentFirewallLifecycleTaskID()
if selected := configuredSystemFirewallBackend(); selected != "" {
status.Name, status.Backend = selected, selected
}
@@ -163,6 +172,157 @@ func (c firewallLifecycleClient) Restart() error {
return c.Client.Restart()
}
func currentFirewallLifecycleTaskID() string {
firewallLifecycleTaskMu.Lock()
defer firewallLifecycleTaskMu.Unlock()
return firewallLifecycleTaskID
}
func lockFirewallLifecycleIdle() error {
if !firewallLifecycleTaskMu.TryLock() {
return buserr.New("TaskIsExecuting")
}
if firewallLifecycleTaskID != "" {
firewallLifecycleTaskMu.Unlock()
return buserr.New("TaskIsExecuting")
}
return nil
}
func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) {
response := dto.FirewallLifecycleOperationResponse{}
if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" {
return response, s.OperateFirewall(request)
}
if !firewallLifecycleTaskMu.TryLock() {
return response, buserr.New("TaskIsExecuting")
}
defer firewallLifecycleTaskMu.Unlock()
if firewallLifecycleTaskID != "" {
if request == firewallLifecycleRequest {
return dto.FirewallLifecycleOperationResponse{TaskID: firewallLifecycleTaskID, Queued: true}, nil
}
return response, buserr.New("TaskIsExecuting")
}
running, err := s.CurrentRuleSyncTask()
if err != nil {
return response, err
}
if running.Executing {
return response, buserr.New("TaskIsExecuting")
}
loadClient := s.baseClient
if loadClient == nil {
loadClient = selectedSystemFirewallClient
}
client, err := loadClient()
if err != nil {
return response, err
}
if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) ||
(request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationRestart)) {
return response, s.OperateFirewall(request)
}
operation, label := task.TaskExec, "Start"
if request.Operation == string(lifecycle.OperationRestart) {
operation, label = task.TaskRestart, task.TaskRestart
}
name := task.GetTaskName(client.Name(), label, task.TaskScopeFirewall)
taskItem, err := task.NewTask(name, operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return response, err
}
taskItem.AddSubTaskWithOps(name, func(t *task.Task) error {
return s.runFirewallLifecycleTask(t, client, request)
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
return response, err
}
firewallLifecycleTaskID, firewallLifecycleRequest = taskItem.TaskID, request
go func() {
defer func() {
closeUnstartedFirewallTask(taskItem)
firewallLifecycleTaskMu.Lock()
defer firewallLifecycleTaskMu.Unlock()
if firewallLifecycleTaskID == taskItem.TaskID {
firewallLifecycleTaskID = ""
}
}()
if err := taskItem.Execute(); err != nil && taskItem.Task.Status == constant.StatusExecuting {
taskItem.LogFailedWithErr(name, err)
taskItem.Task.Status = constant.StatusFailed
taskItem.Task.ErrorMsg = err.Error()
taskItem.Task.EndAt = time.Now()
_ = repo.NewITaskRepo().Update(context.Background(), taskItem.Task)
}
}()
return dto.FirewallLifecycleOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
t.Log(i18n.GetWithName("TaskStart", name))
started := time.Now()
err := t.TaskCtx.Err()
if err == nil {
err = action()
}
t.LogWithStatus(fmt.Sprintf("%s (%.2fs)", name, time.Since(started).Seconds()), err)
return err
}
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation) error {
ctx := t.TaskCtx
provider := filter.Provider(client.Name())
operator := lifecycle.NewOperator(firewallLifecycleClient{client})
operator.RunAction = func(operation, name string, action func() error) error {
return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action)
}
operationErr := operator.Operate(lifecycle.Operation(request.Operation), request.WithDockerRestart, func(lifecycle.Client) error {
rulesErr := runFirewallLifecycleAction(t, i18n.GetWithName("FirewallRestoreRulesStep", client.Name()), func() error {
return s.restoreStoredFirewallRules(ctx, provider, t)
})
whitelistErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
return s.SyncSystemPorts(ctx, nil, systemPorts(ports))
})
return errors.Join(rulesErr, whitelistErr)
})
var recoveryErr *lifecycle.CompletedOperationError
if operationErr != nil && !errors.As(operationErr, &recoveryErr) {
return operationErr
}
var forwardingErr error
if provider == filter.ProviderFirewalld {
forwardingErr = runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func() error {
if s.restoreForwarding != nil {
return s.restoreForwarding(ctx)
}
return newForwardingService().Restore(ctx)
})
}
dockerErr := runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func() error {
if provider == filter.ProviderFirewalld {
active := s.dockerActive
if active == nil {
active = func() (bool, error) { return controller.CheckActive("docker") }
}
running, err := active()
if err != nil || !running {
return err
}
}
if s.restoreDockerGuard != nil {
return s.restoreDockerGuard(ctx)
}
return ReconcileDockerPortGuard(ctx)
})
return errors.Join(operationErr, forwardingErr, dockerErr)
}
func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error {
switch request.Operation {
case "disableBanPing":
@@ -248,7 +408,7 @@ func (s *FirewallService) OperateFilterChain(request dto.FilterChainOperation) e
return nil
}
ctx := context.Background()
if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider)); err != nil {
if err := s.restoreStoredFirewallRules(ctx, filter.Provider(provider), nil); err != nil {
return err
}
return s.syncConfiguredFirewallPorts(ctx)
@@ -280,7 +440,7 @@ func (s *FirewallService) QueueFilterChainInitialization(
return s.operateFilterChainBase(provider, request)
}, nil)
taskItem.AddSubTask(i18n.GetWithName("FirewallRestoreRulesStep", provider), func(t *task.Task) error {
return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider))
return s.restoreStoredFirewallRules(t.TaskCtx, filter.Provider(provider), nil)
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func(t *task.Task) error {
return s.syncConfiguredFirewallPorts(t.TaskCtx)
@@ -318,6 +478,10 @@ func (s *FirewallService) operateFilterChainBaseLocked(provider string, request
}
func (s *FirewallService) Reset(ctx context.Context, request dto.FirewallRuleReset) (dto.FirewallRuleResetResponse, error) {
if err := lockFirewallLifecycleIdle(); err != nil {
return dto.FirewallRuleResetResponse{}, err
}
defer firewallLifecycleTaskMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
@@ -2791,7 +2955,7 @@ func (s *FirewallService) addPortsBeforeStart(client lifecycle.Client) error {
return nil
}
}
if err := s.restoreStoredFirewallRules(ctx, provider); err != nil {
if err := s.restoreStoredFirewallRules(ctx, provider, nil); err != nil {
recordFailure("restore stored firewall rules", err)
}
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {

View File

@@ -372,6 +372,10 @@ func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFa
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
}
@@ -385,7 +389,7 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
}
if request.Operation == "initialize" {
service := newFirewallService()
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil {
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil); err != nil {
return err
}
return service.syncConfiguredFirewallPorts(ctx)

View File

@@ -195,6 +195,7 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
created, removed, unexecuted := 0, 0, 0
stopped := make(map[string]error)
failedRemovals := make(map[string]error)
var firewalldFinalSnapshot *filter.Snapshot
record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) {
item := rule.FirewallRuleSyncItem
if operation == "TaskDelete" && rule.observed != nil {
@@ -234,13 +235,15 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
}
if skipped {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
} else if operation == task.TaskSync && cause == nil {
t.Logf("%s %s", label, i18n.GetMsgByKey("FirewallSyncRuleUnchanged"))
} else {
t.LogWithStatus(label, cause)
}
}
defer func() {
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted}))
t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted}))
}
}()
blocked := false
@@ -287,6 +290,10 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
return syncObservedPosition(queue[i].observed) > syncObservedPosition(queue[j].observed)
})
}
if scope.Provider == filter.ProviderFirewalld && operation == filter.ChangeCreate && len(failedRemovals) == 0 && stopped[scope.Key()] == nil {
firewalldFinalSnapshot = syncFirewalldCreates(ctx, runtime, initial, removed > 0, queue, t, record)
continue
}
for start := 0; start < len(queue); {
rule := queue[start]
cause := stopped[scope.Key()]
@@ -325,21 +332,21 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
changes = append(changes, change)
} else {
after := *entry.Rule
after.OrderIndex = nil
if len(batch) == 1 && scope.Provider != filter.ProviderFirewalld {
markers := make([]string, 0)
for _, candidate := range rules {
if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" {
markers = append(markers, candidate.desired.Marker)
}
}
after.OrderIndex = nil
if len(batch) == 1 {
after.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, entry.desired.Marker)
}
changes = append(changes, filter.DesiredChange{Operation: operation, After: &after, Append: scope.Provider == filter.ProviderUFW && after.OrderIndex == nil})
}
}
if err == nil {
err = runtime.ExecuteSync(ctx, snapshot, changes)
_, err = runtime.ExecuteSync(ctx, snapshot, changes)
}
for _, entry := range batch {
record(name, entry, err, false)
@@ -351,9 +358,141 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
}
}
}
if firewalldFinalSnapshot != nil && result.Failed == 0 && created > 0 {
verifyErr := verifyFirewalldSyncSnapshot(*firewalldFinalSnapshot, rules)
if t != nil {
t.LogWithStatus(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), verifyErr)
}
if verifyErr != nil {
return result, verifyErr
}
}
return result, nil
}
func syncFirewalldCreates(
ctx context.Context,
runtime *filterruntime.Engine,
initial filter.Snapshot,
refresh bool,
queue []*firewallSyncRule,
t *task.Task,
record func(string, *firewallSyncRule, error, bool),
) *filter.Snapshot {
if len(queue) == 0 {
return nil
}
snapshot := initial
snapshot.Rules = append([]filter.ObservedRule(nil), initial.Rules...)
indices := make(map[string]int)
indexRules := func() {
clear(indices)
for index, rule := range snapshot.Rules {
indices[rule.Locator.Canonical] = index
}
}
indexRules()
pending := make([]*firewallSyncRule, 0, len(queue))
var readErr error
for index, entry := range queue {
if refresh {
snapshot, readErr = runtime.ObserveMutation(ctx, initial.Scope)
if readErr != nil {
record(task.TaskCreate, entry, readErr, false)
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, readErr, true)
}
break
}
indexRules()
refresh = false
}
if t != nil {
t.Logf("[%d/%d] %s %s", index+1, len(queue), i18n.GetMsgByKey(task.TaskCreate), entry.SourceUUID)
}
after := *entry.Rule
after.OrderIndex = nil
applied, err := runtime.ExecuteSync(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &after}})
if err == nil && len(applied.Applied) != 1 {
err = filter.ErrVerificationFailed
}
if err == nil {
observed := applied.Applied[0]
if position, exists := indices[observed.Locator.Canonical]; exists {
snapshot.Rules[position] = observed
} else {
indices[observed.Locator.Canonical] = len(snapshot.Rules)
snapshot.Rules = append(snapshot.Rules, observed)
}
snapshot, err = filter.NewSnapshot(snapshot.Scope, snapshot.Rules)
}
if err != nil {
record(task.TaskCreate, entry, err, false)
if firewallCreateUnavailable(err) {
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, err, true)
}
break
}
refresh = true
continue
}
pending = append(pending, entry)
}
var actual filter.Snapshot
if readErr == nil {
actual, readErr = runtime.ObserveMutation(ctx, initial.Scope)
}
if readErr != nil {
for _, entry := range pending {
record(task.TaskCreate, entry, readErr, false)
}
return nil
}
states := firewalldRuleStates(actual)
for _, entry := range pending {
key, err := filter.RuleKey(*entry.Rule)
if err == nil && states[key] != 1 {
err = filter.ErrVerificationFailed
}
record(task.TaskCreate, entry, err, false)
}
return &actual
}
func firewalldRuleStates(snapshot filter.Snapshot) map[string]int {
states := make(map[string]int, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported || observed.Persistence != filter.PersistenceStatusConverged {
continue
}
if key, err := filter.RuleKey(observed.Rule); err == nil {
states[key]++
}
}
return states
}
func verifyFirewalldSyncSnapshot(snapshot filter.Snapshot, rules []*firewallSyncRule) error {
states := firewalldRuleStates(snapshot)
for _, entry := range rules {
if entry.Status == firewallsync.StatusRemove {
continue
}
if entry.Rule == nil {
return filter.ErrVerificationFailed
}
key, err := filter.RuleKey(*entry.Rule)
if err != nil {
return err
}
if states[key] != 1 {
return fmt.Errorf("%w: %s", filter.ErrVerificationFailed, entry.SourceUUID)
}
}
return nil
}
func syncObservedPosition(rule *filter.ObservedRule) int {
if rule.Locator.Position != nil {
return *rule.Locator.Position
@@ -361,8 +500,8 @@ func syncObservedPosition(rule *filter.ObservedRule) int {
return 0
}
func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider) error {
result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, nil)
func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider, t *task.Task) error {
result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, t)
if err != nil {
return fmt.Errorf("restore database firewall rules: %w", err)
}
@@ -393,6 +532,10 @@ func (s *FirewallService) syncSystemRules(
clientIP string,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
if err := lockFirewallLifecycleIdle(); err != nil {
return dto.FirewallRuleSyncResult{}, err
}
defer firewallLifecycleTaskMu.Unlock()
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'

View File

@@ -693,7 +693,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'

View File

@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"

View File

@@ -409,13 +409,17 @@ func nativeCanonical(rule filter.FirewallRule) string {
func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]filter.NativeCommand, []filter.NativeCommand) {
commands, rollback := pairedCommands(rule, "add", "remove")
canonical := nativeCanonical(rule)
var runtimeExists, permanentExists bool
for _, observed := range snapshot.Rules {
if observed.Locator.Canonical != nativeCanonical(rule) {
if observed.Locator.Canonical != canonical {
continue
}
runtimeExists = runtimeExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusRuntimeOnly
permanentExists = permanentExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusPermanentOnly
if runtimeExists && permanentExists {
break
}
}
var changes, inverses []filter.NativeCommand
for index, exists := range []bool{runtimeExists, permanentExists} {

View File

@@ -249,9 +249,9 @@ func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, ch
return err
}
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.ApplyResult, error) {
if err := ctx.Err(); err != nil {
return err
return filter.ApplyResult{}, err
}
changes = append([]filter.DesiredChange(nil), changes...)
for index := range changes {
@@ -259,11 +259,10 @@ func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, chan
}
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
return filter.ApplyResult{}, err
}
plan.CommandOnly = true
_, err = e.adapter.Apply(ctx, plan)
return err
return e.adapter.Apply(ctx, plan)
}
func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) {

View File

@@ -21,6 +21,7 @@ const (
type Operator struct {
client Client
RunAction func(operation, name string, action func() error) error
}
// DockerRestartError reports that the requested firewall operation completed,
@@ -54,11 +55,18 @@ func NewOperator(client Client) *Operator {
return &Operator{client: client}
}
func (o *Operator) runAction(operation, name string, action func() error) error {
if o.RunAction != nil {
return o.RunAction(operation, name, action)
}
return action()
}
func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error {
var recoveryErrors []error
switch operation {
case OperationStart:
if err := o.client.Start(); err != nil {
if err := o.runAction("Start", o.client.Name(), o.client.Start); err != nil {
return err
}
if prepareStart != nil {
@@ -69,7 +77,7 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
case OperationStop:
return o.StopWithPrepare(withDockerRestart, nil)
case OperationRestart:
if err := o.client.Restart(); err != nil {
if err := o.runAction("TaskRestart", o.client.Name(), o.client.Restart); err != nil {
return err
}
if prepareStart != nil {
@@ -82,12 +90,12 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
}
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err})
}
}
if o.client.Name() == ProviderFirewalld && operation == OperationStart {
if err := restoreFail2BanAfterFirewallStart(); err != nil {
if err := o.runAction("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil {
recoveryErrors = append(recoveryErrors, err)
}
}

View File

@@ -45,6 +45,7 @@ export namespace Firewall {
queued?: boolean;
}
export interface FirewallBase {
lifecycleTaskID?: string;
name: string;
backend: string;
conflictBackend?: string;

View File

@@ -16,7 +16,11 @@ export const searchForwardRule = (request: Firewall.ForwardRuleSearch) =>
http.post<ResPage<Firewall.RuleInfo>>('/hosts/firewall/forward/search', request, TimeoutEnum.T_40S);
export const operateFire = (operation: string, withDockerRestart: boolean) =>
http.post('/hosts/firewall/operate', { operation, withDockerRestart }, TimeoutEnum.T_10M);
http.post<Firewall.FilterChainOperationResult>(
'/hosts/firewall/operate',
{ operation, withDockerRestart },
TimeoutEnum.T_10M,
);
export const operateForwardRule = (request: { rules: Firewall.RuleForward[]; forceDelete?: boolean }) =>
http.postWithConfig<Firewall.FilterChainOperationResult>('/hosts/firewall/forward/operate', request, {

View File

@@ -69,6 +69,7 @@
v-node-admin
type="primary"
v-if="baseInfo.isActive"
:disabled="lifecycleBusy"
@click="onOperate('stop')"
link
>
@@ -79,14 +80,35 @@
:content="$t('firewall.firewallNotStart')"
placement="bottom"
>
<el-button v-permission v-node-admin type="primary" @click="onOperate('start')" link>
<el-button
v-permission
v-node-admin
type="primary"
:disabled="lifecycleBusy"
@click="onOperate('start')"
link
>
{{ $t('commons.button.start') }}
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-button v-permission v-node-admin type="primary" @click="onOperate('restart')" link>
<el-button
v-permission
v-node-admin
type="primary"
:disabled="lifecycleBusy"
@click="onOperate('restart')"
link
>
{{ $t('commons.button.restart') }}
</el-button>
<template v-if="baseInfo.lifecycleTaskID">
<el-divider direction="vertical" />
<el-button type="primary" link @click="openLifecycleTask">
{{ $t('commons.status.executing') }}
<el-icon class="ml-1"><Document /></el-icon>
</el-button>
</template>
</template>
<template v-if="isDirectManaged">
<el-divider v-if="isDirectBase || !anyFamilyBound" direction="vertical" />
@@ -186,9 +208,9 @@ import NoSuchService from '@/components/layout-content/no-such-service.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { computed, nextTick, ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
import { loadDockerStatus } from '@/api/modules/container';
import { WarningFilled } from '@element-plus/icons-vue';
import { Document, WarningFilled } from '@element-plus/icons-vue';
import { routerToName, routerToNameWithQuery } from '@/utils/router';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/id';
@@ -222,6 +244,11 @@ const dockerStatus = ref();
const withDockerRestart = ref(false);
const familyRetrying = ref(false);
const taskLogRef = ref();
const lifecycleSubmitting = ref(false);
const lifecycleBusy = computed(() => lifecycleSubmitting.value || !!baseInfo.value.lifecycleTaskID);
let lifecycleTimer: ReturnType<typeof setTimeout>;
let disposed = false;
let baseInfoRequestID = 0;
const backendName = computed(() => baseInfo.value.backend || baseInfo.value.name);
const backendUnavailable = computed(() => baseInfo.value.reason === 'backend_not_installed' || !baseInfo.value.isExist);
const isServiceBackend = computed(() => backendName.value === 'firewalld' || backendName.value === 'ufw');
@@ -293,9 +320,13 @@ const emit = defineEmits([
]);
const loadBaseInfo = async (search: boolean) => {
const loader = props.currentTab === 'forward' ? loadForwardBaseInfo() : loadFireBaseInfo(props.currentTab);
await loader
.then(async (res) => {
if (disposed) return;
const requestID = ++baseInfoRequestID;
clearTimeout(lifecycleTimer);
try {
const res = await (props.currentTab === 'forward' ? loadForwardBaseInfo() : loadFireBaseInfo(props.currentTab));
if (disposed || requestID !== baseInfoRequestID) return;
const lifecycleCompleted = Boolean(baseInfo.value.lifecycleTaskID) && !res.data.lifecycleTaskID;
baseInfo.value = {
...res.data,
ipv4: res.data.ipv4 || { available: true, initialized: res.data.isInit, bound: res.data.isBind },
@@ -316,19 +347,23 @@ const loadBaseInfo = async (search: boolean) => {
emit('update:is-bind', isDirectManaged.value ? anyFamilyBound.value : baseInfo.value.isBind);
emit('update:version', baseInfo.value.version);
if (search) {
if ((search || lifecycleCompleted) && !baseInfo.value.lifecycleTaskID) {
await nextTick();
emit('search');
} else {
emit('update:loading', false);
}
})
.catch(() => {
} catch {
if (disposed || requestID !== baseInfoRequestID) return;
emit('update:loading', false);
emit('update:is-init', false);
emit('update:name', '-');
emit('update:version', '');
});
} finally {
if (!disposed && requestID === baseInfoRequestID && baseInfo.value.lifecycleTaskID) {
lifecycleTimer = setTimeout(() => loadBaseInfo(false), 3000);
}
}
};
const loadDocker = async () => {
@@ -434,34 +469,53 @@ const onUnBind = async () => {
};
const onOperate = async (op: string) => {
if (lifecycleBusy.value) return;
operation.value = op;
if (backendName.value === 'iptables' || backendName.value === 'nftables' || !dockerStatus.value) {
emit('update:loading', true);
await operateFire(operation.value, false)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
})
.catch(() => {
loadBaseInfo(true);
});
await submitLifecycleOperation(false);
} else {
dockerRef.value.acceptParams({ title: i18n.global.t('firewall.dockerRestart') });
}
};
const onSubmit = async () => {
emit('update:loading', true);
await operateFire(operation.value, withDockerRestart.value)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
})
.catch(() => {
loadBaseInfo(true);
});
await submitLifecycleOperation(withDockerRestart.value);
};
const openLifecycleTask = () => {
if (baseInfo.value.lifecycleTaskID) {
taskLogRef.value?.openWithTaskID(baseInfo.value.lifecycleTaskID, true);
}
};
const submitLifecycleOperation = async (restartDocker: boolean) => {
if (lifecycleBusy.value) return;
lifecycleSubmitting.value = true;
emit('update:loading', true);
try {
const result = (await operateFire(operation.value, restartDocker)).data;
if (disposed) return;
if (result?.queued && result.taskID) {
baseInfo.value.lifecycleTaskID = result.taskID;
emit('update:loading', false);
openLifecycleTask();
await loadBaseInfo(false);
return;
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await loadBaseInfo(true);
} catch {
if (!disposed) await loadBaseInfo(true);
} finally {
lifecycleSubmitting.value = false;
}
};
onBeforeUnmount(() => {
disposed = true;
clearTimeout(lifecycleTimer);
});
defineExpose({
acceptParams,
});