diff --git a/frontend/src/api.js b/frontend/src/api.js index a9a4d41..74c9aab 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -90,6 +90,9 @@ export default { AdminInstancesDelete(id) { return ajax(`admin/instances/${id}`, 'delete', {}) }, + AdminInstancesForceDelete(id) { + return ajax(`admin/instances/${id}/force`, 'delete', {}) + }, AdminUserList(params) { return ajax('admin/users', 'get', { params }) diff --git a/frontend/src/views/admin/Instances.vue b/frontend/src/views/admin/Instances.vue index 5e99057..0a59bb0 100644 --- a/frontend/src/views/admin/Instances.vue +++ b/frontend/src/views/admin/Instances.vue @@ -288,6 +288,7 @@ const instanceMenuItemsTemplate = ref([ { label: '重启实例', icon: 'pi pi-refresh !text-sky-500', command: () => { instanceRestart(instanceDetail.value.id) } }, { label: '调整配置', icon: 'pi pi-sliders-h !text-indigo-500', command: () => { openInstanceModify() } }, { label: '删除实例', icon: 'pi pi-trash !text-red-500', command: () => { openInstanceDelete() } }, + { label: '强制删除', icon: 'pi pi-exclamation-triangle !text-red-500', command: () => { openInstanceForceDelete() } } ]) const instanceMenuItems = ref([]) @@ -410,6 +411,11 @@ const showMenu = (event, instance) => { newItem.disabled = true } break + case '强制删除': + if (instanceDetail.value.status !== statusFail.value) { + newItem.disabled = true + } + break } instanceMenuItems.value.push(newItem) }) @@ -517,6 +523,20 @@ const instanceDelete = async (id) => { }) } +const instanceForceDelete = async (id) => { + toast.add({ severity: 'info', summary: '强制释放实例', detail: '正在强制释放实例', life: 3000 }); + setTimeout(() => { + getInstances() + }, 100); + await api.AdminInstancesForceDelete(id, { force: true }).then(async (res) => { + toast.add({ severity: 'success', summary: '强制释放实例', detail: '实例已强制释放', life: 3000 }); + await getInstances() + }).catch(err => { + console.error(err) + toast.add({ severity: 'error', summary: '强制释放实例失败', detail: err.response.data.msg, life: 3000 }); + }) +} + const openInstanceModify = async () => { instanceConfiguration.value.gpu_count = instanceDetail.value.gpu_count instanceConfiguration.value.volume_size = instanceDetail.value.volume_size @@ -595,6 +615,27 @@ const openInstanceDelete = () => { }); } +const openInstanceForceDelete = () => { + confirm.require({ + header: '确认强制删除 实例ID: ' + instanceDetail.value.id, + message: '强制实例删除后,数据将无法恢复,请确认删除', + icon: 'pi pi-info-circle', + rejectProps: { + label: '取消', + severity: 'secondary', + outlined: true + }, + acceptProps: { + label: '强制删除', + severity: 'danger' + }, + accept: async () => { + await instanceForceDelete(instanceDetail.value.id) + }, + reject: () => { } + }); +} + const instanceModifyLabel = (id, label) => { api.AdminInstancesModifyLabel(id, { label: label }).then(async (res) => { toast.add({ severity: 'success', summary: '修改备注成功', detail: '已保存备注', life: 3000 }); diff --git a/routers/api/v1/admin/instance/forceDelete.go b/routers/api/v1/admin/instance/forceDelete.go new file mode 100644 index 0000000..dfe0be3 --- /dev/null +++ b/routers/api/v1/admin/instance/forceDelete.go @@ -0,0 +1,54 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func forceDeleteHandler(ctx iris.Context) { + l.SetFunction("forceDeleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("get instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if status != models.InstanceStatusFail { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if instance.FromAction == models.InstanceActionStop || instance.FromAction == models.InstanceActionPause || instance.FromAction == models.InstanceActionRestart { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30)) + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Delete, + Status: status, + InstanceID: instance.ID, + Force: true, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) + +} diff --git a/services/dispatcher/delete.go b/services/dispatcher/delete.go index 952dd0e..daf2d04 100644 --- a/services/dispatcher/delete.go +++ b/services/dispatcher/delete.go @@ -34,26 +34,47 @@ func delete(serverID uint, data Data) (err error) { err = instanceController.Delete(&instance) if err != nil { lc.Error("delete instance error: %v", err) - ctx := context.Background() - if data.Status == models.InstanceStatusRunning || data.Status == models.InstanceStatusPaused { - redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(-instance.GpuCount)) + if !data.Force { + ctx := context.Background() + if data.Status == models.InstanceStatusRunning || data.Status == models.InstanceStatusPaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(-instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(-instance.VolumeSize-30)) + database.DB.Model(&instance).Update("status", models.InstanceStatusFail).Update("from_action", models.InstanceActionDelete) + return } - redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(-instance.VolumeSize-30)) - database.DB.Model(&instance).Update("status", models.InstanceStatusFail).Update("from_action", models.InstanceActionDelete) + } + + if !data.Force { + server.VolumeUsed -= instance.VolumeSize + 30 + if data.Status == models.InstanceStatusRunning || data.Status == models.InstanceStatusPaused { + server.GpuUsed -= instance.GpuCount + } + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + lc.Info("delete instance success: %v", instance.ID) + } else { + if instance.FromAction != models.InstanceActionCreate { + server.VolumeUsed -= instance.VolumeSize + 30 + } + if instance.FromAction == models.InstanceActionStop || instance.FromAction == models.InstanceActionPause || instance.FromAction == models.InstanceActionRestart { + server.GpuUsed -= instance.GpuCount + } + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + result = database.DB.Delete(&instance) + if result.Error != nil { + lc.Error("force delete instance error: %v", result.Error) + } + lc.Info("force delete instance success: %v", instance.ID) return } - - server.VolumeUsed -= instance.VolumeSize + 30 - if data.Status == models.InstanceStatusRunning || data.Status == models.InstanceStatusPaused { - server.GpuUsed -= instance.GpuCount - } - result = database.DB.Save(&server) - if result.Error != nil { - lc.Error("save server error: %v", result.Error) - return result.Error - } - - lc.Info("delete instance success: %v", instance.ID) - return } diff --git a/services/dispatcher/interface.go b/services/dispatcher/interface.go index 61805a1..82666ba 100644 --- a/services/dispatcher/interface.go +++ b/services/dispatcher/interface.go @@ -21,6 +21,7 @@ type Data struct { VolumeSize *int `json:"volume_size,omitempty"` Action instanceController.Action `json:"action,omitempty"` + Force bool `json:"force,omitempty"` } type Type int