]> git.proxmox.com Git - qemu-server.git/blame - PVE/QemuServer/Drive.pm
Move path_is_scsi to QemuServer/Drive.pm
[qemu-server.git] / PVE / QemuServer / Drive.pm
CommitLineData
e0fd2b2f
FE
1package PVE::QemuServer::Drive;
2
3use strict;
4use warnings;
5
c1accf9d
FE
6use Storable qw(dclone);
7
ad464e0b
HD
8use IO::File;
9
e0fd2b2f
FE
10use PVE::Storage;
11use PVE::JSONSchema qw(get_standard_option);
12
13use base qw(Exporter);
14
15our @EXPORT_OK = qw(
16is_valid_drivename
17drive_is_cloudinit
18drive_is_cdrom
75748d44 19drive_is_read_only
e0fd2b2f
FE
20parse_drive
21print_drive
ad464e0b 22path_is_scsi
e0fd2b2f
FE
23);
24
25our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
26
27PVE::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
34my $MAX_IDE_DISKS = 4;
35my $MAX_SCSI_DISKS = 31;
36my $MAX_VIRTIO_DISKS = 16;
37our $MAX_SATA_DISKS = 6;
38our $MAX_UNUSED_DISKS = 256;
39
40our $drivedesc_hash;
c1accf9d
FE
41# Schema when disk allocation is possible.
42our $drivedesc_hash_with_alloc = {};
e0fd2b2f
FE
43
44my %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',
59e59342 127 enum => [qw(native threads io_uring)],
e0fd2b2f
FE
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
159my %iothread_fmt = ( iothread => {
160 type => 'boolean',
161 description => "Whether to use iothreads for this drive",
162 optional => 1,
163});
164
165my %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
176my %queues_fmt = (
177 queues => {
178 type => 'integer',
179 description => "Number of queues.",
180 minimum => 2,
181 optional => 1
182 }
183);
184
12e1d472
DC
185my %readonly_fmt = (
186 ro => {
187 type => 'boolean',
188 description => "Whether the drive is read-only.",
189 optional => 1,
190 },
191);
192
e0fd2b2f
FE
193my %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
202my %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
210my %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
220my $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
264my $ide_fmt = {
265 %drivedesc_base,
266 %model_fmt,
267 %ssd_fmt,
268 %wwn_fmt,
269};
270PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
271
272my $idedesc = {
273 optional => 1,
274 type => 'string', format => $ide_fmt,
c1accf9d 275 description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS - 1) . ").",
e0fd2b2f
FE
276};
277PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
278
279my $scsi_fmt = {
280 %drivedesc_base,
281 %iothread_fmt,
282 %queues_fmt,
12e1d472 283 %readonly_fmt,
e0fd2b2f
FE
284 %scsiblock_fmt,
285 %ssd_fmt,
286 %wwn_fmt,
287};
288my $scsidesc = {
289 optional => 1,
290 type => 'string', format => $scsi_fmt,
c1accf9d 291 description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
e0fd2b2f
FE
292};
293PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
294
295my $sata_fmt = {
296 %drivedesc_base,
297 %ssd_fmt,
298 %wwn_fmt,
299};
300my $satadesc = {
301 optional => 1,
302 type => 'string', format => $sata_fmt,
c1accf9d 303 description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
e0fd2b2f
FE
304};
305PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
306
307my $virtio_fmt = {
308 %drivedesc_base,
309 %iothread_fmt,
12e1d472 310 %readonly_fmt,
e0fd2b2f
FE
311};
312my $virtiodesc = {
313 optional => 1,
314 type => 'string', format => $virtio_fmt,
c1accf9d 315 description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
e0fd2b2f
FE
316};
317PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
318
b5099b4f
SR
319my %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"
6d1ac42b 325 . " if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).",
b5099b4f
SR
326 optional => 1,
327 default => '2m',
328 },
a064e511 329 'pre-enrolled-keys' => {
b5099b4f 330 type => 'boolean',
a064e511
TL
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.",
b5099b4f
SR
334 optional => 1,
335 default => 0,
336 },
337);
338
e0fd2b2f
FE
339my $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 },
b5099b4f 356 %efitype_fmt,
e0fd2b2f
FE
357};
358
359my $efidisk_desc = {
360 optional => 1,
361 type => 'string', format => $efidisk_fmt,
1183c8f1 362 description => "Configure a disk for storing EFI vars.",
e0fd2b2f
FE
363};
364
365PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
366
f9dde219
SR
367my %tpmversion_fmt = (
368 version => {
369 type => 'string',
370 enum => [qw(v1.2 v2.0)],
2b9ee944
TL
371 description => "The TPM interface version. v2.0 is newer and should be preferred."
372 ." Note that this cannot be changed later on.",
f9dde219
SR
373 optional => 1,
374 default => 'v2.0',
375 },
376);
377my $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};
395my $tpmstate_desc = {
396 optional => 1,
397 type => 'string', format => $tpmstate_fmt,
c1accf9d 398 description => "Configure a Disk for storing TPM state. The format is fixed to 'raw'.",
f9dde219
SR
399};
400use constant TPMSTATE_DISK_SIZE => 4 * 1024 * 1024;
401
402my $alldrive_fmt = {
403 %drivedesc_base,
404 %iothread_fmt,
405 %model_fmt,
406 %queues_fmt,
12e1d472 407 %readonly_fmt,
f9dde219
SR
408 %scsiblock_fmt,
409 %ssd_fmt,
410 %wwn_fmt,
411 %tpmversion_fmt,
b5099b4f 412 %efitype_fmt,
f9dde219
SR
413};
414
e6ac9fed
DJ
415my %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
c1accf9d
FE
427my $alldrive_fmt_with_alloc = {
428 %$alldrive_fmt,
e6ac9fed 429 %import_from_fmt,
c1accf9d
FE
430};
431
43c4c7b6
FE
432my $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
443my $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
c1accf9d
FE
449my $with_alloc_desc_cache = {
450 unused => $unuseddesc, # Allocation for unused is not supported currently.
451};
452my $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
e6ac9fed
DJ
459 $new_desc->{format}->{'import-from'} = $import_from_fmt{'import-from'};
460
c1accf9d
FE
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 ".
e6ac9fed
DJ
470 "volume.${extra_note} Use STORAGE_ID:0 and the 'import-from' parameter to import from an ".
471 "existing volume.";
c1accf9d
FE
472
473 $with_alloc_desc_cache->{$type} = $new_desc;
474
475 return $new_desc;
476};
477
e0fd2b2f
FE
478for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) {
479 $drivedesc_hash->{"ide$i"} = $idedesc;
c1accf9d 480 $drivedesc_hash_with_alloc->{"ide$i"} = $desc_with_alloc->('ide', $idedesc);
e0fd2b2f
FE
481}
482
483for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) {
484 $drivedesc_hash->{"sata$i"} = $satadesc;
c1accf9d 485 $drivedesc_hash_with_alloc->{"sata$i"} = $desc_with_alloc->('sata', $satadesc);
e0fd2b2f
FE
486}
487
488for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) {
489 $drivedesc_hash->{"scsi$i"} = $scsidesc;
c1accf9d 490 $drivedesc_hash_with_alloc->{"scsi$i"} = $desc_with_alloc->('scsi', $scsidesc);
e0fd2b2f
FE
491}
492
493for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) {
494 $drivedesc_hash->{"virtio$i"} = $virtiodesc;
c1accf9d 495 $drivedesc_hash_with_alloc->{"virtio$i"} = $desc_with_alloc->('virtio', $virtiodesc);
e0fd2b2f
FE
496}
497
498$drivedesc_hash->{efidisk0} = $efidisk_desc;
c1accf9d
FE
499$drivedesc_hash_with_alloc->{efidisk0} = $desc_with_alloc->('efidisk', $efidisk_desc);
500
f9dde219 501$drivedesc_hash->{tpmstate0} = $tpmstate_desc;
c1accf9d 502$drivedesc_hash_with_alloc->{tpmstate0} = $desc_with_alloc->('tpmstate', $tpmstate_desc);
e0fd2b2f 503
43c4c7b6
FE
504for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
505 $drivedesc_hash->{"unused$i"} = $unuseddesc;
c1accf9d 506 $drivedesc_hash_with_alloc->{"unused$i"} = $desc_with_alloc->('unused', $unuseddesc);
43c4c7b6 507}
e0fd2b2f 508
1319908f
DC
509sub valid_drive_names_for_boot {
510 return grep { $_ ne 'efidisk0' && $_ ne 'tpmstate0' } valid_drive_names();
511}
512
e0fd2b2f
FE
513sub 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))),
f9dde219
SR
519 'efidisk0',
520 'tpmstate0');
e0fd2b2f
FE
521}
522
10713730
AL
523sub valid_drive_names_with_unused {
524 return (valid_drive_names(), map {"unused$_"} (0 .. ($MAX_UNUSED_DISKS - 1)));
525}
526
e0fd2b2f
FE
527sub is_valid_drivename {
528 my $dev = shift;
529
43c4c7b6 530 return defined($drivedesc_hash->{$dev}) && $dev !~ /^unused\d+$/;
e0fd2b2f
FE
531}
532
533PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
534sub verify_bootdisk {
535 my ($value, $noerr) = @_;
536
537 return $value if is_valid_drivename($value);
538
d1c1af4b 539 return if $noerr;
e0fd2b2f
FE
540
541 die "invalid boot disk '$value'\n";
542}
543
544sub drive_is_cloudinit {
545 my ($drive) = @_;
f5a88e98 546 return $drive->{file} =~ m@[:/](?:vm-\d+-)?cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
e0fd2b2f
FE
547}
548
549sub 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
75748d44
FG
557sub 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
e0fd2b2f
FE
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
572sub parse_drive {
c1accf9d 573 my ($key, $data, $with_alloc) = @_;
e0fd2b2f
FE
574
575 my ($interface, $index);
576
577 if ($key =~ m/^([^\d]+)(\d+)$/) {
578 $interface = $1;
579 $index = $2;
580 } else {
d1c1af4b 581 return;
e0fd2b2f
FE
582 }
583
c1accf9d
FE
584 my $desc_hash = $with_alloc ? $drivedesc_hash_with_alloc : $drivedesc_hash;
585
586 if (!defined($desc_hash->{$key})) {
e0fd2b2f 587 warn "invalid drive key: $key\n";
d1c1af4b 588 return;
e0fd2b2f 589 }
43c4c7b6 590
c1accf9d 591 my $desc = $desc_hash->{$key}->{format};
e0fd2b2f 592 my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
d1c1af4b 593 return if !$res;
e0fd2b2f
FE
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
d1c1af4b 630 return if $error;
e0fd2b2f 631
d1c1af4b
TL
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};
e0fd2b2f
FE
636
637 if ($res->{media} && ($res->{media} eq 'cdrom')) {
d1c1af4b
TL
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';
e0fd2b2f
FE
641 }
642
643 if (my $size = $res->{size}) {
d1c1af4b 644 return if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
e0fd2b2f
FE
645 }
646
647 return $res;
648}
649
650sub print_drive {
c1accf9d 651 my ($drive, $with_alloc) = @_;
e0fd2b2f 652 my $skip = [ 'index', 'interface' ];
c1accf9d
FE
653 my $fmt = $with_alloc ? $alldrive_fmt_with_alloc : $alldrive_fmt;
654 return PVE::JSONSchema::print_property_string($drive, $fmt, $skip);
e0fd2b2f
FE
655}
656
2141a802
SR
657sub get_bootdisks {
658 my ($conf) = @_;
659
f7d1505b
TL
660 my $bootcfg;
661 $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $conf->{boot}) if $conf->{boot};
2141a802
SR
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
776c5f50 673sub bootdisk_size {
e0fd2b2f
FE
674 my ($storecfg, $conf) = @_;
675
2141a802 676 my $bootdisks = get_bootdisks($conf);
d1c1af4b 677 return if !@$bootdisks;
30664f14
DC
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 }
e0fd2b2f 688
30664f14 689 return;
e0fd2b2f
FE
690}
691
692sub update_disksize {
9b29cbd0 693 my ($drive, $newsize) = @_;
e0fd2b2f 694
d1c1af4b 695 return if !defined($newsize);
e0fd2b2f 696
63e313f3 697 my $oldsize = $drive->{size} // 0;
e0fd2b2f 698
63e313f3 699 if ($newsize != $oldsize) {
e0fd2b2f
FE
700 $drive->{size} = $newsize;
701
702 my $old_fmt = PVE::JSONSchema::format_size($oldsize);
703 my $new_fmt = PVE::JSONSchema::format_size($newsize);
704
9b29cbd0
FE
705 my $msg = "size of disk '$drive->{file}' updated from $old_fmt to $new_fmt";
706
707 return ($drive, $msg);
e0fd2b2f
FE
708 }
709
d1c1af4b 710 return;
e0fd2b2f
FE
711}
712
713sub 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 {
ab5b97d8 719 my ($cref) = @_;
e0fd2b2f
FE
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;
a0d8d859 735 return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file});
e0fd2b2f
FE
736 }
737 }
738 }
739
740 return 0;
741 };
742
743 return 1 if &$scan_config($conf);
744
745 undef $skip_drive;
746
ab5b97d8
FE
747 for my $snap (values %{$conf->{snapshots}}) {
748 return 1 if $scan_config->($snap);
e0fd2b2f
FE
749 }
750
751 return 0;
752}
753
754sub resolve_first_disk {
5cfa9f5f 755 my ($conf, $cdrom) = @_;
1319908f 756 my @disks = valid_drive_names_for_boot();
5cfa9f5f 757 foreach my $ds (@disks) {
e0fd2b2f
FE
758 next if !$conf->{$ds};
759 my $disk = parse_drive($ds, $conf->{$ds});
5cfa9f5f
SR
760 next if drive_is_cdrom($disk) xor $cdrom;
761 return $ds;
e0fd2b2f 762 }
d1c1af4b 763 return;
e0fd2b2f
FE
764}
765
ad464e0b
HD
766sub 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
816sub 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
e0fd2b2f 8261;