From e0fd2b2f8402086902fe5e38d4bbe152194bfccc Mon Sep 17 00:00:00 2001 From: Fabian Ebner Date: Mon, 2 Mar 2020 11:33:44 +0100 Subject: [PATCH] Create Drive.pm and move drive-related code there The initialization for the drive keys in $confdesc is changed to be a single for-loop iterating over the keys of $drivedesc_hash and the initialization of the unusedN keys is move to directly below it. To avoid the need to change all the call sites, functions with more than a few callers are exported from the submodule and imported into QemuServer.pm. For callers of the now imported functions within QemuServer.pm, the prefix PVE::QemuServer is dropped, because it is unnecessary and now even confusing. Signed-off-by: Fabian Ebner --- PVE/API2/Qemu.pm | 11 +- PVE/CLI/qm.pm | 3 +- PVE/QemuMigrate.pm | 3 +- PVE/QemuServer.pm | 652 +--------------------------------------- PVE/QemuServer/Drive.pm | 645 +++++++++++++++++++++++++++++++++++++++ PVE/QemuServer/Makefile | 1 + 6 files changed, 670 insertions(+), 645 deletions(-) create mode 100644 PVE/QemuServer/Drive.pm diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index caca430a..3b613348 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -20,6 +20,7 @@ use PVE::ReplicationConfig; use PVE::GuestHelpers; use PVE::QemuConfig; use PVE::QemuServer; +use PVE::QemuServer::Drive; use PVE::QemuServer::Monitor qw(mon_cmd); use PVE::QemuMigrate; use PVE::RPCEnvironment; @@ -601,7 +602,7 @@ __PACKAGE__->register_method({ $vollist = &$create_disks($rpcenv, $authuser, $conf, $arch, $storecfg, $vmid, $pool, $param, $storage); if (!$conf->{bootdisk}) { - my $firstdisk = PVE::QemuServer::resolve_first_disk($conf); + my $firstdisk = PVE::QemuServer::Drive::resolve_first_disk($conf); $conf->{bootdisk} = $firstdisk if $firstdisk; } @@ -2992,7 +2993,7 @@ __PACKAGE__->register_method({ disk => { type => 'string', description => "The disk you want to move.", - enum => [ PVE::QemuServer::valid_drive_names() ], + enum => [PVE::QemuServer::Drive::valid_drive_names()], }, storage => get_standard_option('pve-storage-id', { description => "Target storage.", @@ -3069,7 +3070,7 @@ __PACKAGE__->register_method({ (!$format || !$oldfmt || $oldfmt eq $format); # this only checks snapshots because $disk is passed! - my $snapshotted = PVE::QemuServer::is_volume_in_use($storecfg, $conf, $disk, $old_volid); + my $snapshotted = PVE::QemuServer::Drive::is_volume_in_use($storecfg, $conf, $disk, $old_volid); die "you can't move a disk with snapshots and delete the source\n" if $snapshotted && $param->{delete}; @@ -3490,7 +3491,7 @@ __PACKAGE__->register_method({ disk => { type => 'string', description => "The disk you want to resize.", - enum => [PVE::QemuServer::valid_drive_names()], + enum => [PVE::QemuServer::Drive::valid_drive_names()], }, size => { type => 'string', @@ -3990,7 +3991,7 @@ __PACKAGE__->register_method({ optional => 1, type => 'string', description => "If you want to convert only 1 disk to base image.", - enum => [PVE::QemuServer::valid_drive_names()], + enum => [PVE::QemuServer::Drive::valid_drive_names()], }, }, diff --git a/PVE/CLI/qm.pm b/PVE/CLI/qm.pm index 5e4f96f0..f99d4015 100755 --- a/PVE/CLI/qm.pm +++ b/PVE/CLI/qm.pm @@ -28,6 +28,7 @@ use PVE::Tools qw(extract_param); use PVE::API2::Qemu::Agent; use PVE::API2::Qemu; use PVE::QemuConfig; +use PVE::QemuServer::Drive; use PVE::QemuServer::Helpers; use PVE::QemuServer::Agent qw(agent_available); use PVE::QemuServer::ImportDisk; @@ -654,7 +655,7 @@ __PACKAGE__->register_method ({ # reload after disks entries have been created $conf = PVE::QemuConfig->load_config($vmid); - my $firstdisk = PVE::QemuServer::resolve_first_disk($conf); + my $firstdisk = PVE::QemuServer::Drive::resolve_first_disk($conf); $conf->{bootdisk} = $firstdisk if $firstdisk; PVE::QemuConfig->write_config($vmid, $conf); }; diff --git a/PVE/QemuMigrate.pm b/PVE/QemuMigrate.pm index 1753a107..a88b82f8 100644 --- a/PVE/QemuMigrate.pm +++ b/PVE/QemuMigrate.pm @@ -11,6 +11,7 @@ use PVE::Tools; use PVE::Cluster; use PVE::Storage; use PVE::QemuServer; +use PVE::QemuServer::Drive; use PVE::QemuServer::Machine; use PVE::QemuServer::Monitor qw(mon_cmd); use Time::HiRes qw( usleep ); @@ -447,7 +448,7 @@ sub sync_disks { my $volid_hash = PVE::QemuServer::scan_volids($self->{storecfg}, $vmid); PVE::QemuServer::foreach_drive($conf, sub { my ($key, $drive) = @_; - my ($updated, $old_size, $new_size) = PVE::QemuServer::update_disksize($drive, $volid_hash); + my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash); if (defined($updated)) { $conf->{$key} = PVE::QemuServer::print_drive($updated); $self->log('info', "size of disk '$updated->{file}' ($key) updated from $old_size to $new_size\n"); diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index ffc1a91c..53aec189 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -44,6 +44,7 @@ use PVE::QemuConfig; use PVE::QemuServer::Helpers qw(min_version config_aware_timeout); use PVE::QemuServer::Cloudinit; use PVE::QemuServer::CPUConfig qw(print_cpu_device get_cpu_options); +use PVE::QemuServer::Drive qw(is_valid_drivename drive_is_cloudinit drive_is_cdrom parse_drive print_drive foreach_drive foreach_volid); use PVE::QemuServer::Machine; use PVE::QemuServer::Memory; use PVE::QemuServer::Monitor qw(mon_cmd); @@ -64,8 +65,6 @@ my $OVMF = { my $cpuinfo = PVE::ProcFSTools::read_cpuinfo(); -my $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/; - # Note about locking: we use flock on the config file protect # against concurent actions. # Aditionaly, we have a 'lock' setting in the config file. This @@ -84,13 +83,6 @@ PVE::JSONSchema::register_standard_option('pve-qm-stateuri', { optional => 1, }); -PVE::JSONSchema::register_standard_option('pve-qm-image-format', { - type => 'string', - enum => [qw(raw cow qcow qed qcow2 vmdk cloop)], - description => "The drive's backing file's data format.", - optional => 1, -}); - PVE::JSONSchema::register_standard_option('pve-qemu-machine', { description => "Specifies the Qemu machine type.", type => 'string', @@ -739,13 +731,8 @@ while (my ($k, $v) = each %$confdesc) { PVE::JSONSchema::register_standard_option("pve-qm-$k", $v); } -my $MAX_IDE_DISKS = 4; -my $MAX_SCSI_DISKS = 31; -my $MAX_VIRTIO_DISKS = 16; -my $MAX_SATA_DISKS = 6; my $MAX_USB_DEVICES = 5; my $MAX_NETS = 32; -my $MAX_UNUSED_DISKS = 256; my $MAX_HOSTPCI_DEVICES = 16; my $MAX_SERIAL_PORTS = 4; my $MAX_PARALLEL_PORTS = 3; @@ -948,310 +935,6 @@ sub verify_volume_id_or_qm_path { return $volid; } -my $drivedesc_hash; - -my %drivedesc_base = ( - volume => { alias => 'file' }, - file => { - type => 'string', - format => 'pve-volume-id-or-qm-path', - default_key => 1, - format_description => 'volume', - description => "The drive's backing volume.", - }, - media => { - type => 'string', - enum => [qw(cdrom disk)], - description => "The drive's media type.", - default => 'disk', - optional => 1 - }, - cyls => { - type => 'integer', - description => "Force the drive's physical geometry to have a specific cylinder count.", - optional => 1 - }, - heads => { - type => 'integer', - description => "Force the drive's physical geometry to have a specific head count.", - optional => 1 - }, - secs => { - type => 'integer', - description => "Force the drive's physical geometry to have a specific sector count.", - optional => 1 - }, - trans => { - type => 'string', - enum => [qw(none lba auto)], - description => "Force disk geometry bios translation mode.", - optional => 1, - }, - snapshot => { - type => 'boolean', - description => "Controls qemu's snapshot mode feature." - . " If activated, changes made to the disk are temporary and will" - . " be discarded when the VM is shutdown.", - optional => 1, - }, - cache => { - type => 'string', - enum => [qw(none writethrough writeback unsafe directsync)], - description => "The drive's cache mode", - optional => 1, - }, - format => get_standard_option('pve-qm-image-format'), - size => { - type => 'string', - format => 'disk-size', - format_description => 'DiskSize', - description => "Disk size. This is purely informational and has no effect.", - optional => 1, - }, - backup => { - type => 'boolean', - description => "Whether the drive should be included when making backups.", - optional => 1, - }, - replicate => { - type => 'boolean', - description => 'Whether the drive should considered for replication jobs.', - optional => 1, - default => 1, - }, - rerror => { - type => 'string', - enum => [qw(ignore report stop)], - description => 'Read error action.', - optional => 1, - }, - werror => { - type => 'string', - enum => [qw(enospc ignore report stop)], - description => 'Write error action.', - optional => 1, - }, - aio => { - type => 'string', - enum => [qw(native threads)], - description => 'AIO type to use.', - optional => 1, - }, - discard => { - type => 'string', - enum => [qw(ignore on)], - description => 'Controls whether to pass discard/trim requests to the underlying storage.', - optional => 1, - }, - detect_zeroes => { - type => 'boolean', - description => 'Controls whether to detect and try to optimize writes of zeroes.', - optional => 1, - }, - serial => { - type => 'string', - format => 'urlencoded', - format_description => 'serial', - maxLength => 20*3, # *3 since it's %xx url enoded - description => "The drive's reported serial number, url-encoded, up to 20 bytes long.", - optional => 1, - }, - shared => { - type => 'boolean', - description => 'Mark this locally-managed volume as available on all nodes', - verbose_description => "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!", - optional => 1, - default => 0, - } -); - -my %iothread_fmt = ( iothread => { - type => 'boolean', - description => "Whether to use iothreads for this drive", - optional => 1, -}); - -my %model_fmt = ( - model => { - type => 'string', - format => 'urlencoded', - format_description => 'model', - maxLength => 40*3, # *3 since it's %xx url enoded - description => "The drive's reported model name, url-encoded, up to 40 bytes long.", - optional => 1, - }, -); - -my %queues_fmt = ( - queues => { - type => 'integer', - description => "Number of queues.", - minimum => 2, - optional => 1 - } -); - -my %scsiblock_fmt = ( - scsiblock => { - type => 'boolean', - description => "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", - optional => 1, - default => 0, - }, -); - -my %ssd_fmt = ( - ssd => { - type => 'boolean', - description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.", - optional => 1, - }, -); - -my %wwn_fmt = ( - wwn => { - type => 'string', - pattern => qr/^(0x)[0-9a-fA-F]{16}/, - format_description => 'wwn', - description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", - optional => 1, - }, -); - -my $add_throttle_desc = sub { - my ($key, $type, $what, $unit, $longunit, $minimum) = @_; - my $d = { - type => $type, - format_description => $unit, - description => "Maximum $what in $longunit.", - optional => 1, - }; - $d->{minimum} = $minimum if defined($minimum); - $drivedesc_base{$key} = $d; -}; -# throughput: (leaky bucket) -$add_throttle_desc->('bps', 'integer', 'r/w speed', 'bps', 'bytes per second'); -$add_throttle_desc->('bps_rd', 'integer', 'read speed', 'bps', 'bytes per second'); -$add_throttle_desc->('bps_wr', 'integer', 'write speed', 'bps', 'bytes per second'); -$add_throttle_desc->('mbps', 'number', 'r/w speed', 'mbps', 'megabytes per second'); -$add_throttle_desc->('mbps_rd', 'number', 'read speed', 'mbps', 'megabytes per second'); -$add_throttle_desc->('mbps_wr', 'number', 'write speed', 'mbps', 'megabytes per second'); -$add_throttle_desc->('iops', 'integer', 'r/w I/O', 'iops', 'operations per second'); -$add_throttle_desc->('iops_rd', 'integer', 'read I/O', 'iops', 'operations per second'); -$add_throttle_desc->('iops_wr', 'integer', 'write I/O', 'iops', 'operations per second'); - -# pools: (pool of IO before throttling starts taking effect) -$add_throttle_desc->('mbps_max', 'number', 'unthrottled r/w pool', 'mbps', 'megabytes per second'); -$add_throttle_desc->('mbps_rd_max', 'number', 'unthrottled read pool', 'mbps', 'megabytes per second'); -$add_throttle_desc->('mbps_wr_max', 'number', 'unthrottled write pool', 'mbps', 'megabytes per second'); -$add_throttle_desc->('iops_max', 'integer', 'unthrottled r/w I/O pool', 'iops', 'operations per second'); -$add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool', 'iops', 'operations per second'); -$add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second'); - -# burst lengths -$add_throttle_desc->('bps_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1); -$add_throttle_desc->('bps_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1); -$add_throttle_desc->('bps_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1); -$add_throttle_desc->('iops_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1); -$add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1); -$add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1); - -# legacy support -$drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' }; -$drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' }; -$drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' }; -$drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' }; - -my $ide_fmt = { - %drivedesc_base, - %model_fmt, - %ssd_fmt, - %wwn_fmt, -}; -PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt); - -my $idedesc = { - optional => 1, - type => 'string', format => $ide_fmt, - description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").", -}; -PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc); - -my $scsi_fmt = { - %drivedesc_base, - %iothread_fmt, - %queues_fmt, - %scsiblock_fmt, - %ssd_fmt, - %wwn_fmt, -}; -my $scsidesc = { - optional => 1, - type => 'string', format => $scsi_fmt, - description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").", -}; -PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc); - -my $sata_fmt = { - %drivedesc_base, - %ssd_fmt, - %wwn_fmt, -}; -my $satadesc = { - optional => 1, - type => 'string', format => $sata_fmt, - description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").", -}; -PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc); - -my $virtio_fmt = { - %drivedesc_base, - %iothread_fmt, -}; -my $virtiodesc = { - optional => 1, - type => 'string', format => $virtio_fmt, - description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").", -}; -PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc); - -my $alldrive_fmt = { - %drivedesc_base, - %iothread_fmt, - %model_fmt, - %queues_fmt, - %scsiblock_fmt, - %ssd_fmt, - %wwn_fmt, -}; - -my $efidisk_fmt = { - volume => { alias => 'file' }, - file => { - type => 'string', - format => 'pve-volume-id-or-qm-path', - default_key => 1, - format_description => 'volume', - description => "The drive's backing volume.", - }, - format => get_standard_option('pve-qm-image-format'), - size => { - type => 'string', - format => 'disk-size', - format_description => 'DiskSize', - description => "Disk size. This is purely informational and has no effect.", - optional => 1, - }, -}; - -my $efidisk_desc = { - optional => 1, - type => 'string', format => $efidisk_fmt, - description => "Configure a Disk for storing EFI vars", -}; - -PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc); - my $usb_fmt = { host => { default_key => 1, @@ -1398,43 +1081,18 @@ for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++) { $confdesc->{"hostpci$i"} = $hostpcidesc; } -for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) { - $drivedesc_hash->{"ide$i"} = $idedesc; - $confdesc->{"ide$i"} = $idedesc; -} - -for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) { - $drivedesc_hash->{"sata$i"} = $satadesc; - $confdesc->{"sata$i"} = $satadesc; +for my $key (keys %{$PVE::QemuServer::Drive::drivedesc_hash}) { + $confdesc->{$key} = $PVE::QemuServer::Drive::drivedesc_hash->{$key}; } -for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) { - $drivedesc_hash->{"scsi$i"} = $scsidesc; - $confdesc->{"scsi$i"} = $scsidesc ; +for (my $i = 0; $i < $PVE::QemuServer::Drive::MAX_UNUSED_DISKS; $i++) { + $confdesc->{"unused$i"} = $PVE::QemuServer::Drive::unuseddesc; } -for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) { - $drivedesc_hash->{"virtio$i"} = $virtiodesc; - $confdesc->{"virtio$i"} = $virtiodesc; -} - -$drivedesc_hash->{efidisk0} = $efidisk_desc; -$confdesc->{efidisk0} = $efidisk_desc; - for (my $i = 0; $i < $MAX_USB_DEVICES; $i++) { $confdesc->{"usb$i"} = $usbdesc; } -my $unuseddesc = { - optional => 1, - type => 'string', format => 'pve-volume-id', - description => "Reference to unused volumes. This is used internally, and should not be modified manually.", -}; - -for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) { - $confdesc->{"unused$i"} = $unuseddesc; -} - my $kvm_api_version = 0; sub kvm_version { @@ -1483,21 +1141,6 @@ sub kernel_has_vhost_net { return -c '/dev/vhost-net'; } -sub valid_drive_names { - # order is important - used to autoselect boot disk - return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))), - (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))), - (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))), - (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))), - 'efidisk0'); -} - -sub is_valid_drivename { - my $dev = shift; - - return defined($drivedesc_hash->{$dev}); -} - sub option_exists { my $key = shift; return defined($confdesc->{$key}); @@ -1614,94 +1257,6 @@ sub pve_verify_hotplug_features { die "unable to parse hotplug option\n"; } -# ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]] -# [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no] -# [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop] -# [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off] -# [,iothread=on][,serial=serial][,model=model] - -sub parse_drive { - my ($key, $data) = @_; - - my ($interface, $index); - - if ($key =~ m/^([^\d]+)(\d+)$/) { - $interface = $1; - $index = $2; - } else { - return undef; - } - - my $desc = $key =~ /^unused\d+$/ ? $alldrive_fmt - : $drivedesc_hash->{$key}->{format}; - if (!$desc) { - warn "invalid drive key: $key\n"; - return undef; - } - my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) }; - return undef if !$res; - $res->{interface} = $interface; - $res->{index} = $index; - - my $error = 0; - foreach my $opt (qw(bps bps_rd bps_wr)) { - if (my $bps = defined(delete $res->{$opt})) { - if (defined($res->{"m$opt"})) { - warn "both $opt and m$opt specified\n"; - ++$error; - next; - } - $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0)); - } - } - - # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases" - for my $requirement ( - [mbps_max => 'mbps'], - [mbps_rd_max => 'mbps_rd'], - [mbps_wr_max => 'mbps_wr'], - [miops_max => 'miops'], - [miops_rd_max => 'miops_rd'], - [miops_wr_max => 'miops_wr'], - [bps_max_length => 'mbps_max'], - [bps_rd_max_length => 'mbps_rd_max'], - [bps_wr_max_length => 'mbps_wr_max'], - [iops_max_length => 'iops_max'], - [iops_rd_max_length => 'iops_rd_max'], - [iops_wr_max_length => 'iops_wr_max']) { - my ($option, $requires) = @$requirement; - if ($res->{$option} && !$res->{$requires}) { - warn "$option requires $requires\n"; - ++$error; - } - } - - return undef if $error; - - return undef if $res->{mbps_rd} && $res->{mbps}; - return undef if $res->{mbps_wr} && $res->{mbps}; - return undef if $res->{iops_rd} && $res->{iops}; - return undef if $res->{iops_wr} && $res->{iops}; - - if ($res->{media} && ($res->{media} eq 'cdrom')) { - return undef if $res->{snapshot} || $res->{trans} || $res->{format}; - return undef if $res->{heads} || $res->{secs} || $res->{cyls}; - return undef if $res->{interface} eq 'virtio'; - } - - if (my $size = $res->{size}) { - return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size)); - } - - return $res; -} - -sub print_drive { - my ($drive) = @_; - my $skip = [ 'index', 'interface' ]; - return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip); -} - sub scsi_inquiry { my($fh, $noerr) = @_; @@ -1839,7 +1394,7 @@ sub print_drivedevice_full { $device .= ",wwn=$drive->{wwn}" if $drive->{wwn}; } elsif ($drive->{interface} eq 'ide' || $drive->{interface} eq 'sata') { - my $maxdev = ($drive->{interface} eq 'sata') ? $MAX_SATA_DISKS : 2; + my $maxdev = ($drive->{interface} eq 'sata') ? $PVE::QemuServer::Drive::MAX_SATA_DISKS : 2; my $controller = int($drive->{index} / $maxdev); my $unit = $drive->{index} % $maxdev; my $devicetype = ($drive->{media} && $drive->{media} eq 'cdrom') ? "cd" : "hd"; @@ -2124,20 +1679,6 @@ sub print_vga_device { return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}"; } -sub drive_is_cloudinit { - my ($drive) = @_; - return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@; -} - -sub drive_is_cdrom { - my ($drive, $exclude_cloudinit) = @_; - - return 0 if $exclude_cloudinit && drive_is_cloudinit($drive); - - return $drive && $drive->{media} && ($drive->{media} eq 'cdrom'); - -} - sub parse_number_sets { my ($set) = @_; my $res = []; @@ -2347,17 +1888,6 @@ sub print_smbios1 { PVE::JSONSchema::register_format('pve-qm-smbios1', $smbios1_fmt); -PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk); -sub verify_bootdisk { - my ($value, $noerr) = @_; - - return $value if is_valid_drivename($value); - - return undef if $noerr; - - die "invalid boot disk '$value'\n"; -} - sub parse_watchdog { my ($value) = @_; @@ -2892,26 +2422,6 @@ sub vzlist { return $vzlist; } -sub disksize { - my ($storecfg, $conf) = @_; - - my $bootdisk = $conf->{bootdisk}; - return undef if !$bootdisk; - return undef if !is_valid_drivename($bootdisk); - - return undef if !$conf->{$bootdisk}; - - my $drive = parse_drive($bootdisk, $conf->{$bootdisk}); - return undef if !defined($drive); - - return undef if drive_is_cdrom($drive); - - my $volid = $drive->{file}; - return undef if !$volid; - - return $drive->{size}; -} - our $vmstatus_return_properties = { vmid => get_standard_option('pve-vmid'), status => { @@ -2999,7 +2509,7 @@ sub vmstatus { # fixme: better status? $d->{status} = $list->{$vmid}->{pid} ? 'running' : 'stopped'; - my $size = disksize($storecfg, $conf); + my $size = PVE::QemuServer::Drive::disksize($storecfg, $conf); if (defined($size)) { $d->{disk} = 0; # no info available $d->{maxdisk} = $size; @@ -3178,65 +2688,6 @@ sub vmstatus { return $res; } -sub foreach_drive { - my ($conf, $func, @param) = @_; - - foreach my $ds (valid_drive_names()) { - next if !defined($conf->{$ds}); - - my $drive = parse_drive($ds, $conf->{$ds}); - next if !$drive; - - &$func($ds, $drive, @param); - } -} - -sub foreach_volid { - my ($conf, $func, @param) = @_; - - my $volhash = {}; - - my $test_volid = sub { - my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_; - - return if !$volid; - - $volhash->{$volid}->{cdrom} //= 1; - $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom; - - $volhash->{$volid}->{replicate} //= 0; - $volhash->{$volid}->{replicate} = 1 if $replicate; - - $volhash->{$volid}->{shared} //= 0; - $volhash->{$volid}->{shared} = 1 if $shared; - - $volhash->{$volid}->{referenced_in_config} //= 0; - $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname); - - $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1 - if defined($snapname); - $volhash->{$volid}->{size} = $size if $size; - }; - - foreach_drive($conf, sub { - my ($ds, $drive) = @_; - $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size}); - }); - - foreach my $snapname (keys %{$conf->{snapshots}}) { - my $snap = $conf->{snapshots}->{$snapname}; - $test_volid->($snap->{vmstate}, 0, 1, $snapname); - foreach_drive($snap, sub { - my ($ds, $drive) = @_; - $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname); - }); - } - - foreach my $volid (keys %$volhash) { - &$func($volid, $volhash->{$volid}, @param); - } -} - sub conf_has_serial { my ($conf) = @_; @@ -4023,7 +3474,7 @@ sub config_to_command { } if ($drive->{interface} eq 'sata') { - my $controller = int($drive->{index} / $MAX_SATA_DISKS); + my $controller = int($drive->{index} / $PVE::QemuServer::Drive::MAX_SATA_DISKS); $pciaddr = print_pci_addr("ahci$controller", $bridges, $arch, $machine_type); push @$devices, '-device', "ahci,id=ahci$controller,multifunction=on$pciaddr" if !$ahcicontroller->{$controller}; $ahcicontroller->{$controller}=1; @@ -4492,8 +3943,8 @@ sub qemu_deletescsihw { my $devices_list = vm_devices_list($vmid); foreach my $opt (keys %{$devices_list}) { - if (PVE::QemuServer::is_valid_drivename($opt)) { - my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); + if (is_valid_drivename($opt)) { + my $drive = parse_drive($opt, $conf->{$opt}); if($drive->{interface} eq 'scsi' && $drive->{index} < (($maxdev-1)*($controller+1))) { return 1; } @@ -5005,7 +4456,7 @@ sub try_deallocate_drive { # check if the disk is really unused die "unable to delete '$volid' - volume is still in use (snapshot?)\n" - if is_volume_in_use($storecfg, $conf, $key, $volid); + if PVE::QemuServer::Drive::is_volume_in_use($storecfg, $conf, $key, $volid); PVE::Storage::vdisk_free($storecfg, $volid); return 1; } else { @@ -6113,68 +5564,6 @@ sub scan_volids { return $volid_hash; } -sub is_volume_in_use { - my ($storecfg, $conf, $skip_drive, $volid) = @_; - - my $path = PVE::Storage::path($storecfg, $volid); - - my $scan_config = sub { - my ($cref, $snapname) = @_; - - foreach my $key (keys %$cref) { - my $value = $cref->{$key}; - if (is_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); - return 1 if $volid eq $drive->{file}; - if ($drive->{file} =~ m!^/!) { - return 1 if $drive->{file} eq $path; - } 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; - return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname); - } - } - } - - return 0; - }; - - return 1 if &$scan_config($conf); - - undef $skip_drive; - - foreach my $snapname (keys %{$conf->{snapshots}}) { - return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname); - } - - return 0; -} - -sub update_disksize { - my ($drive, $volid_hash) = @_; - - my $volid = $drive->{file}; - return undef if !defined($volid); - - my $oldsize = $drive->{size}; - my $newsize = $volid_hash->{$volid}->{size}; - - if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) { - $drive->{size} = $newsize; - - my $old_fmt = PVE::JSONSchema::format_size($oldsize); - my $new_fmt = PVE::JSONSchema::format_size($newsize); - - return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive; - } - - return undef; -} - sub update_disk_config { my ($vmid, $conf, $volid_hash) = @_; @@ -6207,7 +5596,7 @@ sub update_disk_config { next if drive_is_cdrom($drive); next if !$volid_hash->{$volid}; - my ($updated, $old_size, $new_size) = update_disksize($drive, $volid_hash); + my ($updated, $old_size, $new_size) = PVE::QemuServer::Drive::update_disksize($drive, $volid_hash); if (defined($updated)) { $changes = 1; $conf->{$opt} = print_drive($updated); @@ -6850,7 +6239,7 @@ sub qemu_img_convert { $cachemode = 'none' if $src_scfg->{type} eq 'zfspool'; } elsif (-f $src_volid) { $src_path = $src_volid; - if ($src_path =~ m/\.($QEMU_FORMAT_RE)$/) { + if ($src_path =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) { $src_format = $1; } } @@ -6911,7 +6300,7 @@ sub qemu_img_convert { sub qemu_img_format { my ($scfg, $volname) = @_; - if ($scfg->{path} && $volname =~ m/\.($QEMU_FORMAT_RE)$/) { + if ($scfg->{path} && $volname =~ m/\.($PVE::QemuServer::Drive::QEMU_FORMAT_RE)$/) { return $1; } else { return "raw"; @@ -7271,19 +6660,6 @@ sub resolve_dst_disk_format { return $format; } -sub resolve_first_disk { - my $conf = shift; - my @disks = PVE::QemuServer::valid_drive_names(); - my $firstdisk; - foreach my $ds (reverse @disks) { - next if !$conf->{$ds}; - my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds}); - next if PVE::QemuServer::drive_is_cdrom($disk); - $firstdisk = $ds; - } - return $firstdisk; -} - # NOTE: if this logic changes, please update docs & possibly gui logic sub find_vmstate_storage { my ($conf, $storecfg) = @_; diff --git a/PVE/QemuServer/Drive.pm b/PVE/QemuServer/Drive.pm new file mode 100644 index 00000000..ec4b6caf --- /dev/null +++ b/PVE/QemuServer/Drive.pm @@ -0,0 +1,645 @@ +package PVE::QemuServer::Drive; + +use strict; +use warnings; + +use PVE::Storage; +use PVE::JSONSchema qw(get_standard_option); + +use base qw(Exporter); + +our @EXPORT_OK = qw( +is_valid_drivename +drive_is_cloudinit +drive_is_cdrom +parse_drive +print_drive +foreach_drive +foreach_volid +); + +our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/; + +PVE::JSONSchema::register_standard_option('pve-qm-image-format', { + type => 'string', + enum => [qw(raw cow qcow qed qcow2 vmdk cloop)], + description => "The drive's backing file's data format.", + optional => 1, +}); + +my $MAX_IDE_DISKS = 4; +my $MAX_SCSI_DISKS = 31; +my $MAX_VIRTIO_DISKS = 16; +our $MAX_SATA_DISKS = 6; +our $MAX_UNUSED_DISKS = 256; + +our $drivedesc_hash; + +my %drivedesc_base = ( + volume => { alias => 'file' }, + file => { + type => 'string', + format => 'pve-volume-id-or-qm-path', + default_key => 1, + format_description => 'volume', + description => "The drive's backing volume.", + }, + media => { + type => 'string', + enum => [qw(cdrom disk)], + description => "The drive's media type.", + default => 'disk', + optional => 1 + }, + cyls => { + type => 'integer', + description => "Force the drive's physical geometry to have a specific cylinder count.", + optional => 1 + }, + heads => { + type => 'integer', + description => "Force the drive's physical geometry to have a specific head count.", + optional => 1 + }, + secs => { + type => 'integer', + description => "Force the drive's physical geometry to have a specific sector count.", + optional => 1 + }, + trans => { + type => 'string', + enum => [qw(none lba auto)], + description => "Force disk geometry bios translation mode.", + optional => 1, + }, + snapshot => { + type => 'boolean', + description => "Controls qemu's snapshot mode feature." + . " If activated, changes made to the disk are temporary and will" + . " be discarded when the VM is shutdown.", + optional => 1, + }, + cache => { + type => 'string', + enum => [qw(none writethrough writeback unsafe directsync)], + description => "The drive's cache mode", + optional => 1, + }, + format => get_standard_option('pve-qm-image-format'), + size => { + type => 'string', + format => 'disk-size', + format_description => 'DiskSize', + description => "Disk size. This is purely informational and has no effect.", + optional => 1, + }, + backup => { + type => 'boolean', + description => "Whether the drive should be included when making backups.", + optional => 1, + }, + replicate => { + type => 'boolean', + description => 'Whether the drive should considered for replication jobs.', + optional => 1, + default => 1, + }, + rerror => { + type => 'string', + enum => [qw(ignore report stop)], + description => 'Read error action.', + optional => 1, + }, + werror => { + type => 'string', + enum => [qw(enospc ignore report stop)], + description => 'Write error action.', + optional => 1, + }, + aio => { + type => 'string', + enum => [qw(native threads)], + description => 'AIO type to use.', + optional => 1, + }, + discard => { + type => 'string', + enum => [qw(ignore on)], + description => 'Controls whether to pass discard/trim requests to the underlying storage.', + optional => 1, + }, + detect_zeroes => { + type => 'boolean', + description => 'Controls whether to detect and try to optimize writes of zeroes.', + optional => 1, + }, + serial => { + type => 'string', + format => 'urlencoded', + format_description => 'serial', + maxLength => 20*3, # *3 since it's %xx url enoded + description => "The drive's reported serial number, url-encoded, up to 20 bytes long.", + optional => 1, + }, + shared => { + type => 'boolean', + description => 'Mark this locally-managed volume as available on all nodes', + verbose_description => "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!", + optional => 1, + default => 0, + } +); + +my %iothread_fmt = ( iothread => { + type => 'boolean', + description => "Whether to use iothreads for this drive", + optional => 1, +}); + +my %model_fmt = ( + model => { + type => 'string', + format => 'urlencoded', + format_description => 'model', + maxLength => 40*3, # *3 since it's %xx url enoded + description => "The drive's reported model name, url-encoded, up to 40 bytes long.", + optional => 1, + }, +); + +my %queues_fmt = ( + queues => { + type => 'integer', + description => "Number of queues.", + minimum => 2, + optional => 1 + } +); + +my %scsiblock_fmt = ( + scsiblock => { + type => 'boolean', + description => "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host", + optional => 1, + default => 0, + }, +); + +my %ssd_fmt = ( + ssd => { + type => 'boolean', + description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.", + optional => 1, + }, +); + +my %wwn_fmt = ( + wwn => { + type => 'string', + pattern => qr/^(0x)[0-9a-fA-F]{16}/, + format_description => 'wwn', + description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.", + optional => 1, + }, +); + +my $add_throttle_desc = sub { + my ($key, $type, $what, $unit, $longunit, $minimum) = @_; + my $d = { + type => $type, + format_description => $unit, + description => "Maximum $what in $longunit.", + optional => 1, + }; + $d->{minimum} = $minimum if defined($minimum); + $drivedesc_base{$key} = $d; +}; +# throughput: (leaky bucket) +$add_throttle_desc->('bps', 'integer', 'r/w speed', 'bps', 'bytes per second'); +$add_throttle_desc->('bps_rd', 'integer', 'read speed', 'bps', 'bytes per second'); +$add_throttle_desc->('bps_wr', 'integer', 'write speed', 'bps', 'bytes per second'); +$add_throttle_desc->('mbps', 'number', 'r/w speed', 'mbps', 'megabytes per second'); +$add_throttle_desc->('mbps_rd', 'number', 'read speed', 'mbps', 'megabytes per second'); +$add_throttle_desc->('mbps_wr', 'number', 'write speed', 'mbps', 'megabytes per second'); +$add_throttle_desc->('iops', 'integer', 'r/w I/O', 'iops', 'operations per second'); +$add_throttle_desc->('iops_rd', 'integer', 'read I/O', 'iops', 'operations per second'); +$add_throttle_desc->('iops_wr', 'integer', 'write I/O', 'iops', 'operations per second'); + +# pools: (pool of IO before throttling starts taking effect) +$add_throttle_desc->('mbps_max', 'number', 'unthrottled r/w pool', 'mbps', 'megabytes per second'); +$add_throttle_desc->('mbps_rd_max', 'number', 'unthrottled read pool', 'mbps', 'megabytes per second'); +$add_throttle_desc->('mbps_wr_max', 'number', 'unthrottled write pool', 'mbps', 'megabytes per second'); +$add_throttle_desc->('iops_max', 'integer', 'unthrottled r/w I/O pool', 'iops', 'operations per second'); +$add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool', 'iops', 'operations per second'); +$add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second'); + +# burst lengths +$add_throttle_desc->('bps_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1); +$add_throttle_desc->('bps_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1); +$add_throttle_desc->('bps_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1); +$add_throttle_desc->('iops_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1); +$add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1); +$add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1); + +# legacy support +$drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' }; +$drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' }; +$drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' }; +$drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' }; + +my $ide_fmt = { + %drivedesc_base, + %model_fmt, + %ssd_fmt, + %wwn_fmt, +}; +PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt); + +my $idedesc = { + optional => 1, + type => 'string', format => $ide_fmt, + description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").", +}; +PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc); + +my $scsi_fmt = { + %drivedesc_base, + %iothread_fmt, + %queues_fmt, + %scsiblock_fmt, + %ssd_fmt, + %wwn_fmt, +}; +my $scsidesc = { + optional => 1, + type => 'string', format => $scsi_fmt, + description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").", +}; +PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc); + +my $sata_fmt = { + %drivedesc_base, + %ssd_fmt, + %wwn_fmt, +}; +my $satadesc = { + optional => 1, + type => 'string', format => $sata_fmt, + description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").", +}; +PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc); + +my $virtio_fmt = { + %drivedesc_base, + %iothread_fmt, +}; +my $virtiodesc = { + optional => 1, + type => 'string', format => $virtio_fmt, + description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").", +}; +PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc); + +my $alldrive_fmt = { + %drivedesc_base, + %iothread_fmt, + %model_fmt, + %queues_fmt, + %scsiblock_fmt, + %ssd_fmt, + %wwn_fmt, +}; + +my $efidisk_fmt = { + volume => { alias => 'file' }, + file => { + type => 'string', + format => 'pve-volume-id-or-qm-path', + default_key => 1, + format_description => 'volume', + description => "The drive's backing volume.", + }, + format => get_standard_option('pve-qm-image-format'), + size => { + type => 'string', + format => 'disk-size', + format_description => 'DiskSize', + description => "Disk size. This is purely informational and has no effect.", + optional => 1, + }, +}; + +my $efidisk_desc = { + optional => 1, + type => 'string', format => $efidisk_fmt, + description => "Configure a Disk for storing EFI vars", +}; + +PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc); + +for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) { + $drivedesc_hash->{"ide$i"} = $idedesc; +} + +for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) { + $drivedesc_hash->{"sata$i"} = $satadesc; +} + +for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) { + $drivedesc_hash->{"scsi$i"} = $scsidesc; +} + +for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) { + $drivedesc_hash->{"virtio$i"} = $virtiodesc; +} + +$drivedesc_hash->{efidisk0} = $efidisk_desc; + +our $unuseddesc = { + optional => 1, + type => 'string', format => 'pve-volume-id', + description => "Reference to unused volumes. This is used internally, and should not be modified manually.", +}; + +sub valid_drive_names { + # order is important - used to autoselect boot disk + return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))), + (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))), + (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))), + (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))), + 'efidisk0'); +} + +sub is_valid_drivename { + my $dev = shift; + + return defined($drivedesc_hash->{$dev}); +} + +PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk); +sub verify_bootdisk { + my ($value, $noerr) = @_; + + return $value if is_valid_drivename($value); + + return undef if $noerr; + + die "invalid boot disk '$value'\n"; +} + +sub drive_is_cloudinit { + my ($drive) = @_; + return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@; +} + +sub drive_is_cdrom { + my ($drive, $exclude_cloudinit) = @_; + + return 0 if $exclude_cloudinit && drive_is_cloudinit($drive); + + return $drive && $drive->{media} && ($drive->{media} eq 'cdrom'); +} + +# ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]] +# [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no] +# [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop] +# [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off] +# [,iothread=on][,serial=serial][,model=model] + +sub parse_drive { + my ($key, $data) = @_; + + my ($interface, $index); + + if ($key =~ m/^([^\d]+)(\d+)$/) { + $interface = $1; + $index = $2; + } else { + return undef; + } + + my $desc = $key =~ /^unused\d+$/ ? $alldrive_fmt + : $drivedesc_hash->{$key}->{format}; + if (!$desc) { + warn "invalid drive key: $key\n"; + return undef; + } + my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) }; + return undef if !$res; + $res->{interface} = $interface; + $res->{index} = $index; + + my $error = 0; + foreach my $opt (qw(bps bps_rd bps_wr)) { + if (my $bps = defined(delete $res->{$opt})) { + if (defined($res->{"m$opt"})) { + warn "both $opt and m$opt specified\n"; + ++$error; + next; + } + $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0)); + } + } + + # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases" + for my $requirement ( + [mbps_max => 'mbps'], + [mbps_rd_max => 'mbps_rd'], + [mbps_wr_max => 'mbps_wr'], + [miops_max => 'miops'], + [miops_rd_max => 'miops_rd'], + [miops_wr_max => 'miops_wr'], + [bps_max_length => 'mbps_max'], + [bps_rd_max_length => 'mbps_rd_max'], + [bps_wr_max_length => 'mbps_wr_max'], + [iops_max_length => 'iops_max'], + [iops_rd_max_length => 'iops_rd_max'], + [iops_wr_max_length => 'iops_wr_max']) { + my ($option, $requires) = @$requirement; + if ($res->{$option} && !$res->{$requires}) { + warn "$option requires $requires\n"; + ++$error; + } + } + + return undef if $error; + + return undef if $res->{mbps_rd} && $res->{mbps}; + return undef if $res->{mbps_wr} && $res->{mbps}; + return undef if $res->{iops_rd} && $res->{iops}; + return undef if $res->{iops_wr} && $res->{iops}; + + if ($res->{media} && ($res->{media} eq 'cdrom')) { + return undef if $res->{snapshot} || $res->{trans} || $res->{format}; + return undef if $res->{heads} || $res->{secs} || $res->{cyls}; + return undef if $res->{interface} eq 'virtio'; + } + + if (my $size = $res->{size}) { + return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size)); + } + + return $res; +} + +sub print_drive { + my ($drive) = @_; + my $skip = [ 'index', 'interface' ]; + return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip); +} + +sub foreach_drive { + my ($conf, $func, @param) = @_; + + foreach my $ds (valid_drive_names()) { + next if !defined($conf->{$ds}); + + my $drive = parse_drive($ds, $conf->{$ds}); + next if !$drive; + + &$func($ds, $drive, @param); + } +} + +sub foreach_volid { + my ($conf, $func, @param) = @_; + + my $volhash = {}; + + my $test_volid = sub { + my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_; + + return if !$volid; + + $volhash->{$volid}->{cdrom} //= 1; + $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom; + + $volhash->{$volid}->{replicate} //= 0; + $volhash->{$volid}->{replicate} = 1 if $replicate; + + $volhash->{$volid}->{shared} //= 0; + $volhash->{$volid}->{shared} = 1 if $shared; + + $volhash->{$volid}->{referenced_in_config} //= 0; + $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname); + + $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1 + if defined($snapname); + $volhash->{$volid}->{size} = $size if $size; + }; + + foreach_drive($conf, sub { + my ($ds, $drive) = @_; + $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size}); + }); + + foreach my $snapname (keys %{$conf->{snapshots}}) { + my $snap = $conf->{snapshots}->{$snapname}; + $test_volid->($snap->{vmstate}, 0, 1, $snapname); + foreach_drive($snap, sub { + my ($ds, $drive) = @_; + $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname); + }); + } + + foreach my $volid (keys %$volhash) { + &$func($volid, $volhash->{$volid}, @param); + } +} + +sub disksize { + my ($storecfg, $conf) = @_; + + my $bootdisk = $conf->{bootdisk}; + return undef if !$bootdisk; + return undef if !is_valid_drivename($bootdisk); + + return undef if !$conf->{$bootdisk}; + + my $drive = parse_drive($bootdisk, $conf->{$bootdisk}); + return undef if !defined($drive); + + return undef if drive_is_cdrom($drive); + + my $volid = $drive->{file}; + return undef if !$volid; + + return $drive->{size}; +} + +sub update_disksize { + my ($drive, $volid_hash) = @_; + + my $volid = $drive->{file}; + return undef if !defined($volid); + + my $oldsize = $drive->{size}; + my $newsize = $volid_hash->{$volid}->{size}; + + if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) { + $drive->{size} = $newsize; + + my $old_fmt = PVE::JSONSchema::format_size($oldsize); + my $new_fmt = PVE::JSONSchema::format_size($newsize); + + return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive; + } + + return undef; +} + +sub is_volume_in_use { + my ($storecfg, $conf, $skip_drive, $volid) = @_; + + my $path = PVE::Storage::path($storecfg, $volid); + + my $scan_config = sub { + my ($cref, $snapname) = @_; + + foreach my $key (keys %$cref) { + my $value = $cref->{$key}; + if (is_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); + return 1 if $volid eq $drive->{file}; + if ($drive->{file} =~ m!^/!) { + return 1 if $drive->{file} eq $path; + } 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; + return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname); + } + } + } + + return 0; + }; + + return 1 if &$scan_config($conf); + + undef $skip_drive; + + foreach my $snapname (keys %{$conf->{snapshots}}) { + return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname); + } + + return 0; +} + +sub resolve_first_disk { + my $conf = shift; + my @disks = valid_drive_names(); + my $firstdisk; + foreach my $ds (reverse @disks) { + next if !$conf->{$ds}; + my $disk = parse_drive($ds, $conf->{$ds}); + next if drive_is_cdrom($disk); + $firstdisk = $ds; + } + return $firstdisk; +} + +1; diff --git a/PVE/QemuServer/Makefile b/PVE/QemuServer/Makefile index 6a49626d..fd8cfbbe 100644 --- a/PVE/QemuServer/Makefile +++ b/PVE/QemuServer/Makefile @@ -9,6 +9,7 @@ SOURCES=PCI.pm \ Monitor.pm \ Machine.pm \ CPUConfig.pm \ + Drive.pm \ .PHONY: install install: ${SOURCES} -- 2.39.5