use PVE::INotify;
use PVE::ProcFSTools;
use PVE::QMPClient;
+use PVE::RPCEnvironment;
use Time::HiRes qw(gettimeofday);
my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
balloon => {
optional => 1,
type => 'integer',
- description => "Amount of target RAM for the VM in MB.",
- minimum => 16,
+ description => "Amount of target RAM for the VM in MB. Using zero disables the ballon driver.",
+ minimum => 0,
+ },
+ shares => {
+ optional => 1,
+ type => 'integer',
+ description => "Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning",
+ minimum => 0,
+ maximum => 50000,
+ default => 1000,
},
keyboard => {
optional => 1,
ostype => {
optional => 1,
type => 'string',
- enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 l24 l26)],
+ enum => [qw(other wxp w2k w2k3 w2k8 wvista win7 win8 l24 l26)],
description => <<EODESC,
Used to enable special optimization/features for specific
operating systems:
w2k8 => Microsoft Windows 2008
wvista => Microsoft Windows Vista
win7 => Microsoft Windows 7
+win8 => Microsoft Windows 8/2012
l24 => Linux 2.4 Kernel
l26 => Linux 2.6/3.X Kernel
-other|l24|l26 ... no special behaviour
-wxp|w2k|w2k3|w2k8|wvista|win7 ... use --localtime switch
+other|l24|l26 ... no special behaviour
+wxp|w2k|w2k3|w2k8|wvista|win7|win8 ... use --localtime switch
EODESC
},
boot => {
tdf => {
optional => 1,
type => 'boolean',
- description => "Enable/disable time drift fix. This is ignored for kvm versions newer that 1.0 (not needed anymore).",
- default => 1,
+ description => "Enable/disable time drift fix.",
+ default => 0,
},
localtime => {
optional => 1,
vga => {
optional => 1,
type => 'string',
- description => "Select VGA type. If you want to use high resolution modes (>= 1280x1024x16) then you should use option 'std' or 'vmware'. Default is 'std' for win7/w2k8, and 'cirrur' for other OS types",
+ description => "Select VGA type. If you want to use high resolution modes (>= 1280x1024x16) then you should use option 'std' or 'vmware'. Default is 'std' for win8/win7/w2k8, and 'cirrur' for other OS types",
enum => [qw(std cirrus vmware)],
},
watchdog => {
optional => 1,
description => "Emulated CPU type.",
type => 'string',
- 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) ],
+ enum => [ qw(486 athlon pentium pentium2 pentium3 coreduo core2duo kvm32 kvm64 qemu32 qemu64 phenom Conroe Penryn Nehalem Westmere SandyBridge Haswell Opteron_G1 Opteron_G2 Opteron_G3 Opteron_G4 Opteron_G5 host) ],
default => 'qemu64',
},
parent => get_standard_option('pve-snapshot-name', {
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 ?
'ne2k_isa', 'i82551', 'i82557b', 'i82559er'];
my $nic_model_list_txt = join(' ', sort @$nic_model_list);
-# fixme:
my $netdesc = {
optional => 1,
type => 'string', format => 'pve-qm-net',
my $tmp = `kvm -help 2>/dev/null`;
- if ($tmp =~ m/^QEMU( PC)? emulator version (\d+\.\d+(\.\d+)?) /) {
+ if ($tmp =~ m/^QEMU( PC)? emulator version (\d+\.\d+(\.\d+)?)[,\s]/) {
$kvm_user_version = $2;
}
w2k8 => 'Windows 2008',
wvista => 'Windows Vista',
win7 => 'Windows 7',
+ win8 => 'Windows 8/2012',
l24 => 'Linux 2.4',
l26 => 'Linux 2.6',
};
return $key;
}
-# fixme: remove all thos $noerr parameters?
-
PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
sub verify_bootdisk {
my ($value, $noerr) = @_;
my $prop = shift;
foreach my $opt (keys %$confdesc) {
- next if $opt eq 'parent' || $opt eq 'snaptime';
+ next if $opt eq 'parent' || $opt eq 'snaptime' || $opt eq 'vmstate';
$prop->{$opt} = $confdesc->{$opt};
}
$d->{name} = $conf->{name} || "VM $vmid";
$d->{maxmem} = $conf->{memory} ? $conf->{memory}*(1024*1024) : 0;
+ if ($conf->{balloon}) {
+ $d->{balloon_min} = $conf->{balloon}*(1024*1024);
+ $d->{shares} = $conf->{shares} || 1000;
+ }
+
$d->{uptime} = 0;
$d->{cpu} = 0;
$d->{mem} = 0;
my $qmpclient = PVE::QMPClient->new();
+ my $ballooncb = sub {
+ my ($vmid, $resp) = @_;
+
+ my $info = $resp->{'return'};
+ return if !$info->{max_mem};
+
+ my $d = $res->{$vmid};
+
+ # use memory assigned to VM
+ $d->{maxmem} = $info->{max_mem};
+ $d->{balloon} = $info->{actual};
+
+ if (defined($info->{total_mem}) && defined($info->{free_mem})) {
+ $d->{mem} = $info->{total_mem} - $info->{free_mem};
+ $d->{freemem} = $info->{free_mem};
+ }
+
+ };
+
my $blockstatscb = sub {
my ($vmid, $resp) = @_;
my $data = $resp->{'return'} || [];
my $statuscb = sub {
my ($vmid, $resp) = @_;
+
$qmpclient->queue_cmd($vmid, $blockstatscb, 'query-blockstats');
+ # this fails if ballon driver is not loaded, so this must be
+ # the last commnand (following command are aborted if this fails).
+ $qmpclient->queue_cmd($vmid, $ballooncb, 'query-balloon');
my $status = 'unknown';
if (!defined($status = $resp->{'return'}->{status})) {
}
}
+sub foreach_volid {
+ my ($conf, $func) = @_;
+
+ my $volhash = {};
+
+ my $test_volid = sub {
+ my ($volid, $is_cdrom) = @_;
+
+ return if !$volid;
+
+ $volhash->{$volid} = $is_cdrom || 0;
+ };
+
+ PVE::QemuServer::foreach_drive($conf, sub {
+ my ($ds, $drive) = @_;
+ &$test_volid($drive->{file}, drive_is_cdrom($drive));
+ });
+
+ foreach my $snapname (keys %{$conf->{snapshots}}) {
+ my $snap = $conf->{snapshots}->{$snapname};
+ &$test_volid($snap->{vmstate}, 0);
+ PVE::QemuServer::foreach_drive($snap, sub {
+ my ($ds, $drive) = @_;
+ &$test_volid($drive->{file}, drive_is_cdrom($drive));
+ });
+ }
+
+ foreach my $volid (keys %$volhash) {
+ &$func($volid, $volhash->{$volid});
+ }
+}
+
sub config_to_command {
- my ($storecfg, $vmid, $conf, $defaults, $migrate_uri) = @_;
+ my ($storecfg, $vmid, $conf, $defaults) = @_;
my $cmd = [];
+ my $globalFlags = [];
+ my $machineFlags = [];
+ my $rtcFlags = [];
my $devices = [];
my $pciaddr = '';
my $bridges = {};
push @$cmd, '-daemonize';
- push @$cmd, '-incoming', $migrate_uri if $migrate_uri;
-
- push @$cmd, '-S' if $migrate_uri;
-
my $use_usb2 = 0;
for (my $i = 0; $i < $MAX_USB_DEVICES; $i++) {
next if !$conf->{"usb$i"};
my $vga = $conf->{vga};
if (!$vga) {
- if ($conf->{ostype} && ($conf->{ostype} eq 'win7' || $conf->{ostype} eq 'w2k8')) {
+ if ($conf->{ostype} && ($conf->{ostype} eq 'win8' || $conf->{ostype} eq 'win7' || $conf->{ostype} eq 'w2k8')) {
$vga = 'std';
} else {
$vga = 'cirrus';
# time drift fix
my $tdf = defined($conf->{tdf}) ? $conf->{tdf} : $defaults->{tdf};
- # ignore - no longer supported by newer kvm
- # push @$cmd, '-tdf' if $tdf;
my $nokvm = defined($conf->{kvm}) && $conf->{kvm} == 0 ? 1 : 0;
+ my $useLocaltime = $conf->{localtime};
if (my $ost = $conf->{ostype}) {
- # other, wxp, w2k, w2k3, w2k8, wvista, win7, l24, l26
+ # other, wxp, w2k, w2k3, w2k8, wvista, win7, win8, l24, l26
if ($ost =~ m/^w/) { # windows
- push @$cmd, '-localtime' if !defined($conf->{localtime});
+ $useLocaltime = 1 if !defined($conf->{localtime});
- # use rtc-td-hack when acpi is enabled
+ # use time drift fix when acpi is enabled
if (!(defined($conf->{acpi}) && $conf->{acpi} == 0)) {
- push @$cmd, '-rtc-td-hack';
+ $tdf = 1 if !defined($conf->{tdf});
}
}
- if ($ost eq 'win7' || $ost eq 'w2k8' || $ost eq 'wvista') {
- push @$cmd, '-no-kvm-pit-reinjection';
+ if ($ost eq 'win7' || $ost eq 'win8' || $ost eq 'w2k8' ||
+ $ost eq 'wvista') {
+ push @$globalFlags, 'kvm-pit.lost_tick_policy=discard';
push @$cmd, '-no-hpet';
}
-
- # -tdf ?
- # -no-acpi
- # -no-kvm
- # -win2k-hack ?
}
+ push @$rtcFlags, 'driftfix=slew' if $tdf;
+
if ($nokvm) {
- push @$cmd, '-no-kvm';
+ push @$machineFlags, 'accel=tcg';
} else {
die "No accelerator found!\n" if !$cpuinfo->{hvm};
}
- push @$cmd, '-localtime' if $conf->{localtime};
-
- push @$cmd, '-startdate', $conf->{startdate} if $conf->{startdate};
+ if ($conf->{startdate}) {
+ push @$rtcFlags, "base=$conf->{startdate}";
+ } elsif ($useLocaltime) {
+ push @$rtcFlags, 'base=localtime';
+ }
push @$cmd, '-S' if $conf->{freeze};
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};
+ # enable balloon by default, unless explicitly disabled
+ if (!defined($conf->{balloon}) || $conf->{balloon}) {
+ $pciaddr = print_pci_addr("balloon0", $bridges);
+ push @$devices, '-device', "virtio-balloon-pci,id=balloon0$pciaddr";
+ }
if ($conf->{watchdog}) {
my $wdopts = parse_watchdog($conf->{watchdog});
}
push @$cmd, @$devices;
+ push @$cmd, '-rtc', join(',', @$rtcFlags)
+ if scalar(@$rtcFlags);
+ push @$cmd, '-machine', join(',', @$machineFlags)
+ if scalar(@$machineFlags);
+ push @$cmd, '-global', join(',', @$globalFlags)
+ if scalar(@$globalFlags);
+
return wantarray ? ($cmd, $vollist) : $cmd;
}
sub qemu_volume_snapshot_delete {
my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
- #need to implement statefile location
- my $statefile="/tmp/$vmid-$snap";
-
- unlink $statefile if -e $statefile;
-
my $running = PVE::QemuServer::check_running($vmid);
return if !PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
return if !$running;
- #need to split delvm monitor command like savevm
-
-}
-
-sub qemu_snapshot_start {
- my ($vmid, $snap) = @_;
-
- #need to implement statefile location
- my $statefile="/tmp/$vmid-$snap";
-
- vm_mon_cmd($vmid, "snapshot-start", statefile => $statefile);
-
-}
-
-sub qemu_snapshot_end {
- my ($vmid) = @_;
-
- vm_mon_cmd($vmid, "snapshot-end");
-
+ vm_mon_cmd($vmid, "delete-drive-snapshot", device => $deviceid, name => $snap);
}
sub qga_freezefs {
}
sub vm_start {
- my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom) = @_;
+ my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused) = @_;
lock_config($vmid, sub {
my $conf = load_config($vmid, $migratedfrom);
die "VM $vmid already running\n" if check_running($vmid, undef, $migratedfrom);
- my $migrate_uri;
+ my $defaults = load_defaults();
+
+ # set environment variable useful inside network script
+ $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
+
+ my ($cmd, $vollist) = config_to_command($storecfg, $vmid, $conf, $defaults);
+
my $migrate_port = 0;
if ($statefile) {
if ($statefile eq 'tcp') {
$migrate_port = next_migrate_port();
- $migrate_uri = "tcp:localhost:${migrate_port}";
+ my $migrate_uri = "tcp:localhost:${migrate_port}";
+ push @$cmd, '-incoming', $migrate_uri;
+ push @$cmd, '-S';
} else {
- if (-f $statefile) {
- $migrate_uri = "exec:cat $statefile";
- } else {
- warn "state file '$statefile' does not exist - doing normal startup\n";
- }
+ push @$cmd, '-loadstate', $statefile;
}
+ } elsif ($paused) {
+ push @$cmd, '-S';
}
- my $defaults = load_defaults();
-
- # set environment variable useful inside network script
- $ENV{PVE_MIGRATED_FROM} = $migratedfrom if $migratedfrom;
-
- my ($cmd, $vollist) = config_to_command($storecfg, $vmid, $conf, $defaults, $migrate_uri);
# host pci devices
for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++) {
my $d = parse_hostpci($conf->{"hostpci$i"});
PVE::Storage::activate_volumes($storecfg, $vollist);
- eval { run_command($cmd, timeout => $migrate_uri ? undef : 30); };
+ eval { run_command($cmd, timeout => $statefile ? undef : 30,
+ umask => 0077); };
my $err = $@;
die "start failed: $err" if $err;
- if ($statefile) {
+ print "migration listens on port $migrate_port\n" if $migrate_port;
- 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 ($statefile && $statefile ne 'tcp') {
+ eval { vm_mon_cmd_nocheck($vmid, "cont"); };
+ warn $@ if $@;
}
# always set migrate speed (overwrite kvm default of 32m)
$migrate_speed = $conf->{migrate_speed} || $migrate_speed;
$migrate_speed = $migrate_speed * 1048576;
eval {
- vm_mon_cmd($vmid, "migrate_set_speed", value => $migrate_speed);
+ vm_mon_cmd_nocheck($vmid, "migrate_set_speed", value => $migrate_speed);
};
my $migrate_downtime = $defaults->{migrate_downtime};
$migrate_downtime = $conf->{migrate_downtime} if defined($conf->{migrate_downtime});
if (defined($migrate_downtime)) {
- eval { vm_mon_cmd($vmid, "migrate_set_downtime", value => $migrate_downtime); };
+ eval { vm_mon_cmd_nocheck($vmid, "migrate_set_downtime", value => $migrate_downtime); };
}
if($migratedfrom) {
eval { PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "migrate-set-capabilities", capabilities => [$capabilities]); };
}
- vm_balloonset($vmid, $conf->{balloon}) if $conf->{balloon};
+ # fixme: how do we handle that on migration?
+ if (!defined($conf->{balloon}) || $conf->{balloon}) {
+ vm_mon_cmd_nocheck($vmid, "balloon", value => $conf->{balloon}*1024*1024)
+ if $conf->{balloon};
+ vm_mon_cmd_nocheck($vmid, 'qom-set',
+ path => "machine/peripheral/balloon0",
+ property => "stats-polling-interval",
+ value => 2);
+ }
});
}
my ($conf) = @_;
my $vollist = [];
- foreach_drive($conf, sub {
- my ($ds, $drive) = @_;
+ foreach_volid($conf, sub {
+ my ($volid, $is_cdrom) = @_;
- my ($sid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
- return if !$sid;
+ return if $volid =~ m|^/|;
- my $volid = $drive->{file};
- return if !$volid || $volid =~ m|^/|;
+ my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+ return if !$sid;
push @$vollist, $volid;
});
}
-sub vm_balloonset {
- my ($vmid, $value) = @_;
-
- vm_mon_cmd($vmid, "balloon", value => $value);
-}
-
# vzdump restore implementaion
sub archive_read_firstfile {
sub restore_archive {
my ($archive, $vmid, $user, $opts) = @_;
+ my $format = $opts->{format};
+ my $comp;
+
+ if ($archive =~ m/\.tgz$/ || $archive =~ m/\.tar\.gz$/) {
+ $format = 'tar' if !$format;
+ $comp = 'gzip';
+ } elsif ($archive =~ m/\.tar$/) {
+ $format = 'tar' if !$format;
+ } elsif ($archive =~ m/.tar.lzo$/) {
+ $format = 'tar' if !$format;
+ $comp = 'lzop';
+ } elsif ($archive =~ m/\.vma$/) {
+ $format = 'vma' if !$format;
+ } elsif ($archive =~ m/\.vma\.gz$/) {
+ $format = 'vma' if !$format;
+ $comp = 'gzip';
+ } elsif ($archive =~ m/\.vma\.lzo$/) {
+ $format = 'vma' if !$format;
+ $comp = 'lzop';
+ } else {
+ $format = 'vma' if !$format; # default
+ }
+
+ # try to detect archive format
+ if ($format eq 'tar') {
+ return restore_tar_archive($archive, $vmid, $user, $opts);
+ } else {
+ return restore_vma_archive($archive, $vmid, $user, $opts, $comp);
+ }
+}
+
+sub restore_update_config_line {
+ my ($outfd, $cookie, $vmid, $map, $line, $unique) = @_;
+
+ return if $line =~ m/^\#qmdump\#/;
+ return if $line =~ m/^\#vzdump\#/;
+ return if $line =~ m/^lock:/;
+ return if $line =~ m/^unused\d+:/;
+ return if $line =~ m/^parent:/;
+
+ if (($line =~ m/^(vlan(\d+)):\s*(\S+)\s*$/)) {
+ # try to convert old 1.X settings
+ my ($id, $ind, $ethcfg) = ($1, $2, $3);
+ foreach my $devconfig (PVE::Tools::split_list($ethcfg)) {
+ my ($model, $macaddr) = split(/\=/, $devconfig);
+ $macaddr = PVE::Tools::random_ether_addr() if !$macaddr || $unique;
+ my $net = {
+ model => $model,
+ bridge => "vmbr$ind",
+ macaddr => $macaddr,
+ };
+ my $netstr = print_net($net);
+
+ print $outfd "net$cookie->{netcount}: $netstr\n";
+ $cookie->{netcount}++;
+ }
+ } elsif (($line =~ m/^(net\d+):\s*(\S+)\s*$/) && $unique) {
+ my ($id, $netstr) = ($1, $2);
+ my $net = parse_net($netstr);
+ $net->{macaddr} = PVE::Tools::random_ether_addr() if $net->{macaddr};
+ $netstr = print_net($net);
+ print $outfd "$id: $netstr\n";
+ } elsif ($line =~ m/^((ide|scsi|virtio|sata)\d+):\s*(\S+)\s*$/) {
+ my $virtdev = $1;
+ my $value = $2;
+ if ($line =~ m/backup=no/) {
+ print $outfd "#$line";
+ } elsif ($virtdev && $map->{$virtdev}) {
+ my $di = PVE::QemuServer::parse_drive($virtdev, $value);
+ $di->{file} = $map->{$virtdev};
+ $value = PVE::QemuServer::print_drive($vmid, $di);
+ print $outfd "$virtdev: $value\n";
+ } else {
+ print $outfd $line;
+ }
+ } else {
+ print $outfd $line;
+ }
+}
+
+sub scan_volids {
+ my ($cfg, $vmid) = @_;
+
+ my $info = PVE::Storage::vdisk_list($cfg, undef, $vmid);
+
+ my $volid_hash = {};
+ foreach my $storeid (keys %$info) {
+ foreach my $item (@{$info->{$storeid}}) {
+ next if !($item->{volid} && $item->{size});
+ $volid_hash->{$item->{volid}} = $item;
+ }
+ }
+
+ return $volid_hash;
+}
+
+sub update_disksize {
+ my ($vmid, $conf, $volid_hash) = @_;
+
+ my $changes;
+
+ my $used = {};
+
+ # update size info
+ foreach my $opt (keys %$conf) {
+ if (PVE::QemuServer::valid_drivename($opt)) {
+ my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt});
+ my $volid = $drive->{file};
+ next if !$volid;
+
+ $used->{$volid} = 1;
+
+ next if PVE::QemuServer::drive_is_cdrom($drive);
+ next if !$volid_hash->{$volid};
+
+ $drive->{size} = $volid_hash->{$volid}->{size};
+ $changes = 1;
+ $conf->{$opt} = PVE::QemuServer::print_drive($vmid, $drive);
+ }
+ }
+
+ foreach my $volid (sort keys %$volid_hash) {
+ next if $volid =~ m/vm-$vmid-state-/;
+ next if $used->{$volid};
+ $changes = 1;
+ PVE::QemuServer::add_unused_volume($conf, $volid);
+ }
+
+ return $changes;
+}
+
+sub rescan {
+ my ($vmid, $nolock) = @_;
+
+ my $cfg = PVE::Cluster::cfs_read_file("storage.cfg");
+
+ my $volid_hash = scan_volids($cfg, $vmid);
+
+ my $updatefn = sub {
+ my ($vmid) = @_;
+
+ my $conf = PVE::QemuServer::load_config($vmid);
+
+ PVE::QemuServer::check_lock($conf);
+
+ my $changes = PVE::QemuServer::update_disksize($vmid, $conf, $volid_hash);
+
+ PVE::QemuServer::update_config_nolock($vmid, $conf, 1) if $changes;
+ };
+
+ if (defined($vmid)) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ PVE::QemuServer::lock_config($vmid, $updatefn, $vmid);
+ }
+ } else {
+ my $vmlist = config_list();
+ foreach my $vmid (keys %$vmlist) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ PVE::QemuServer::lock_config($vmid, $updatefn, $vmid);
+ }
+ }
+ }
+}
+
+sub restore_vma_archive {
+ my ($archive, $vmid, $user, $opts, $comp) = @_;
+
+ my $input = $archive eq '-' ? "<&STDIN" : undef;
+ my $readfrom = $archive;
+
+ my $uncomp = '';
+ if ($comp) {
+ $readfrom = '-';
+ my $qarchive = PVE::Tools::shellquote($archive);
+ if ($comp eq 'gzip') {
+ $uncomp = "zcat $qarchive|";
+ } elsif ($comp eq 'lzop') {
+ $uncomp = "lzop -d -c $qarchive|";
+ } else {
+ die "unknown compression method '$comp'\n";
+ }
+
+ }
+
+ my $tmpdir = "/var/tmp/vzdumptmp$$";
+ rmtree $tmpdir;
+
+ # disable interrupts (always do cleanups)
+ local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub {
+ warn "got interrupt - ignored\n";
+ };
+
+ my $mapfifo = "/var/tmp/vzdumptmp$$.fifo";
+ POSIX::mkfifo($mapfifo, 0600);
+ my $fifofh;
+
+ my $openfifo = sub {
+ open($fifofh, '>', $mapfifo) || die $!;
+ };
+
+ my $cmd = "${uncomp}vma extract -v -r $mapfifo $readfrom $tmpdir";
+
+ my $oldtimeout;
+ my $timeout = 5;
+
+ my $devinfo = {};
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $conffile = PVE::QemuServer::config_file($vmid);
+ my $tmpfn = "$conffile.$$.tmp";
+
+ my $print_devmap = sub {
+ my $virtdev_hash = {};
+
+ my $cfgfn = "$tmpdir/qemu-server.conf";
+
+ # we can read the config - that is already extracted
+ my $fh = IO::File->new($cfgfn, "r") ||
+ "unable to read qemu-server.conf - $!\n";
+
+ while (defined(my $line = <$fh>)) {
+ if ($line =~ m/^\#qmdump\#map:(\S+):(\S+):(\S*):(\S*):$/) {
+ my ($virtdev, $devname, $storeid, $format) = ($1, $2, $3, $4);
+ die "archive does not contain data for drive '$virtdev'\n"
+ if !$devinfo->{$devname};
+ if (defined($opts->{storage})) {
+ $storeid = $opts->{storage} || 'local';
+ } elsif (!$storeid) {
+ $storeid = 'local';
+ }
+ $format = 'raw' if !$format;
+ $devinfo->{$devname}->{devname} = $devname;
+ $devinfo->{$devname}->{virtdev} = $virtdev;
+ $devinfo->{$devname}->{format} = $format;
+ $devinfo->{$devname}->{storeid} = $storeid;
+
+ # check permission on storage
+ my $pool = $opts->{pool}; # todo: do we need that?
+ if ($user ne 'root@pam') {
+ $rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
+ }
+
+ $virtdev_hash->{$virtdev} = $devinfo->{$devname};
+ }
+ }
+
+ foreach my $devname (keys %$devinfo) {
+ die "found no device mapping information for device '$devname'\n"
+ if !$devinfo->{$devname}->{virtdev};
+ }
+
+ my $map = {};
+ my $cfg = cfs_read_file('storage.cfg');
+ foreach my $virtdev (sort keys %$virtdev_hash) {
+ my $d = $virtdev_hash->{$virtdev};
+ my $alloc_size = int(($d->{size} + 1024 - 1)/1024);
+ my $scfg = PVE::Storage::storage_config($cfg, $d->{storeid});
+ my $volid = PVE::Storage::vdisk_alloc($cfg, $d->{storeid}, $vmid,
+ $d->{format}, undef, $alloc_size);
+ print STDERR "new volume ID is '$volid'\n";
+ $d->{volid} = $volid;
+ my $path = PVE::Storage::path($cfg, $volid);
+
+ my $write_zeros = 1;
+ # fixme: what other storages types initialize volumes with zero?
+ if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') {
+ $write_zeros = 0;
+ }
+
+ print $fifofh "${write_zeros}:$d->{devname}=$path\n";
+
+ print "map '$d->{devname}' to '$path' (write zeros = ${write_zeros})\n";
+ $map->{$virtdev} = $volid;
+ }
+
+ $fh->seek(0, 0) || die "seek failed - $!\n";
+
+ my $outfd = new IO::File ($tmpfn, "w") ||
+ die "unable to write config for VM $vmid\n";
+
+ my $cookie = { netcount => 0 };
+ while (defined(my $line = <$fh>)) {
+ restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
+ }
+
+ $fh->close();
+ $outfd->close();
+ };
+
+ eval {
+ # enable interrupts
+ local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
+ die "interrupted by signal\n";
+ };
+ local $SIG{ALRM} = sub { die "got timeout\n"; };
+
+ $oldtimeout = alarm($timeout);
+
+ my $parser = sub {
+ my $line = shift;
+
+ print "$line\n";
+
+ if ($line =~ m/^DEV:\sdev_id=(\d+)\ssize:\s(\d+)\sdevname:\s(\S+)$/) {
+ my ($dev_id, $size, $devname) = ($1, $2, $3);
+ $devinfo->{$devname} = { size => $size, dev_id => $dev_id };
+ } elsif ($line =~ m/^CTIME: /) {
+ &$print_devmap();
+ print $fifofh "done\n";
+ my $tmp = $oldtimeout || 0;
+ $oldtimeout = undef;
+ alarm($tmp);
+ close($fifofh);
+ }
+ };
+
+ print "restore vma archive: $cmd\n";
+ run_command($cmd, input => $input, outfunc => $parser, afterfork => $openfifo);
+ };
+ my $err = $@;
+
+ alarm($oldtimeout) if $oldtimeout;
+
+ unlink $mapfifo;
+
+ if ($err) {
+ rmtree $tmpdir;
+ unlink $tmpfn;
+
+ my $cfg = cfs_read_file('storage.cfg');
+ foreach my $devname (keys %$devinfo) {
+ my $volid = $devinfo->{$devname}->{volid};
+ next if !$volid;
+ eval {
+ if ($volid =~ m|^/|) {
+ unlink $volid || die 'unlink failed\n';
+ } else {
+ PVE::Storage::vdisk_free($cfg, $volid);
+ }
+ print STDERR "temporary volume '$volid' sucessfuly removed\n";
+ };
+ print STDERR "unable to cleanup '$volid' - $@" if $@;
+ }
+ die $err;
+ }
+
+ rmtree $tmpdir;
+
+ rename $tmpfn, $conffile ||
+ die "unable to commit configuration file '$conffile'\n";
+
+ eval { rescan($vmid, 1); };
+ warn $@ if $@;
+}
+
+sub restore_tar_archive {
+ my ($archive, $vmid, $user, $opts) = @_;
+
if ($archive ne '-') {
my $firstfile = archive_read_firstfile($archive);
die "ERROR: file '$archive' dos not lock like a QemuServer vzdump backup\n"
my $outfd = new IO::File ($tmpfn, "w") ||
die "unable to write config for VM $vmid\n";
- my $netcount = 0;
-
+ my $cookie = { netcount => 0 };
while (defined (my $line = <$srcfd>)) {
- next if $line =~ m/^\#vzdump\#/;
- next if $line =~ m/^lock:/;
- next if $line =~ m/^unused\d+:/;
-
- if (($line =~ m/^(vlan(\d+)):\s*(\S+)\s*$/)) {
- # try to convert old 1.X settings
- my ($id, $ind, $ethcfg) = ($1, $2, $3);
- foreach my $devconfig (PVE::Tools::split_list($ethcfg)) {
- my ($model, $macaddr) = split(/\=/, $devconfig);
- $macaddr = PVE::Tools::random_ether_addr() if !$macaddr || $opts->{unique};
- my $net = {
- model => $model,
- bridge => "vmbr$ind",
- macaddr => $macaddr,
- };
- my $netstr = print_net($net);
- print $outfd "net${netcount}: $netstr\n";
- $netcount++;
- }
- } elsif (($line =~ m/^(net\d+):\s*(\S+)\s*$/) && ($opts->{unique})) {
- my ($id, $netstr) = ($1, $2);
- my $net = parse_net($netstr);
- $net->{macaddr} = PVE::Tools::random_ether_addr() if $net->{macaddr};
- $netstr = print_net($net);
- print $outfd "$id: $netstr\n";
- } elsif ($line =~ m/^((ide|scsi|virtio)\d+):\s*(\S+)\s*$/) {
- my $virtdev = $1;
- my $value = $2;
- if ($line =~ m/backup=no/) {
- print $outfd "#$line";
- } elsif ($virtdev && $map->{$virtdev}) {
- my $di = PVE::QemuServer::parse_drive($virtdev, $value);
- $di->{file} = $map->{$virtdev};
- $value = PVE::QemuServer::print_drive($vmid, $di);
- print $outfd "$virtdev: $value\n";
- } else {
- print $outfd $line;
- }
- } else {
- print $outfd $line;
- }
+ restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
}
$srcfd->close();
rename $tmpfn, $conffile ||
die "unable to commit configuration file '$conffile'\n";
+
+ eval { rescan($vmid, 1); };
+ warn $@ if $@;
};
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};
snapshots => $conf->{snapshots},
};
- # keep list of unused disks
+ # keep description and list of unused disks
foreach my $k (keys %$conf) {
- next if $k !~ m/^unused\d+$/;
+ next if !($k =~ m/^unused\d+$/ || $k eq 'description');
$newconf->{$k} = $conf->{$k};
}
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 = 500; # assume 32MB is enough to safe all driver state;
+ # we abort live save after $conf->{memory}, so we need at max twice that space
+ my $size = $conf->{memory}*2 + $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, $comment) = @_;
+ my ($vmid, $snapname, $save_vmstate, $comment) = @_;
my $snap;
my $storecfg = PVE::Storage::config();
- PVE::QemuServer::foreach_drive($conf, sub {
+ foreach_drive($conf, sub {
my ($ds, $drive) = @_;
return if drive_is_cdrom($drive);
my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
die "can't snapshot volume '$volid'\n"
if !(($scfg->{path} && $volname =~ m/\.qcow2$/) ||
+ ($scfg->{type} eq 'nexenta') ||
($scfg->{type} eq 'rbd') ||
($scfg->{type} eq 'sheepdog'));
} elsif ($volid =~ m|^(/.+)$| && -e $volid) {
}
});
+
$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";
my $newconf = &$snapshot_apply_config($conf, $snap);
+ $newconf->{parent} = $snapname;
+
update_config_nolock($vmid, $newconf, 1);
};
my $prepare = 1;
+ my $storecfg = PVE::Storage::config();
+
my $updatefn = sub {
my $conf = load_config($vmid);
- check_lock($conf) if $prepare;
+ $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) {
+ 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);
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);
}
update_config_nolock($vmid, $conf, 1);
+
+ if (!$prepare && $snap->{vmstate}) {
+ my $statefile = PVE::Storage::path($storecfg, $snap->{vmstate});
+ vm_start($storecfg, $vmid, $statefile);
+ }
};
lock_config($vmid, $updatefn);
-
- my $storecfg = PVE::Storage::config();
foreach_drive($snap, sub {
my ($ds, $drive) = @_;
lock_config($vmid, $updatefn);
}
+my $savevm_wait = sub {
+ my ($vmid) = @_;
+
+ for(;;) {
+ my $stat = PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "query-savevm");
+ if (!$stat->{status}) {
+ die "savevm not active\n";
+ } elsif ($stat->{status} eq 'active') {
+ sleep(1);
+ next;
+ } elsif ($stat->{status} eq 'completed') {
+ last;
+ } else {
+ die "query-savevm returned status '$stat->{status}'\n";
+ }
+ }
+};
+
sub snapshot_create {
- my ($vmid, $snapname, $vmstate, $freezefs, $comment) = @_;
+ my ($vmid, $snapname, $save_vmstate, $freezefs, $comment) = @_;
- my $snap = &$snapshot_prepare($vmid, $snapname, $comment);
+ my $snap = &$snapshot_prepare($vmid, $snapname, $save_vmstate, $comment);
- $freezefs = $vmstate = 0 if !check_running($vmid);
+ $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
-
- qemu_snapshot_start($vmid, $snapname) if $vmstate;
-
- qga_freezefs($vmid) if $freezefs;
my $storecfg = PVE::Storage::config();
+
+ if ($running) {
+ if ($snap->{vmstate}) {
+ my $path = PVE::Storage::path($storecfg, $snap->{vmstate});
+ vm_mon_cmd($vmid, "savevm-start", statefile => $path);
+ &$savevm_wait($vmid);
+ } else {
+ vm_mon_cmd($vmid, "savevm-start");
+ }
+ };
+
+ qga_freezefs($vmid) if $running && $freezefs;
foreach_drive($snap, sub {
my ($ds, $drive) = @_;
};
my $err = $@;
- eval { gqa_unfreezefs($vmid) if $freezefs; };
+ eval { gqa_unfreezefs($vmid) if $running && $freezefs; };
warn $@ if $@;
- eval { qemu_snapshot_end($vmid) if $vmstate; };
+ eval { vm_mon_cmd($vmid, "savevm-end") if $running; };
warn $@ if $@;
if ($err) {
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) = @_;
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;
- my $snapref = $conf->{snapshots}->{$sn};
- if ($snapref->{parent} && $snapref->{parent} eq $snapname) {
- if ($snap->{parent}) {
- $snapref->{parent} = $snap->{parent};
- } else {
- delete $snapref->{parent};
- }
- }
+ &$unlink_parent($conf->{snapshots}->{$sn}, $snap->{parent});
}
if ($remove_drive) {
- my $drive = parse_drive($remove_drive, $snap->{$remove_drive});
- my $volid = $drive->{file};
- delete $snap->{$remove_drive};
- add_unused_volume($conf, $volid);
+ 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->{parent} if $conf->{parent} && $conf->{parent} eq $snapname;
delete $conf->{snapshots}->{$snapname};
delete $conf->{lock} if $drivehash;
foreach my $volid (@$unused) {
lock_config($vmid, $updatefn);
- # now remove all internal snapshots
+ # now remove vmstate file
my $storecfg = PVE::Storage::config();
- PVE::QemuServer::foreach_drive($snap, sub {
+ 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);