Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions api/Snapshot/Snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ func (h *Handler) CreateExternalSnapshot(w http.ResponseWriter, r *http.Request)
snapName, err := externalsnapshot.CreateExternalSnapshot(dom, param.Name, &externalsnapshot.ExternalSnapshotOptions{
BaseDir: param.BaseDir,
Description: param.Description,
Quiesce: param.Quiesce,
Live: param.Live,
})
if err != nil {
resp.ResponseWriteErr(w, err, http.StatusInternalServerError)
Expand Down Expand Up @@ -259,6 +257,40 @@ func (h *Handler) RevertSnapshot(w http.ResponseWriter, r *http.Request) {
resp.ResponseWriteOK(w, nil)
}

func (h *Handler) DeleteExternalSnapshot(w http.ResponseWriter, r *http.Request) {
param := &ExternalSnapshotRequest{}
resp := httputil.ResponseGen[any]("Delete External Snapshot")

if err := httputil.HttpDecoder(r, param); err != nil {
resp.ResponseWriteErr(w, err, http.StatusBadRequest)
h.Logger.Error("external snapshot delete decode failed", zap.Error(err))
return
}

if param.Name == "" {
resp.ResponseWriteErr(w, virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("snapshot name required")), http.StatusBadRequest)
return
}

h.Logger.Info("external snapshot delete start", zap.String("domain_uuid", param.UUID), zap.String("snapshot_name", param.Name))

dom, err := h.DomainControl.GetDomain(param.UUID)
if err != nil {
resp.ResponseWriteErr(w, err, http.StatusInternalServerError)
h.Logger.Error("external snapshot delete failed - domain not found", zap.String("domain_uuid", param.UUID), zap.Error(err))
return
}

if err := externalsnapshot.DeleteExternalSnapshot(dom, param.Name); err != nil {
resp.ResponseWriteErr(w, err, http.StatusInternalServerError)
h.Logger.Error("external snapshot delete failed", zap.String("domain_uuid", param.UUID), zap.String("snapshot_name", param.Name), zap.Error(err))
return
}

h.Logger.Info("external snapshot delete success", zap.String("domain_uuid", param.UUID), zap.String("snapshot_name", param.Name))
resp.ResponseWriteOK(w, nil)
}

func (h *Handler) DeleteSnapshot(w http.ResponseWriter, r *http.Request) {
param := &SnapshotRequest{}
resp := httputil.ResponseGen[any]("Delete Snapshot")
Expand Down
Binary file added cmd/doddle/doddle
Binary file not shown.
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func InitServer(portNum int, h Handlers, logger *zap.Logger) {
mux.HandleFunc("POST /RevertSnapshot", h.Snapshot.RevertSnapshot)
mux.HandleFunc("POST /RevertExternalSnapshot", h.Snapshot.RevertExternalSnapshot)
mux.HandleFunc("POST /MergeExternalSnapshot", h.Snapshot.MergeExternalSnapshot)
mux.HandleFunc("POST /DeleteExternalSnapshot", h.Snapshot.DeleteExternalSnapshot)
mux.HandleFunc("POST /DeleteSnapshot", h.Snapshot.DeleteSnapshot)

mux.HandleFunc("GET /metrics", h.Metric.DefaultMetric().ServeHTTP)
Expand Down
31 changes: 4 additions & 27 deletions internal/snapshotlibvirt/external_snapshot_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,13 @@ package snapshotlibvirt

import "libvirt.org/go/libvirt"

type ExternalSnapshotCreateOptions struct {
Live bool
Quiesce bool
Atomic bool
}

func CreateExternalSnapshot(domain *libvirt.Domain, snapshotXML string, opts ExternalSnapshotCreateOptions) (*libvirt.DomainSnapshot, error) {
flags := libvirt.DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
if opts.Live {
flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_LIVE
}
if opts.Quiesce {
flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_QUIESCE
}
if opts.Atomic {
flags |= libvirt.DOMAIN_SNAPSHOT_CREATE_ATOMIC
}

// RegisterExternalSnapshot registers an external snapshot using pre-existing overlay files
// created by qemu-img. REUSE_EXT tells libvirt to reuse the files rather than creating new ones.
func RegisterExternalSnapshot(domain *libvirt.Domain, snapshotXML string) (*libvirt.DomainSnapshot, error) {
flags := libvirt.DOMAIN_SNAPSHOT_CREATE_DISK_ONLY | libvirt.DOMAIN_SNAPSHOT_CREATE_REUSE_EXT
return domain.CreateSnapshotXML(snapshotXML, flags)
}

func StartBlockCommit(domain *libvirt.Domain, disk, base, top string) error {
flags := libvirt.DOMAIN_BLOCK_COMMIT_ACTIVE | libvirt.DOMAIN_BLOCK_COMMIT_DELETE
return domain.BlockCommit(disk, base, top, 0, flags)
}

func AbortBlockJobPivot(domain *libvirt.Domain, disk string) error {
return domain.BlockJobAbort(disk, libvirt.DOMAIN_BLOCK_JOB_ABORT_PIVOT)
}

func UpdateDeviceConfig(domain *libvirt.Domain, deviceXML string) error {
return domain.UpdateDeviceFlags(deviceXML, libvirt.DOMAIN_DEVICE_MODIFY_CONFIG)
}
6 changes: 5 additions & 1 deletion internal/snapshotlibvirt/snapshot_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
"libvirt.org/go/libvirt"
)

// metadataOnlyFlag tells libvirt to remove only the snapshot metadata record,
// not the external overlay files on disk. We always manage overlay files ourselves.
const metadataOnlyFlag = libvirt.DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY

type LibvirtSnapshotHandle struct {
snapshot *libvirt.DomainSnapshot
}
Expand All @@ -25,7 +29,7 @@ func (s *LibvirtSnapshotHandle) Delete() error {
if s == nil || s.snapshot == nil {
return fmt.Errorf("nil snapshot")
}
return s.snapshot.Delete(0)
return s.snapshot.Delete(metadataOnlyFlag)
}

func (s *LibvirtSnapshotHandle) Revert() error {
Expand Down
35 changes: 16 additions & 19 deletions services/snapshot/external_snap/create_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ func CreateExternalSnapshot(domain *domCon.Domain, name string, opts *ExternalSn
return "", virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain"))
}

active, err := domain.Domain.IsActive()
domainActive := err == nil && active

xmlDesc, err := domain.Domain.GetXMLDesc(0)
if err != nil {
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to get domain xml: %w", err))
Expand All @@ -28,10 +25,10 @@ func CreateExternalSnapshot(domain *domCon.Domain, name string, opts *ExternalSn
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to resolve domain uuid: %w", err))
}

return createExternalSnapshot(newExternalSnapshotDomain(domain.Domain), domainActive, domainUUID, xmlDesc, name, opts)
return createExternalSnapshot(newExternalSnapshotDomain(domain.Domain), newQemuImg(), domainUUID, xmlDesc, name, opts)
}

func createExternalSnapshot(domain SnapshotDomain, domainActive bool, domainUUID, xmlDesc, name string, opts *ExternalSnapshotOptions) (string, error) {
func createExternalSnapshot(domain SnapshotDomain, qimg QemuImg, domainUUID, xmlDesc, name string, opts *ExternalSnapshotOptions) (string, error) {
if domain == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateExternalSnapshot 에서 데이터를 검증하는 부분에 중복이 있는 것 같습니다.
이름,도메인 검증등
또한 지금은 Create(외부) 와 create(내부)가 강하게 엮여있어서 함수를 분리하는 것이 괜찮을까 생각이듭니다 :)

검증(외부) -> 생성(내부) 처럼 역할을 분리시키면 유지하기 쉬워질거 같아요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검증용 함수를 따로 구현해 분리시키도록 하겠습니다.

return "", virerr.ErrorGen(virerr.InvalidParameter, fmt.Errorf("nil domain"))
}
Expand Down Expand Up @@ -61,17 +58,25 @@ func createExternalSnapshot(domain SnapshotDomain, domainActive bool, domainUUID

snapDisks := make([]snapshotDisk, 0, len(disks))
for _, d := range disks {
overlayPath := filepath.Join(snapshotDir, fmt.Sprintf("%s.qcow2", d.TargetDev))

backingFormat := d.Driver
if backingFormat == "" {
backingFormat = "qcow2"
}
if err := qimg.Create(d.Source, backingFormat, overlayPath); err != nil {
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to create overlay for disk %s: %w", d.TargetDev, err))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 서비스 함수 내부에서 데이터 검증, 파싱, 디렉토리 생성, qemu 이미지 생성, xml 생성 및 도메인 업데이트의 모든 로직이 많은 경우 직접호출로 진행되고 있는 것 같습니다.

실패시 롤백이나, 함수 수정, 테스트 작성이 쉽지 않을 것 같아요.
흐름에는 문제가 없는 것 같은데 역할별로 분리 시킬 수 있으면 더 좋을것 같습니다

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},
Source: &snapshotSource{File: overlayPath},
})
}

Expand All @@ -91,23 +96,15 @@ func createExternalSnapshot(domain SnapshotDomain, domainActive bool, domainUUID
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to build snapshot xml: %w", err))
}

live := opts != nil && opts.Live && domainActive

createOpts := externalSnapshotCreateExecOptions{
Live: live,
Quiesce: opts != nil && opts.Quiesce,
Atomic: len(disks) > 1,
}

snap, err := domain.CreateExternalSnapshot(string(snapBytes), createOpts)
snap, err := domain.RegisterExternalSnapshot(string(snapBytes))
if err != nil {
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to create external snapshot: %w", err))
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to register external snapshot: %w", err))
}
defer snap.Free()

snapName, err := snap.Name()
if err != nil {
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot created but failed to read name: %w", err))
return "", virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot registered but failed to read name: %w", err))
}

return snapName, nil
Expand Down
19 changes: 18 additions & 1 deletion services/snapshot/external_snap/delete_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package external

import (
"fmt"
"os"

domCon "github.com/easy-cloud-Knet/KWS_Core/DomCon"
virerr "github.com/easy-cloud-Knet/KWS_Core/internal/error"
Expand Down Expand Up @@ -33,14 +34,30 @@ func deleteExternalSnapshot(domain SnapshotDomain, snapName string) error {
if err != nil {
return err
}

if target == nil {
return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("snapshot %s not found", snapName))
}

// Collect overlay file paths before removing libvirt metadata.
overlayFiles, err := extractExternalSnapshotSources(target)
if err != nil {
return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to extract snapshot sources: %w", err))
}

if err := target.Delete(); err != nil {
return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to delete external snapshot %s: %w", snapName, err))
}

// Remove overlay files from disk. Libvirt does not delete external overlay
// files automatically, so we handle it here.
for _, path := range overlayFiles {
if path == "" {
continue
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return virerr.ErrorGen(virerr.SnapshotError, fmt.Errorf("failed to remove overlay file %s: %w", path, err))
}
}

return nil
}
53 changes: 3 additions & 50 deletions services/snapshot/external_snap/domain_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,11 @@ import (
)

type SnapshotDomain interface {
CreateExternalSnapshot(snapshotXML string, opts externalSnapshotCreateExecOptions) (SnapshotHandle, error)
RegisterExternalSnapshot(snapshotXML string) (SnapshotHandle, error)
ListAllSnapshots() ([]SnapshotHandle, error)
StartBlockCommit(disk, base, top string) error
BlockJobInfo(disk string) (externalBlockJobInfo, error)
AbortBlockJobPivot(disk string) error
UpdateDeviceConfig(deviceXML string) error
}

type externalBlockJobInfo struct {
Cur uint64
End uint64
}

type externalSnapshotCreateExecOptions struct {
Live bool
Quiesce bool
Atomic bool
}

type SnapshotHandle interface {
Name() (string, error)
XMLDesc() (string, error)
Expand All @@ -42,16 +28,12 @@ func newExternalSnapshotDomain(domain *libvirt.Domain) SnapshotDomain {
return &libvirtExternalSnapshotDomain{domain: domain}
}

func (d *libvirtExternalSnapshotDomain) CreateExternalSnapshot(snapshotXML string, opts externalSnapshotCreateExecOptions) (SnapshotHandle, error) {
func (d *libvirtExternalSnapshotDomain) RegisterExternalSnapshot(snapshotXML string) (SnapshotHandle, error) {
if d == nil || d.domain == nil {
return nil, fmt.Errorf("nil domain")
}

snapshot, err := snapshotlibvirt.CreateExternalSnapshot(d.domain, snapshotXML, snapshotlibvirt.ExternalSnapshotCreateOptions{
Live: opts.Live,
Quiesce: opts.Quiesce,
Atomic: opts.Atomic,
})
snapshot, err := snapshotlibvirt.RegisterExternalSnapshot(d.domain, snapshotXML)
if err != nil {
return nil, err
}
Expand All @@ -77,35 +59,6 @@ func (d *libvirtExternalSnapshotDomain) ListAllSnapshots() ([]SnapshotHandle, er
return handles, nil
}

func (d *libvirtExternalSnapshotDomain) StartBlockCommit(disk, base, top string) error {
if d == nil || d.domain == nil {
return fmt.Errorf("nil domain")
}

return snapshotlibvirt.StartBlockCommit(d.domain, disk, base, top)
}

func (d *libvirtExternalSnapshotDomain) BlockJobInfo(disk string) (externalBlockJobInfo, error) {
if d == nil || d.domain == nil {
return externalBlockJobInfo{}, fmt.Errorf("nil domain")
}

job, err := d.domain.GetBlockJobInfo(disk, 0)
if err != nil {
return externalBlockJobInfo{}, err
}

return externalBlockJobInfo{Cur: job.Cur, End: job.End}, nil
}

func (d *libvirtExternalSnapshotDomain) AbortBlockJobPivot(disk string) error {
if d == nil || d.domain == nil {
return fmt.Errorf("nil domain")
}

return snapshotlibvirt.AbortBlockJobPivot(d.domain, disk)
}

func (d *libvirtExternalSnapshotDomain) UpdateDeviceConfig(deviceXML string) error {
if d == nil || d.domain == nil {
return fmt.Errorf("nil domain")
Expand Down
Loading
Loading