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