]> git.proxmox.com Git - qemu-server.git/blob - PVE/QemuServer/Drive.pm
bump version to 8.2.1
[qemu-server.git] / PVE / QemuServer / Drive.pm
1 package PVE::QemuServer::Drive;
2
3 use strict;
4 use warnings;
5
6 use Storable qw(dclone);
7
8 use IO::File;
9
10 use PVE::Storage;
11 use PVE::JSONSchema qw(get_standard_option);
12
13 use base qw(Exporter);
14
15 our @EXPORT_OK = qw(
16 is_valid_drivename
17 drive_is_cloudinit
18 drive_is_cdrom
19 drive_is_read_only
20 get_scsi_devicetype
21 parse_drive
22 print_drive
23 );
24
25 our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
26
27 PVE::JSONSchema::register_standard_option('pve-qm-image-format', {
28 type => 'string',
29 enum => [qw(raw cow qcow qed qcow2 vmdk cloop)],
30 description => "The drive's backing file's data format.",
31 optional => 1,
32 });
33
34 my $MAX_IDE_DISKS = 4;
35 my $MAX_SCSI_DISKS = 31;
36 my $MAX_VIRTIO_DISKS = 16;
37 our $MAX_SATA_DISKS = 6;
38 our $MAX_UNUSED_DISKS = 256;
39 our $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
40
41 our $drivedesc_hash;
42 # Schema when disk allocation is possible.
43 our $drivedesc_hash_with_alloc = {};
44
45 my %drivedesc_base = (
46 volume => { alias => 'file' },
47 file => {
48 type => 'string',
49 format => 'pve-volume-id-or-qm-path',
50 default_key => 1,
51 format_description => 'volume',
52 description => "The drive's backing volume.",
53 },
54 media => {
55 type => 'string',
56 enum => [qw(cdrom disk)],
57 description => "The drive's media type.",
58 default => 'disk',
59 optional => 1
60 },
61 cyls => {
62 type => 'integer',
63 description => "Force the drive's physical geometry to have a specific cylinder count.",
64 optional => 1
65 },
66 heads => {
67 type => 'integer',
68 description => "Force the drive's physical geometry to have a specific head count.",
69 optional => 1
70 },
71 secs => {
72 type => 'integer',
73 description => "Force the drive's physical geometry to have a specific sector count.",
74 optional => 1
75 },
76 trans => {
77 type => 'string',
78 enum => [qw(none lba auto)],
79 description => "Force disk geometry bios translation mode.",
80 optional => 1,
81 },
82 snapshot => {
83 type => 'boolean',
84 description => "Controls qemu's snapshot mode feature."
85 . " If activated, changes made to the disk are temporary and will"
86 . " be discarded when the VM is shutdown.",
87 optional => 1,
88 },
89 cache => {
90 type => 'string',
91 enum => [qw(none writethrough writeback unsafe directsync)],
92 description => "The drive's cache mode",
93 optional => 1,
94 },
95 format => get_standard_option('pve-qm-image-format'),
96 size => {
97 type => 'string',
98 format => 'disk-size',
99 format_description => 'DiskSize',
100 description => "Disk size. This is purely informational and has no effect.",
101 optional => 1,
102 },
103 backup => {
104 type => 'boolean',
105 description => "Whether the drive should be included when making backups.",
106 optional => 1,
107 },
108 replicate => {
109 type => 'boolean',
110 description => 'Whether the drive should considered for replication jobs.',
111 optional => 1,
112 default => 1,
113 },
114 rerror => {
115 type => 'string',
116 enum => [qw(ignore report stop)],
117 description => 'Read error action.',
118 optional => 1,
119 },
120 werror => {
121 type => 'string',
122 enum => [qw(enospc ignore report stop)],
123 description => 'Write error action.',
124 optional => 1,
125 },
126 aio => {
127 type => 'string',
128 enum => [qw(native threads io_uring)],
129 description => 'AIO type to use.',
130 optional => 1,
131 },
132 discard => {
133 type => 'string',
134 enum => [qw(ignore on)],
135 description => 'Controls whether to pass discard/trim requests to the underlying storage.',
136 optional => 1,
137 },
138 detect_zeroes => {
139 type => 'boolean',
140 description => 'Controls whether to detect and try to optimize writes of zeroes.',
141 optional => 1,
142 },
143 serial => {
144 type => 'string',
145 format => 'urlencoded',
146 format_description => 'serial',
147 maxLength => 20*3, # *3 since it's %xx url enoded
148 description => "The drive's reported serial number, url-encoded, up to 20 bytes long.",
149 optional => 1,
150 },
151 shared => {
152 type => 'boolean',
153 description => 'Mark this locally-managed volume as available on all nodes',
154 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!",
155 optional => 1,
156 default => 0,
157 }
158 );
159
160 my %iothread_fmt = ( iothread => {
161 type => 'boolean',
162 description => "Whether to use iothreads for this drive",
163 optional => 1,
164 });
165
166 my %product_fmt = (
167 product => {
168 type => 'string',
169 pattern => '[A-Za-z0-9\-_\s]{,16}', # QEMU (8.1) will quietly only use 16 bytes
170 format_description => 'product',
171 description => "The drive's product name, up to 16 bytes long.",
172 optional => 1,
173 },
174 );
175
176 my %vendor_fmt = (
177 vendor => {
178 type => 'string',
179 pattern => '[A-Za-z0-9\-_\s]{,8}', # QEMU (8.1) will quietly only use 8 bytes
180 format_description => 'vendor',
181 description => "The drive's vendor name, up to 8 bytes long.",
182 optional => 1,
183 },
184 );
185
186 my %model_fmt = (
187 model => {
188 type => 'string',
189 format => 'urlencoded',
190 format_description => 'model',
191 maxLength => 40*3, # *3 since it's %xx url enoded
192 description => "The drive's reported model name, url-encoded, up to 40 bytes long.",
193 optional => 1,
194 },
195 );
196
197 my %queues_fmt = (
198 queues => {
199 type => 'integer',
200 description => "Number of queues.",
201 minimum => 2,
202 optional => 1
203 }
204 );
205
206 my %readonly_fmt = (
207 ro => {
208 type => 'boolean',
209 description => "Whether the drive is read-only.",
210 optional => 1,
211 },
212 );
213
214 my %scsiblock_fmt = (
215 scsiblock => {
216 type => 'boolean',
217 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",
218 optional => 1,
219 default => 0,
220 },
221 );
222
223 my %ssd_fmt = (
224 ssd => {
225 type => 'boolean',
226 description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.",
227 optional => 1,
228 },
229 );
230
231 my %wwn_fmt = (
232 wwn => {
233 type => 'string',
234 pattern => qr/^(0x)[0-9a-fA-F]{16}/,
235 format_description => 'wwn',
236 description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.",
237 optional => 1,
238 },
239 );
240
241 my $add_throttle_desc = sub {
242 my ($key, $type, $what, $unit, $longunit, $minimum) = @_;
243 my $d = {
244 type => $type,
245 format_description => $unit,
246 description => "Maximum $what in $longunit.",
247 optional => 1,
248 };
249 $d->{minimum} = $minimum if defined($minimum);
250 $drivedesc_base{$key} = $d;
251 };
252 # throughput: (leaky bucket)
253 $add_throttle_desc->('bps', 'integer', 'r/w speed', 'bps', 'bytes per second');
254 $add_throttle_desc->('bps_rd', 'integer', 'read speed', 'bps', 'bytes per second');
255 $add_throttle_desc->('bps_wr', 'integer', 'write speed', 'bps', 'bytes per second');
256 $add_throttle_desc->('mbps', 'number', 'r/w speed', 'mbps', 'megabytes per second');
257 $add_throttle_desc->('mbps_rd', 'number', 'read speed', 'mbps', 'megabytes per second');
258 $add_throttle_desc->('mbps_wr', 'number', 'write speed', 'mbps', 'megabytes per second');
259 $add_throttle_desc->('iops', 'integer', 'r/w I/O', 'iops', 'operations per second');
260 $add_throttle_desc->('iops_rd', 'integer', 'read I/O', 'iops', 'operations per second');
261 $add_throttle_desc->('iops_wr', 'integer', 'write I/O', 'iops', 'operations per second');
262
263 # pools: (pool of IO before throttling starts taking effect)
264 $add_throttle_desc->('mbps_max', 'number', 'unthrottled r/w pool', 'mbps', 'megabytes per second');
265 $add_throttle_desc->('mbps_rd_max', 'number', 'unthrottled read pool', 'mbps', 'megabytes per second');
266 $add_throttle_desc->('mbps_wr_max', 'number', 'unthrottled write pool', 'mbps', 'megabytes per second');
267 $add_throttle_desc->('iops_max', 'integer', 'unthrottled r/w I/O pool', 'iops', 'operations per second');
268 $add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool', 'iops', 'operations per second');
269 $add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second');
270
271 # burst lengths
272 $add_throttle_desc->('bps_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
273 $add_throttle_desc->('bps_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
274 $add_throttle_desc->('bps_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
275 $add_throttle_desc->('iops_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
276 $add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
277 $add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
278
279 # legacy support
280 $drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' };
281 $drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' };
282 $drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' };
283 $drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' };
284
285 my $ide_fmt = {
286 %drivedesc_base,
287 %model_fmt,
288 %ssd_fmt,
289 %wwn_fmt,
290 };
291 PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
292
293 my $idedesc = {
294 optional => 1,
295 type => 'string', format => $ide_fmt,
296 description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS - 1) . ").",
297 };
298 PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
299
300 my $scsi_fmt = {
301 %drivedesc_base,
302 %iothread_fmt,
303 %product_fmt,
304 %queues_fmt,
305 %readonly_fmt,
306 %scsiblock_fmt,
307 %ssd_fmt,
308 %vendor_fmt,
309 %wwn_fmt,
310 };
311 my $scsidesc = {
312 optional => 1,
313 type => 'string', format => $scsi_fmt,
314 description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
315 };
316 PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
317
318 my $sata_fmt = {
319 %drivedesc_base,
320 %ssd_fmt,
321 %wwn_fmt,
322 };
323 my $satadesc = {
324 optional => 1,
325 type => 'string', format => $sata_fmt,
326 description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
327 };
328 PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
329
330 my $virtio_fmt = {
331 %drivedesc_base,
332 %iothread_fmt,
333 %readonly_fmt,
334 };
335 my $virtiodesc = {
336 optional => 1,
337 type => 'string', format => $virtio_fmt,
338 description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
339 };
340 PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
341
342 my %efitype_fmt = (
343 efitype => {
344 type => 'string',
345 enum => [qw(2m 4m)],
346 description => "Size and type of the OVMF EFI vars. '4m' is newer and recommended,"
347 . " and required for Secure Boot. For backwards compatibility, '2m' is used"
348 . " if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).",
349 optional => 1,
350 default => '2m',
351 },
352 'pre-enrolled-keys' => {
353 type => 'boolean',
354 description => "Use am EFI vars template with distribution-specific and Microsoft Standard"
355 ." keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by"
356 ." default, though it can still be turned off from within the VM.",
357 optional => 1,
358 default => 0,
359 },
360 );
361
362 my $efidisk_fmt = {
363 volume => { alias => 'file' },
364 file => {
365 type => 'string',
366 format => 'pve-volume-id-or-qm-path',
367 default_key => 1,
368 format_description => 'volume',
369 description => "The drive's backing volume.",
370 },
371 format => get_standard_option('pve-qm-image-format'),
372 size => {
373 type => 'string',
374 format => 'disk-size',
375 format_description => 'DiskSize',
376 description => "Disk size. This is purely informational and has no effect.",
377 optional => 1,
378 },
379 %efitype_fmt,
380 };
381
382 my $efidisk_desc = {
383 optional => 1,
384 type => 'string', format => $efidisk_fmt,
385 description => "Configure a disk for storing EFI vars.",
386 };
387
388 PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
389
390 my %tpmversion_fmt = (
391 version => {
392 type => 'string',
393 enum => [qw(v1.2 v2.0)],
394 description => "The TPM interface version. v2.0 is newer and should be preferred."
395 ." Note that this cannot be changed later on.",
396 optional => 1,
397 default => 'v2.0',
398 },
399 );
400 my $tpmstate_fmt = {
401 volume => { alias => 'file' },
402 file => {
403 type => 'string',
404 format => 'pve-volume-id-or-qm-path',
405 default_key => 1,
406 format_description => 'volume',
407 description => "The drive's backing volume.",
408 },
409 size => {
410 type => 'string',
411 format => 'disk-size',
412 format_description => 'DiskSize',
413 description => "Disk size. This is purely informational and has no effect.",
414 optional => 1,
415 },
416 %tpmversion_fmt,
417 };
418 my $tpmstate_desc = {
419 optional => 1,
420 type => 'string', format => $tpmstate_fmt,
421 description => "Configure a Disk for storing TPM state. The format is fixed to 'raw'.",
422 };
423 use constant TPMSTATE_DISK_SIZE => 4 * 1024 * 1024;
424
425 my $alldrive_fmt = {
426 %drivedesc_base,
427 %iothread_fmt,
428 %model_fmt,
429 %product_fmt,
430 %queues_fmt,
431 %readonly_fmt,
432 %scsiblock_fmt,
433 %ssd_fmt,
434 %vendor_fmt,
435 %wwn_fmt,
436 %tpmversion_fmt,
437 %efitype_fmt,
438 };
439
440 my %import_from_fmt = (
441 'import-from' => {
442 type => 'string',
443 format => 'pve-volume-id-or-absolute-path',
444 format_description => 'source volume',
445 description => "Create a new disk, importing from this source (volume ID or absolute ".
446 "path). When an absolute path is specified, it's up to you to ensure that the source ".
447 "is not actively used by another process during the import!",
448 optional => 1,
449 },
450 );
451
452 my $alldrive_fmt_with_alloc = {
453 %$alldrive_fmt,
454 %import_from_fmt,
455 };
456
457 my $unused_fmt = {
458 volume => { alias => 'file' },
459 file => {
460 type => 'string',
461 format => 'pve-volume-id',
462 default_key => 1,
463 format_description => 'volume',
464 description => "The drive's backing volume.",
465 },
466 };
467
468 my $unuseddesc = {
469 optional => 1,
470 type => 'string', format => $unused_fmt,
471 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
472 };
473
474 my $with_alloc_desc_cache = {
475 unused => $unuseddesc, # Allocation for unused is not supported currently.
476 };
477 my $desc_with_alloc = sub {
478 my ($type, $desc) = @_;
479
480 return $with_alloc_desc_cache->{$type} if $with_alloc_desc_cache->{$type};
481
482 my $new_desc = dclone($desc);
483
484 $new_desc->{format}->{'import-from'} = $import_from_fmt{'import-from'};
485
486 my $extra_note = '';
487 if ($type eq 'efidisk') {
488 $extra_note = " Note that SIZE_IN_GiB is ignored here and that the default EFI vars are ".
489 "copied to the volume instead.";
490 } elsif ($type eq 'tpmstate') {
491 $extra_note = " Note that SIZE_IN_GiB is ignored here and 4 MiB will be used instead.";
492 }
493
494 $new_desc->{description} .= " Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new ".
495 "volume.${extra_note} Use STORAGE_ID:0 and the 'import-from' parameter to import from an ".
496 "existing volume.";
497
498 $with_alloc_desc_cache->{$type} = $new_desc;
499
500 return $new_desc;
501 };
502
503 for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) {
504 $drivedesc_hash->{"ide$i"} = $idedesc;
505 $drivedesc_hash_with_alloc->{"ide$i"} = $desc_with_alloc->('ide', $idedesc);
506 }
507
508 for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) {
509 $drivedesc_hash->{"sata$i"} = $satadesc;
510 $drivedesc_hash_with_alloc->{"sata$i"} = $desc_with_alloc->('sata', $satadesc);
511 }
512
513 for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) {
514 $drivedesc_hash->{"scsi$i"} = $scsidesc;
515 $drivedesc_hash_with_alloc->{"scsi$i"} = $desc_with_alloc->('scsi', $scsidesc);
516 }
517
518 for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) {
519 $drivedesc_hash->{"virtio$i"} = $virtiodesc;
520 $drivedesc_hash_with_alloc->{"virtio$i"} = $desc_with_alloc->('virtio', $virtiodesc);
521 }
522
523 $drivedesc_hash->{efidisk0} = $efidisk_desc;
524 $drivedesc_hash_with_alloc->{efidisk0} = $desc_with_alloc->('efidisk', $efidisk_desc);
525
526 $drivedesc_hash->{tpmstate0} = $tpmstate_desc;
527 $drivedesc_hash_with_alloc->{tpmstate0} = $desc_with_alloc->('tpmstate', $tpmstate_desc);
528
529 for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
530 $drivedesc_hash->{"unused$i"} = $unuseddesc;
531 $drivedesc_hash_with_alloc->{"unused$i"} = $desc_with_alloc->('unused', $unuseddesc);
532 }
533
534 sub valid_drive_names_for_boot {
535 return grep { $_ ne 'efidisk0' && $_ ne 'tpmstate0' } valid_drive_names();
536 }
537
538 sub valid_drive_names {
539 # order is important - used to autoselect boot disk
540 return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
541 (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
542 (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
543 (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
544 'efidisk0',
545 'tpmstate0');
546 }
547
548 sub valid_drive_names_with_unused {
549 return (valid_drive_names(), map {"unused$_"} (0 .. ($MAX_UNUSED_DISKS - 1)));
550 }
551
552 sub is_valid_drivename {
553 my $dev = shift;
554
555 return defined($drivedesc_hash->{$dev}) && $dev !~ /^unused\d+$/;
556 }
557
558 PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
559 sub verify_bootdisk {
560 my ($value, $noerr) = @_;
561
562 return $value if is_valid_drivename($value);
563
564 return if $noerr;
565
566 die "invalid boot disk '$value'\n";
567 }
568
569 sub drive_is_cloudinit {
570 my ($drive) = @_;
571 return $drive->{file} =~ m@[:/](?:vm-\d+-)?cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
572 }
573
574 sub drive_is_cdrom {
575 my ($drive, $exclude_cloudinit) = @_;
576
577 return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
578
579 return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
580 }
581
582 sub drive_is_read_only {
583 my ($conf, $drive) = @_;
584
585 return 0 if !PVE::QemuConfig->is_template($conf);
586
587 # don't support being marked read-only
588 return $drive->{interface} ne 'sata' && $drive->{interface} ne 'ide';
589 }
590
591 # ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
592 # [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
593 # [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
594 # [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
595 # [,iothread=on][,serial=serial][,model=model]
596
597 sub parse_drive {
598 my ($key, $data, $with_alloc) = @_;
599
600 my ($interface, $index);
601
602 if ($key =~ m/^([^\d]+)(\d+)$/) {
603 $interface = $1;
604 $index = $2;
605 } else {
606 return;
607 }
608
609 my $desc_hash = $with_alloc ? $drivedesc_hash_with_alloc : $drivedesc_hash;
610
611 if (!defined($desc_hash->{$key})) {
612 warn "invalid drive key: $key\n";
613 return;
614 }
615
616 my $desc = $desc_hash->{$key}->{format};
617 my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
618 return if !$res;
619 $res->{interface} = $interface;
620 $res->{index} = $index;
621
622 my $error = 0;
623 foreach my $opt (qw(bps bps_rd bps_wr)) {
624 if (my $bps = defined(delete $res->{$opt})) {
625 if (defined($res->{"m$opt"})) {
626 warn "both $opt and m$opt specified\n";
627 ++$error;
628 next;
629 }
630 $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
631 }
632 }
633
634 # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
635 for my $requirement (
636 [mbps_max => 'mbps'],
637 [mbps_rd_max => 'mbps_rd'],
638 [mbps_wr_max => 'mbps_wr'],
639 [miops_max => 'miops'],
640 [miops_rd_max => 'miops_rd'],
641 [miops_wr_max => 'miops_wr'],
642 [bps_max_length => 'mbps_max'],
643 [bps_rd_max_length => 'mbps_rd_max'],
644 [bps_wr_max_length => 'mbps_wr_max'],
645 [iops_max_length => 'iops_max'],
646 [iops_rd_max_length => 'iops_rd_max'],
647 [iops_wr_max_length => 'iops_wr_max']) {
648 my ($option, $requires) = @$requirement;
649 if ($res->{$option} && !$res->{$requires}) {
650 warn "$option requires $requires\n";
651 ++$error;
652 }
653 }
654
655 return if $error;
656
657 return if $res->{mbps_rd} && $res->{mbps};
658 return if $res->{mbps_wr} && $res->{mbps};
659 return if $res->{iops_rd} && $res->{iops};
660 return if $res->{iops_wr} && $res->{iops};
661
662 if ($res->{media} && ($res->{media} eq 'cdrom')) {
663 return if $res->{snapshot} || $res->{trans} || $res->{format};
664 return if $res->{heads} || $res->{secs} || $res->{cyls};
665 return if $res->{interface} eq 'virtio';
666 }
667
668 if (my $size = $res->{size}) {
669 return if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
670 }
671
672 return $res;
673 }
674
675 sub print_drive {
676 my ($drive, $with_alloc) = @_;
677 my $skip = [ 'index', 'interface' ];
678 my $fmt = $with_alloc ? $alldrive_fmt_with_alloc : $alldrive_fmt;
679 return PVE::JSONSchema::print_property_string($drive, $fmt, $skip);
680 }
681
682 sub get_bootdisks {
683 my ($conf) = @_;
684
685 my $bootcfg;
686 $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $conf->{boot}) if $conf->{boot};
687
688 if (!defined($bootcfg) || $bootcfg->{legacy}) {
689 return [$conf->{bootdisk}] if $conf->{bootdisk};
690 return [];
691 }
692
693 my @list = PVE::Tools::split_list($bootcfg->{order});
694 @list = grep {is_valid_drivename($_)} @list;
695 return \@list;
696 }
697
698 sub bootdisk_size {
699 my ($storecfg, $conf) = @_;
700
701 my $bootdisks = get_bootdisks($conf);
702 return if !@$bootdisks;
703 for my $bootdisk (@$bootdisks) {
704 next if !is_valid_drivename($bootdisk);
705 next if !$conf->{$bootdisk};
706 my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
707 next if !defined($drive);
708 next if drive_is_cdrom($drive);
709 my $volid = $drive->{file};
710 next if !$volid;
711 return $drive->{size};
712 }
713
714 return;
715 }
716
717 sub update_disksize {
718 my ($drive, $newsize) = @_;
719
720 return if !defined($newsize);
721
722 my $oldsize = $drive->{size} // 0;
723
724 if ($newsize != $oldsize) {
725 $drive->{size} = $newsize;
726
727 my $old_fmt = PVE::JSONSchema::format_size($oldsize);
728 my $new_fmt = PVE::JSONSchema::format_size($newsize);
729
730 my $msg = "size of disk '$drive->{file}' updated from $old_fmt to $new_fmt";
731
732 return ($drive, $msg);
733 }
734
735 return;
736 }
737
738 sub is_volume_in_use {
739 my ($storecfg, $conf, $skip_drive, $volid) = @_;
740
741 my $path = PVE::Storage::path($storecfg, $volid);
742
743 my $scan_config = sub {
744 my ($cref) = @_;
745
746 foreach my $key (keys %$cref) {
747 my $value = $cref->{$key};
748 if (is_valid_drivename($key)) {
749 next if $skip_drive && $key eq $skip_drive;
750 my $drive = parse_drive($key, $value);
751 next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
752 return 1 if $volid eq $drive->{file};
753 if ($drive->{file} =~ m!^/!) {
754 return 1 if $drive->{file} eq $path;
755 } else {
756 my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
757 next if !$storeid;
758 my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
759 next if !$scfg;
760 return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file});
761 }
762 }
763 }
764
765 return 0;
766 };
767
768 return 1 if &$scan_config($conf);
769
770 undef $skip_drive;
771
772 for my $snap (values %{$conf->{snapshots}}) {
773 return 1 if $scan_config->($snap);
774 }
775
776 return 0;
777 }
778
779 sub resolve_first_disk {
780 my ($conf, $cdrom) = @_;
781 my @disks = valid_drive_names_for_boot();
782 foreach my $ds (@disks) {
783 next if !$conf->{$ds};
784 my $disk = parse_drive($ds, $conf->{$ds});
785 next if drive_is_cdrom($disk) xor $cdrom;
786 return $ds;
787 }
788 return;
789 }
790
791 sub scsi_inquiry {
792 my($fh, $noerr) = @_;
793
794 my $SG_IO = 0x2285;
795 my $SG_GET_VERSION_NUM = 0x2282;
796
797 my $versionbuf = "\x00" x 8;
798 my $ret = ioctl($fh, $SG_GET_VERSION_NUM, $versionbuf);
799 if (!$ret) {
800 die "scsi ioctl SG_GET_VERSION_NUM failoed - $!\n" if !$noerr;
801 return;
802 }
803 my $version = unpack("I", $versionbuf);
804 if ($version < 30000) {
805 die "scsi generic interface too old\n" if !$noerr;
806 return;
807 }
808
809 my $buf = "\x00" x 36;
810 my $sensebuf = "\x00" x 8;
811 my $cmd = pack("C x3 C x1", 0x12, 36);
812
813 # see /usr/include/scsi/sg.h
814 my $sg_io_hdr_t = "i i C C s I P P P I I i P C C C C S S i I I";
815
816 my $packet = pack(
817 $sg_io_hdr_t, ord('S'), -3, length($cmd), length($sensebuf), 0, length($buf), $buf, $cmd, $sensebuf, 6000
818 );
819
820 $ret = ioctl($fh, $SG_IO, $packet);
821 if (!$ret) {
822 die "scsi ioctl SG_IO failed - $!\n" if !$noerr;
823 return;
824 }
825
826 my @res = unpack($sg_io_hdr_t, $packet);
827 if ($res[17] || $res[18]) {
828 die "scsi ioctl SG_IO status error - $!\n" if !$noerr;
829 return;
830 }
831
832 my $res = {};
833 $res->@{qw(type removable vendor product revision)} = unpack("C C x6 A8 A16 A4", $buf);
834
835 $res->{removable} = $res->{removable} & 128 ? 1 : 0;
836 $res->{type} &= 0x1F;
837
838 return $res;
839 }
840
841 sub path_is_scsi {
842 my ($path) = @_;
843
844 my $fh = IO::File->new("+<$path") || return;
845 my $res = scsi_inquiry($fh, 1);
846 close($fh);
847
848 return $res;
849 }
850
851 sub get_scsi_device_type {
852 my ($drive, $storecfg, $machine_version) = @_;
853
854 my $devicetype = 'hd';
855 my $path = '';
856 if (drive_is_cdrom($drive) || drive_is_cloudinit($drive)) {
857 $devicetype = 'cd';
858 } else {
859 if ($drive->{file} =~ m|^/|) {
860 $path = $drive->{file};
861 if (my $info = path_is_scsi($path)) {
862 if ($info->{type} == 0 && $drive->{scsiblock}) {
863 $devicetype = 'block';
864 } elsif ($info->{type} == 1) { # tape
865 $devicetype = 'generic';
866 }
867 }
868 } elsif ($drive->{file} =~ $NEW_DISK_RE){
869 # special syntax cannot be parsed to path
870 return $devicetype;
871 } else {
872 $path = PVE::Storage::path($storecfg, $drive->{file});
873 }
874
875 # for compatibility only, we prefer scsi-hd (#2408, #2355, #2380)
876 if ($path =~ m/^iscsi\:\/\// &&
877 !PVE::QemuServer::Helpers::min_version($machine_version, 4, 1)) {
878 $devicetype = 'generic';
879 }
880 }
881
882 return $devicetype;
883 }
884 1;