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.
102 changes: 102 additions & 0 deletions internal/qemuimg/qemuimg.go

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.

qemu-img CLI를 감싸는 패키지입니다.

  • Runner interface를 통해서 각 메서드를 qemu-img를 직접 호출하지 않게 합니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package qemuimg

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

// Runner abstracts qemu-img command execution for testability.
type Runner interface {
Create(backingFile, backingFormat, overlayPath string) error
// ReplaceOverlay removes an existing file at overlayPath if present, then creates a fresh overlay.
ReplaceOverlay(backingFile, backingFormat, overlayPath string) error
// RemoveOverlay deletes the overlay file at path after verifying it resides under baseDir.
// Returns nil if the file does not exist.
RemoveOverlay(baseDir, path string) error
Info(diskPath string) (backingFile, backingFormat string, err error)
// Convert flattens the full backing chain of src into a new standalone dst file.
// Unlike Commit, it never writes to any backing file so shared base images stay untouched.
Convert(src, dst string) error
// Commit writes all changes recorded in overlay into base, collapsing every
// intermediate layer between them. Only base needs a write lock; files above
// base in the chain are read-only during the operation.
Commit(overlay, base string) error
}

type real struct{}

func New() Runner {
return &real{}
}

func (q *real) Create(backingFile, backingFormat, overlayPath string) error {
args := []string{"create", "-f", "qcow2", "-b", backingFile}
if backingFormat != "" {
args = append(args, "-F", backingFormat)
}
args = append(args, overlayPath)

out, err := exec.Command("qemu-img", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("qemu-img create failed: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}

func (q *real) RemoveOverlay(baseDir, path string) error {
cleanBase := filepath.Clean(baseDir)
cleanPath := filepath.Clean(path)
if !strings.HasPrefix(cleanPath, cleanBase+string(filepath.Separator)) {
return fmt.Errorf("path %s is outside base directory %s", path, baseDir)
}
if err := os.Remove(cleanPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove overlay %s: %w", path, err)
}
return nil
}

func (q *real) ReplaceOverlay(backingFile, backingFormat, overlayPath string) error {
if err := os.Remove(overlayPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove stale overlay %s: %w", overlayPath, err)
}
return q.Create(backingFile, backingFormat, overlayPath)
}

type infoResult struct {
BackingFilename string `json:"backing-filename"`
BackingFileFormat string `json:"backing-filename-format"`
}

func (q *real) Info(diskPath string) (backingFile, backingFormat string, err error) {
out, execErr := exec.Command("qemu-img", "info", "--output=json", diskPath).CombinedOutput()
if execErr != nil {
return "", "", fmt.Errorf("qemu-img info failed: %w: %s", execErr, strings.TrimSpace(string(out)))
}

var result infoResult
if jsonErr := json.Unmarshal(out, &result); jsonErr != nil {
return "", "", fmt.Errorf("failed to parse qemu-img info output: %w", jsonErr)
}

return result.BackingFilename, result.BackingFileFormat, nil
}

func (q *real) Convert(src, dst string) error {
out, err := exec.Command("qemu-img", "convert", "-f", "qcow2", "-O", "qcow2", src, dst).CombinedOutput()
if err != nil {
return fmt.Errorf("qemu-img convert failed: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}

func (q *real) Commit(overlay, base string) error {
out, err := exec.Command("qemu-img", "commit", "-b", base, overlay).CombinedOutput()
if err != nil {
return fmt.Errorf("qemu-img commit failed: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
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
Loading
Loading