]> git.proxmox.com Git - qemu-server.git/blob - PVE/QemuServer/Drive.pm
schema: mention special syntax for allocating a new volume
[qemu-server.git] / PVE / QemuServer / Drive.pm
1 package PVE::QemuServer::Drive;
2
3 use strict;
4 use warnings;
5
6 use PVE::Storage;
7 use PVE::JSONSchema qw(get_standard_option);
8
9 use base qw(Exporter);
10
11 our @EXPORT_OK = qw(
12 is_valid_drivename
13 drive_is_cloudinit
14 drive_is_cdrom
15 parse_drive
16 print_drive
17 );
18
19 our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
20
21 PVE::JSONSchema::register_standard_option('pve-qm-image-format', {
22 type => 'string',
23 enum => [qw(raw cow qcow qed qcow2 vmdk cloop)],
24 description => "The drive's backing file's data format.",
25 optional => 1,
26 });
27
28 my $MAX_IDE_DISKS = 4;
29 my $MAX_SCSI_DISKS = 31;
30 my $MAX_VIRTIO_DISKS = 16;
31 our $MAX_SATA_DISKS = 6;
32 our $MAX_UNUSED_DISKS = 256;
33
34 our $drivedesc_hash;
35
36 my %drivedesc_base = (
37 volume => { alias => 'file' },
38 file => {
39 type => 'string',
40 format => 'pve-volume-id-or-qm-path',
41 default_key => 1,
42 format_description => 'volume',
43 description => "The drive's backing volume.",
44 },
45 media => {
46 type => 'string',
47 enum => [qw(cdrom disk)],
48 description => "The drive's media type.",
49 default => 'disk',
50 optional => 1
51 },
52 cyls => {
53 type => 'integer',
54 description => "Force the drive's physical geometry to have a specific cylinder count.",
55 optional => 1
56 },
57 heads => {
58 type => 'integer',
59 description => "Force the drive's physical geometry to have a specific head count.",
60 optional => 1
61 },
62 secs => {
63 type => 'integer',
64 description => "Force the drive's physical geometry to have a specific sector count.",
65 optional => 1
66 },
67 trans => {
68 type => 'string',
69 enum => [qw(none lba auto)],
70 description => "Force disk geometry bios translation mode.",
71 optional => 1,
72 },
73 snapshot => {
74 type => 'boolean',
75 description => "Controls qemu's snapshot mode feature."
76 . " If activated, changes made to the disk are temporary and will"
77 . " be discarded when the VM is shutdown.",
78 optional => 1,
79 },
80 cache => {
81 type => 'string',
82 enum => [qw(none writethrough writeback unsafe directsync)],
83 description => "The drive's cache mode",
84 optional => 1,
85 },
86 format => get_standard_option('pve-qm-image-format'),
87 size => {
88 type => 'string',
89 format => 'disk-size',
90 format_description => 'DiskSize',
91 description => "Disk size. This is purely informational and has no effect.",
92 optional => 1,
93 },
94 backup => {
95 type => 'boolean',
96 description => "Whether the drive should be included when making backups.",
97 optional => 1,
98 },
99 replicate => {
100 type => 'boolean',
101 description => 'Whether the drive should considered for replication jobs.',
102 optional => 1,
103 default => 1,
104 },
105 rerror => {
106 type => 'string',
107 enum => [qw(ignore report stop)],
108 description => 'Read error action.',
109 optional => 1,
110 },
111 werror => {
112 type => 'string',
113 enum => [qw(enospc ignore report stop)],
114 description => 'Write error action.',
115 optional => 1,
116 },
117 aio => {
118 type => 'string',
119 enum => [qw(native threads)],
120 description => 'AIO type to use.',
121 optional => 1,
122 },
123 discard => {
124 type => 'string',
125 enum => [qw(ignore on)],
126 description => 'Controls whether to pass discard/trim requests to the underlying storage.',
127 optional => 1,
128 },
129 detect_zeroes => {
130 type => 'boolean',
131 description => 'Controls whether to detect and try to optimize writes of zeroes.',
132 optional => 1,
133 },
134 serial => {
135 type => 'string',
136 format => 'urlencoded',
137 format_description => 'serial',
138 maxLength => 20*3, # *3 since it's %xx url enoded
139 description => "The drive's reported serial number, url-encoded, up to 20 bytes long.",
140 optional => 1,
141 },
142 shared => {
143 type => 'boolean',
144 description => 'Mark this locally-managed volume as available on all nodes',
145 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!",
146 optional => 1,
147 default => 0,
148 }
149 );
150
151 my %iothread_fmt = ( iothread => {
152 type => 'boolean',
153 description => "Whether to use iothreads for this drive",
154 optional => 1,
155 });
156
157 my %model_fmt = (
158 model => {
159 type => 'string',
160 format => 'urlencoded',
161 format_description => 'model',
162 maxLength => 40*3, # *3 since it's %xx url enoded
163 description => "The drive's reported model name, url-encoded, up to 40 bytes long.",
164 optional => 1,
165 },
166 );
167
168 my %queues_fmt = (
169 queues => {
170 type => 'integer',
171 description => "Number of queues.",
172 minimum => 2,
173 optional => 1
174 }
175 );
176
177 my %scsiblock_fmt = (
178 scsiblock => {
179 type => 'boolean',
180 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",
181 optional => 1,
182 default => 0,
183 },
184 );
185
186 my %ssd_fmt = (
187 ssd => {
188 type => 'boolean',
189 description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.",
190 optional => 1,
191 },
192 );
193
194 my %wwn_fmt = (
195 wwn => {
196 type => 'string',
197 pattern => qr/^(0x)[0-9a-fA-F]{16}/,
198 format_description => 'wwn',
199 description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.",
200 optional => 1,
201 },
202 );
203
204 my $add_throttle_desc = sub {
205 my ($key, $type, $what, $unit, $longunit, $minimum) = @_;
206 my $d = {
207 type => $type,
208 format_description => $unit,
209 description => "Maximum $what in $longunit.",
210 optional => 1,
211 };
212 $d->{minimum} = $minimum if defined($minimum);
213 $drivedesc_base{$key} = $d;
214 };
215 # throughput: (leaky bucket)
216 $add_throttle_desc->('bps', 'integer', 'r/w speed', 'bps', 'bytes per second');
217 $add_throttle_desc->('bps_rd', 'integer', 'read speed', 'bps', 'bytes per second');
218 $add_throttle_desc->('bps_wr', 'integer', 'write speed', 'bps', 'bytes per second');
219 $add_throttle_desc->('mbps', 'number', 'r/w speed', 'mbps', 'megabytes per second');
220 $add_throttle_desc->('mbps_rd', 'number', 'read speed', 'mbps', 'megabytes per second');
221 $add_throttle_desc->('mbps_wr', 'number', 'write speed', 'mbps', 'megabytes per second');
222 $add_throttle_desc->('iops', 'integer', 'r/w I/O', 'iops', 'operations per second');
223 $add_throttle_desc->('iops_rd', 'integer', 'read I/O', 'iops', 'operations per second');
224 $add_throttle_desc->('iops_wr', 'integer', 'write I/O', 'iops', 'operations per second');
225
226 # pools: (pool of IO before throttling starts taking effect)
227 $add_throttle_desc->('mbps_max', 'number', 'unthrottled r/w pool', 'mbps', 'megabytes per second');
228 $add_throttle_desc->('mbps_rd_max', 'number', 'unthrottled read pool', 'mbps', 'megabytes per second');
229 $add_throttle_desc->('mbps_wr_max', 'number', 'unthrottled write pool', 'mbps', 'megabytes per second');
230 $add_throttle_desc->('iops_max', 'integer', 'unthrottled r/w I/O pool', 'iops', 'operations per second');
231 $add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool', 'iops', 'operations per second');
232 $add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second');
233
234 # burst lengths
235 $add_throttle_desc->('bps_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
236 $add_throttle_desc->('bps_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
237 $add_throttle_desc->('bps_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
238 $add_throttle_desc->('iops_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
239 $add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
240 $add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
241
242 # legacy support
243 $drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' };
244 $drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' };
245 $drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' };
246 $drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' };
247
248 my $ide_fmt = {
249 %drivedesc_base,
250 %model_fmt,
251 %ssd_fmt,
252 %wwn_fmt,
253 };
254 PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
255
256 my $ALLOCATION_SYNTAX_DESC =
257 "Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.";
258
259 my $idedesc = {
260 optional => 1,
261 type => 'string', format => $ide_fmt,
262 description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . "). " .
263 $ALLOCATION_SYNTAX_DESC,
264 };
265 PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
266
267 my $scsi_fmt = {
268 %drivedesc_base,
269 %iothread_fmt,
270 %queues_fmt,
271 %scsiblock_fmt,
272 %ssd_fmt,
273 %wwn_fmt,
274 };
275 my $scsidesc = {
276 optional => 1,
277 type => 'string', format => $scsi_fmt,
278 description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . "). " .
279 $ALLOCATION_SYNTAX_DESC,
280 };
281 PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
282
283 my $sata_fmt = {
284 %drivedesc_base,
285 %ssd_fmt,
286 %wwn_fmt,
287 };
288 my $satadesc = {
289 optional => 1,
290 type => 'string', format => $sata_fmt,
291 description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). "). " .
292 $ALLOCATION_SYNTAX_DESC,
293 };
294 PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
295
296 my $virtio_fmt = {
297 %drivedesc_base,
298 %iothread_fmt,
299 };
300 my $virtiodesc = {
301 optional => 1,
302 type => 'string', format => $virtio_fmt,
303 description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . "). " .
304 $ALLOCATION_SYNTAX_DESC,
305 };
306 PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
307
308 my $alldrive_fmt = {
309 %drivedesc_base,
310 %iothread_fmt,
311 %model_fmt,
312 %queues_fmt,
313 %scsiblock_fmt,
314 %ssd_fmt,
315 %wwn_fmt,
316 };
317
318 my $efidisk_fmt = {
319 volume => { alias => 'file' },
320 file => {
321 type => 'string',
322 format => 'pve-volume-id-or-qm-path',
323 default_key => 1,
324 format_description => 'volume',
325 description => "The drive's backing volume.",
326 },
327 format => get_standard_option('pve-qm-image-format'),
328 size => {
329 type => 'string',
330 format => 'disk-size',
331 format_description => 'DiskSize',
332 description => "Disk size. This is purely informational and has no effect.",
333 optional => 1,
334 },
335 };
336
337 my $efidisk_desc = {
338 optional => 1,
339 type => 'string', format => $efidisk_fmt,
340 description => "Configure a Disk for storing EFI vars. " .
341 $ALLOCATION_SYNTAX_DESC . " Note that SIZE_IN_GiB is ignored here " .
342 "and that the default EFI vars are copied to the volume instead.",
343 };
344
345 PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
346
347 my $unused_fmt = {
348 volume => { alias => 'file' },
349 file => {
350 type => 'string',
351 format => 'pve-volume-id',
352 default_key => 1,
353 format_description => 'volume',
354 description => "The drive's backing volume.",
355 },
356 };
357
358 my $unuseddesc = {
359 optional => 1,
360 type => 'string', format => $unused_fmt,
361 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
362 };
363
364 for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) {
365 $drivedesc_hash->{"ide$i"} = $idedesc;
366 }
367
368 for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) {
369 $drivedesc_hash->{"sata$i"} = $satadesc;
370 }
371
372 for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) {
373 $drivedesc_hash->{"scsi$i"} = $scsidesc;
374 }
375
376 for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) {
377 $drivedesc_hash->{"virtio$i"} = $virtiodesc;
378 }
379
380 $drivedesc_hash->{efidisk0} = $efidisk_desc;
381
382 for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
383 $drivedesc_hash->{"unused$i"} = $unuseddesc;
384 }
385
386 sub valid_drive_names {
387 # order is important - used to autoselect boot disk
388 return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
389 (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
390 (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
391 (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
392 'efidisk0');
393 }
394
395 sub is_valid_drivename {
396 my $dev = shift;
397
398 return defined($drivedesc_hash->{$dev}) && $dev !~ /^unused\d+$/;
399 }
400
401 PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
402 sub verify_bootdisk {
403 my ($value, $noerr) = @_;
404
405 return $value if is_valid_drivename($value);
406
407 return if $noerr;
408
409 die "invalid boot disk '$value'\n";
410 }
411
412 sub drive_is_cloudinit {
413 my ($drive) = @_;
414 return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
415 }
416
417 sub drive_is_cdrom {
418 my ($drive, $exclude_cloudinit) = @_;
419
420 return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
421
422 return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
423 }
424
425 # ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
426 # [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
427 # [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
428 # [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
429 # [,iothread=on][,serial=serial][,model=model]
430
431 sub parse_drive {
432 my ($key, $data) = @_;
433
434 my ($interface, $index);
435
436 if ($key =~ m/^([^\d]+)(\d+)$/) {
437 $interface = $1;
438 $index = $2;
439 } else {
440 return;
441 }
442
443 if (!defined($drivedesc_hash->{$key})) {
444 warn "invalid drive key: $key\n";
445 return;
446 }
447
448 my $desc = $drivedesc_hash->{$key}->{format};
449 my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
450 return if !$res;
451 $res->{interface} = $interface;
452 $res->{index} = $index;
453
454 my $error = 0;
455 foreach my $opt (qw(bps bps_rd bps_wr)) {
456 if (my $bps = defined(delete $res->{$opt})) {
457 if (defined($res->{"m$opt"})) {
458 warn "both $opt and m$opt specified\n";
459 ++$error;
460 next;
461 }
462 $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
463 }
464 }
465
466 # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
467 for my $requirement (
468 [mbps_max => 'mbps'],
469 [mbps_rd_max => 'mbps_rd'],
470 [mbps_wr_max => 'mbps_wr'],
471 [miops_max => 'miops'],
472 [miops_rd_max => 'miops_rd'],
473 [miops_wr_max => 'miops_wr'],
474 [bps_max_length => 'mbps_max'],
475 [bps_rd_max_length => 'mbps_rd_max'],
476 [bps_wr_max_length => 'mbps_wr_max'],
477 [iops_max_length => 'iops_max'],
478 [iops_rd_max_length => 'iops_rd_max'],
479 [iops_wr_max_length => 'iops_wr_max']) {
480 my ($option, $requires) = @$requirement;
481 if ($res->{$option} && !$res->{$requires}) {
482 warn "$option requires $requires\n";
483 ++$error;
484 }
485 }
486
487 return if $error;
488
489 return if $res->{mbps_rd} && $res->{mbps};
490 return if $res->{mbps_wr} && $res->{mbps};
491 return if $res->{iops_rd} && $res->{iops};
492 return if $res->{iops_wr} && $res->{iops};
493
494 if ($res->{media} && ($res->{media} eq 'cdrom')) {
495 return if $res->{snapshot} || $res->{trans} || $res->{format};
496 return if $res->{heads} || $res->{secs} || $res->{cyls};
497 return if $res->{interface} eq 'virtio';
498 }
499
500 if (my $size = $res->{size}) {
501 return if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
502 }
503
504 return $res;
505 }
506
507 sub print_drive {
508 my ($drive) = @_;
509 my $skip = [ 'index', 'interface' ];
510 return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip);
511 }
512
513 sub get_bootdisks {
514 my ($conf) = @_;
515
516 my $bootcfg;
517 $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $conf->{boot}) if $conf->{boot};
518
519 if (!defined($bootcfg) || $bootcfg->{legacy}) {
520 return [$conf->{bootdisk}] if $conf->{bootdisk};
521 return [];
522 }
523
524 my @list = PVE::Tools::split_list($bootcfg->{order});
525 @list = grep {is_valid_drivename($_)} @list;
526 return \@list;
527 }
528
529 sub bootdisk_size {
530 my ($storecfg, $conf) = @_;
531
532 my $bootdisks = get_bootdisks($conf);
533 return if !@$bootdisks;
534 my $bootdisk = $bootdisks->[0];
535 return if !is_valid_drivename($bootdisk);
536
537 return if !$conf->{$bootdisk};
538
539 my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
540 return if !defined($drive);
541
542 return if drive_is_cdrom($drive);
543
544 my $volid = $drive->{file};
545 return if !$volid;
546
547 return $drive->{size};
548 }
549
550 sub update_disksize {
551 my ($drive, $newsize) = @_;
552
553 return if !defined($newsize);
554
555 my $oldsize = $drive->{size} // 0;
556
557 if ($newsize != $oldsize) {
558 $drive->{size} = $newsize;
559
560 my $old_fmt = PVE::JSONSchema::format_size($oldsize);
561 my $new_fmt = PVE::JSONSchema::format_size($newsize);
562
563 my $msg = "size of disk '$drive->{file}' updated from $old_fmt to $new_fmt";
564
565 return ($drive, $msg);
566 }
567
568 return;
569 }
570
571 sub is_volume_in_use {
572 my ($storecfg, $conf, $skip_drive, $volid) = @_;
573
574 my $path = PVE::Storage::path($storecfg, $volid);
575
576 my $scan_config = sub {
577 my ($cref, $snapname) = @_;
578
579 foreach my $key (keys %$cref) {
580 my $value = $cref->{$key};
581 if (is_valid_drivename($key)) {
582 next if $skip_drive && $key eq $skip_drive;
583 my $drive = parse_drive($key, $value);
584 next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
585 return 1 if $volid eq $drive->{file};
586 if ($drive->{file} =~ m!^/!) {
587 return 1 if $drive->{file} eq $path;
588 } else {
589 my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
590 next if !$storeid;
591 my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
592 next if !$scfg;
593 return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
594 }
595 }
596 }
597
598 return 0;
599 };
600
601 return 1 if &$scan_config($conf);
602
603 undef $skip_drive;
604
605 foreach my $snapname (keys %{$conf->{snapshots}}) {
606 return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
607 }
608
609 return 0;
610 }
611
612 sub resolve_first_disk {
613 my ($conf, $cdrom) = @_;
614 my @disks = valid_drive_names();
615 foreach my $ds (@disks) {
616 next if !$conf->{$ds};
617 my $disk = parse_drive($ds, $conf->{$ds});
618 next if drive_is_cdrom($disk) xor $cdrom;
619 return $ds;
620 }
621 return;
622 }
623
624 1;