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::Tools qw(run_command lock_file lock_file_full 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;
use PVE::QMPClient;
+use PVE::RPCEnvironment;
use Time::HiRes qw(gettimeofday);
my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
hotplug => {
optional => 1,
type => 'boolean',
- description => "Activate hotplug for disk and network device",
+ description => "Allow hotplug for disk and network device",
default => 0,
},
reboot => {
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,
typetext => '[[order=]\d+] [,up=\d+] [,down=\d+] ',
description => "Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped.",
},
+ template => {
+ optional => 1,
+ type => 'boolean',
+ description => "Enable/disable Template.",
+ default => 0,
+ },
args => {
optional => 1,
type => 'string',
},
migrate_downtime => {
optional => 1,
- type => 'integer',
+ type => 'number',
description => "Set maximum tolerated downtime (in seconds) for migrations.",
minimum => 0,
- default => 1,
+ default => 0.1,
},
cdrom => {
optional => 1,
description => "Emulated CPU type.",
type => 'string',
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',
+ default => 'kvm64',
},
parent => get_standard_option('pve-snapshot-name', {
optional => 1,
type => 'string', format => 'pve-volume-id',
description => "Reference to a volume which stores the VM state. This is used internally for snapshots.",
},
+ machine => {
+ description => "Specific the Qemu machine type.",
+ type => 'string',
+ pattern => '(pc|pc(-i440fx)?-\d+\.\d+|q35|pc-q35-\d+\.\d+)',
+ maxLength => 40,
+ optional => 1,
+ },
};
# 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',
return undef if !$res->{file};
+ if($res->{file} =~ m/\.(raw|cow|qcow|qcow2|vmdk|cloop)$/){
+ $res->{format} = $1;
+ }
+
return undef if $res->{cache} &&
$res->{cache} !~ m/^(off|none|writethrough|writeback|unsafe|directsync)$/;
return undef if $res->{snapshot} && $res->{snapshot} !~ m/^(on|off)$/;
} else {
$path = PVE::Storage::path($storecfg, $volid);
}
- if (!$drive->{cache} && ($path =~ m|^/dev/| || $path =~ m|\.raw$|)) {
- $opts .= ",cache=none";
- }
}
+ $opts .= ",cache=none" if !$drive->{cache} && !drive_is_cdrom($drive);
+
my $pathinfo = $path ? "file=$path," : '';
return "${pathinfo}if=none,id=drive-$drive->{interface}$drive->{index}$opts";
if ($kvp =~ m/^(ne2k_pci|e1000|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i) {
my $model = lc($1);
- my $mac = uc($3) || PVE::Tools::random_ether_addr();
+ my $mac = defined($3) ? uc($3) : PVE::Tools::random_ether_addr();
$res->{model} = $model;
$res->{macaddr} = $mac;
} elsif ($kvp =~ m/^bridge=(\S+)$/) {
return $key;
}
-# fixme: remove all thos $noerr parameters?
-
PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
sub verify_bootdisk {
my ($value, $noerr) = @_;
} elsif ($type eq 'integer') {
return int($1) if $value =~ m/^(\d+)$/;
die "type check ('integer') failed - got '$value'\n";
+ } elsif ($type eq 'number') {
+ return $value if $value =~ m/^(\d+)(\.\d+)?$/;
+ die "type check ('number') failed - got '$value'\n";
} elsif ($type eq 'string') {
if (my $fmt = $confdesc->{$key}->{format}) {
if ($fmt eq 'pve-qm-drive') {
return $res;
}
+sub lock_config_mode {
+ my ($vmid, $timeout, $shared, $code, @param) = @_;
+
+ my $filename = config_file_lock($vmid);
+
+ my $res = lock_file_full($filename, $timeout, $shared, $code, @param);
+
+ die $@ if $@;
+
+ return $res;
+}
+
sub lock_config {
my ($vmid, $code, @param) = @_;
return if drive_is_cdrom($drive);
my $volid = $drive->{file};
+
return if !$volid || $volid =~ m|^/|;
my ($path, $owner) = PVE::Storage::path($storecfg, $volid);
my $used_volids = {};
my $cleanup_config = sub {
- my ($cref) = @_;
+ my ($cref, $snapname) = @_;
foreach my $key (keys %$cref) {
next if $key eq 'digest' || $key eq 'description' || $key eq 'snapshots' ||
$cref->{$key} = $value;
- if (valid_drivename($key)) {
- my $drive = PVE::QemuServer::parse_drive($key, $value);
+ if (!$snapname && valid_drivename($key)) {
+ my $drive = 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});
+ &$cleanup_config($conf->{snapshots}->{$snapname}, $snapname);
}
# remove 'unusedX' settings if we re-add a volume
return $loc_res;
}
-# check is used storages are available on all nodes (use by migrate)
+# check if used storages are available on all nodes (use by migrate)
sub check_storage_availability {
my ($storecfg, $conf, $node) = @_;
});
}
+# list nodes where all VM images are available (used by has_feature API)
+sub shared_nodes {
+ my ($conf, $storecfg) = @_;
+
+ my $nodelist = PVE::Cluster::get_nodelist();
+ my $nodehash = { map { $_ => 1 } @$nodelist };
+ my $nodename = PVE::INotify::nodename();
+
+ foreach_drive($conf, sub {
+ my ($ds, $drive) = @_;
+
+ my $volid = $drive->{file};
+ return if !$volid;
+
+ my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+ if ($storeid) {
+ my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+ if ($scfg->{disable}) {
+ $nodehash = {};
+ } elsif (my $avail = $scfg->{nodes}) {
+ foreach my $node (keys %$nodehash) {
+ delete $nodehash->{$node} if !$avail->{$node};
+ }
+ } elsif (!$scfg->{shared}) {
+ foreach my $node (keys %$nodehash) {
+ delete $nodehash->{$node} if $node ne $nodename
+ }
+ }
+ }
+ });
+
+ return $nodehash
+}
+
sub check_lock {
my ($conf) = @_;
my @param = split(/\0/, $line);
my $cmd = $param[0];
- return if !$cmd || ($cmd !~ m|kvm$|);
+ return if !$cmd || ($cmd !~ m|kvm$| && $cmd !~ m|qemu-system-x86_64$|);
for (my $i = 0; $i < scalar (@param); $i++) {
my $p = $param[$i];
$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} = defined($conf->{shares}) ? $conf->{shares} : 1000;
+ }
+
$d->{uptime} = 0;
$d->{cpu} = 0;
$d->{mem} = 0;
$d->{diskread} = 0;
$d->{diskwrite} = 0;
+ $d->{template} = is_template($conf);
+
$res->{$vmid} = $d;
}
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})) {
$volhash->{$volid} = $is_cdrom || 0;
};
- PVE::QemuServer::foreach_drive($conf, sub {
+ 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 {
+ foreach_drive($snap, sub {
my ($ds, $drive) = @_;
&$test_volid($drive->{file}, drive_is_cdrom($drive));
});
}
sub config_to_command {
- my ($storecfg, $vmid, $conf, $defaults) = @_;
+ my ($storecfg, $vmid, $conf, $defaults, $forcemachine) = @_;
my $cmd = [];
my $globalFlags = [];
push @$cmd, '-daemonize';
+ $pciaddr = print_pci_addr("piix3", $bridges);
+ push @$devices, '-device', "piix3-usb-uhci,id=uhci$pciaddr.0x2";
+
my $use_usb2 = 0;
for (my $i = 0; $i < $MAX_USB_DEVICES; $i++) {
next if !$conf->{"usb$i"};
# enable absolute mouse coordinates (needed by vnc)
my $tablet = defined($conf->{tablet}) ? $conf->{tablet} : $defaults->{tablet};
- if ($tablet) {
- if ($use_usb2) {
- push @$devices, '-device', 'usb-tablet,bus=ehci.0,port=6';
- } else {
- push @$devices, '-usbdevice', 'tablet';
- }
- }
+ push @$devices, '-device', 'usb-tablet,id=tablet,bus=uhci.0,port=1' if $tablet;
# host pci devices
for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++) {
die "No accelerator found!\n" if !$cpuinfo->{hvm};
}
+ my $machine_type = $forcemachine || $conf->{machine};
+ if ($machine_type) {
+ push @$machineFlags, "type=${machine_type}";
+ }
+
if ($conf->{startdate}) {
push @$rtcFlags, "base=$conf->{startdate}";
} elsif ($useLocaltime) {
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});
return "${var_run_tmpdir}/$vmid.pid";
}
-sub next_migrate_port {
-
- for (my $p = 60000; $p < 60010; $p++) {
-
- my $sock = IO::Socket::INET->new(Listen => 5,
- LocalAddr => 'localhost',
- LocalPort => $p,
- ReuseAddr => 1,
- Proto => 0);
-
- if ($sock) {
- close($sock);
- return $p;
- }
- }
-
- die "unable to find free migration port";
-}
-
sub vm_devices_list {
my ($vmid) = @_;
sub vm_deviceplug {
my ($storecfg, $conf, $vmid, $deviceid, $device) = @_;
- return 1 if !check_running($vmid) || !$conf->{hotplug};
+ return 1 if !check_running($vmid);
+
+ if ($deviceid eq 'tablet') {
+ my $devicefull = "usb-tablet,id=tablet,bus=uhci.0,port=1";
+ qemu_deviceadd($vmid, $devicefull);
+ return 1;
+ }
+
+ return 1 if !$conf->{hotplug};
my $devices_list = vm_devices_list($vmid);
return 1 if defined($devices_list->{$deviceid});
sub vm_deviceunplug {
my ($vmid, $conf, $deviceid) = @_;
- return 1 if !check_running ($vmid) || !$conf->{hotplug};
+ return 1 if !check_running ($vmid);
+
+ if ($deviceid eq 'tablet') {
+ qemu_devicedel($vmid, $deviceid);
+ return 1;
+ }
+
+ return 1 if !$conf->{hotplug};
my $devices_list = vm_devices_list($vmid);
return 1 if !defined($devices_list->{$deviceid});
die "can't unplug bootdisk" if $conf->{bootdisk} && $conf->{bootdisk} eq $deviceid;
if ($deviceid =~ m/^(virtio)(\d+)$/) {
- return undef if !qemu_drivedel($vmid, $deviceid);
qemu_devicedel($vmid, $deviceid);
return undef if !qemu_devicedelverify($vmid, $deviceid);
+ return undef if !qemu_drivedel($vmid, $deviceid);
}
if ($deviceid =~ m/^(lsi)(\d+)$/) {
}
if ($deviceid =~ m/^(net)(\d+)$/) {
- return undef if !qemu_netdevdel($vmid, $deviceid);
qemu_devicedel($vmid, $deviceid);
return undef if !qemu_devicedelverify($vmid, $deviceid);
+ return undef if !qemu_netdevdel($vmid, $deviceid);
}
return 1;
sub qemu_deviceadd {
my ($vmid, $devicefull) = @_;
- my $ret = vm_human_monitor_command($vmid, "device_add $devicefull");
- $ret =~ s/^\s+//;
- # Otherwise, if the command succeeds, no output is sent. So any non-empty string shows an error
- return 1 if $ret eq "";
- syslog("err", "error on hotplug device : $ret");
- return undef;
+ $devicefull = "driver=".$devicefull;
+ my %options = split(/[=,]/, $devicefull);
+ vm_mon_cmd($vmid, "device_add" , %options);
+ return 1;
}
sub qemu_devicedel {
my($vmid, $deviceid) = @_;
-
- my $ret = vm_human_monitor_command($vmid, "device_del $deviceid");
- $ret =~ s/^\s+//;
- return 1 if $ret eq "";
- syslog("err", "detaching device $deviceid failed : $ret");
- return undef;
+ my $ret = vm_mon_cmd($vmid, "device_del", id => $deviceid);
+ return 1;
}
sub qemu_driveadd {
while (my ($k, $v) = each %$bridges) {
$bridgeid = $k;
}
- return if $bridgeid < 1;
+ return if !$bridgeid || $bridgeid < 1;
my $bridge = "pci.$bridgeid";
my $devices_list = vm_devices_list($vmid);
my ($vmid, $conf, $device, $deviceid) = @_;
my $netdev = print_netdev_full($vmid, $conf, $device, $deviceid);
- my $ret = vm_human_monitor_command($vmid, "netdev_add $netdev");
- $ret =~ s/^\s+//;
+ my %options = split(/[=,]/, $netdev);
- #if the command succeeds, no output is sent. So any non-empty string shows an error
- return 1 if $ret eq "";
- syslog("err", "adding netdev failed: $ret");
- return undef;
+ vm_mon_cmd($vmid, "netdev_add", %options);
+ return 1;
}
sub qemu_netdevdel {
my ($vmid, $deviceid) = @_;
- my $ret = vm_human_monitor_command($vmid, "netdev_del $deviceid");
- $ret =~ s/^\s+//;
- #if the command succeeds, no output is sent. So any non-empty string shows an error
- return 1 if $ret eq "";
- syslog("err", "deleting netdev failed: $ret");
- return undef;
+ vm_mon_cmd($vmid, "netdev_del", id => $deviceid);
+ return 1;
}
sub qemu_block_set_io_throttle {
return if !check_running($vmid) ;
- $bps = 0 if !$bps;
- $bps_rd = 0 if !$bps_rd;
- $bps_wr = 0 if !$bps_wr;
- $iops = 0 if !$iops;
- $iops_rd = 0 if !$iops_rd;
- $iops_wr = 0 if !$iops_wr;
-
vm_mon_cmd($vmid, "block_set_io_throttle", device => $deviceid, bps => int($bps), bps_rd => int($bps_rd), bps_wr => int($bps_wr), iops => int($iops), iops_rd => int($iops_rd), iops_wr => int($iops_wr));
}
sub qemu_block_resize {
my ($vmid, $deviceid, $storecfg, $volid, $size) = @_;
- my $running = PVE::QemuServer::check_running($vmid);
+ my $running = check_running($vmid);
return if !PVE::Storage::volume_resize($storecfg, $volid, $size, $running);
sub qemu_volume_snapshot {
my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
- my $running = PVE::QemuServer::check_running($vmid);
+ my $running = check_running($vmid);
return if !PVE::Storage::volume_snapshot($storecfg, $volid, $snap, $running);
sub qemu_volume_snapshot_delete {
my ($vmid, $deviceid, $storecfg, $volid, $snap) = @_;
- my $running = PVE::QemuServer::check_running($vmid);
+ my $running = check_running($vmid);
return if !PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snap, $running);
}
sub vm_start {
- my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom) = @_;
+ my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused, $forcemachine) = @_;
lock_config($vmid, sub {
my $conf = load_config($vmid, $migratedfrom);
+ die "you can't start a vm if it's a template\n" if is_template($conf);
+
check_lock($conf) if !$skiplock;
die "VM $vmid already running\n" if check_running($vmid, undef, $migratedfrom);
# 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 ($cmd, $vollist) = config_to_command($storecfg, $vmid, $conf, $defaults, $forcemachine);
my $migrate_port = 0;
if ($statefile) {
if ($statefile eq 'tcp') {
- $migrate_port = next_migrate_port();
+ $migrate_port = PVE::Tools::next_migrate_port();
my $migrate_uri = "tcp:localhost:${migrate_port}";
push @$cmd, '-incoming', $migrate_uri;
push @$cmd, '-S';
} else {
push @$cmd, '-loadstate', $statefile;
}
+ } elsif ($paused) {
+ push @$cmd, '-S';
}
# host pci devices
print "migration listens on port $migrate_port\n" if $migrate_port;
if ($statefile && $statefile ne 'tcp') {
- eval { vm_mon_cmd($vmid, "cont"); };
+ eval { vm_mon_cmd_nocheck($vmid, "cont"); };
warn $@ if $@;
}
- # always set migrate speed (overwrite kvm default of 32m)
- # we set a very hight default of 8192m which is basically unlimited
- my $migrate_speed = $defaults->{migrate_speed} || 8192;
- $migrate_speed = $conf->{migrate_speed} || $migrate_speed;
- $migrate_speed = $migrate_speed * 1048576;
- eval {
- vm_mon_cmd($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); };
- }
-
if($migratedfrom) {
my $capabilities = {};
$capabilities->{capability} = "xbzrle";
$capabilities->{state} = JSON::true;
- eval { PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "migrate-set-capabilities", capabilities => [$capabilities]); };
+ eval { vm_mon_cmd_nocheck($vmid, "migrate-set-capabilities", capabilities => [$capabilities]); };
+ }
+ else{
+
+ if (!$statefile && (!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 => "guest-stats-polling-interval",
+ value => 2);
+ }
}
-
- vm_balloonset($vmid, $conf->{balloon}) if $conf->{balloon};
-
});
}
eval {
die "VM $vmid not running\n" if !check_running($vmid, $nocheck);
- my $sname = PVE::QemuServer::qmp_socket($vmid);
+ my $sname = qmp_socket($vmid);
if (-e $sname) {
my $qmpclient = PVE::QMPClient->new();
my $conf = load_config($vmid);
- check_lock($conf) if !$skiplock;
+ check_lock($conf) if !($skiplock || ($conf->{lock} && $conf->{lock} eq 'backup'));
vm_mon_cmd($vmid, "stop");
});
my $conf = load_config($vmid);
- check_lock($conf) if !$skiplock;
+ check_lock($conf) if !($skiplock || ($conf->{lock} && $conf->{lock} eq 'backup'));
vm_mon_cmd($vmid, "cont");
});
my $res = '';
my $devices = {
- #addr1 : ide,parallel,serial (motherboard)
+ piix3 => { bus => 0, addr => 1 },
#addr2 : first videocard
balloon0 => { bus => 0, addr => 3 },
watchdog => { bus => 0, addr => 4 },
}
-sub vm_balloonset {
- my ($vmid, $value) = @_;
-
- vm_mon_cmd($vmid, "balloon", value => $value*1024*1024);
-}
-
# vzdump restore implementaion
-sub archive_read_firstfile {
+sub tar_archive_read_firstfile {
my $archive = shift;
die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
return $firstfile;
}
-sub restore_cleanup {
- my $statfile = shift;
+sub tar_restore_cleanup {
+ my ($storecfg, $statfile) = @_;
print STDERR "starting cleanup\n";
if ($volid =~ m|^/|) {
unlink $volid || die 'unlink failed\n';
} else {
- my $cfg = cfs_read_file('storage.cfg');
- PVE::Storage::vdisk_free($cfg, $volid);
+ PVE::Storage::vdisk_free($storecfg, $volid);
}
print STDERR "temporary volume '$volid' sucessfuly removed\n";
};
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:/;
+ return if $line =~ m/^template:/; # restored VM is never a template
+
+ 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 = $3;
+ if ($line =~ m/backup=no/) {
+ print $outfd "#$line";
+ } elsif ($virtdev && $map->{$virtdev}) {
+ my $di = parse_drive($virtdev, $value);
+ delete $di->{format}; # format can change on restore
+ $di->{file} = $map->{$virtdev};
+ $value = 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});
+ $item->{path} = PVE::Storage::path($cfg, $item->{volid});
+ $volid_hash->{$item->{volid}} = $item;
+ }
+ }
+
+ return $volid_hash;
+}
+
+sub get_used_paths {
+ my ($vmid, $storecfg, $conf, $scan_snapshots, $skip_drive) = @_;
+
+ my $used_path = {};
+
+ my $scan_config = sub {
+ my ($cref, $snapname) = @_;
+
+ foreach my $key (keys %$cref) {
+ my $value = $cref->{$key};
+ if (valid_drivename($key)) {
+ next if $skip_drive && $key eq $skip_drive;
+ my $drive = parse_drive($key, $value);
+ next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
+ if ($drive->{file} =~ m!^/!) {
+ $used_path->{$drive->{file}}++; # = 1;
+ } else {
+ my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
+ next if !$storeid;
+ my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
+ next if !$scfg;
+ my $path = PVE::Storage::path($storecfg, $drive->{file}, $snapname);
+ $used_path->{$path}++; # = 1;
+ }
+ }
+ }
+ };
+
+ &$scan_config($conf);
+
+ undef $skip_drive;
+
+ if ($scan_snapshots) {
+ foreach my $snapname (keys %{$conf->{snapshots}}) {
+ &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
+ }
+ }
+
+ return $used_path;
+}
+
+sub update_disksize {
+ my ($vmid, $conf, $volid_hash) = @_;
+
+ my $changes;
+
+ my $used = {};
+
+ # Note: it is allowed to define multiple storages with same path (alias), so
+ # we need to check both 'volid' and real 'path' (two different volid can point
+ # to the same path).
+
+ my $usedpath = {};
+
+ # update size info
+ foreach my $opt (keys %$conf) {
+ if (valid_drivename($opt)) {
+ my $drive = parse_drive($opt, $conf->{$opt});
+ my $volid = $drive->{file};
+ next if !$volid;
+
+ $used->{$volid} = 1;
+ if ($volid_hash->{$volid} &&
+ (my $path = $volid_hash->{$volid}->{path})) {
+ $usedpath->{$path} = 1;
+ }
+
+ next if drive_is_cdrom($drive);
+ next if !$volid_hash->{$volid};
+
+ $drive->{size} = $volid_hash->{$volid}->{size};
+ my $new = print_drive($vmid, $drive);
+ if ($new ne $conf->{$opt}) {
+ $changes = 1;
+ $conf->{$opt} = $new;
+ }
+ }
+ }
+
+ # remove 'unusedX' entry if volume is used
+ foreach my $opt (keys %$conf) {
+ next if $opt !~ m/^unused\d+$/;
+ my $volid = $conf->{$opt};
+ my $path = $volid_hash->{$volid}->{path} if $volid_hash->{$volid};
+ if ($used->{$volid} || ($path && $usedpath->{$path})) {
+ $changes = 1;
+ delete $conf->{$opt};
+ }
+ }
+
+ foreach my $volid (sort keys %$volid_hash) {
+ next if $volid =~ m/vm-$vmid-state-/;
+ next if $used->{$volid};
+ my $path = $volid_hash->{$volid}->{path};
+ next if !$path; # just to be sure
+ next if $usedpath->{$path};
+ $changes = 1;
+ add_unused_volume($conf, $volid);
+ $usedpath->{$path} = 1; # avoid to add more than once (aliases)
+ }
+
+ 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 = load_config($vmid);
+
+ check_lock($conf);
+
+ my $vm_volids = {};
+ foreach my $volid (keys %$volid_hash) {
+ my $info = $volid_hash->{$volid};
+ $vm_volids->{$volid} = $info if $info->{vmid} && $info->{vmid} == $vmid;
+ }
+
+ my $changes = update_disksize($vmid, $conf, $vm_volids);
+
+ update_config_nolock($vmid, $conf, 1) if $changes;
+ };
+
+ if (defined($vmid)) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ lock_config($vmid, $updatefn, $vmid);
+ }
+ } else {
+ my $vmlist = config_list();
+ foreach my $vmid (keys %$vmlist) {
+ if ($nolock) {
+ &$updatefn($vmid);
+ } else {
+ 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 = config_file($vmid);
+ my $tmpfn = "$conffile.$$.tmp";
+
+ # Note: $oldconf is undef if VM does not exists
+ my $oldconf = PVE::Cluster::cfs_read_file(cfs_config_path($vmid));
+
+ 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 $cfg = cfs_read_file('storage.cfg');
+
+ # create empty/temp config
+ if ($oldconf) {
+ PVE::Tools::file_set_contents($conffile, "memory: 128\n");
+ foreach_drive($oldconf, sub {
+ my ($ds, $drive) = @_;
+
+ return if drive_is_cdrom($drive);
+
+ my $volid = $drive->{file};
+
+ return if !$volid || $volid =~ m|^/|;
+
+ my ($path, $owner) = PVE::Storage::path($cfg, $volid);
+ return if !$path || !$owner || ($owner != $vmid);
+
+ # Note: only delete disk we want to restore
+ # other volumes will become unused
+ if ($virtdev_hash->{$ds}) {
+ PVE::Storage::vdisk_free($cfg, $volid);
+ }
+ });
+ }
+
+ my $map = {};
+ 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});
+
+ # test if requested format is supported
+ my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($cfg, $d->{storeid});
+ my $supported = grep { $_ eq $d->{format} } @$validFormats;
+ $d->{format} = $defFormat if !$supported;
+
+ 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' ||
+ $scfg->{type} eq 'sheepdog' || $scfg->{type} eq 'rbd') {
+ $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";
+
+ PVE::Cluster::cfs_update(); # make sure we read new file
+
+ eval { rescan($vmid, 1); };
+ warn $@ if $@;
+}
+
+sub restore_tar_archive {
+ my ($archive, $vmid, $user, $opts) = @_;
+
if ($archive ne '-') {
- my $firstfile = archive_read_firstfile($archive);
+ my $firstfile = tar_archive_read_firstfile($archive);
die "ERROR: file '$archive' dos not lock like a QemuServer vzdump backup\n"
if $firstfile ne 'qemu-server.conf';
}
+ my $storecfg = cfs_read_file('storage.cfg');
+
+ # destroy existing data - keep empty config
+ my $vmcfgfn = PVE::QemuServer::config_file($vmid);
+ destroy_vm($storecfg, $vmid, 1) if -f $vmcfgfn;
+
my $tocmd = "/usr/lib/qemu-server/qmextract";
$tocmd .= " --storage " . PVE::Tools::shellquote($opts->{storage}) if $opts->{storage};
local $ENV{VZDUMP_VMID} = $vmid;
local $ENV{VZDUMP_USER} = $user;
- my $conffile = PVE::QemuServer::config_file($vmid);
+ my $conffile = config_file($vmid);
my $tmpfn = "$conffile.$$.tmp";
# disable interrupts (always do cleanups)
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|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;
- }
+ restore_update_config_line($outfd, $cookie, $vmid, $map, $line, $opts->{unique});
}
$srcfd->close();
unlink $tmpfn;
- restore_cleanup("$tmpdir/qmrestore.stat") if !$opts->{info};
+ tar_restore_cleanup($storecfg, "$tmpdir/qmrestore.stat") if !$opts->{info};
die $err;
}
rename $tmpfn, $conffile ||
die "unable to commit configuration file '$conffile'\n";
+
+ PVE::Cluster::cfs_update(); # make sure we read new file
+
+ eval { rescan($vmid, 1); };
+ warn $@ if $@;
};
my $conf = load_config($vmid);
+ die "you can't take a snapshot if it's a template\n"
+ if is_template($conf);
+
check_lock($conf);
$conf->{lock} = 'snapshot';
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 'nexenta') ||
- ($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";
- }
- });
-
+ die "snapshot feature is not available" if !has_feature('snapshot', $conf, $storecfg);
$snap = $conf->{snapshots}->{$snapname} = {};
$snap->{snaptime} = time();
$snap->{description} = $comment if $comment;
+ # always overwrite machine if we save vmstate. This makes sure we
+ # can restore it later using correct machine type
+ $snap->{machine} = get_current_qemu_machine($vmid) if $snap->{vmstate};
+
update_config_nolock($vmid, $conf, 1);
};
my $conf = load_config($vmid);
+ die "you can't rollback if vm is a template\n" if is_template($conf);
+
$snap = $conf->{snapshots}->{$snapname};
die "snapshot '$snapname' does not exist\n" if !defined($snap);
delete $conf->{lock};
}
+ my $forcemachine;
+
if (!$prepare) {
+ my $has_machine_config = defined($conf->{machine});
+
# copy snapshot config to current config
$conf = &$snapshot_apply_config($conf, $snap);
$conf->{parent} = $snapname;
+
+ # Note: old code did not store 'machine', so we try to be smart
+ # and guess the snapshot was generated with kvm 1.4 (pc-i440fx-1.4).
+ $forcemachine = $conf->{machine} || 'pc-i440fx-1.4';
+ # we remove the 'machine' configuration if not explicitly specified
+ # in the original config.
+ delete $conf->{machine} if $snap->{vmstate} && !$has_machine_config;
}
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);
+ vm_start($storecfg, $vmid, $statefile, undef, undef, undef, $forcemachine);
}
-
};
lock_config($vmid, $updatefn);
my ($vmid) = @_;
for(;;) {
- my $stat = PVE::QemuServer::vm_mon_cmd_nocheck($vmid, "query-savevm");
+ my $stat = vm_mon_cmd_nocheck($vmid, "query-savevm");
if (!$stat->{status}) {
die "savevm not active\n";
} elsif ($stat->{status} eq 'active') {
};
my $err = $@;
- eval { gqa_unfreezefs($vmid) if $running && $freezefs; };
+ eval { qga_unfreezefs($vmid) if $running && $freezefs; };
warn $@ if $@;
eval { vm_mon_cmd($vmid, "savevm-end") if $running; };
my $conf = load_config($vmid);
- check_lock($conf) if !$drivehash;
+ if (!$drivehash) {
+ check_lock($conf);
+ die "you can't delete a snapshot if vm is a template\n"
+ if is_template($conf);
+ }
$snap = $conf->{snapshots}->{$snapname};
lock_config($vmid, $updatefn);
}
+sub has_feature {
+ my ($feature, $conf, $storecfg, $snapname, $running) = @_;
+
+ my $err;
+ foreach_drive($conf, sub {
+ my ($ds, $drive) = @_;
+
+ return if drive_is_cdrom($drive);
+ my $volid = $drive->{file};
+ $err = 1 if !PVE::Storage::volume_has_feature($storecfg, $feature, $volid, $snapname, $running);
+ });
+
+ return $err ? 0 : 1;
+}
+
+sub template_create {
+ my ($vmid, $conf, $disk) = @_;
+
+ my $storecfg = PVE::Storage::config();
+
+ foreach_drive($conf, sub {
+ my ($ds, $drive) = @_;
+
+ return if drive_is_cdrom($drive);
+ return if $disk && $ds ne $disk;
+
+ my $volid = $drive->{file};
+ return if !PVE::Storage::volume_has_feature($storecfg, 'template', $volid);
+
+ my $voliddst = PVE::Storage::vdisk_create_base($storecfg, $volid);
+ $drive->{file} = $voliddst;
+ $conf->{$ds} = print_drive($vmid, $drive);
+ update_config_nolock($vmid, $conf, 1);
+ });
+}
+
+sub is_template {
+ my ($conf) = @_;
+
+ return 1 if defined $conf->{template} && $conf->{template} == 1;
+}
+
+sub qemu_img_convert {
+ my ($src_volid, $dst_volid, $size, $snapname) = @_;
+
+ my $storecfg = PVE::Storage::config();
+ my ($src_storeid, $src_volname) = PVE::Storage::parse_volume_id($src_volid, 1);
+ my ($dst_storeid, $dst_volname) = PVE::Storage::parse_volume_id($dst_volid, 1);
+
+ if ($src_storeid && $dst_storeid) {
+ my $src_scfg = PVE::Storage::storage_config($storecfg, $src_storeid);
+ my $dst_scfg = PVE::Storage::storage_config($storecfg, $dst_storeid);
+
+ my $src_format = qemu_img_format($src_scfg, $src_volname);
+ my $dst_format = qemu_img_format($dst_scfg, $dst_volname);
+
+ my $src_path = PVE::Storage::path($storecfg, $src_volid, $snapname);
+ my $dst_path = PVE::Storage::path($storecfg, $dst_volid);
+
+ my $cmd = [];
+ push @$cmd, '/usr/bin/qemu-img', 'convert', '-t', 'writeback', '-p', '-C';
+ push @$cmd, '-s', $snapname if($snapname && $src_format eq "qcow2");
+ push @$cmd, '-f', $src_format, '-O', $dst_format, $src_path, $dst_path;
+
+ my $parser = sub {
+ my $line = shift;
+ if($line =~ m/\((\S+)\/100\%\)/){
+ my $percent = $1;
+ my $transferred = int($size * $percent / 100);
+ my $remaining = $size - $transferred;
+
+ print "transferred: $transferred bytes remaining: $remaining bytes total: $size bytes progression: $percent %\n";
+ }
+
+ };
+
+ eval { run_command($cmd, timeout => undef, outfunc => $parser); };
+ my $err = $@;
+ die "copy failed: $err" if $err;
+ }
+}
+
+sub qemu_img_format {
+ my ($scfg, $volname) = @_;
+
+ if ($scfg->{path} && $volname =~ m/\.(raw|qcow2|qed|vmdk)$/) {
+ return $1;
+ } elsif ($scfg->{type} eq 'iscsi') {
+ return "host_device";
+ } else {
+ return "raw";
+ }
+}
+
+sub qemu_drive_mirror {
+ my ($vmid, $drive, $dst_volid, $vmiddst, $maxwait) = @_;
+
+ my $count = 1;
+ my $old_len = 0;
+ my $frozen = undef;
+
+ my $storecfg = PVE::Storage::config();
+ my ($dst_storeid, $dst_volname) = PVE::Storage::parse_volume_id($dst_volid, 1);
+
+ if ($dst_storeid) {
+ my $dst_scfg = PVE::Storage::storage_config($storecfg, $dst_storeid);
+
+ my $format;
+ if ($dst_volname =~ m/\.(raw|qcow2)$/){
+ $format = $1;
+ }
+
+ my $dst_path = PVE::Storage::path($storecfg, $dst_volid);
+
+ if ($format) {
+ #fixme : sometime drive-mirror timeout, but works fine after.
+ # (I have see the problem with big volume > 200GB), so we need to eval
+ eval { vm_mon_cmd($vmid, "drive-mirror", timeout => 10, device => "drive-$drive", mode => "existing",
+ sync => "full", target => $dst_path, format => $format); };
+ } else {
+ eval { vm_mon_cmd($vmid, "drive-mirror", timeout => 10, device => "drive-$drive", mode => "existing",
+ sync => "full", target => $dst_path); };
+ }
+
+ eval {
+ while (1) {
+ my $stats = vm_mon_cmd($vmid, "query-block-jobs");
+ my $stat = @$stats[0];
+ die "mirroring job seem to have die. Maybe do you have bad sectors?" if !$stat;
+ die "error job is not mirroring" if $stat->{type} ne "mirror";
+
+ my $transferred = $stat->{offset};
+ my $total = $stat->{len};
+ my $remaining = $total - $transferred;
+ my $percent = sprintf "%.2f", ($transferred * 100 / $total);
+
+ print "transferred: $transferred bytes remaining: $remaining bytes total: $total bytes progression: $percent %\n";
+
+ last if ($stat->{len} == $stat->{offset});
+ if ($old_len == $stat->{offset}) {
+ if ($maxwait && $count > $maxwait) {
+ # if writes to disk occurs the disk needs to be freezed
+ # to be able to complete the migration
+ vm_suspend($vmid,1);
+ $count = 0;
+ $frozen = 1;
+ } else {
+ $count++ unless $frozen;
+ }
+ } elsif ($frozen) {
+ vm_resume($vmid,1);
+ $count = 0;
+ }
+ $old_len = $stat->{offset};
+ sleep 1;
+ }
+
+ if ($vmiddst == $vmid) {
+ # switch the disk if source and destination are on the same guest
+ vm_mon_cmd($vmid, "block-job-complete", device => "drive-$drive");
+ }
+ };
+ if (my $err = $@) {
+ eval { vm_mon_cmd($vmid, "block-job-cancel", device => "drive-$drive"); };
+ die "mirroring error: $err";
+ }
+
+ if ($vmiddst != $vmid) {
+ # if we clone a disk for a new target vm, we don't switch the disk
+ vm_mon_cmd($vmid, "block-job-cancel", device => "drive-$drive");
+ }
+ }
+}
+
+sub clone_disk {
+ my ($storecfg, $vmid, $running, $drivename, $drive, $snapname,
+ $newvmid, $storage, $format, $full, $newvollist) = @_;
+
+ my $newvolid;
+
+ if (!$full) {
+ print "create linked clone of drive $drivename ($drive->{file})\n";
+ $newvolid = PVE::Storage::vdisk_clone($storecfg, $drive->{file}, $newvmid);
+ push @$newvollist, $newvolid;
+ } else {
+ my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file});
+ $storeid = $storage if $storage;
+
+ my ($defFormat, $validFormats) = PVE::Storage::storage_default_format($storecfg, $storeid);
+ if (!$format) {
+ $format = $drive->{format} || $defFormat;
+ }
+
+ # test if requested format is supported - else use default
+ my $supported = grep { $_ eq $format } @$validFormats;
+ $format = $defFormat if !$supported;
+
+ my ($size) = PVE::Storage::volume_size_info($storecfg, $drive->{file}, 3);
+
+ print "create full clone of drive $drivename ($drive->{file})\n";
+ $newvolid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $newvmid, $format, undef, ($size/1024));
+ push @$newvollist, $newvolid;
+
+ if (!$running || $snapname) {
+ qemu_img_convert($drive->{file}, $newvolid, $size, $snapname);
+ } else {
+ qemu_drive_mirror($vmid, $drivename, $newvolid, $newvmid);
+ }
+ }
+
+ my ($size) = PVE::Storage::volume_size_info($storecfg, $newvolid, 3);
+
+ my $disk = $drive;
+ $disk->{format} = undef;
+ $disk->{file} = $newvolid;
+ $disk->{size} = $size;
+
+ return $disk;
+}
+
+# this only works if VM is running
+sub get_current_qemu_machine {
+ my ($vmid) = @_;
+
+ my $cmd = { execute => 'query-machines', arguments => {} };
+ my $res = PVE::QemuServer::vm_qmp_command($vmid, $cmd);
+
+ my ($current, $default);
+ foreach my $e (@$res) {
+ $default = $e->{name} if $e->{'is-default'};
+ $current = $e->{name} if $e->{'is-current'};
+ }
+
+ # fallback to the default machine if current is not supported by qemu
+ return $current || $default || 'pc';
+}
+
1;