]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
include additional info in snapshot list
[qemu-server.git] / PVE / QemuServer.pm
index 2c6baea95c912f1b1d385a941080bcec85e1efae..8f95bf5341bee7ac1ceb1f806690c48ba66ba10c 100644 (file)
@@ -22,6 +22,7 @@ use Storable qw(dclone);
 use PVE::Exception qw(raise raise_param_exc);
 use PVE::Storage;
 use PVE::Tools qw(run_command lock_file file_read_firstline);
+use PVE::JSONSchema qw(get_standard_option);
 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
 use PVE::INotify;
 use PVE::ProcFSTools;
@@ -33,7 +34,7 @@ my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
 # Note about locking: we use flock on the config file protect
 # against concurent actions.
 # Aditionaly, we have a 'lock' setting in the config file. This
-# can be set to 'migrate' or 'backup'. Most actions are not
+# can be set to 'migrate', 'backup', 'snapshot' or 'rollback'. Most actions are not
 # allowed when such lock is set. But you can ignore this kind of
 # lock with the --skiplock flag.
 
@@ -54,6 +55,12 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', {
     optional => 1,
 });
 
+PVE::JSONSchema::register_standard_option('pve-snapshot-name', {
+    description => "The name of the snapshot.",
+    type => 'string', format => 'pve-configid',
+    maxLength => 40,
+});
+
 #no warnings 'redefine';
 
 unless(defined(&_VZSYSCALLS_H_)) {
@@ -171,7 +178,7 @@ my $confdesc = {
        optional => 1,
        type => 'string',
        description => "Lock/unlock the VM.",
-       enum => [qw(migrate backup)],
+       enum => [qw(migrate backup snapshot rollback)],
     },
     cpulimit => {
        optional => 1,
@@ -287,6 +294,12 @@ EODESC
        description => "Enable/disable ACPI.",
        default => 1,
     },
+    agent => {
+       optional => 1,
+       type => 'boolean',
+       description => "Enable/disable Qemu GuestAgent.",
+       default => 0,
+    },
     kvm => {
        optional => 1,
        type => 'boolean',
@@ -377,6 +390,21 @@ EODESCR
        enum => [ qw(486 athlon pentium pentium2 pentium3 coreduo core2duo kvm32 kvm64 qemu32 qemu64 phenom cpu64-rhel6 cpu64-rhel5 Conroe Penryn Nehalem Westmere Opteron_G1 Opteron_G2 Opteron_G3 host) ],
        default => 'qemu64',
     },
+    parent => get_standard_option('pve-snapshot-name', {
+       optional => 1,
+       description => "Parent snapshot name. This is used internally, and should not be modified.",
+    }),
+    snaptime => {
+       optional => 1,
+       description => "Timestamp for snapshots.",
+       type => 'integer',
+       minimum => 0,
+    },
+    vmstate => {
+       optional => 1,
+       type => 'string', format => 'pve-volume-id',
+       description => "Reference to a volume which stores the VM state. This is used internally for snapshots.",
+    },
 };
 
 # what about other qemu settings ?
@@ -1376,6 +1404,7 @@ sub json_config_properties {
     my $prop = shift;
 
     foreach my $opt (keys %$confdesc) {
+       next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate';
        $prop->{$opt} = $confdesc->{$opt};
     }
 
@@ -1541,6 +1570,7 @@ sub parse_vm_config {
 
     my $res = {
        digest => Digest::SHA::sha1_hex($raw),
+       snapshots => {},
     };
 
     $filename =~ m|/qemu-server/(\d+)\.conf$|
@@ -1548,12 +1578,20 @@ sub parse_vm_config {
 
     my $vmid = $1;
 
+    my $conf = $res;
     my $descr = '';
 
-    while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
-       my $line = $1;
-
+    my @lines = split(/\n/, $raw);
+    foreach my $line (@lines) {
        next if $line =~ m/^\s*$/;
+       
+       if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
+           my $snapname = $1;
+           $conf->{description} = $descr if $descr;
+           $descr = '';
+           $conf = $res->{snapshots}->{$snapname} = {}; 
+           next;
+       }
 
        if ($line =~ m/^\#(.*)\s*$/) {
            $descr .= PVE::Tools::decode_text($1) . "\n";
@@ -1562,10 +1600,12 @@ sub parse_vm_config {
 
        if ($line =~ m/^(description):\s*(.*\S)\s*$/) {
            $descr .= PVE::Tools::decode_text($2);
+       } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
+           $conf->{snapstate} = $1;
        } elsif ($line =~ m/^(args):\s*(.*\S)\s*$/) {
            my $key = $1;
            my $value = $2;
-           $res->{$key} = $value;
+           $conf->{$key} = $value;
        } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S+)\s*$/) {
            my $key = $1;
            my $value = $2;
@@ -1586,21 +1626,17 @@ sub parse_vm_config {
                }
 
                if ($key eq 'cdrom') {
-                   $res->{ide2} = $value;
+                   $conf->{ide2} = $value;
                } else {
-                   $res->{$key} = $value;
+                   $conf->{$key} = $value;
                }
            }
        }
     }
 
-    $res->{description} = $descr if $descr;
+    $conf->{description} = $descr if $descr;
 
-    # convert old smp to sockets
-    if ($res->{smp} && !$res->{sockets}) {
-       $res->{sockets} = $res->{smp};
-    }
-    delete $res->{smp};
+    delete $res->{snapstate}; # just to be sure
 
     return $res;
 }
@@ -1608,6 +1644,8 @@ sub parse_vm_config {
 sub write_vm_config {
     my ($filename, $conf) = @_;
 
+    delete $conf->{snapstate}; # just to be sure
+
     if ($conf->{cdrom}) {
        die "option ide2 conflicts with cdrom\n" if $conf->{ide2};
        $conf->{ide2} = $conf->{cdrom};
@@ -1623,41 +1661,62 @@ sub write_vm_config {
        delete $conf->{smp};
     }
 
-    my $new_volids = {};
-    foreach my $key (keys %$conf) {
-       next if $key eq 'digest' || $key eq 'description';
-       my $value = $conf->{$key};
-       eval { $value = check_type($key, $value); };
-       die "unable to parse value of '$key' - $@" if $@;
+    my $used_volids = {};
+
+    my $cleanup_config = sub {
+       my ($cref) = @_;
 
-       $conf->{$key} = $value;
+       foreach my $key (keys %$cref) {
+           next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots' ||
+               $key eq 'snapstate';
+           my $value = $cref->{$key};
+           eval { $value = check_type($key, $value); };
+           die "unable to parse value of '$key' - $@" if $@;
+
+           $cref->{$key} = $value;
 
-       if (valid_drivename($key)) {
-           my $drive = PVE::QemuServer::parse_drive($key, $value);
-           $new_volids->{$drive->{file}} = 1 if $drive && $drive->{file};
+           if (valid_drivename($key)) {
+               my $drive = PVE::QemuServer::parse_drive($key, $value);
+               $used_volids->{$drive->{file}} = 1 if $drive && $drive->{file};
+           }
        }
+    };
+
+    &$cleanup_config($conf);
+    foreach my $snapname (keys %{$conf->{snapshots}}) {
+       &$cleanup_config($conf->{snapshots}->{$snapname});
     }
 
     # remove 'unusedX' settings if we re-add a volume
     foreach my $key (keys %$conf) {
        my $value = $conf->{$key};
-       if ($key =~ m/^unused/ && $new_volids->{$value}) {
+       if ($key =~ m/^unused/ && $used_volids->{$value}) {
            delete $conf->{$key};
        }
     }
+  
+    my $generate_raw_config = sub {
+       my ($conf) = @_;
 
-    # gererate RAW data
-    my $raw = '';
+       my $raw = '';
 
-    # add description as comment to top of file
-    my $descr = $conf->{description} || '';
-    foreach my $cl (split(/\n/, $descr)) {
-       $raw .= '#' .  PVE::Tools::encode_text($cl) . "\n";
-    }
+       # add description as comment to top of file
+       my $descr = $conf->{description} || '';
+       foreach my $cl (split(/\n/, $descr)) {
+           $raw .= '#' .  PVE::Tools::encode_text($cl) . "\n";
+       }
+
+       foreach my $key (sort keys %$conf) {
+           next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots';
+           $raw .= "$key: $conf->{$key}\n";
+       }
+       return $raw;
+    };
 
-    foreach my $key (sort keys %$conf) {
-       next if $key eq 'digest' || $key eq 'description';
-       $raw .= "$key: $conf->{$key}\n";
+    my $raw = &$generate_raw_config($conf);
+    foreach my $snapname (sort keys %{$conf->{snapshots}}) {
+       $raw .= "\n[$snapname]\n";
+       $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
     }
 
     return $raw;
@@ -2210,6 +2269,15 @@ sub config_to_command {
     #my $soundhw = $conf->{soundhw} || $defaults->{soundhw};
     #push @$cmd, '-soundhw', 'es1370';
     #push @$cmd, '-soundhw', $soundhw if $soundhw;
+
+    if($conf->{agent}) {
+       my $qgasocket = qga_socket($vmid);
+       my $pciaddr = print_pci_addr("qga0", $bridges);
+       push @$devices, '-chardev', "socket,path=$qgasocket,server,nowait,id=qga0";
+       push @$devices, '-device', "virtio-serial,id=qga0$pciaddr";
+       push @$devices, '-device', 'virtserialport,chardev=qga0,name=org.qemu.guest_agent.0';
+    }
+
     $pciaddr = print_pci_addr("balloon0", $bridges);
     push @$devices, '-device', "virtio-balloon-pci,id=balloon0$pciaddr" if $conf->{balloon};
 
@@ -2328,6 +2396,11 @@ sub qmp_socket {
     return "${var_run_tmpdir}/$vmid.qmp";
 }
 
+sub qga_socket {
+    my ($vmid) = @_;
+    return "${var_run_tmpdir}/$vmid.qga";
+}
+
 sub pidfile_name {
     my ($vmid) = @_;
     return "${var_run_tmpdir}/$vmid.pid";
@@ -2733,6 +2806,43 @@ sub qemu_block_resize {
 
 }
 
+sub qemu_volume_snapshot {
+    my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
+
+    my $running = PVE::QemuServer::check_running($vmid);
+
+    return if !PVE::Storage::volume_snapshot($storecfg, $volid, $snap, $running);
+
+    return if !$running;
+
+    vm_mon_cmd($vmid, "snapshot-drive", device => $deviceid, name => $snap);
+
+}
+
+sub qemu_volume_snapshot_delete {
+    my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
+
+    my $running = PVE::QemuServer::check_running($vmid);
+
+    return if !PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
+
+    return if !$running;
+
+    vm_mon_cmd($vmid, "delete-drive-snapshot", device => $deviceid, name => $snap);
+}
+
+sub qga_freezefs {
+    my ($vmid) = @_;
+
+    #need to impplement call to qemu-ga
+}
+
+sub qga_unfreezefs {
+    my ($vmid) = @_;
+
+    #need to impplement call to qemu-ga
+}
+
 sub vm_start {
     my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom) = @_;
 
@@ -2787,9 +2897,11 @@ sub vm_start {
            if ($statefile eq 'tcp') {
                print "migration listens on port $migrate_port\n";
            } else {
-               unlink $statefile;
-               # fixme: send resume - is that necessary ?
-               eval { vm_mon_cmd($vmid, "cont"); };
+               if ($migratedfrom) {
+                   unlink $statefile;
+                   # fixme: send resume - is that necessary ?
+                   eval { vm_mon_cmd($vmid, "cont"); };
+               }
            }
        }
 
@@ -2936,7 +3048,7 @@ sub vm_stop_cleanup {
            PVE::Storage::deactivate_volumes($storecfg, $vollist);
        }
 
-       foreach my $ext (qw(mon qmp pid vnc)) {
+       foreach my $ext (qw(mon qmp pid vnc qga)) {
            unlink "/var/run/qemu-server/${vmid}.$ext";
        }
     };
@@ -3181,6 +3293,7 @@ sub print_pci_addr {
        scsihw0 => { bus => 0, addr => 5 },
        scsihw1 => { bus => 0, addr => 6 },
        ahci0 => { bus => 0, addr => 7 },
+       qga0 => { bus => 0, addr => 8 },
        virtio0 => { bus => 0, addr => 10 },
        virtio1 => { bus => 0, addr => 11 },
        virtio2 => { bus => 0, addr => 12 },
@@ -3439,4 +3552,431 @@ sub restore_archive {
        die "unable to commit configuration file '$conffile'\n";
 };
 
+
+# Internal snapshots
+
+# NOTE: Snapshot create/delete involves several non-atomic
+# action, and can take a long time.
+# So we try to avoid locking the file and use 'lock' variable
+# inside the config file instead.
+
+my $snapshot_copy_config = sub {
+    my ($source, $dest) = @_;
+
+    foreach my $k (keys %$source) {
+       next if $k eq 'snapshots';
+       next if $k eq 'snapstate';
+       next if $k eq 'snaptime';
+       next if $k eq 'vmstate';
+       next if $k eq 'lock';
+       next if $k eq 'digest';
+       next if $k eq 'description';
+       next if $k =~ m/^unused\d+$/;
+               
+       $dest->{$k} = $source->{$k};
+    }
+};
+
+my $snapshot_apply_config = sub {
+    my ($conf, $snap) = @_;
+
+    # copy snapshot list
+    my $newconf = {
+       snapshots => $conf->{snapshots},
+    };
+
+    # keep description and list of unused disks
+    foreach my $k (keys %$conf) {
+       next if !($k =~ m/^unused\d+$/ || $k eq 'description');
+       $newconf->{$k} = $conf->{$k};
+    }
+
+    &$snapshot_copy_config($snap, $newconf);
+
+    return $newconf;
+};
+
+sub foreach_writable_storage {
+    my ($conf, $func) = @_;
+
+    my $sidhash = {};
+
+    foreach my $ds (keys %$conf) {
+       next if !valid_drivename($ds);
+
+       my $drive = parse_drive($ds, $conf->{$ds});
+       next if !$drive;
+       next if drive_is_cdrom($drive);
+
+       my $volid = $drive->{file};
+
+       my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+       $sidhash->{$sid} = $sid if $sid;        
+    }
+
+    foreach my $sid (sort keys %$sidhash) {
+       &$func($sid);
+    }
+}
+
+my $alloc_vmstate_volid = sub {
+    my ($storecfg, $vmid, $conf, $snapname) = @_;
+    
+    # Note: we try to be smart when selecting a $target storage
+
+    my $target;
+
+    # search shared storage first
+    foreach_writable_storage($conf, sub {
+       my ($sid) = @_;
+       my $scfg = PVE::Storage::storage_config($storecfg, $sid);
+       return if !$scfg->{shared};
+
+       $target = $sid if !$target || $scfg->{path}; # prefer file based storage
+    });
+
+    if (!$target) {
+       # now search local storage
+       foreach_writable_storage($conf, sub {
+           my ($sid) = @_;
+           my $scfg = PVE::Storage::storage_config($storecfg, $sid);
+           return if $scfg->{shared};
+
+           $target = $sid if !$target || $scfg->{path}; # prefer file based storage;
+       });
+    }
+
+    $target = 'local' if !$target;
+
+    my $driver_state_size = 32; # assume 32MB is enough to safe all driver state;
+    my $size = $conf->{memory} + $driver_state_size;
+
+    my $name = "vm-$vmid-state-$snapname";
+    my $scfg = PVE::Storage::storage_config($storecfg, $target);
+    $name .= ".raw" if $scfg->{path}; # add filename extension for file base storage
+    my $volid = PVE::Storage::vdisk_alloc($storecfg, $target, $vmid, 'raw', $name, $size*1024);
+
+    return $volid;
+};
+
+my $snapshot_prepare = sub {
+    my ($vmid, $snapname, $save_vmstate, $comment) = @_;
+
+    my $snap;
+
+    my $updatefn =  sub {
+
+       my $conf = load_config($vmid);
+
+       check_lock($conf);
+
+       $conf->{lock} = 'snapshot';
+
+       die "snapshot name '$snapname' already used\n" 
+           if defined($conf->{snapshots}->{$snapname}); 
+
+       my $storecfg = PVE::Storage::config();
+
+       foreach_drive($conf, sub {
+           my ($ds, $drive) = @_;
+
+           return if drive_is_cdrom($drive);
+           my $volid = $drive->{file};
+
+           my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+           if ($storeid) {
+               my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+               die "can't snapshot volume '$volid'\n"          
+                   if !(($scfg->{path} && $volname =~ m/\.qcow2$/) ||
+                        ($scfg->{type} eq 'rbd') || 
+                        ($scfg->{type} eq 'sheepdog'));
+           } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+               die "snapshot device '$volid' is not possible\n";
+           } else {
+               die "can't snapshot volume '$volid'\n";
+           }
+       });
+
+
+       $snap = $conf->{snapshots}->{$snapname} = {};
+
+       if ($save_vmstate && check_running($vmid)) {
+           $snap->{vmstate} = &$alloc_vmstate_volid($storecfg, $vmid, $conf, $snapname);
+       }
+
+       &$snapshot_copy_config($conf, $snap);
+
+       $snap->{snapstate} = "prepare";
+       $snap->{snaptime} = time();
+       $snap->{description} = $comment if $comment;
+
+       update_config_nolock($vmid, $conf, 1);
+    };
+
+    lock_config($vmid, $updatefn);
+
+    return $snap;
+};
+
+my $snapshot_commit = sub {
+    my ($vmid, $snapname) = @_;
+
+    my $updatefn = sub {
+
+       my $conf = load_config($vmid);
+
+       die "missing snapshot lock\n" 
+           if !($conf->{lock} && $conf->{lock} eq 'snapshot'); 
+
+       my $snap = $conf->{snapshots}->{$snapname};
+
+       die "snapshot '$snapname' does not exist\n" if !defined($snap); 
+
+       die "wrong snapshot state\n" 
+           if !($snap->{snapstate} && $snap->{snapstate} eq "prepare"); 
+       
+       delete $snap->{snapstate};
+       delete $conf->{lock};
+
+       my $newconf = &$snapshot_apply_config($conf, $snap);
+
+       $newconf->{parent} = $snapname;
+
+       update_config_nolock($vmid, $newconf, 1);
+    };
+
+    lock_config($vmid, $updatefn);
+};
+
+sub snapshot_rollback {
+    my ($vmid, $snapname) = @_;
+
+    my $snap;
+
+    my $prepare = 1;
+
+    my $storecfg = PVE::Storage::config();
+    my $updatefn = sub {
+
+       my $conf = load_config($vmid);
+
+       if ($prepare) {
+           check_lock($conf);
+           vm_stop($storecfg, $vmid, undef, undef, 5, undef, undef);
+       }
+
+       die "unable to rollback vm $vmid: vm is running\n"
+           if check_running($vmid);
+
+       if ($prepare) {
+           $conf->{lock} = 'rollback';
+       } else {
+           die "got wrong lock\n" if !($conf->{lock} && $conf->{lock} eq 'rollback');
+           delete $conf->{lock};
+       }
+
+       $snap = $conf->{snapshots}->{$snapname};
+
+       die "snapshot '$snapname' does not exist\n" if !defined($snap); 
+
+       die "unable to rollback to incomplete snapshot (snapstate = $snap->{snapstate})\n" 
+           if $snap->{snapstate};
+
+       if (!$prepare) {
+           # copy snapshot config to current config
+           $conf = &$snapshot_apply_config($conf, $snap);
+           $conf->{parent} = $snapname;
+       }
+
+       update_config_nolock($vmid, $conf, 1);
+
+       if (!$prepare && $snap->{vmstate}) {
+           my $statefile = PVE::Storage::path($storecfg, $snap->{vmstate});
+           # fixme: this only forws for files currently
+           vm_start($storecfg, $vmid, $statefile);
+       }
+
+    };
+
+    lock_config($vmid, $updatefn);
+    
+    foreach_drive($snap, sub {
+       my ($ds, $drive) = @_;
+
+       return if drive_is_cdrom($drive);
+
+       my $volid = $drive->{file};
+       my $device = "drive-$ds";
+
+       PVE::Storage::volume_snapshot_rollback($storecfg, $volid, $snapname);
+    });
+
+    $prepare = 0;
+    lock_config($vmid, $updatefn);
+}
+
+sub snapshot_create {
+    my ($vmid, $snapname, $save_vmstate, $freezefs, $comment) = @_;
+
+    my $snap = &$snapshot_prepare($vmid, $snapname, $save_vmstate, $comment);
+
+    $freezefs = $save_vmstate = 0 if !$snap->{vmstate}; # vm is not running
+
+    my $drivehash = {};
+
+    my $running = check_running($vmid);
+
+    eval {
+       # create internal snapshots of all drives
+
+       my $storecfg = PVE::Storage::config();
+
+       if ($running) {
+           if ($snap->{vmstate}) {
+               my $path = PVE::Storage::path($storecfg, $snap->{vmstate});     
+               vm_mon_cmd($vmid, "snapshot-start", statefile => $path);
+           } else {
+               vm_mon_cmd($vmid, "snapshot-start");
+           }
+       };
+
+       qga_freezefs($vmid) if $running && $freezefs;
+       foreach_drive($snap, sub {
+           my ($ds, $drive) = @_;
+
+           return if drive_is_cdrom($drive);
+
+           my $volid = $drive->{file};
+           my $device = "drive-$ds";
+
+           qemu_volume_snapshot($vmid, $device, $storecfg, $volid, $snapname);
+           $drivehash->{$ds} = 1;
+       });
+    };
+    my $err = $@;
+
+    eval { gqa_unfreezefs($vmid) if $running && $freezefs; };
+    warn $@ if $@;
+
+    eval { vm_mon_cmd($vmid, "snapshot-end") if $running; };
+    warn $@ if $@;
+
+    if ($err) {
+       warn "snapshot create failed: starting cleanup\n";
+       eval { snapshot_delete($vmid, $snapname, 0, $drivehash); };
+       warn $@ if $@;
+       die $err;
+    }
+
+    &$snapshot_commit($vmid, $snapname);
+}
+
+# Note: $drivehash is only set when called from snapshot_create.
+sub snapshot_delete {
+    my ($vmid, $snapname, $force, $drivehash) = @_;
+
+    my $prepare = 1;
+
+    my $snap;
+    my $unused = [];
+
+    my $unlink_parent = sub {
+       my ($confref, $new_parent) = @_;
+
+       if ($confref->{parent} && $confref->{parent} eq $snapname) {
+           if ($new_parent) {
+               $confref->{parent} = $new_parent;
+           } else {
+               delete $confref->{parent};
+           }
+       }
+    };
+    my $updatefn =  sub {
+       my ($remove_drive) = @_;
+
+       my $conf = load_config($vmid);
+
+       check_lock($conf) if !$drivehash;
+
+       $snap = $conf->{snapshots}->{$snapname};
+
+       die "snapshot '$snapname' does not exist\n" if !defined($snap); 
+
+       # remove parent refs
+       &$unlink_parent($conf, $snap->{parent});
+       foreach my $sn (keys %{$conf->{snapshots}}) {
+           next if $sn eq $snapname;
+           &$unlink_parent($conf->{snapshots}->{$sn}, $snap->{parent});
+       }
+
+       if ($remove_drive) {
+           if ($remove_drive eq 'vmstate') {
+               delete $snap->{$remove_drive};
+           } else {
+               my $drive = parse_drive($remove_drive, $snap->{$remove_drive});
+               my $volid = $drive->{file};
+               delete $snap->{$remove_drive};
+               add_unused_volume($conf, $volid);
+           }
+       }
+
+       if ($prepare) {
+           $snap->{snapstate} = 'delete';
+       } else {
+           delete $conf->{snapshots}->{$snapname};
+           delete $conf->{lock} if $drivehash;
+           foreach my $volid (@$unused) {
+               add_unused_volume($conf, $volid);
+           }
+       }
+
+       update_config_nolock($vmid, $conf, 1);
+    };
+
+    lock_config($vmid, $updatefn);
+
+    # now remove vmstate file
+
+    my $storecfg = PVE::Storage::config();
+
+    if ($snap->{vmstate}) {
+       eval {  PVE::Storage::vdisk_free($storecfg, $snap->{vmstate}); };
+       if (my $err = $@) {
+           die $err if !$force;
+           warn $err;
+       }
+       # save changes (remove vmstate from snapshot)
+       lock_config($vmid, $updatefn, 'vmstate') if !$force;
+    };
+
+    # now remove all internal snapshots
+    foreach_drive($snap, sub {
+       my ($ds, $drive) = @_;
+
+       return if drive_is_cdrom($drive);
+
+       my $volid = $drive->{file};
+       my $device = "drive-$ds";
+
+       if (!$drivehash || $drivehash->{$ds}) {
+           eval { qemu_volume_snapshot_delete($vmid, $device, $storecfg, $volid, $snapname); };
+           if (my $err = $@) {
+               die $err if !$force;
+               warn $err;
+           }
+       }
+
+       # save changes (remove drive fron snapshot)
+       lock_config($vmid, $updatefn, $ds) if !$force;
+       push @$unused, $volid;
+    });
+
+    # now cleanup config
+    $prepare = 0;
+    lock_config($vmid, $updatefn);
+}
+
 1;