diff --git a/api/snapshot.go b/api/snapshot.go index aaa5e74..01700ad 100644 --- a/api/snapshot.go +++ b/api/snapshot.go @@ -6,7 +6,8 @@ import ( virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" httputil "github.com/easy-cloud-Knet/KWS_Core/pkg/httputil" - snapshotpkg "github.com/easy-cloud-Knet/KWS_Core/services/snapshot" + externalsnapshot "github.com/easy-cloud-Knet/KWS_Core/services/snapshot/external_snap" + internalsnapshot "github.com/easy-cloud-Knet/KWS_Core/services/snapshot/internal_snap" "go.uber.org/zap" ) @@ -37,6 +38,16 @@ type ExternalSnapshotListResponse struct { SnapNames []string `json:"SnapNames"` } +type ExternalSnapshotMergeRequest struct { + UUID string `json:"UUID"` + Disk string `json:"Disk,omitempty"` +} + +type ExternalSnapshotMergeResponse struct { + UUID string `json:"UUID"` + MergedDisks []string `json:"MergedDisks"` +} + // CreateSnapshot creates a snapshot for the specified domain UUID func (i *InstHandler) CreateSnapshot(w http.ResponseWriter, r *http.Request) { param := &SnapshotRequest{} @@ -63,7 +74,7 @@ func (i *InstHandler) CreateSnapshot(w http.ResponseWriter, r *http.Request) { return } - snapName, err := snapshotpkg.CreateSnapshot(dom, name, &snapshotpkg.SnapshotOptions{ + snapName, err := internalsnapshot.CreateSnapshot(dom, name, &internalsnapshot.SnapshotOptions{ Description: param.Description, Quiesce: param.Quiesce, }) @@ -104,7 +115,7 @@ func (i *InstHandler) CreateExternalSnapshot(w http.ResponseWriter, r *http.Requ } snapName := name - createdName, err := snapshotpkg.CreateExternalSnapshot(dom, name, &snapshotpkg.ExternalSnapshotOptions{ + createdName, err := externalsnapshot.CreateExternalSnapshot(dom, name, &externalsnapshot.ExternalSnapshotOptions{ BaseDir: param.BaseDir, Description: param.Description, Quiesce: param.Quiesce, @@ -144,7 +155,7 @@ func (i *InstHandler) ListExternalSnapshots(w http.ResponseWriter, r *http.Reque return } - names, err := snapshotpkg.ListExternalSnapshots(dom) + names, err := externalsnapshot.ListExternalSnapshots(dom) if err != nil { resp.ResponseWriteErr(w, err, http.StatusInternalServerError) i.Logger.Error("external snapshot list failed", zap.String("domain_uuid", param.UUID), zap.Error(err)) @@ -183,7 +194,7 @@ func (i *InstHandler) RevertExternalSnapshot(w http.ResponseWriter, r *http.Requ return } - if err := snapshotpkg.RevertExternalSnapshot(dom, param.Name); err != nil { + if err := externalsnapshot.RevertExternalSnapshot(dom, param.Name); err != nil { resp.ResponseWriteErr(w, err, http.StatusInternalServerError) i.Logger.Error("external snapshot revert failed", zap.String("domain_uuid", param.UUID), zap.String("snapshot_name", param.Name), zap.Error(err)) return @@ -196,6 +207,40 @@ func (i *InstHandler) RevertExternalSnapshot(w http.ResponseWriter, r *http.Requ }) } +// MergeExternalSnapshot merges active external snapshot layers into backing files. +func (i *InstHandler) MergeExternalSnapshot(w http.ResponseWriter, r *http.Request) { + param := &ExternalSnapshotMergeRequest{} + resp := httputil.ResponseGen[ExternalSnapshotMergeResponse]("Merge External Snapshot") + + if err := httputil.HttpDecoder(r, param); err != nil { + resp.ResponseWriteErr(w, err, http.StatusInternalServerError) + i.Logger.Error("external snapshot merge decode failed", zap.Error(err)) + return + } + + i.Logger.Info("external snapshot merge start", zap.String("domain_uuid", param.UUID), zap.String("disk", param.Disk)) + + dom, err := i.DomainControl.GetDomain(param.UUID) + if err != nil { + resp.ResponseWriteErr(w, err, http.StatusInternalServerError) + i.Logger.Error("external snapshot merge failed - domain not found", zap.String("domain_uuid", param.UUID), zap.Error(err)) + return + } + + mergedDisks, err := externalsnapshot.MergeExternalSnapshot(dom, param.Disk) + if err != nil { + resp.ResponseWriteErr(w, err, http.StatusInternalServerError) + i.Logger.Error("external snapshot merge failed", zap.String("domain_uuid", param.UUID), zap.String("disk", param.Disk), zap.Error(err)) + return + } + + i.Logger.Info("external snapshot merge success", zap.String("domain_uuid", param.UUID), zap.String("disk", param.Disk), zap.Int("merged_disk_count", len(mergedDisks))) + resp.ResponseWriteOK(w, &ExternalSnapshotMergeResponse{ + UUID: param.UUID, + MergedDisks: mergedDisks, + }) +} + // ListSnapshots returns all snapshot names for the specified domain UUID func (i *InstHandler) ListSnapshots(w http.ResponseWriter, r *http.Request) { param := &SnapshotRequest{} @@ -216,7 +261,7 @@ func (i *InstHandler) ListSnapshots(w http.ResponseWriter, r *http.Request) { return } - names, err := snapshotpkg.ListSnapshots(dom) + names, err := internalsnapshot.ListSnapshots(dom) if err != nil { resp.ResponseWriteErr(w, err, http.StatusInternalServerError) i.Logger.Error("snapshot list failed", zap.String("uuid", param.UUID), zap.Error(err)) @@ -252,7 +297,7 @@ func (i *InstHandler) RevertSnapshot(w http.ResponseWriter, r *http.Request) { return } - if err := snapshotpkg.RevertToSnapshot(dom, param.Name); err != nil { + if err := internalsnapshot.RevertToSnapshot(dom, param.Name); err != nil { resp.ResponseWriteErr(w, err, http.StatusInternalServerError) i.Logger.Error("snapshot revert failed", zap.String("uuid", param.UUID), zap.String("snapshot_name", param.Name), zap.Error(err)) return @@ -287,7 +332,7 @@ func (i *InstHandler) DeleteSnapshot(w http.ResponseWriter, r *http.Request) { return } - if err := snapshotpkg.DeleteSnapshot(dom, param.Name); err != nil { + if err := internalsnapshot.DeleteSnapshot(dom, param.Name); err != nil { resp.ResponseWriteErr(w, err, http.StatusInternalServerError) i.Logger.Error("snapshot delete failed", zap.String("uuid", param.UUID), zap.String("snapshot_name", param.Name), zap.Error(err)) return diff --git a/doddle b/doddle new file mode 100755 index 0000000..e4f6520 Binary files /dev/null and b/doddle differ diff --git a/internal/server/server.go b/internal/server/server.go index cd1669b..50e6b5f 100755 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -30,6 +30,7 @@ func InitServer(portNum int, libvirtInst *api.InstHandler, logger *zap.Logger) { mux.HandleFunc("GET /ListExternalSnapshots", libvirtInst.ListExternalSnapshots) mux.HandleFunc("POST /RevertSnapshot", libvirtInst.RevertSnapshot) mux.HandleFunc("POST /RevertExternalSnapshot", libvirtInst.RevertExternalSnapshot) + mux.HandleFunc("POST /MergeExternalSnapshot", libvirtInst.MergeExternalSnapshot) mux.HandleFunc("POST /DeleteSnapshot", libvirtInst.DeleteSnapshot) libvirtHandler := middleware.LibvirtMiddleware(libvirtInst.IsConnected, logger)(mux) diff --git a/services/snapshot/external.go b/services/snapshot/external.go deleted file mode 100644 index e63c1ad..0000000 --- a/services/snapshot/external.go +++ /dev/null @@ -1,285 +0,0 @@ -package snapshot - -import ( - "encoding/xml" - "fmt" - "path/filepath" - "strings" - - domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" - "libvirt.org/go/libvirt" -) - -func CreateExternalSnapshot(domain *domCon.Domain, name string, opts *ExternalSnapshotOptions) (string, error) { - if domain == nil || domain.Domain == nil { - return "", fmt.Errorf("nil domain") - } - - if name == "" { - return "", fmt.Errorf("snapshot name required") - } - - if !isSafeSnapshotName(name) { - return "", fmt.Errorf("invalid snapshot name") - } - - disks, err := listFileDisks(domain) - if err != nil { - return "", err - } - if len(disks) == 0 { - return "", fmt.Errorf("no file-based disks found for snapshot") - } - - snapDisks := make([]snapshotDisk, 0, len(disks)) - for _, d := range disks { - var driver *snapshotDriver - if d.Driver != "" { - driver = &snapshotDriver{Type: d.Driver} - } - - snapDisks = append(snapDisks, snapshotDisk{ - Name: d.TargetDev, - Snapshot: "external", - Driver: driver, - }) - } - - description := "external snapshot created by KWS" - if opts != nil && opts.Description != "" { - description = opts.Description - } - - snapXML := snapshotXML{ - Name: name, - Description: description, - Disks: snapshotDisks{Disks: snapDisks}, - } - - snapBytes, err := xml.Marshal(snapXML) - if err != nil { - return "", fmt.Errorf("failed to build snapshot xml: %w", err) - } - - flags := libvirt.DOMAIN_SNAPSHOT_CREATE_DISK_ONLY - active, err := domain.Domain.IsActive() - if opts != nil && opts.Live && err == nil && active { - flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_LIVE - } - if opts != nil && opts.Quiesce { - flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE - } - if len(disks) > 1 { - flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_ATOMIC - } - - snap, err := domain.Domain.CreateSnapshotXML(string(snapBytes), flags) - if err != nil { - return "", fmt.Errorf("failed to create external snapshot: %w", err) - } - defer snap.Free() - - snapName, err := snap.GetName() - if err != nil { - return "", fmt.Errorf("snapshot created but failed to read name: %w", err) - } - - return snapName, nil -} - -// ListExternalSnapshots lists only external snapshot names for the domain. -func ListExternalSnapshots(domain *domCon.Domain) ([]string, error) { - if domain == nil || domain.Domain == nil { - return nil, fmt.Errorf("nil domain") - } - - snaps, err := domain.Domain.ListAllSnapshots(0) - if err != nil { - return nil, fmt.Errorf("failed to list snapshots: %w", err) - } - - names := make([]string, 0, len(snaps)) - for _, s := range snaps { - isExternal, err := isExternalSnapshot(&s) - if err == nil && isExternal { - name, err := s.GetName() - if err == nil { - names = append(names, name) - } - } - s.Free() - } - - return names, nil -} - -// RevertExternalSnapshot reverts the domain to an external snapshot. -func RevertExternalSnapshot(domain *domCon.Domain, snapName string) error { - if domain == nil || domain.Domain == nil { - return fmt.Errorf("nil domain") - } - - active, err := domain.Domain.IsActive() - if err == nil && active { - return fmt.Errorf("external snapshot revert requires the domain to be shut down") - } - - snaps, err := domain.Domain.ListAllSnapshots(0) - if err != nil { - return fmt.Errorf("failed to list snapshots: %w", err) - } - defer func() { - for _, s := range snaps { - s.Free() - } - }() - - var target *libvirt.DomainSnapshot - for i := range snaps { - name, err := snaps[i].GetName() - if err != nil || name != snapName { - continue - } - isExternal, err := isExternalSnapshot(&snaps[i]) - if err != nil { - return err - } - if !isExternal { - return fmt.Errorf("snapshot %s is not external", snapName) - } - target = &snaps[i] - break - } - - if target == nil { - return fmt.Errorf("snapshot %s not found", snapName) - } - - disks, err := listFileDisks(domain) - if err != nil { - return err - } - - updated := false - for _, d := range disks { - if d.BackingSource == "" { - continue - } - diskXML := buildDiskDeviceXML(d, d.BackingSource) - if err := domain.Domain.UpdateDeviceFlags(diskXML, libvirt.DOMAIN_DEVICE_MODIFY_CONFIG); err != nil { - return fmt.Errorf("failed to update disk %s: %w", d.TargetDev, err) - } - updated = true - } - - if !updated { - return fmt.Errorf("no backingStore entries found to restore") - } - - return nil -} - -func listFileDisks(domain *domCon.Domain) ([]diskInfo, error) { - xmlDesc, err := domain.Domain.GetXMLDesc(0) - if err != nil { - return nil, fmt.Errorf("failed to get domain xml: %w", err) - } - - var doc domainXML - if err := xml.Unmarshal([]byte(xmlDesc), &doc); err != nil { - return nil, fmt.Errorf("failed to parse domain xml: %w", err) - } - - out := make([]diskInfo, 0, len(doc.Devices.Disks)) - for _, d := range doc.Devices.Disks { - if d.Device != "disk" || d.Type != "file" { - continue - } - if d.Source == nil || d.Target == nil || d.Target.Dev == "" || d.Source.File == "" { - continue - } - driverType := "" - driverName := "" - if d.Driver != nil { - driverType = d.Driver.Type - driverName = d.Driver.Name - } - backingSource := "" - if d.BackingStore != nil && d.BackingStore.Source != nil { - backingSource = d.BackingStore.Source.File - } - out = append(out, diskInfo{ - TargetDev: d.Target.Dev, - TargetBus: d.Target.Bus, - Source: d.Source.File, - BackingSource: backingSource, - Driver: driverType, - DriverName: driverName, - }) - } - - return out, nil -} - -func buildDiskDeviceXML(info diskInfo, source string) string { - driverXML := "" - if info.Driver != "" || info.DriverName != "" { - driverXML = "%s%s", driverXML, source, targetXML) -} - -func isExternalSnapshot(snapshot *libvirt.DomainSnapshot) (bool, error) { - if snapshot == nil { - return false, fmt.Errorf("nil snapshot") - } - - xmlDesc, err := snapshot.GetXMLDesc(0) - if err != nil { - return false, fmt.Errorf("failed to get snapshot xml: %w", err) - } - - var doc snapshotXML - if err := xml.Unmarshal([]byte(xmlDesc), &doc); err != nil { - return false, fmt.Errorf("failed to parse snapshot xml: %w", err) - } - - for _, d := range doc.Disks.Disks { - if strings.EqualFold(d.Snapshot, "external") { - return true, nil - } - } - - return false, nil -} - -func isSafeSnapshotName(name string) bool { - if name == "" { - return false - } - clean := filepath.Clean(name) - if clean != name { - return false - } - if strings.Contains(name, "..") { - return false - } - if strings.ContainsAny(name, `/\\`) { - return false - } - return true -} diff --git a/services/snapshot/external_snap/create_ext.go b/services/snapshot/external_snap/create_ext.go new file mode 100644 index 0000000..ad5b0f8 --- /dev/null +++ b/services/snapshot/external_snap/create_ext.go @@ -0,0 +1,102 @@ +package external + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + "libvirt.org/go/libvirt" +) + +func CreateExternalSnapshot(domain *domCon.Domain, name string, opts *ExternalSnapshotOptions) (string, error) { + if domain == nil || domain.Domain == nil { + return "", fmt.Errorf("nil domain") + } + if name == "" { + return "", fmt.Errorf("snapshot name required") + } + if !isSafeSnapshotName(name) { + return "", fmt.Errorf("invalid snapshot name") + } + + disks, err := listFileDisks(domain) + if err != nil { + return "", err + } + if len(disks) == 0 { + return "", fmt.Errorf("no file-based disks found for snapshot") + } + + snapshotRoot, err := resolveSnapshotRoot(opts) + if err != nil { + return "", err + } + domainUUID, err := resolveDomainUUID(domain) + if err != nil { + return "", err + } + + snapshotDir := filepath.Join(snapshotRoot, domainUUID, "snapshots", name) + if err := os.MkdirAll(snapshotDir, 0755); err != nil { + return "", fmt.Errorf("failed to create snapshot directory: %w", err) + } + + snapDisks := make([]snapshotDisk, 0, len(disks)) + for _, d := range disks { + var driver *snapshotDriver + if d.Driver != "" { + driver = &snapshotDriver{Type: d.Driver} + } + + snapshotFile := filepath.Join(snapshotDir, fmt.Sprintf("%s.qcow2", d.TargetDev)) + snapDisks = append(snapDisks, snapshotDisk{ + Name: d.TargetDev, + Snapshot: "external", + Driver: driver, + Source: &snapshotSource{File: snapshotFile}, + }) + } + + description := "external snapshot created by KWS" + if opts != nil && opts.Description != "" { + description = opts.Description + } + + snapXML := snapshotXML{ + Name: name, + Description: description, + Disks: snapshotDisks{Disks: snapDisks}, + } + + snapBytes, err := xml.Marshal(snapXML) + if err != nil { + return "", fmt.Errorf("failed to build snapshot xml: %w", err) + } + + flags := libvirt.DOMAIN_SNAPSHOT_CREATE_DISK_ONLY + active, err := domain.Domain.IsActive() + if opts != nil && opts.Live && err == nil && active { + flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_LIVE + } + if opts != nil && opts.Quiesce { + flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE + } + if len(disks) > 1 { + flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_ATOMIC + } + + snap, err := domain.Domain.CreateSnapshotXML(string(snapBytes), flags) + if err != nil { + return "", fmt.Errorf("failed to create external snapshot: %w", err) + } + defer snap.Free() + + snapName, err := snap.GetName() + if err != nil { + return "", fmt.Errorf("snapshot created but failed to read name: %w", err) + } + + return snapName, nil +} diff --git a/services/snapshot/external_snap/delete_ext.go b/services/snapshot/external_snap/delete_ext.go new file mode 100644 index 0000000..1962901 --- /dev/null +++ b/services/snapshot/external_snap/delete_ext.go @@ -0,0 +1,54 @@ +package external + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + "libvirt.org/go/libvirt" +) + +func DeleteExternalSnapshot(domain *domCon.Domain, snapName string) error { + if domain == nil || domain.Domain == nil { + return fmt.Errorf("nil domain") + } + if snapName == "" { + return fmt.Errorf("snapshot name required") + } + + snaps, err := domain.Domain.ListAllSnapshots(0) + if err != nil { + return fmt.Errorf("failed to list snapshots: %w", err) + } + defer func() { + for _, s := range snaps { + s.Free() + } + }() + + var target *libvirt.DomainSnapshot + for i := range snaps { + name, err := snaps[i].GetName() + if err != nil || name != snapName { + continue + } + isExternal, err := isExternalSnapshot(&snaps[i]) + if err != nil { + return err + } + if !isExternal { + return fmt.Errorf("snapshot %s is not external", snapName) + } + target = &snaps[i] + break + } + + if target == nil { + return fmt.Errorf("snapshot %s not found", snapName) + } + + if err := target.Delete(0); err != nil { + return fmt.Errorf("failed to delete external snapshot %s: %w", snapName, err) + } + + return nil +} diff --git a/services/snapshot/external_snap/helpers.go b/services/snapshot/external_snap/helpers.go new file mode 100644 index 0000000..e55c6eb --- /dev/null +++ b/services/snapshot/external_snap/helpers.go @@ -0,0 +1,169 @@ +package external + +import ( + "encoding/xml" + "fmt" + "path/filepath" + "strings" + "time" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + "libvirt.org/go/libvirt" +) + +func waitBlockJobReady(domain *libvirt.Domain, disk string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for { + job, err := domain.GetBlockJobInfo(disk, 0) + if err != nil { + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for block job on disk %s: %w", disk, err) + } + time.Sleep(500 * time.Millisecond) + continue + } + + if job.End > 0 && job.Cur >= job.End { + return nil + } + + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for block commit to complete on disk %s", disk) + } + time.Sleep(500 * time.Millisecond) + } +} + +func listFileDisks(domain *domCon.Domain) ([]diskInfo, error) { + xmlDesc, err := domain.Domain.GetXMLDesc(0) + if err != nil { + return nil, fmt.Errorf("failed to get domain xml: %w", err) + } + + var doc domainXML + if err := xml.Unmarshal([]byte(xmlDesc), &doc); err != nil { + return nil, fmt.Errorf("failed to parse domain xml: %w", err) + } + + out := make([]diskInfo, 0, len(doc.Devices.Disks)) + for _, d := range doc.Devices.Disks { + if d.Device != "disk" || d.Type != "file" { + continue + } + if d.Source == nil || d.Target == nil || d.Target.Dev == "" || d.Source.File == "" { + continue + } + driverType := "" + driverName := "" + if d.Driver != nil { + driverType = d.Driver.Type + driverName = d.Driver.Name + } + backingSource := "" + if d.BackingStore != nil && d.BackingStore.Source != nil { + backingSource = d.BackingStore.Source.File + } + out = append(out, diskInfo{ + TargetDev: d.Target.Dev, + TargetBus: d.Target.Bus, + Source: d.Source.File, + BackingSource: backingSource, + Driver: driverType, + DriverName: driverName, + }) + } + + return out, nil +} + +func buildDiskDeviceXML(info diskInfo, source string) string { + driverXML := "" + if info.Driver != "" || info.DriverName != "" { + driverXML = "%s%s", driverXML, source, targetXML) +} + +func isExternalSnapshot(snapshot *libvirt.DomainSnapshot) (bool, error) { + if snapshot == nil { + return false, fmt.Errorf("nil snapshot") + } + + xmlDesc, err := snapshot.GetXMLDesc(0) + if err != nil { + return false, fmt.Errorf("failed to get snapshot xml: %w", err) + } + + var doc snapshotXML + if err := xml.Unmarshal([]byte(xmlDesc), &doc); err != nil { + return false, fmt.Errorf("failed to parse snapshot xml: %w", err) + } + + for _, d := range doc.Disks.Disks { + if strings.EqualFold(d.Snapshot, "external") { + return true, nil + } + } + + return false, nil +} + +func isSafeSnapshotName(name string) bool { + if name == "" { + return false + } + clean := filepath.Clean(name) + if clean != name { + return false + } + if strings.Contains(name, "..") { + return false + } + if strings.ContainsAny(name, `/\\`) { + return false + } + return true +} + +func extractExternalSnapshotSources(snapshot *libvirt.DomainSnapshot) (map[string]string, error) { + if snapshot == nil { + return nil, fmt.Errorf("nil snapshot") + } + + xmlDesc, err := snapshot.GetXMLDesc(0) + if err != nil { + return nil, fmt.Errorf("failed to get snapshot xml: %w", err) + } + + var doc snapshotXML + if err := xml.Unmarshal([]byte(xmlDesc), &doc); err != nil { + return nil, fmt.Errorf("failed to parse snapshot xml: %w", err) + } + + out := make(map[string]string) + for _, d := range doc.Disks.Disks { + if !strings.EqualFold(d.Snapshot, "external") { + continue + } + if d.Name == "" || d.Source == nil || d.Source.File == "" { + continue + } + out[d.Name] = d.Source.File + } + + return out, nil +} diff --git a/services/snapshot/external_snap/list_ext.go b/services/snapshot/external_snap/list_ext.go new file mode 100644 index 0000000..e239858 --- /dev/null +++ b/services/snapshot/external_snap/list_ext.go @@ -0,0 +1,32 @@ +package external + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" +) + +func ListExternalSnapshots(domain *domCon.Domain) ([]string, error) { + if domain == nil || domain.Domain == nil { + return nil, fmt.Errorf("nil domain") + } + + snaps, err := domain.Domain.ListAllSnapshots(0) + if err != nil { + return nil, fmt.Errorf("failed to list snapshots: %w", err) + } + + names := make([]string, 0, len(snaps)) + for _, s := range snaps { + isExternal, err := isExternalSnapshot(&s) + if err == nil && isExternal { + name, err := s.GetName() + if err == nil { + names = append(names, name) + } + } + s.Free() + } + + return names, nil +} diff --git a/services/snapshot/external_snap/merge_ext.go b/services/snapshot/external_snap/merge_ext.go new file mode 100644 index 0000000..69222f7 --- /dev/null +++ b/services/snapshot/external_snap/merge_ext.go @@ -0,0 +1,70 @@ +package external + +import ( + "fmt" + "time" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + "libvirt.org/go/libvirt" +) + +func MergeExternalSnapshot(domain *domCon.Domain, targetDisk string) ([]string, error) { + if domain == nil || domain.Domain == nil { + return nil, fmt.Errorf("nil domain") + } + + active, err := domain.Domain.IsActive() + if err != nil { + return nil, fmt.Errorf("failed to check domain state: %w", err) + } + if !active { + return nil, fmt.Errorf("external snapshot merge requires the domain to be running") + } + + disks, err := listFileDisks(domain) + if err != nil { + return nil, err + } + + merged := make([]string, 0, len(disks)) + for _, d := range disks { + if targetDisk != "" && d.TargetDev != targetDisk { + continue + } + + backingSource := d.BackingSource + + if backingSource == "" { + continue + } + + flags := libvirt.DOMAIN_BLOCK_COMMIT_ACTIVE | libvirt.DOMAIN_BLOCK_COMMIT_DELETE + if err := domain.Domain.BlockCommit(d.TargetDev, backingSource, d.Source, 0, flags); err != nil { + return nil, fmt.Errorf("failed to start block commit on disk %s: %w", d.TargetDev, err) + } + + if err := waitBlockJobReady(domain.Domain, d.TargetDev, 2*time.Minute); err != nil { + return nil, err + } + + if err := domain.Domain.BlockJobAbort(d.TargetDev, libvirt.DOMAIN_BLOCK_JOB_ABORT_PIVOT); err != nil { + return nil, fmt.Errorf("failed to pivot disk %s after commit: %w", d.TargetDev, err) + } + + diskXML := buildDiskDeviceXML(d, backingSource) + if err := domain.Domain.UpdateDeviceFlags(diskXML, libvirt.DOMAIN_DEVICE_MODIFY_CONFIG); err != nil { + return nil, fmt.Errorf("failed to update disk %s after merge: %w", d.TargetDev, err) + } + + merged = append(merged, d.TargetDev) + } + + if targetDisk != "" && len(merged) == 0 { + return nil, fmt.Errorf("disk %s is not mergeable or has no external backing chain", targetDisk) + } + if len(merged) == 0 { + return nil, fmt.Errorf("no mergeable external snapshot disks found") + } + + return merged, nil +} diff --git a/services/snapshot/external_metadata.go b/services/snapshot/external_snap/metadata.go similarity index 55% rename from services/snapshot/external_metadata.go rename to services/snapshot/external_snap/metadata.go index 25d09d5..4e4de03 100644 --- a/services/snapshot/external_metadata.go +++ b/services/snapshot/external_snap/metadata.go @@ -1,21 +1,15 @@ -package snapshot +package external import ( "fmt" "path/filepath" "strings" + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" "github.com/easy-cloud-Knet/KWS_Core/internal/config" ) -type ExternalSnapshotOptions struct { - BaseDir string - Description string - Quiesce bool - Live bool -} - -func resolveSnapshotRoot(opts *ExternalSnapshotOptions) (string, error) { //nolint:unused +func resolveSnapshotRoot(opts *ExternalSnapshotOptions) (string, error) { if opts == nil || opts.BaseDir == "" { return config.StorageBase, nil } @@ -30,3 +24,16 @@ func resolveSnapshotRoot(opts *ExternalSnapshotOptions) (string, error) { //noli return clean, nil } + +func resolveDomainUUID(domain *domCon.Domain) (string, error) { + if domain == nil || domain.Domain == nil { + return "", fmt.Errorf("nil domain") + } + + uuid, err := domain.Domain.GetUUIDString() + if err != nil { + return "", fmt.Errorf("failed to get domain uuid: %w", err) + } + + return uuid, nil +} diff --git a/services/snapshot/external_snap/revert_ext.go b/services/snapshot/external_snap/revert_ext.go new file mode 100644 index 0000000..9b2d026 --- /dev/null +++ b/services/snapshot/external_snap/revert_ext.go @@ -0,0 +1,82 @@ +package external + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + "libvirt.org/go/libvirt" +) + +func RevertExternalSnapshot(domain *domCon.Domain, snapName string) error { + if domain == nil || domain.Domain == nil { + return fmt.Errorf("nil domain") + } + + active, err := domain.Domain.IsActive() + if err == nil && active { + return fmt.Errorf("external snapshot revert requires the domain to be shut down") + } + + snaps, err := domain.Domain.ListAllSnapshots(0) + if err != nil { + return fmt.Errorf("failed to list snapshots: %w", err) + } + defer func() { + for _, s := range snaps { + s.Free() + } + }() + + var target *libvirt.DomainSnapshot + for i := range snaps { + name, err := snaps[i].GetName() + if err != nil || name != snapName { + continue + } + isExternal, err := isExternalSnapshot(&snaps[i]) + if err != nil { + return err + } + if !isExternal { + return fmt.Errorf("snapshot %s is not external", snapName) + } + target = &snaps[i] + break + } + + if target == nil { + return fmt.Errorf("snapshot %s not found", snapName) + } + + targetSources, err := extractExternalSnapshotSources(target) + if err != nil { + return err + } + if len(targetSources) == 0 { + return fmt.Errorf("snapshot %s has no external disk sources", snapName) + } + + disks, err := listFileDisks(domain) + if err != nil { + return err + } + + updated := false + for _, d := range disks { + targetSource, ok := targetSources[d.TargetDev] + if !ok || targetSource == "" { + continue + } + diskXML := buildDiskDeviceXML(d, targetSource) + if err := domain.Domain.UpdateDeviceFlags(diskXML, libvirt.DOMAIN_DEVICE_MODIFY_CONFIG); err != nil { + return fmt.Errorf("failed to update disk %s: %w", d.TargetDev, err) + } + updated = true + } + + if !updated { + return fmt.Errorf("no backingStore entries found to restore") + } + + return nil +} diff --git a/services/snapshot/type.go b/services/snapshot/external_snap/type.go similarity index 90% rename from services/snapshot/type.go rename to services/snapshot/external_snap/type.go index 01e911e..ba2ae30 100644 --- a/services/snapshot/type.go +++ b/services/snapshot/external_snap/type.go @@ -1,8 +1,15 @@ -package snapshot +package external import "encoding/xml" -// External snapshot XML types (used by external.go). +// ExternalSnapshotOptions defines options for creating external snapshots. +type ExternalSnapshotOptions struct { + BaseDir string + Description string + Quiesce bool + Live bool +} + type domainXML struct { Devices domainDevices `xml:"devices"` } @@ -72,9 +79,3 @@ type snapshotDriver struct { type snapshotSource struct { File string `xml:"file,attr"` } - -// Internal snapshot options (used by operations.go). -type SnapshotOptions struct { - Description string - Quiesce bool -} diff --git a/services/snapshot/internal_snap/create_int.go b/services/snapshot/internal_snap/create_int.go new file mode 100644 index 0000000..d6cae77 --- /dev/null +++ b/services/snapshot/internal_snap/create_int.go @@ -0,0 +1,40 @@ +package internal + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" + "libvirt.org/go/libvirt" +) + +func CreateSnapshot(domain *domCon.Domain, name string, opts *SnapshotOptions) (string, error) { + if domain == nil || domain.Domain == nil { + return "", virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) + } + + description := "snapshot created by KWS" + if opts != nil && opts.Description != "" { + description = opts.Description + } + + snapXML := fmt.Sprintf(`%s%s`, name, description) + + flags := libvirt.DomainSnapshotCreateFlags(0) + if opts != nil && opts.Quiesce { + flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE + } + + snap, err := domain.Domain.CreateSnapshotXML(snapXML, flags) + if err != nil { + return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to create snapshot: %w", err)) + } + defer snap.Free() + + snapName, err := snap.GetName() + if err != nil { + return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot created but failed to read name: %w", err)) + } + + return snapName, nil +} diff --git a/services/snapshot/internal_snap/delete_int.go b/services/snapshot/internal_snap/delete_int.go new file mode 100644 index 0000000..bf9b508 --- /dev/null +++ b/services/snapshot/internal_snap/delete_int.go @@ -0,0 +1,41 @@ +package internal + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" + "libvirt.org/go/libvirt" +) + +func DeleteSnapshot(domain *domCon.Domain, snapName string) error { + if domain == nil || domain.Domain == nil { + return virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) + } + + var listFlags libvirt.DomainSnapshotListFlags + snaps, err := domain.Domain.ListAllSnapshots(listFlags) + if err != nil { + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) + } + defer func() { + for _, s := range snaps { + s.Free() + } + }() + + for _, s := range snaps { + n, err := s.GetName() + if err != nil { + continue + } + if n == snapName { + if err := s.Delete(0); err != nil { + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to delete snapshot %s: %w", snapName, err)) + } + return nil + } + } + + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot %s not found", snapName)) +} diff --git a/services/snapshot/internal_snap/list_int.go b/services/snapshot/internal_snap/list_int.go new file mode 100644 index 0000000..d1517ab --- /dev/null +++ b/services/snapshot/internal_snap/list_int.go @@ -0,0 +1,32 @@ +package internal + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" + "libvirt.org/go/libvirt" +) + +func ListSnapshots(domain *domCon.Domain) ([]string, error) { + if domain == nil || domain.Domain == nil { + return nil, virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) + } + + var listFlags libvirt.DomainSnapshotListFlags + snaps, err := domain.Domain.ListAllSnapshots(listFlags) + if err != nil { + return nil, virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) + } + + names := make([]string, 0, len(snaps)) + for _, s := range snaps { + n, err := s.GetName() + if err == nil { + names = append(names, n) + } + s.Free() + } + + return names, nil +} diff --git a/services/snapshot/internal_snap/revert_int.go b/services/snapshot/internal_snap/revert_int.go new file mode 100644 index 0000000..b292895 --- /dev/null +++ b/services/snapshot/internal_snap/revert_int.go @@ -0,0 +1,48 @@ +package internal + +import ( + "fmt" + + domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" + virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" + "libvirt.org/go/libvirt" +) + +func RevertToSnapshot(domain *domCon.Domain, snapName string) error { + if domain == nil || domain.Domain == nil { + return virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) + } + + var listFlags libvirt.DomainSnapshotListFlags + snaps, err := domain.Domain.ListAllSnapshots(listFlags) + if err != nil { + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) + } + defer func() { + for _, s := range snaps { + s.Free() + } + }() + + var target *libvirt.DomainSnapshot + for i := range snaps { + n, err := snaps[i].GetName() + if err != nil { + continue + } + if n == snapName { + target = &snaps[i] + break + } + } + + if target == nil { + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot %s not found", snapName)) + } + + if err := target.RevertToSnapshot(0); err != nil { + return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to revert to snapshot %s: %w", snapName, err)) + } + + return nil +} diff --git a/services/snapshot/internal_snap/type.go b/services/snapshot/internal_snap/type.go new file mode 100644 index 0000000..9e71fbe --- /dev/null +++ b/services/snapshot/internal_snap/type.go @@ -0,0 +1,7 @@ +package internal + +// SnapshotOptions defines options for creating internal snapshots. +type SnapshotOptions struct { + Description string + Quiesce bool +} diff --git a/services/snapshot/operations.go b/services/snapshot/operations.go deleted file mode 100644 index ec57522..0000000 --- a/services/snapshot/operations.go +++ /dev/null @@ -1,138 +0,0 @@ -package snapshot - -import ( - "fmt" - - domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon" - virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error" - "libvirt.org/go/libvirt" -) - -// CreateSnapshot creates a libvirt snapshot and records basic metadata. -func CreateSnapshot(domain *domCon.Domain, name string, opts *SnapshotOptions) (string, error) { - if domain == nil || domain.Domain == nil { - return "", virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) - } - - description := "snapshot created by KWS" - if opts != nil && opts.Description != "" { - description = opts.Description - } - - snapXML := fmt.Sprintf(`%s%s`, name, description) - - flags := libvirt.DomainSnapshotCreateFlags(0) - if opts != nil && opts.Quiesce { - flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE - } - - snap, err := domain.Domain.CreateSnapshotXML(snapXML, flags) - if err != nil { - return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to create snapshot: %w", err)) - } - defer snap.Free() - - snapName, err := snap.GetName() - if err != nil { - return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot created but failed to read name: %w", err)) - } - - return snapName, nil -} - -// ListSnapshots lists snapshot names for the domain. -func ListSnapshots(domain *domCon.Domain) ([]string, error) { - if domain == nil || domain.Domain == nil { - return nil, virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) - } - - var listFlags libvirt.DomainSnapshotListFlags - snaps, err := domain.Domain.ListAllSnapshots(listFlags) - if err != nil { - return nil, virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) - } - - names := make([]string, 0, len(snaps)) - for _, s := range snaps { - n, err := s.GetName() - if err == nil { - names = append(names, n) - } - s.Free() - } - - return names, nil -} - -// RevertToSnapshot reverts the domain to the given snapshot name. -func RevertToSnapshot(domain *domCon.Domain, snapName string) error { - if domain == nil || domain.Domain == nil { - return virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) - } - - var listFlags libvirt.DomainSnapshotListFlags - snaps, err := domain.Domain.ListAllSnapshots(listFlags) - if err != nil { - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) - } - defer func() { - for _, s := range snaps { - s.Free() - } - }() - - var target *libvirt.DomainSnapshot - for i := range snaps { - n, err := snaps[i].GetName() - if err != nil { - continue - } - if n == snapName { - target = &snaps[i] - break - } - } - - if target == nil { - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot %s not found", snapName)) - } - - if err := target.RevertToSnapshot(0); err != nil { - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to revert to snapshot %s: %w", snapName, err)) - } - - return nil -} - -// DeleteSnapshot deletes a snapshot by name. -func DeleteSnapshot(domain *domCon.Domain, snapName string) error { - if domain == nil || domain.Domain == nil { - return virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain")) - } - - var listFlags libvirt.DomainSnapshotListFlags - snaps, err := domain.Domain.ListAllSnapshots(listFlags) - if err != nil { - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to list snapshots: %w", err)) - } - defer func() { - for _, s := range snaps { - s.Free() - } - }() - - for _, s := range snaps { - n, err := s.GetName() - if err != nil { - continue - } - if n == snapName { - if err := s.Delete(0); err != nil { - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to delete snapshot %s: %w", snapName, err)) - } - return nil - } - } - - return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot %s not found", snapName)) -}