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 = ""
- }
-
- targetXML := fmt.Sprintf(""
-
- return fmt.Sprintf("%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 = ""
+ }
+
+ targetXML := fmt.Sprintf(""
+
+ return fmt.Sprintf("%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))
-}