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 ?
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)$/;
return $res;
}
-sub lock_config_shared {
- my ($vmid, $timeout, $code, @param) = @_;
+sub lock_config_mode {
+ my ($vmid, $timeout, $shared, $code, @param) = @_;
my $filename = config_file_lock($vmid);
- my $res = lock_file_full($filename, $timeout, 1, $code, @param);
+ my $res = lock_file_full($filename, $timeout, $shared, $code, @param);
die $@ if $@;
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)) {
+ 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) = @_;
}
sub config_to_command {
- my ($storecfg, $vmid, $conf, $defaults) = @_;
+ my ($storecfg, $vmid, $conf, $defaults, $forcemachine) = @_;
my $cmd = [];
my $globalFlags = [];
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) {
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) = @_;
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);
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 vm_start {
- my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused) = @_;
+ my ($storecfg, $vmid, $statefile, $skiplock, $migratedfrom, $paused, $forcemachine) = @_;
lock_config($vmid, sub {
my $conf = load_config($vmid, $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{
- if (!defined($conf->{balloon}) || $conf->{balloon}) {
+ 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',
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
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";
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 $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)) {
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;
- $conf->{$opt} = print_drive($vmid, $drive);
+ 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;
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";
$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);
};
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});
- vm_start($storecfg, $vmid, $statefile);
+ vm_start($storecfg, $vmid, $statefile, undef, undef, undef, $forcemachine);
}
};
};
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; };
sub has_feature {
my ($feature, $conf, $storecfg, $snapname, $running) = @_;
- my $err = undef;
+ my $err;
foreach_drive($conf, sub {
my ($ds, $drive) = @_;
$err = 1 if !PVE::Storage::volume_has_feature($storecfg, $feature, $volid, $snapname, $running);
});
- return 1 if !$err;
+ return $err ? 0 : 1;
}
sub template_create {
my $voliddst = PVE::Storage::vdisk_create_base($storecfg, $volid);
$drive->{file} = $voliddst;
- $conf->{$ds} = PVE::QemuServer::print_drive($vmid, $drive);
- PVE::QemuServer::update_config_nolock($vmid, $conf, 1);
+ $conf->{$ds} = print_drive($vmid, $drive);
+ update_config_nolock($vmid, $conf, 1);
});
}
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;