diff --git a/cmd/climc/shell/instance_snapshots.go b/cmd/climc/shell/instance_snapshots.go new file mode 100644 index 0000000000..f5039e9c6b --- /dev/null +++ b/cmd/climc/shell/instance_snapshots.go @@ -0,0 +1,49 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + type InstanceSnapshotsListOptions struct { + options.BaseListOptions + + GuestId string `help:"guest id" json:"guest_id"` + } + R(&InstanceSnapshotsListOptions{}, "instance-snapshot-list", "Show instance snapshots", func(s *mcclient.ClientSession, args *InstanceSnapshotsListOptions) error { + params, err := options.ListStructToParams(args) + if err != nil { + return err + } + result, err := modules.InstanceSnapshots.List(s, params) + if err != nil { + return err + } + printList(result, modules.InstanceSnapshots.GetColumns(s)) + return nil + }) + + type InstanceSnapshotDeleteOptions struct { + ID []string `help:"Delete snapshot id"` + } + R(&InstanceSnapshotDeleteOptions{}, "instance-snapshot-delete", "Delete snapshots", func(s *mcclient.ClientSession, args *InstanceSnapshotDeleteOptions) error { + ret := modules.InstanceSnapshots.BatchDelete(s, args.ID, nil) + printBatchResults(ret, modules.InstanceSnapshots.GetColumns(s)) + return nil + }) + + type InstanceSnapshotShowOptions struct { + ID string `help:"ID or Name of snapshot"` + } + R(&InstanceSnapshotShowOptions{}, "snapshot-show", "Show snapshot details", func(s *mcclient.ClientSession, args *InstanceSnapshotShowOptions) error { + result, err := modules.InstanceSnapshots.Get(s, args.ID, nil) + if err != nil { + return err + } + printObject(result) + return nil + }) + +} diff --git a/cmd/climc/shell/servers.go b/cmd/climc/shell/servers.go index 7b355da620..896a1461fe 100644 --- a/cmd/climc/shell/servers.go +++ b/cmd/climc/shell/servers.go @@ -156,6 +156,25 @@ func init() { return nil }) + R(&options.ServerCreateFromInstanceSnapshot{}, "server-create-from-instance-snapshot", "server create from instance snapshot", + func(s *mcclient.ClientSession, opts *options.ServerCreateFromInstanceSnapshot) error { + params := &compute.ServerCreateInput{} + params.InstanceSnapshotId = opts.InstaceSnapshotId + params.Name = opts.NAME + params.AutoStart = opts.AutoStart + params.Eip = opts.Eip + params.EipChargeType = opts.EipChargeType + params.EipBw = opts.EipBw + + server, err := modules.Servers.Create(s, params.JSON(params)) + if err != nil { + return err + } + printObject(server) + return nil + }, + ) + R(&options.ServerCreateOptions{}, "server-create", "Create a server", func(s *mcclient.ClientSession, opts *options.ServerCreateOptions) error { params, err := opts.Params() if err != nil { @@ -1080,4 +1099,52 @@ func init() { printObject(result) return nil }) + type ServerCreateSnapshot struct { + ID string `help:"ID or name of VM" json:"-"` + SNAPSHOT string `help:"Instance snapshot name" json:"name"` + } + R(&ServerCreateSnapshot{}, "instance-snapshot-create", "create instance snapshot", func(s *mcclient.ClientSession, opts *ServerCreateSnapshot) error { + params := jsonutils.Marshal(opts) + result, err := modules.Servers.PerformAction(s, opts.ID, "instance-snapshot", params) + if err != nil { + return err + } + printObject(result) + return nil + }) + + type ServerSnapshotAndClone struct { + ID string `help:"ID or name of VM" json:"-"` + NAME string `help:"Newly instance name" json:"name"` + AutoStart bool `help:"Auto start new guest"` + AllowDelete bool `help:"Allow new guest delete" json:"-"` + Count int `help:"Guest count"` + } + R(&ServerSnapshotAndClone{}, "instance-snapshot-and-clone", "create instance snapshot and clone new instance", func(s *mcclient.ClientSession, opts *ServerSnapshotAndClone) error { + params := jsonutils.Marshal(opts) + dictParams := params.(*jsonutils.JSONDict) + if opts.AllowDelete { + dictParams.Set("disable_delete", jsonutils.JSONFalse) + } + result, err := modules.Servers.PerformAction(s, opts.ID, "snapshot-and-clone", dictParams) + if err != nil { + return err + } + printObject(result) + return nil + }) + + type ServerRollBackSnapshot struct { + ID string `help:"ID or name of VM" json:"-"` + InstanceSnapshot string `help:"Instance snapshot id or name" json:"instance_snapshot"` + } + R(&ServerRollBackSnapshot{}, "instance-snapshot-reset", "reset instance snapshot", func(s *mcclient.ClientSession, opts *ServerRollBackSnapshot) error { + params := jsonutils.Marshal(opts) + result, err := modules.Servers.PerformAction(s, opts.ID, "instance-snapshot-reset", params) + if err != nil { + return err + } + printObject(result) + return nil + }) } diff --git a/pkg/apis/compute/api.go b/pkg/apis/compute/api.go index b26180d7d9..0f88619295 100644 --- a/pkg/apis/compute/api.go +++ b/pkg/apis/compute/api.go @@ -187,6 +187,7 @@ type ServerCreateInput struct { EipBw int `json:"eip_bw,omitzero"` EipChargeType string `json:"eip_charge_type,omitempty"` Eip string `json:"eip,omitempty"` + InstanceSnapshotId string `json:"instance_snapshot_id,omitempty"` OsType string `json:"os_type"` // Fill by server diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 86eecff003..ab42290fe5 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -84,6 +84,12 @@ const ( VM_DISK_RESET = "disk_reset" VM_DISK_RESET_FAIL = "disk_reset_failed" + VM_START_INSTANCE_SNAPSHOT = "start_instance_snapshot" + VM_INSTANCE_SNAPSHOT_FAILED = "instance_snapshot_failed" + VM_START_SNAPSHOT_RESET = "start_snapshot_reset" + VM_SNAPSHOT_RESET_FAILED = "snapshot_reset_failed" + VM_SNAPSHOT_AND_CLONE_FAILED = "clone_from_snapshot_failed" + VM_SYNCING_STATUS = "syncing" VM_SYNC_CONFIG = "sync_config" VM_SYNC_FAIL = "sync_fail" diff --git a/pkg/apis/compute/snapshot_const.go b/pkg/apis/compute/snapshot_const.go index b3d8611907..626ad598e2 100644 --- a/pkg/apis/compute/snapshot_const.go +++ b/pkg/apis/compute/snapshot_const.go @@ -43,4 +43,9 @@ const ( SNAPSHOT_POLICY_DISK_READY = "ready" SNAPSHOT_POLICY_DISK_DELETING = "deleting" SNAPSHOT_POLICY_DISK_DELETE_FAILED = "delete_failed" + + INSTANCE_SNAPSHOT_READY = "ready" + INSTANCE_SNAPSHOT_FAILED = "instance_snapshot_create_failed" + INSTANCE_SNAPSHOT_START_DELETE = "instance_snapshot_start_delete" + INSTANCE_SNAPSHOT_DELETE_FAILED = "instance_snapshot_delete_failed" ) diff --git a/pkg/cloudcommon/db/opslog.go b/pkg/cloudcommon/db/opslog.go index d73daf7872..3dc4acfd81 100644 --- a/pkg/cloudcommon/db/opslog.go +++ b/pkg/cloudcommon/db/opslog.go @@ -47,6 +47,7 @@ const ( ACT_DETACH = "detach" ACT_ATTACH_FAIL = "attach_fail" ACT_DETACH_FAIL = "detach_fail" + ACT_DELETE_FAIL = "delete_fail" ACT_SYNC_UPDATE = "sync_update" ACT_SYNC_CREATE = "sync_create" @@ -103,6 +104,11 @@ const ( ACT_APPLY_SNAPSHOT_POLICY_FAILED = "apply_snapshot_policy_failed" ACT_CANCEL_SNAPSHOT_POLICY = "cancel_snapshot_policy" ACT_CANCEL_SNAPSHOT_POLICY_FAILED = "cancel_snapshot_policy_failed" + ACT_VM_SNAPSHOT_AND_CLONE = "vm_snapshot_and_clone" + ACT_VM_SNAPSHOT_AND_CLONE_FAILED = "vm_snapshot_and_clone_failed" + + ACT_VM_RESET_SNAPSHOT = "instance_reset_snapshot" + ACT_VM_RESET_SNAPSHOT_FAILED = "instance_reset_snapshot_failed" ACT_SNAPSHOT_POLICY_BIND_DISK = "snapshot_policy_bind_disk" ACT_SNAPSHOT_POLICY_BIND_DISK_FAIL = "snapshot_policy_bind_disk_fail" diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 293ae7961c..a4c4668cf3 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -234,9 +234,6 @@ func (self *SKVMGuestDriver) RequestDeployGuestOnHost(ctx context.Context, guest return err } log.Debugf("RequestDeployGuestOnHost: %s", config) - if config.Contains("container") { - // ... - } action, err := config.GetString("action") if err != nil { return err diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index 167fc6aeb6..dc0a996367 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -714,22 +714,21 @@ func (self *SDisk) PerformDiskReset(ctx context.Context, userCred mcclient.Token autoStart := jsonutils.QueryBoolean(data, "auto_start", false) snapshotId, _ := data.GetString("snapshot_id") guests := self.GetGuests() - self.StartResetDisk(ctx, userCred, snapshotId, autoStart, guests) - return nil, nil + return nil, self.StartResetDisk(ctx, userCred, snapshotId, autoStart, &guests[0], "") } func (self *SDisk) StartResetDisk( ctx context.Context, userCred mcclient.TokenCredential, - snapshotId string, autoStart bool, guests []SGuest, + snapshotId string, autoStart bool, guest *SGuest, parentTaskId string, ) error { self.SetStatus(userCred, api.DISK_RESET, "") - if len(guests) == 1 { - guests[0].SetStatus(userCred, api.VM_DISK_RESET, "disk reset") + if guest != nil { + guest.SetStatus(userCred, api.VM_DISK_RESET, "disk reset") } params := jsonutils.NewDict() params.Set("snapshot_id", jsonutils.NewString(snapshotId)) params.Set("auto_start", jsonutils.NewBool(autoStart)) - task, err := taskman.TaskManager.NewTask(ctx, "DiskResetTask", self, userCred, params, "", "", nil) + task, err := taskman.TaskManager.NewTask(ctx, "DiskResetTask", self, userCred, params, parentTaskId, "", nil) if err != nil { return err } else { @@ -2012,7 +2011,7 @@ func (self *SDisk) CreateSnapshotAuto( } db.OpsLog.LogEvent(snap, db.ACT_CREATE, "disk create snapshot auto", userCred) - err = snap.StartSnapshotCreateTask(ctx, userCred, nil) + err = snap.StartSnapshotCreateTask(ctx, userCred, nil, "") if err != nil { return errors.Wrap(err, "disk auto snapshot start snapshot task") } diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index a36ef7fab0..f59cdad7be 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -3779,3 +3779,237 @@ func (manager *SGuestManager) StartHostGuestsMigrateTask( task.ScheduleRun(nil) return nil } + +func (self *SGuest) AllowPerformInstanceSnapshot(ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "instance-snapshot") +} + +func (self *SGuest) validateCreateInstanceSnapshot( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) (*SQuota, error) { + + if self.Hypervisor != api.HYPERVISOR_KVM { + return nil, httperrors.NewBadRequestError("guest hypervisor %s can't create instance snapshot", self.Hypervisor) + } + + if len(self.BackupHostId) > 0 { + return nil, httperrors.NewBadRequestError("Can't do instance snapshot with backup guest") + } + + if !utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_READY}) { + return nil, httperrors.NewInvalidStatusError("guest can't do snapshot in status %s", self.Status) + } + + ownerId := self.GetOwnerId() + dataDict := data.(*jsonutils.JSONDict) + name, err := dataDict.GetString("name") + if err != nil || len(name) == 0 { + return nil, httperrors.NewMissingParameterError("name") + } + err = db.NewNameValidator(InstanceSnapshotManager, ownerId, name, "") + if err != nil { + return nil, err + } + + disks := self.GetDisks() + for i := 0; i < len(disks); i++ { + count, err := SnapshotManager.GetDiskManualSnapshotCount(disks[i].DiskId) + if err != nil { + return nil, httperrors.NewInternalServerError(err.Error()) + } + if count >= options.Options.DefaultMaxManualSnapshotCount { + return nil, httperrors.NewBadRequestError("guests disk %d snapshot full, can't take anymore", i) + } + } + quotaPlatform := self.GetQuotaPlatformID() + pendingUsage := &SQuota{Snapshot: len(disks)} + err = QuotaManager.CheckSetPendingQuota(ctx, userCred, rbacutils.ScopeProject, ownerId, quotaPlatform, pendingUsage) + if err != nil { + return nil, httperrors.NewOutOfQuotaError("Check set pending quota error %s", err) + } + return pendingUsage, nil +} + +// 1. validate guest status, guest hypervisor +// 2. validate every disk manual snapshot count +// 3. validate snapshot quota with disk count +func (self *SGuest) PerformInstanceSnapshot( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + pendingUsage, err := self.validateCreateInstanceSnapshot(ctx, userCred, query, data) + if err != nil { + return nil, err + } + name, _ := data.GetString("name") + ownerId := self.GetOwnerId() + instanceSnapshot, err := InstanceSnapshotManager.CreateInstanceSnapshot(ctx, ownerId, self, name) + if err != nil { + QuotaManager.CancelPendingUsage( + ctx, userCred, rbacutils.ScopeProject, ownerId, self.GetQuotaPlatformID(), pendingUsage, pendingUsage) + return nil, httperrors.NewInternalServerError("create instance snapshot failed: %s", err) + } + err = self.InstaceCreateSnapshot(ctx, userCred, ownerId, instanceSnapshot, pendingUsage) + if err != nil { + QuotaManager.CancelPendingUsage( + ctx, userCred, rbacutils.ScopeProject, ownerId, self.GetQuotaPlatformID(), pendingUsage, pendingUsage) + return nil, httperrors.NewInternalServerError("start create snapshot task failed: %s", err) + } + return nil, nil +} + +func (self *SGuest) InstaceCreateSnapshot( + ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, + instanceSnapshot *SInstanceSnapshot, pendingUsage *SQuota) error { + + self.SetStatus(userCred, api.VM_START_INSTANCE_SNAPSHOT, "instance snapshot") + return instanceSnapshot.StartCreateInstanceSnapshotTask(ctx, userCred, ownerId, pendingUsage, "") +} + +func (self *SGuest) AllowPerformInstanceSnapshotReset(ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "instance-snapshot") +} + +func (self *SGuest) PerformInstanceSnapshotReset( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + + if self.Status != api.VM_READY { + return nil, httperrors.NewInvalidStatusError("guest can't do snapshot in status ", self.Status) + } + + dataDict := data.(*jsonutils.JSONDict) + instanceSnapshotV := validators.NewModelIdOrNameValidator( + "instance_snapshot", "instance_snapshot", self.GetOwnerId(), + ) + err := instanceSnapshotV.Validate(dataDict) + if err != nil { + return nil, err + } + instanceSnapshot := instanceSnapshotV.Model.(*SInstanceSnapshot) + if instanceSnapshot.Status != api.INSTANCE_SNAPSHOT_READY { + return nil, httperrors.NewBadRequestError("Instance sanpshot not ready") + } + + err = self.StartSnapshotResetTask(ctx, userCred, instanceSnapshot) + if err != nil { + return nil, httperrors.NewInternalServerError("start snapshot reset failed %s", err) + } + + return nil, nil +} + +func (self *SGuest) StartSnapshotResetTask( + ctx context.Context, userCred mcclient.TokenCredential, instanceSnapshot *SInstanceSnapshot) error { + + self.SetStatus(userCred, api.VM_START_SNAPSHOT_RESET, "start snapshot reset task") + if task, err := taskman.TaskManager.NewTask( + ctx, "InstanceSnapshotResetTask", instanceSnapshot, userCred, nil, "", "", nil, + ); err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (self *SGuest) AllowPerformSnapshotAndClone( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "snapshot-and-clone") +} + +func (self *SGuest) PerformSnapshotAndClone( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + newlyGuestName, err := data.GetString("name") + if err != nil { + return nil, httperrors.NewMissingParameterError("name") + } + err = db.NewNameValidator(GuestManager, self.GetOwnerId(), newlyGuestName, "") + if err != nil { + return nil, err + } + + pendingUsage, err := self.validateCreateInstanceSnapshot(ctx, userCred, query, data) + if err != nil { + return nil, err + } + + lockman.LockClass(ctx, InstanceSnapshotManager, self.ProjectId) + defer lockman.ReleaseClass(ctx, InstanceSnapshotManager, self.ProjectId) + + instanceSnapshotName, err := db.GenerateName(InstanceSnapshotManager, self.GetOwnerId(), "Snapshot-For-"+newlyGuestName) + if err != nil { + QuotaManager.CancelPendingUsage( + ctx, userCred, rbacutils.ScopeProject, self.GetOwnerId(), + self.GetQuotaPlatformID(), pendingUsage, pendingUsage) + return nil, httperrors.NewInternalServerError("Generate snapshot name failed %s", err) + } + instanceSnapshot, err := InstanceSnapshotManager.CreateInstanceSnapshot(ctx, self.GetOwnerId(), self, instanceSnapshotName) + if err != nil { + QuotaManager.CancelPendingUsage( + ctx, userCred, rbacutils.ScopeProject, self.GetOwnerId(), + self.GetQuotaPlatformID(), pendingUsage, pendingUsage) + return nil, httperrors.NewInternalServerError("create instance snapshot failed: %s", err) + } + + err = self.StartInstanceSnapshotAndCloneTask( + ctx, userCred, newlyGuestName, pendingUsage, instanceSnapshot, data.(*jsonutils.JSONDict)) + if err != nil { + QuotaManager.CancelPendingUsage( + ctx, userCred, rbacutils.ScopeProject, self.GetOwnerId(), + self.GetQuotaPlatformID(), pendingUsage, pendingUsage) + return nil, err + } + return nil, nil +} + +func (self *SGuest) StartInstanceSnapshotAndCloneTask( + ctx context.Context, userCred mcclient.TokenCredential, newlyGuestName string, + pendingUsage *SQuota, instanceSnapshot *SInstanceSnapshot, data *jsonutils.JSONDict) error { + + params := jsonutils.NewDict() + params.Set("guest_params", data) + if task, err := taskman.TaskManager.NewTask( + ctx, "InstanceSnapshotAndCloneTask", instanceSnapshot, userCred, params, "", "", pendingUsage); err != nil { + return err + } else { + self.SetStatus(userCred, api.VM_START_INSTANCE_SNAPSHOT, "instance snapshot") + task.ScheduleRun(nil) + return nil + } +} + +func (manager *SGuestManager) CreateGuestFromInstanceSnapshot( + ctx context.Context, userCred mcclient.TokenCredential, guestParams *jsonutils.JSONDict, + isp *SInstanceSnapshot, index int, +) (*SGuest, *jsonutils.JSONDict, error) { + lockman.LockClass(ctx, manager, isp.ProjectId) + defer lockman.ReleaseClass(ctx, manager, isp.ProjectId) + + guestName, err := guestParams.GetString("name") + if err != nil { + return nil, nil, fmt.Errorf("No new guest name provider") + } + if err := db.NewNameValidator(manager, isp.GetOwnerId(), guestName, ""); err != nil { + guestName, err = db.GenerateName2(manager, isp.GetOwnerId(), guestName, nil, index) + if err != nil { + return nil, nil, err + } + } + + guestParams.Set("name", jsonutils.NewString(guestName)) + guestParams.Set("instance_snapshot_id", jsonutils.NewString(isp.Id)) + iGuest, err := db.DoCreate(manager, ctx, userCred, nil, guestParams, isp.GetOwnerId()) + if err != nil { + return nil, nil, err + } + guest := iGuest.(*SGuest) + return guest, guestParams, nil +} diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 0015122ca6..db8a8c376e 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -868,15 +868,37 @@ func (manager *SGuestManager) BatchPreValidate( return nil, nil } +func parseInstanceSnapshot(input *api.ServerCreateInput) (*api.ServerCreateInput, error) { + ispi, err := InstanceSnapshotManager.FetchByIdOrName(nil, input.InstanceSnapshotId) + if err == sql.ErrNoRows { + return nil, httperrors.NewBadRequestError("can't find instance snapshot %s", input.InstanceSnapshotId) + } + if err != nil { + return nil, httperrors.NewInternalServerError("fetch instance snapshot error %s", err) + } + isp := ispi.(*SInstanceSnapshot) + if isp.Status != api.INSTANCE_SNAPSHOT_READY { + return nil, httperrors.NewBadRequestError("Instance snapshot not ready") + } + return isp.ToInstanceCreateInput(input) +} + func (manager *SGuestManager) validateCreateData( ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*api.ServerCreateInput, error) { - // TODO: 定义 api.ServerCreateInput 的 Unmarshal 函数,直接通过 data.Unmarshal(input) 解析参数 input, err := cmdline.FetchServerCreateInputByJSON(data) if err != nil { return nil, err } + + if len(input.InstanceSnapshotId) > 0 { + input, err = parseInstanceSnapshot(input) + if err != nil { + return nil, err + } + } + resetPassword := true if input.ResetPassword != nil { resetPassword = *input.ResetPassword @@ -1144,8 +1166,6 @@ func (manager *SGuestManager) validateCreateData( return nil, httperrors.NewResourceNotFoundError("Keypair %s not found", keypairId) } input.KeypairId = keypairObj.GetId() - } else { - input.KeypairId = "None" // TODO: ??? None? } if input.SecgroupId != "" { @@ -3225,6 +3245,15 @@ func (self *SGuest) DeleteAllDisksInDB(ctx context.Context, userCred mcclient.To return nil } +func (self *SGuest) isNeedDoResetPasswd() bool { + guestdisks := self.GetDisks() + disk := guestdisks[0].GetDisk() + if len(disk.SnapshotId) > 0 { + return false + } + return true +} + func (self *SGuest) GetDeployConfigOnHost(ctx context.Context, userCred mcclient.TokenCredential, host *SHost, params *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { config := jsonutils.NewDict() @@ -3245,12 +3274,13 @@ func (self *SGuest) GetDeployConfigOnHost(ctx context.Context, userCred mcclient deployAction = "deploy" } - // resetPasswd := true - // if deployAction == "deploy" { - resetPasswd := jsonutils.QueryBoolean(params, "reset_password", true) - //} config.Add(jsonutils.NewBool(jsonutils.QueryBoolean(params, "enable_cloud_init", false)), "enable_cloud_init") + resetPasswd := jsonutils.QueryBoolean(params, "reset_password", true) + if deployAction == "create" && resetPasswd { + resetPasswd = self.isNeedDoResetPasswd() + } + if resetPasswd { config.Add(jsonutils.JSONTrue, "reset_password") passwd, _ := params.GetString("password") diff --git a/pkg/compute/models/instance_snapshot_joint.go b/pkg/compute/models/instance_snapshot_joint.go new file mode 100644 index 0000000000..a6eb020106 --- /dev/null +++ b/pkg/compute/models/instance_snapshot_joint.go @@ -0,0 +1,51 @@ +package models + +import "yunion.io/x/onecloud/pkg/cloudcommon/db" + +func init() { + db.InitManager(func() { + InstanceSnapshotJointManager = &SInstanceSnapshotJointManager{ + SVirtualJointResourceBaseManager: db.NewVirtualJointResourceBaseManager( + SInstanceSnapshotJoint{}, + "instancesnapshotjoints_tbl", + "instancesnapshotjoint", + "instancesnapshotjoints", + InstanceSnapshotManager, + SnapshotManager, + ), + } + InstanceSnapshotJointManager.SetVirtualObject(InstanceSnapshotJointManager) + }) +} + +type SInstanceSnapshotJoint struct { + db.SVirtualJointResourceBase + + InstanceSnapshotId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + SnapshotId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + DiskIndex int8 `nullable:"false" default:"0" list:"user" create:"required"` +} + +type SInstanceSnapshotJointManager struct { + db.SVirtualJointResourceBaseManager +} + +func (manager *SInstanceSnapshotJointManager) GetMasterFieldName() string { + return "instance_snapshot_id" +} + +func (manager *SInstanceSnapshotJointManager) GetSlaveFieldName() string { + return "disk_id" +} + +var InstanceSnapshotJointManager *SInstanceSnapshotJointManager + +func (manager *SInstanceSnapshotJointManager) CreateJoint(instanceSnapshotId, snapshotId string, diskIndex int8) error { + instanceSnapshotJoint := &SInstanceSnapshotJoint{} + instanceSnapshotJoint.SetModelManager(manager, instanceSnapshotJoint) + + instanceSnapshotJoint.InstanceSnapshotId = instanceSnapshotId + instanceSnapshotJoint.SnapshotId = snapshotId + instanceSnapshotJoint.DiskIndex = diskIndex + return manager.TableSpec().Insert(instanceSnapshotJoint) +} diff --git a/pkg/compute/models/instance_snapshots.go b/pkg/compute/models/instance_snapshots.go new file mode 100644 index 0000000000..6710e82aa8 --- /dev/null +++ b/pkg/compute/models/instance_snapshots.go @@ -0,0 +1,211 @@ +package models + +import ( + "context" + "database/sql" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/apis/compute" + schedapi "yunion.io/x/onecloud/pkg/apis/scheduler" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/quotas" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" +) + +func init() { + InstanceSnapshotManager = &SInstanceSnapshotManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SInstanceSnapshot{}, + "instance_snapshots_tbl", + "instance_snapshot", + "instance_snapshots", + ), + } + InstanceSnapshotManager.SetVirtualObject(InstanceSnapshotManager) +} + +type SInstanceSnapshot struct { + db.SVirtualResourceBase + + GuestId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` + ServerConfig jsonutils.JSONObject `nullable:"true" list:"user"` +} + +type SInstanceSnapshotManager struct { + db.SVirtualResourceBaseManager +} + +var InstanceSnapshotManager *SInstanceSnapshotManager + +func (manager *SInstanceSnapshotManager) AllowCreateItem( + ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject, +) bool { + return false +} + +func (manager *SInstanceSnapshotManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { + queryDict, ok := query.(*jsonutils.JSONDict) + if !ok { + return nil, fmt.Errorf("invalid querystring format") + } + if guestId, _ := queryDict.GetString("guest_id"); len(guestId) > 0 { + q = q.Equals("guest_id", guestId) + } + return q, nil +} + +func (self *SInstanceSnapshot) AllowUpdateItem(ctx context.Context, userCred mcclient.TokenCredential) bool { + return false +} + +func (self *SInstanceSnapshot) GetCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) *jsonutils.JSONDict { + extra := self.SVirtualResourceBase.GetCustomizeColumns(ctx, userCred, query) + if guest := GuestManager.FetchGuestById(self.GuestId); guest != nil { + extra.Set("guest_status", jsonutils.NewString(guest.Status)) + extra.Set("guest_name", jsonutils.NewString(guest.Name)) + } + return extra +} + +// func (self *SInstanceSnapshot) getMoreDetails() + +func (self *SInstanceSnapshot) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*jsonutils.JSONDict, error) { + extra, err := self.SVirtualResourceBase.GetExtraDetails(ctx, userCred, query) + if err != nil { + return nil, err + } + if guest := GuestManager.FetchGuestById(self.GuestId); guest != nil { + extra.Set("guest_status", jsonutils.NewString(guest.Status)) + extra.Set("guest_name", jsonutils.NewString(guest.Name)) + } + return extra, nil +} +func (self *SInstanceSnapshot) StartCreateInstanceSnapshotTask( + ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, + pendingUsage quotas.IQuota, parentTaskId string) error { + + if task, err := taskman.TaskManager.NewTask( + ctx, "InstanceSnapshotCreateTask", self, userCred, nil, parentTaskId, "", pendingUsage); err != nil { + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (manager *SInstanceSnapshotManager) CreateInstanceSnapshot( + ctx context.Context, ownerId mcclient.IIdentityProvider, guest *SGuest, name string, +) (*SInstanceSnapshot, error) { + instanceSnapshot := &SInstanceSnapshot{} + instanceSnapshot.SetModelManager(manager, instanceSnapshot) + instanceSnapshot.Name = name + instanceSnapshot.ProjectId = ownerId.GetProjectId() + instanceSnapshot.DomainId = ownerId.GetProjectDomainId() + instanceSnapshot.GuestId = guest.Id + guestSchedInput := guest.ToSchedDesc() + + for i := 0; i < len(guestSchedInput.Disks); i++ { + guestSchedInput.Disks[i].ImageId = "" + } + guestSchedInput.Name = "" + guestSchedInput.HostId = "" + guestSchedInput.Project = "" + guestSchedInput.Domain = "" + for i := 0; i < len(guestSchedInput.Networks); i++ { + guestSchedInput.Networks[i].Mac = "" + guestSchedInput.Networks[i].Address = "" + guestSchedInput.Networks[i].Address6 = "" + } + instanceSnapshot.ServerConfig = jsonutils.Marshal(guestSchedInput.ServerConfig) + + err := manager.TableSpec().Insert(instanceSnapshot) + if err != nil { + return nil, err + } + return instanceSnapshot, nil +} + +func (self *SInstanceSnapshot) ToInstanceCreateInput( + sourceInput *compute.ServerCreateInput) (*compute.ServerCreateInput, error) { + + serverConfig := new(schedapi.ServerConfig) + if err := self.ServerConfig.Unmarshal(serverConfig); err != nil { + return nil, errors.Wrap(err, "unmarshal sched input") + } + + isjs := make([]SInstanceSnapshotJoint, 0) + err := InstanceSnapshotJointManager.Query().Equals("instance_snapshot_id", self.Id).Asc("disk_index").All(&isjs) + if err != nil { + return nil, errors.Wrap(err, "fetch instance snapshots") + } + for i := 0; i < len(serverConfig.Disks); i++ { + serverConfig.Disks[i].SnapshotId = isjs[serverConfig.Disks[i].Index].SnapshotId + } + sourceInput.Disks = serverConfig.Disks + sourceInput.VmemSize = serverConfig.Memory + sourceInput.VcpuCount = serverConfig.Ncpu + sourceInput.Networks = serverConfig.Networks + return sourceInput, nil +} + +func (self *SInstanceSnapshot) GetSnapshots() ([]SSnapshot, error) { + isjq := InstanceSnapshotJointManager.Query("snapshot_id").Equals("instance_snapshot_id", self.Id) + snapshots := make([]SSnapshot, 0) + err := SnapshotManager.Query().In("id", isjq).All(&snapshots) + if err != nil && err != sql.ErrNoRows { + return nil, err + } else if err != nil && err == sql.ErrNoRows { + return nil, nil + } else { + for i := 0; i < len(snapshots); i++ { + snapshots[i].SetModelManager(SnapshotManager, &snapshots[i]) + } + return snapshots, nil + } +} + +func (self *SInstanceSnapshot) GetInstanceSnapshotJointAt(diskIndex int) (*SInstanceSnapshotJoint, error) { + ispj := new(SInstanceSnapshotJoint) + err := InstanceSnapshotJointManager.Query(). + Equals("instance_snapshot_id", self.Id).Equals("disk_index", diskIndex).First(ispj) + return ispj, err +} + +func (self *SInstanceSnapshot) ValidateDeleteCondition(ctx context.Context) error { + if self.Status == compute.INSTANCE_SNAPSHOT_START_DELETE { + return httperrors.NewBadRequestError("can't delete snapshot in deleting") + } + return nil +} + +func (self *SInstanceSnapshot) CustomizeDelete( + ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) error { + + return self.StartInstanceSnapshotDeleteTask(ctx, userCred, "") +} + +func (self *SInstanceSnapshot) StartInstanceSnapshotDeleteTask( + ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + + task, err := taskman.TaskManager.NewTask( + ctx, "InstanceSnapshotDeleteTask", self, userCred, nil, parentTaskId, "", nil) + if err != nil { + log.Errorf("%s", err) + return err + } + self.SetStatus(userCred, compute.INSTANCE_SNAPSHOT_START_DELETE, "InstanceSnapshotDeleteTask") + task.ScheduleRun(nil) + return nil +} + +func (self *SInstanceSnapshot) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DeleteModel(ctx, userCred, self) +} diff --git a/pkg/compute/models/snapshots.go b/pkg/compute/models/snapshots.go index 4b4225775f..cf5dbfe3c6 100644 --- a/pkg/compute/models/snapshots.go +++ b/pkg/compute/models/snapshots.go @@ -50,7 +50,7 @@ type SSnapshot struct { SManagedResourceBase - DiskId string `width:"36" charset:"ascii" nullable:"true" create:"required" list:"user"` + DiskId string `width:"36" charset:"ascii" nullable:"true" create:"required" list:"user" index:"true"` // Only onecloud has StorageId StorageId string `width:"36" charset:"ascii" nullable:"true" list:"admin" create:"optional"` @@ -288,11 +288,11 @@ func (self *SSnapshot) CustomizeCreate(ctx context.Context, userCred mcclient.To func (manager *SSnapshotManager) OnCreateComplete(ctx context.Context, items []db.IModel, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { snapshot := items[0].(*SSnapshot) - snapshot.StartSnapshotCreateTask(ctx, userCred, nil) + snapshot.StartSnapshotCreateTask(ctx, userCred, nil, "") } -func (self *SSnapshot) StartSnapshotCreateTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict) error { - task, err := taskman.TaskManager.NewTask(ctx, "SnapshotCreateTask", self, userCred, params, "", "", nil) +func (self *SSnapshot) StartSnapshotCreateTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error { + task, err := taskman.TaskManager.NewTask(ctx, "SnapshotCreateTask", self, userCred, params, parentTaskId, "", nil) if err != nil { return err } @@ -383,6 +383,10 @@ func (self *SSnapshotManager) GetDiskSnapshots(diskId string) []SSnapshot { return dest } +func (self *SSnapshotManager) GetDiskManualSnapshotCount(diskId string) (int, error) { + return self.Query().Equals("disk_id", diskId).Equals("fake_deleted", false).CountWithError() +} + func (self *SSnapshotManager) IsDiskSnapshotsNeedConvert(diskId string) (bool, error) { count, err := self.Query().Equals("disk_id", diskId). In("status", []string{api.SNAPSHOT_READY, api.SNAPSHOT_DELETING}). @@ -471,6 +475,13 @@ func (self *SSnapshot) ValidateDeleteCondition(ctx context.Context) error { if self.Status == api.SNAPSHOT_DELETING { return httperrors.NewBadRequestError("Cannot delete snapshot in status %s", self.Status) } + count, err := InstanceSnapshotJointManager.Query().Equals("snapshot_id", self.Id).CountWithError() + if err != nil { + return httperrors.NewInternalServerError("Fetch instance snapshot error %s", err) + } + if count > 0 { + return httperrors.NewBadRequestError("snapshot referenced by instance snapshot") + } return self.GetRegionDriver().ValidateSnapshotDelete(ctx, self) } diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index ab91ce8689..dbaff2e43a 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -94,6 +94,7 @@ func InitHandlers(app *appsrv.Application) { models.NatGatewayManager, models.NatDEntryManager, models.NatSEntryManager, + models.InstanceSnapshotManager, models.SnapshotManager, models.SnapshotPolicyManager, models.BaremetalagentManager, @@ -156,6 +157,7 @@ func InitHandlers(app *appsrv.Application) { models.DBInstanceNetworkManager, models.NetworkinterfacenetworkManager, models.SnapshotPolicyDiskManager, + models.InstanceSnapshotJointManager, } { db.RegisterModelManager(manager) handler := db.NewJointModelHandler(manager) diff --git a/pkg/compute/tasks/disk_reset_task.go b/pkg/compute/tasks/disk_reset_task.go index 4dd476c4f0..b9846e2039 100644 --- a/pkg/compute/tasks/disk_reset_task.go +++ b/pkg/compute/tasks/disk_reset_task.go @@ -55,7 +55,7 @@ func (self *DiskResetTask) TaskCompleted(ctx context.Context, disk *models.SDisk guests[0].StartGueststartTask(ctx, self.UserCred, nil, self.GetTaskId()) } } else { - if len(guests) == 1 { + if len(guests) == 1 && !self.IsSubtask() { guests[0].SetStatus(self.UserCred, api.VM_READY, "") } // data不能为空指针,否则会导致AddActionLog抛空指针异常 diff --git a/pkg/compute/tasks/guest_disk_snapshot_task.go b/pkg/compute/tasks/guest_disk_snapshot_task.go index a8724a5c64..1ff3a3aee6 100644 --- a/pkg/compute/tasks/guest_disk_snapshot_task.go +++ b/pkg/compute/tasks/guest_disk_snapshot_task.go @@ -52,7 +52,9 @@ func (self *GuestDiskSnapshotTask) DoDiskSnapshot(ctx context.Context, guest *mo self.TaskFailed(ctx, guest, err.Error()) return } - self.SetStage("OnDiskSnapshotComplete", nil) + params := jsonutils.NewDict() + params.Set("guest_old_status", jsonutils.NewString(guest.Status)) + self.SetStage("OnDiskSnapshotComplete", params) guest.SetStatus(self.UserCred, api.VM_SNAPSHOT, "") err = guest.GetDriver().RequestDiskSnapshot(ctx, guest, self, snapshotId, diskId) if err != nil { @@ -87,7 +89,8 @@ func (self *GuestDiskSnapshotTask) OnDiskSnapshotCompleteFailed(ctx context.Cont func (self *GuestDiskSnapshotTask) TaskComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { logclient.AddActionLogWithStartable(self, guest, logclient.ACT_DISK_CREATE_SNAPSHOT, nil, self.UserCred, true) - guest.StartSyncstatus(ctx, self.UserCred, "") + status, _ := self.Params.GetString("guest_old_status") + guest.SetStatus(self.UserCred, status, "on guest disk snapshot complete") self.SetStageComplete(ctx, nil) } diff --git a/pkg/compute/tasks/instance_snapshot_and_clone_task.go b/pkg/compute/tasks/instance_snapshot_and_clone_task.go new file mode 100644 index 0000000000..c4cb16d9b4 --- /dev/null +++ b/pkg/compute/tasks/instance_snapshot_and_clone_task.go @@ -0,0 +1,130 @@ +package tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +type InstanceSnapshotAndCloneTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceSnapshotAndCloneTask{}) +} + +func (self *InstanceSnapshotAndCloneTask) taskFailed( + ctx context.Context, isp *models.SInstanceSnapshot, reason string) { + guest := models.GuestManager.FetchGuestById(isp.GuestId) + guest.SetStatus(self.UserCred, compute.VM_SNAPSHOT_AND_CLONE_FAILED, reason) + logclient.AddActionLogWithContext( + ctx, guest, logclient.ACT_VM_SNAPSHOT_AND_CLONE, reason, self.UserCred, false, + ) + db.OpsLog.LogEvent(guest, db.ACT_VM_SNAPSHOT_AND_CLONE_FAILED, reason, self.UserCred) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotAndCloneTask) taskComplete( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + self.finalReleasePendingUsage(ctx) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + guest.StartSyncstatus(ctx, self.UserCred, "") + db.OpsLog.LogEvent(guest, db.ACT_VM_SNAPSHOT_AND_CLONE, "", self.UserCred) + logclient.AddActionLogWithContext( + ctx, guest, logclient.ACT_VM_SNAPSHOT_AND_CLONE, "", self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceSnapshotAndCloneTask) SetStageFailed(ctx context.Context, reason string) { + self.finalReleasePendingUsage(ctx) + self.STask.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotAndCloneTask) finalReleasePendingUsage(ctx context.Context) { + pendingUsage := models.SQuota{} + err := self.GetPendingUsage(&pendingUsage) + if err == nil && !pendingUsage.IsEmpty() { + isp := self.GetObject().(*models.SInstanceSnapshot) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + quotaPlatform := guest.GetQuotaPlatformID() + models.QuotaManager.CancelPendingUsage( + ctx, self.UserCred, rbacutils.ScopeProject, + guest.GetOwnerId(), quotaPlatform, &pendingUsage, &pendingUsage, + ) + } +} + +func (self *InstanceSnapshotAndCloneTask) OnInit( + ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + + isp := obj.(*models.SInstanceSnapshot) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + + self.SetStage("OnCreateInstanceSnapshot", nil) + err := isp.StartCreateInstanceSnapshotTask(ctx, self.UserCred, guest.GetOwnerId(), nil, self.Id) + if err != nil { + self.taskFailed(ctx, isp, err.Error()) + return + } +} + +func (self *InstanceSnapshotAndCloneTask) OnCreateInstanceSnapshot( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + // start create server + params, err := self.Params.Get("guest_params") + if err != nil { + self.taskFailed(ctx, isp, "Failed get new guest params") + return + } + count, _ := params.Int("count") + if count == 0 { + count = 1 + } + err = self.doGuestCreate(ctx, isp, params, int(count)) + if err != nil { + self.taskFailed(ctx, isp, err.Error()) + return + } + self.taskComplete(ctx, isp, nil) +} + +func (self *InstanceSnapshotAndCloneTask) doGuestCreate( + ctx context.Context, isp *models.SInstanceSnapshot, params jsonutils.JSONObject, count int) error { + dictParmas := params.(*jsonutils.JSONDict) + var errStr string + for i := 0; i < count; i++ { + newGuest, input, err := models.GuestManager.CreateGuestFromInstanceSnapshot( + ctx, self.UserCred, dictParmas.DeepCopy().(*jsonutils.JSONDict), isp, i+1) + if err != nil { + log.Errorln(err) + errStr += err.Error() + "\n" + continue + } + models.GuestManager.OnCreateComplete(ctx, []db.IModel{newGuest}, self.UserCred, nil, input) + } + if len(errStr) > 0 { + return fmt.Errorf(errStr) + } + return nil +} + +func (self *InstanceSnapshotAndCloneTask) OnGuestCreated( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + self.taskComplete(ctx, isp, data) +} + +func (self *InstanceSnapshotAndCloneTask) OnCreateInstanceSnapshotFailed( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + self.taskFailed(ctx, isp, data.String()) +} diff --git a/pkg/compute/tasks/instance_snapshot_create_task.go b/pkg/compute/tasks/instance_snapshot_create_task.go new file mode 100644 index 0000000000..caaba53fd8 --- /dev/null +++ b/pkg/compute/tasks/instance_snapshot_create_task.go @@ -0,0 +1,144 @@ +package tasks + +import ( + "context" + "fmt" + "strconv" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/util/timeutils" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +type InstanceSnapshotCreateTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceSnapshotCreateTask{}) +} + +func (self *InstanceSnapshotCreateTask) SetStageFailed(ctx context.Context, reason string) { + self.finalReleasePendingUsage(ctx) + self.STask.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotCreateTask) finalReleasePendingUsage(ctx context.Context) { + pendingUsage := models.SQuota{} + err := self.GetPendingUsage(&pendingUsage) + if err == nil && !pendingUsage.IsEmpty() { + isp := self.GetObject().(*models.SInstanceSnapshot) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + quotaPlatform := guest.GetQuotaPlatformID() + models.QuotaManager.CancelPendingUsage( + ctx, self.UserCred, rbacutils.ScopeProject, + guest.GetOwnerId(), quotaPlatform, &pendingUsage, &pendingUsage, + ) + } +} + +func (self *InstanceSnapshotCreateTask) taskFail( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, reason string) { + + if guest == nil { + guest = models.GuestManager.FetchGuestById(isp.GuestId) + } + isp.SetStatus(self.UserCred, compute.INSTANCE_SNAPSHOT_FAILED, reason) + guest.SetStatus(self.UserCred, compute.VM_INSTANCE_SNAPSHOT_FAILED, reason) + + db.OpsLog.LogEvent(isp, db.ACT_ALLOCATE_FAIL, reason, self.UserCred) + logclient.AddActionLogWithStartable(self, isp, logclient.ACT_CREATE, false, self.UserCred, false) + notifyclient.NotifySystemError(isp.GetId(), isp.Name, compute.INSTANCE_SNAPSHOT_FAILED, reason) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotCreateTask) taskComplete( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, data jsonutils.JSONObject) { + + self.finalReleasePendingUsage(ctx) + if guest == nil { + guest = models.GuestManager.FetchGuestById(isp.GuestId) + } + isp.SetStatus(self.UserCred, compute.INSTANCE_SNAPSHOT_READY, "") + guest.StartSyncstatus(ctx, self.UserCred, "") + + db.OpsLog.LogEvent(isp, db.ACT_ALLOCATE, "instance snapshot create success", self.UserCred) + logclient.AddActionLogWithStartable(self, isp, logclient.ACT_CREATE, false, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceSnapshotCreateTask) OnInit( + ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + + isp := obj.(*models.SInstanceSnapshot) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + + self.GuestDiskCreateSnapshot(ctx, isp, guest, 0) +} + +func (self *InstanceSnapshotCreateTask) GuestDiskCreateSnapshot( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, diskIndex int) { + + disks := guest.GetDisks() + if diskIndex >= len(disks) { + self.taskComplete(ctx, isp, guest, nil) + return + } + + snapshot, err := models.SnapshotManager.CreateSnapshot( + ctx, self.UserCred, compute.SNAPSHOT_MANUAL, disks[diskIndex].DiskId, guest.Id, + "", fmt.Sprintf("%s-snapshot-%s", guest.Name, timeutils.CompactTime(time.Now())), -1) + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + + params := jsonutils.NewDict() + params.Set("disk_index", jsonutils.NewInt(int64(diskIndex))) + params.Set(strconv.Itoa(diskIndex), jsonutils.NewString(snapshot.Id)) + self.SetStage("OnDiskSnapshot", params) + + if err := snapshot.StartSnapshotCreateTask(ctx, self.UserCred, nil, self.Id); err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } +} + +func (self *InstanceSnapshotCreateTask) OnDiskSnapshot( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + guest := models.GuestManager.FetchGuestById(isp.GuestId) + + diskIndex, err := self.Params.Int("disk_index") + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + + snapshotId, err := self.Params.GetString(strconv.Itoa(int(diskIndex))) + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + + err = models.InstanceSnapshotJointManager.CreateJoint(isp.Id, snapshotId, int8(diskIndex)) + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + + self.GuestDiskCreateSnapshot(ctx, isp, guest, int(diskIndex+1)) +} + +func (self *InstanceSnapshotCreateTask) OnDiskSnapshotFailed( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + self.taskFail(ctx, isp, nil, data.String()) +} diff --git a/pkg/compute/tasks/instance_snapshot_delete_task.go b/pkg/compute/tasks/instance_snapshot_delete_task.go new file mode 100644 index 0000000000..9813048d28 --- /dev/null +++ b/pkg/compute/tasks/instance_snapshot_delete_task.go @@ -0,0 +1,93 @@ +package tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type InstanceSnapshotDeleteTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceSnapshotDeleteTask{}) +} + +func (self *InstanceSnapshotDeleteTask) taskFail( + ctx context.Context, isp *models.SInstanceSnapshot, reason string) { + + isp.SetStatus(self.UserCred, compute.INSTANCE_SNAPSHOT_DELETE_FAILED, "on delete failed") + db.OpsLog.LogEvent(isp, db.ACT_DELETE_FAIL, reason, self.UserCred) + logclient.AddActionLogWithContext(ctx, isp, logclient.ACT_DELETE, reason, self.UserCred, false) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotDeleteTask) taskComplete( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + isp.RealDelete(ctx, self.UserCred) + logclient.AddActionLogWithContext(ctx, isp, logclient.ACT_DELETE, nil, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceSnapshotDeleteTask) OnInit( + ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + + isp := obj.(*models.SInstanceSnapshot) + self.SetStage("OnSnapshotDelete", nil) + self.StartSnapshotDelete(ctx, isp) +} + +func (self *InstanceSnapshotDeleteTask) StartSnapshotDelete( + ctx context.Context, isp *models.SInstanceSnapshot) { + + snapshots, err := isp.GetSnapshots() + if err != nil { + self.taskFail(ctx, isp, err.Error()) + return + } + if len(snapshots) == 0 { + self.taskComplete(ctx, isp, nil) + return + } + + // detach snapshot and instance + isjp := new(models.SInstanceSnapshotJoint) + err = models.InstanceSnapshotJointManager.Query(). + Equals("instance_snapshot_id", isp.Id).Equals("snapshot_id", snapshots[0].Id).First(isjp) + if err != nil { + self.taskFail(ctx, isp, err.Error()) + return + } + isjp.SetModelManager(models.InstanceSnapshotJointManager, isjp) + err = isjp.Delete(ctx, self.UserCred) + if err != nil { + self.taskFail(ctx, isp, err.Error()) + return + } + + err = snapshots[0].StartSnapshotDeleteTask(ctx, self.UserCred, false, self.Id) + if err != nil { + self.taskFail(ctx, isp, err.Error()) + return + } +} + +func (self *InstanceSnapshotDeleteTask) OnSnapshotDelete( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + self.StartSnapshotDelete(ctx, isp) +} + +func (self *InstanceSnapshotDeleteTask) OnSnapshotDeleteFailed( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + self.taskFail(ctx, isp, data.String()) +} diff --git a/pkg/compute/tasks/instance_snapshot_reset_task.go b/pkg/compute/tasks/instance_snapshot_reset_task.go new file mode 100644 index 0000000000..c102c6f237 --- /dev/null +++ b/pkg/compute/tasks/instance_snapshot_reset_task.go @@ -0,0 +1,103 @@ +package tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type InstanceSnapshotResetTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(InstanceSnapshotResetTask{}) +} + +func (self *InstanceSnapshotResetTask) taskFail( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, reason string) { + + if guest == nil { + guest = models.GuestManager.FetchGuestById(isp.GuestId) + } + guest.SetStatus(self.UserCred, compute.VM_SNAPSHOT_RESET_FAILED, reason) + + db.OpsLog.LogEvent(guest, db.ACT_VM_RESET_SNAPSHOT_FAILED, reason, self.UserCred) + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_VM_RESET, false, self.UserCred, false) + notifyclient.NotifySystemError(guest.GetId(), isp.Name, compute.VM_SNAPSHOT_RESET_FAILED, reason) + self.SetStageFailed(ctx, reason) +} + +func (self *InstanceSnapshotResetTask) taskComplete( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, data jsonutils.JSONObject) { + + if guest == nil { + guest = models.GuestManager.FetchGuestById(isp.GuestId) + } + guest.StartSyncstatus(ctx, self.UserCred, "") + + db.OpsLog.LogEvent(isp, db.ACT_VM_RESET_SNAPSHOT, "instance snapshot reset success", self.UserCred) + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_VM_RESET, false, self.UserCred, true) + self.SetStageComplete(ctx, nil) +} + +func (self *InstanceSnapshotResetTask) OnInit( + ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + + isp := obj.(*models.SInstanceSnapshot) + guest := models.GuestManager.FetchGuestById(isp.GuestId) + + self.GuestDiskResetTask(ctx, isp, guest, 0) +} + +func (self *InstanceSnapshotResetTask) GuestDiskResetTask( + ctx context.Context, isp *models.SInstanceSnapshot, guest *models.SGuest, diskIndex int) { + + disks := guest.GetDisks() + if diskIndex >= len(disks) { + self.taskComplete(ctx, isp, guest, nil) + return + } + + isj, err := isp.GetInstanceSnapshotJointAt(diskIndex) + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + + params := jsonutils.NewDict() + params.Set("disk_index", jsonutils.NewInt(int64(diskIndex))) + self.SetStage("OnDiskReset", params) + + disk := disks[diskIndex].GetDisk() + err = disk.StartResetDisk(ctx, self.UserCred, isj.SnapshotId, false, guest, self.Id) + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } +} + +func (self *InstanceSnapshotResetTask) OnDiskReset( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + + guest := models.GuestManager.FetchGuestById(isp.GuestId) + + diskIndex, err := self.Params.Int("disk_index") + if err != nil { + self.taskFail(ctx, isp, guest, err.Error()) + return + } + self.GuestDiskResetTask(ctx, isp, guest, int(diskIndex+1)) +} + +func (self *InstanceSnapshotResetTask) OnDiskResetFailed( + ctx context.Context, isp *models.SInstanceSnapshot, data jsonutils.JSONObject) { + self.taskFail(ctx, isp, nil, data.String()) +} diff --git a/pkg/hostman/guestfs/kvmpart.go b/pkg/hostman/guestfs/kvmpart.go index 2e4f6e1e9b..0b5661d2d2 100644 --- a/pkg/hostman/guestfs/kvmpart.go +++ b/pkg/hostman/guestfs/kvmpart.go @@ -103,6 +103,7 @@ func (p *SKVMGuestDiskPartition) MountPartReadOnly() bool { func (p *SKVMGuestDiskPartition) Mount() bool { if len(p.fs) == 0 || utils.IsInStringArray(p.fs, []string{"swap", "btrfs"}) { + log.Errorf("Mount fs failed: %s", p.fs) return false } err := p.fsck() diff --git a/pkg/hostman/guestman/qemu-kvm.go b/pkg/hostman/guestman/qemu-kvm.go index b3172c4bd5..55f2614bf3 100644 --- a/pkg/hostman/guestman/qemu-kvm.go +++ b/pkg/hostman/guestman/qemu-kvm.go @@ -1183,6 +1183,7 @@ func (s *SKVMGuestInstance) streamDisksComplete(ctx context.Context) { diskpath, _ := disk.GetString("path") d := storageman.GetManager().GetDiskByPath(diskpath) if d != nil { + log.Infof("Disk %s do post create from fuse", d.GetId()) d.PostCreateFromImageFuse() } if jsonutils.QueryBoolean(disk, "merge_snapshot", false) { diff --git a/pkg/mcclient/modules/mod_instance_snapshots.go b/pkg/mcclient/modules/mod_instance_snapshots.go new file mode 100644 index 0000000000..62a8cf65da --- /dev/null +++ b/pkg/mcclient/modules/mod_instance_snapshots.go @@ -0,0 +1,19 @@ +package modules + +import "yunion.io/x/onecloud/pkg/mcclient/modulebase" + +var ( + InstanceSnapshots modulebase.ResourceManager +) + +func init() { + InstanceSnapshots = NewComputeManager("instance_snapshot", "instance_snapshots", + []string{"ID", "Name", + "Status", "GuestId", + "ServerConfig", + }, + []string{}, + ) + + registerCompute(&InstanceSnapshots) +} diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index f7ce4a6452..46b3362776 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -232,6 +232,17 @@ type ServerCloneOptions struct { Eip string `help:"associate with an existing EIP when server is created" json:"eip,omitempty"` } +type ServerCreateFromInstanceSnapshot struct { + InstaceSnapshotId string `help:"Instace snapshot id or name"` + NAME string `help:"Name of newly server" json:"name"` + AutoStart bool `help:"Auto start server after it is created"` + AllowDelete bool `help:"Unlock server to allow deleting"` + + EipBw int `help:"allocate EIP with bandwidth in MB when server is created" json:"eip_bw,omitzero"` + EipChargeType string `help:"newly allocated EIP charge type" choices:"traffic|bandwidth" json:"eip_charge_type,omitempty"` + Eip string `help:"associate with an existing EIP when server is created" json:"eip,omitempty"` +} + type ServerCreateOptions struct { ServerConfigs @@ -259,6 +270,7 @@ type ServerCreateOptions struct { TaskNotify *bool `help:"Setup task notify" json:"-"` DryRun *bool `help:"Dry run to test scheduler" json:"-"` UserDataFile string `help:"user_data file path" json:"-"` + InstanceSnapshot string `help:"instance snapshot" json:"instance_snapshot"` OsType string `help:"os type, e.g. Linux, Windows, etc."` diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index f4ebe9ba22..5573bb5b37 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -124,6 +124,8 @@ const ( ACT_ATTACH_HOST = "关联宿主机" ACT_DETACH_HOST = "取消关联宿主机" ACT_VM_IO_THROTTLE = "虚拟机磁盘限速" + ACT_VM_RESET = "虚拟机回滚快照" + ACT_VM_SNAPSHOT_AND_CLONE = "虚拟机快照并克隆" ACT_IMAGE_SAVE = "上传镜像" ACT_IMAGE_PROBE = "镜像检测" diff --git a/pkg/util/qemuimg/qemuimg.go b/pkg/util/qemuimg/qemuimg.go index 64212c7f0a..bc0f32e614 100644 --- a/pkg/util/qemuimg/qemuimg.go +++ b/pkg/util/qemuimg/qemuimg.go @@ -404,9 +404,9 @@ func (img *SQemuImage) create(sizeMB int, format TImageFormat, options []string) args = append(args, fmt.Sprintf("%dM", sizeMB)) } cmd := exec.Command("ionice", args...) - err := cmd.Run() + output, err := cmd.Output() if err != nil { - log.Errorf("create error %s", err) + log.Errorf("%v create error %s %s", args, output, err) return err } return img.parse()