]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer/Drive.pm
schema: fix description of migrate_downtime parameter
[qemu-server.git] / PVE / QemuServer / Drive.pm
index b71fc9336ef1c6f575c8d78d2e289ba2c05b490d..6a4fafd95cf07c17c06b5d94e83ec8a30d59ab4c 100644 (file)
@@ -3,6 +3,10 @@ package PVE::QemuServer::Drive;
 use strict;
 use warnings;
 
+use Storable qw(dclone);
+
+use IO::File;
+
 use PVE::Storage;
 use PVE::JSONSchema qw(get_standard_option);
 
@@ -12,6 +16,8 @@ our @EXPORT_OK = qw(
 is_valid_drivename
 drive_is_cloudinit
 drive_is_cdrom
+drive_is_read_only
+get_scsi_devicetype
 parse_drive
 print_drive
 );
@@ -30,8 +36,11 @@ my $MAX_SCSI_DISKS = 31;
 my $MAX_VIRTIO_DISKS = 16;
 our $MAX_SATA_DISKS = 6;
 our $MAX_UNUSED_DISKS = 256;
+our $NEW_DISK_RE = qr!^(([^/:\s]+):)?(\d+(\.\d+)?)$!;
 
 our $drivedesc_hash;
+# Schema when disk allocation is possible.
+our $drivedesc_hash_with_alloc = {};
 
 my %drivedesc_base = (
     volume => { alias => 'file' },
@@ -116,7 +125,7 @@ my %drivedesc_base = (
     },
     aio => {
        type => 'string',
-       enum => [qw(native threads)],
+       enum => [qw(native threads io_uring)],
        description => 'AIO type to use.',
        optional => 1,
     },
@@ -154,6 +163,26 @@ my %iothread_fmt = ( iothread => {
        optional => 1,
 });
 
+my %product_fmt = (
+    product => {
+       type => 'string',
+       pattern => '[A-Za-z0-9\-_\s]{,16}', # QEMU (8.1) will quietly only use 16 bytes
+       format_description => 'product',
+       description => "The drive's product name, up to 16 bytes long.",
+       optional => 1,
+    },
+);
+
+my %vendor_fmt = (
+    vendor => {
+       type => 'string',
+       pattern => '[A-Za-z0-9\-_\s]{,8}', # QEMU (8.1) will quietly only use 8 bytes
+       format_description => 'vendor',
+       description => "The drive's vendor name, up to 8 bytes long.",
+       optional => 1,
+    },
+);
+
 my %model_fmt = (
     model => {
        type => 'string',
@@ -174,6 +203,14 @@ my %queues_fmt = (
     }
 );
 
+my %readonly_fmt = (
+    ro => {
+       type => 'boolean',
+       description => "Whether the drive is read-only.",
+       optional => 1,
+    },
+);
+
 my %scsiblock_fmt = (
     scsiblock => {
        type => 'boolean',
@@ -256,16 +293,19 @@ PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
 my $idedesc = {
     optional => 1,
     type => 'string', format => $ide_fmt,
-    description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").",
+    description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS - 1) . ").",
 };
 PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
 
 my $scsi_fmt = {
     %drivedesc_base,
     %iothread_fmt,
+    %product_fmt,
     %queues_fmt,
+    %readonly_fmt,
     %scsiblock_fmt,
     %ssd_fmt,
+    %vendor_fmt,
     %wwn_fmt,
 };
 my $scsidesc = {
@@ -290,6 +330,7 @@ PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
 my $virtio_fmt = {
     %drivedesc_base,
     %iothread_fmt,
+    %readonly_fmt,
 };
 my $virtiodesc = {
     optional => 1,
@@ -298,15 +339,25 @@ my $virtiodesc = {
 };
 PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
 
-my $alldrive_fmt = {
-    %drivedesc_base,
-    %iothread_fmt,
-    %model_fmt,
-    %queues_fmt,
-    %scsiblock_fmt,
-    %ssd_fmt,
-    %wwn_fmt,
-};
+my %efitype_fmt = (
+    efitype => {
+       type => 'string',
+       enum => [qw(2m 4m)],
+       description => "Size and type of the OVMF EFI vars. '4m' is newer and recommended,"
+           . " and required for Secure Boot. For backwards compatibility, '2m' is used"
+           . " if not otherwise specified. Ignored for VMs with arch=aarch64 (ARM).",
+       optional => 1,
+       default => '2m',
+    },
+    'pre-enrolled-keys' => {
+       type => 'boolean',
+       description => "Use am EFI vars template with distribution-specific and Microsoft Standard"
+           ." keys enrolled, if used with 'efitype=4m'. Note that this will enable Secure Boot by"
+           ." default, though it can still be turned off from within the VM.",
+       optional => 1,
+       default => 0,
+    },
+);
 
 my $efidisk_fmt = {
     volume => { alias => 'file' },
@@ -325,16 +376,84 @@ my $efidisk_fmt = {
        description => "Disk size. This is purely informational and has no effect.",
        optional => 1,
     },
+    %efitype_fmt,
 };
 
 my $efidisk_desc = {
     optional => 1,
     type => 'string', format => $efidisk_fmt,
-    description => "Configure a Disk for storing EFI vars",
+    description => "Configure a disk for storing EFI vars.",
 };
 
 PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
 
+my %tpmversion_fmt = (
+    version => {
+       type => 'string',
+       enum => [qw(v1.2 v2.0)],
+       description => "The TPM interface version. v2.0 is newer and should be preferred."
+           ." Note that this cannot be changed later on.",
+       optional => 1,
+       default => 'v2.0',
+    },
+);
+my $tpmstate_fmt = {
+    volume => { alias => 'file' },
+    file => {
+       type => 'string',
+       format => 'pve-volume-id-or-qm-path',
+       default_key => 1,
+       format_description => 'volume',
+       description => "The drive's backing volume.",
+    },
+    size => {
+       type => 'string',
+       format => 'disk-size',
+       format_description => 'DiskSize',
+       description => "Disk size. This is purely informational and has no effect.",
+       optional => 1,
+    },
+    %tpmversion_fmt,
+};
+my $tpmstate_desc = {
+    optional => 1,
+    type => 'string', format => $tpmstate_fmt,
+    description => "Configure a Disk for storing TPM state. The format is fixed to 'raw'.",
+};
+use constant TPMSTATE_DISK_SIZE => 4 * 1024 * 1024;
+
+my $alldrive_fmt = {
+    %drivedesc_base,
+    %iothread_fmt,
+    %model_fmt,
+    %product_fmt,
+    %queues_fmt,
+    %readonly_fmt,
+    %scsiblock_fmt,
+    %ssd_fmt,
+    %vendor_fmt,
+    %wwn_fmt,
+    %tpmversion_fmt,
+    %efitype_fmt,
+};
+
+my %import_from_fmt = (
+    'import-from' => {
+       type => 'string',
+       format => 'pve-volume-id-or-absolute-path',
+       format_description => 'source volume',
+       description => "Create a new disk, importing from this source (volume ID or absolute ".
+           "path). When an absolute path is specified, it's up to you to ensure that the source ".
+           "is not actively used by another process during the import!",
+       optional => 1,
+    },
+);
+
+my $alldrive_fmt_with_alloc = {
+    %$alldrive_fmt,
+    %import_from_fmt,
+};
+
 my $unused_fmt = {
     volume => { alias => 'file' },
     file => {
@@ -352,26 +471,68 @@ my $unuseddesc = {
     description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
 };
 
+my $with_alloc_desc_cache = {
+    unused => $unuseddesc, # Allocation for unused is not supported currently.
+};
+my $desc_with_alloc = sub {
+    my ($type, $desc) = @_;
+
+    return $with_alloc_desc_cache->{$type} if $with_alloc_desc_cache->{$type};
+
+    my $new_desc = dclone($desc);
+
+    $new_desc->{format}->{'import-from'} = $import_from_fmt{'import-from'};
+
+    my $extra_note = '';
+    if ($type eq 'efidisk') {
+       $extra_note = " Note that SIZE_IN_GiB is ignored here and that the default EFI vars are ".
+           "copied to the volume instead.";
+    } elsif ($type eq 'tpmstate') {
+       $extra_note = " Note that SIZE_IN_GiB is ignored here and 4 MiB will be used instead.";
+    }
+
+    $new_desc->{description} .= " Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new ".
+       "volume.${extra_note} Use STORAGE_ID:0 and the 'import-from' parameter to import from an ".
+       "existing volume.";
+
+    $with_alloc_desc_cache->{$type} = $new_desc;
+
+    return $new_desc;
+};
+
 for (my $i = 0; $i < $MAX_IDE_DISKS; $i++)  {
     $drivedesc_hash->{"ide$i"} = $idedesc;
+    $drivedesc_hash_with_alloc->{"ide$i"} = $desc_with_alloc->('ide', $idedesc);
 }
 
 for (my $i = 0; $i < $MAX_SATA_DISKS; $i++)  {
     $drivedesc_hash->{"sata$i"} = $satadesc;
+    $drivedesc_hash_with_alloc->{"sata$i"} = $desc_with_alloc->('sata', $satadesc);
 }
 
 for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++)  {
     $drivedesc_hash->{"scsi$i"} = $scsidesc;
+    $drivedesc_hash_with_alloc->{"scsi$i"} = $desc_with_alloc->('scsi', $scsidesc);
 }
 
 for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++)  {
     $drivedesc_hash->{"virtio$i"} = $virtiodesc;
+    $drivedesc_hash_with_alloc->{"virtio$i"} = $desc_with_alloc->('virtio', $virtiodesc);
 }
 
 $drivedesc_hash->{efidisk0} = $efidisk_desc;
+$drivedesc_hash_with_alloc->{efidisk0} = $desc_with_alloc->('efidisk', $efidisk_desc);
+
+$drivedesc_hash->{tpmstate0} = $tpmstate_desc;
+$drivedesc_hash_with_alloc->{tpmstate0} = $desc_with_alloc->('tpmstate', $tpmstate_desc);
 
 for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
     $drivedesc_hash->{"unused$i"} = $unuseddesc;
+    $drivedesc_hash_with_alloc->{"unused$i"} = $desc_with_alloc->('unused', $unuseddesc);
+}
+
+sub valid_drive_names_for_boot {
+    return grep { $_ ne 'efidisk0' && $_ ne 'tpmstate0' } valid_drive_names();
 }
 
 sub valid_drive_names {
@@ -380,7 +541,12 @@ sub valid_drive_names {
             (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
             (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
             (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
-            'efidisk0');
+            'efidisk0',
+            'tpmstate0');
+}
+
+sub valid_drive_names_with_unused {
+    return (valid_drive_names(), map {"unused$_"} (0 .. ($MAX_UNUSED_DISKS - 1)));
 }
 
 sub is_valid_drivename {
@@ -395,14 +561,14 @@ sub verify_bootdisk {
 
     return $value if is_valid_drivename($value);
 
-    return undef if $noerr;
+    return if $noerr;
 
     die "invalid boot disk '$value'\n";
 }
 
 sub drive_is_cloudinit {
     my ($drive) = @_;
-    return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
+    return $drive->{file} =~ m@[:/](?:vm-\d+-)?cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
 }
 
 sub drive_is_cdrom {
@@ -413,6 +579,15 @@ sub drive_is_cdrom {
     return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
 }
 
+sub drive_is_read_only {
+    my ($conf, $drive) = @_;
+
+    return 0 if !PVE::QemuConfig->is_template($conf);
+
+    # don't support being marked read-only
+    return $drive->{interface} ne 'sata' && $drive->{interface} ne 'ide';
+}
+
 # ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
 #        [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
 #        [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
@@ -420,7 +595,7 @@ sub drive_is_cdrom {
 #        [,iothread=on][,serial=serial][,model=model]
 
 sub parse_drive {
-    my ($key, $data) = @_;
+    my ($key, $data, $with_alloc) = @_;
 
     my ($interface, $index);
 
@@ -428,17 +603,19 @@ sub parse_drive {
        $interface = $1;
        $index = $2;
     } else {
-       return undef;
+       return;
     }
 
-    if (!defined($drivedesc_hash->{$key})) {
+    my $desc_hash = $with_alloc ? $drivedesc_hash_with_alloc : $drivedesc_hash;
+
+    if (!defined($desc_hash->{$key})) {
        warn "invalid drive key: $key\n";
-       return undef;
+       return;
     }
 
-    my $desc = $drivedesc_hash->{$key}->{format};
+    my $desc = $desc_hash->{$key}->{format};
     my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
-    return undef if !$res;
+    return if !$res;
     $res->{interface} = $interface;
     $res->{index} = $index;
 
@@ -475,56 +652,72 @@ sub parse_drive {
        }
     }
 
-    return undef if $error;
+    return if $error;
 
-    return undef if $res->{mbps_rd} && $res->{mbps};
-    return undef if $res->{mbps_wr} && $res->{mbps};
-    return undef if $res->{iops_rd} && $res->{iops};
-    return undef if $res->{iops_wr} && $res->{iops};
+    return if $res->{mbps_rd} && $res->{mbps};
+    return if $res->{mbps_wr} && $res->{mbps};
+    return if $res->{iops_rd} && $res->{iops};
+    return if $res->{iops_wr} && $res->{iops};
 
     if ($res->{media} && ($res->{media} eq 'cdrom')) {
-       return undef if $res->{snapshot} || $res->{trans} || $res->{format};
-       return undef if $res->{heads} || $res->{secs} || $res->{cyls};
-       return undef if $res->{interface} eq 'virtio';
+       return if $res->{snapshot} || $res->{trans} || $res->{format};
+       return if $res->{heads} || $res->{secs} || $res->{cyls};
+       return if $res->{interface} eq 'virtio';
     }
 
     if (my $size = $res->{size}) {
-       return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
+       return if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
     }
 
     return $res;
 }
 
 sub print_drive {
-    my ($drive) = @_;
+    my ($drive, $with_alloc) = @_;
     my $skip = [ 'index', 'interface' ];
-    return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip);
+    my $fmt = $with_alloc ? $alldrive_fmt_with_alloc : $alldrive_fmt;
+    return PVE::JSONSchema::print_property_string($drive, $fmt, $skip);
 }
 
-sub bootdisk_size {
-    my ($storecfg, $conf) = @_;
+sub get_bootdisks {
+    my ($conf) = @_;
 
-    my $bootdisk = $conf->{bootdisk};
-    return undef if !$bootdisk;
-    return undef if !is_valid_drivename($bootdisk);
+    my $bootcfg;
+    $bootcfg = PVE::JSONSchema::parse_property_string('pve-qm-boot', $conf->{boot}) if $conf->{boot};
 
-    return undef if !$conf->{$bootdisk};
+    if (!defined($bootcfg) || $bootcfg->{legacy}) {
+       return [$conf->{bootdisk}] if $conf->{bootdisk};
+       return [];
+    }
 
-    my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
-    return undef if !defined($drive);
+    my @list = PVE::Tools::split_list($bootcfg->{order});
+    @list = grep {is_valid_drivename($_)} @list;
+    return \@list;
+}
 
-    return undef if drive_is_cdrom($drive);
+sub bootdisk_size {
+    my ($storecfg, $conf) = @_;
 
-    my $volid = $drive->{file};
-    return undef if !$volid;
+    my $bootdisks = get_bootdisks($conf);
+    return if !@$bootdisks;
+    for my $bootdisk (@$bootdisks) {
+       next if !is_valid_drivename($bootdisk);
+       next if !$conf->{$bootdisk};
+       my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
+       next if !defined($drive);
+       next if drive_is_cdrom($drive);
+       my $volid = $drive->{file};
+       next if !$volid;
+       return $drive->{size};
+    }
 
-    return $drive->{size};
+    return;
 }
 
 sub update_disksize {
     my ($drive, $newsize) = @_;
 
-    return undef if !defined($newsize);
+    return if !defined($newsize);
 
     my $oldsize = $drive->{size} // 0;
 
@@ -539,7 +732,7 @@ sub update_disksize {
        return ($drive, $msg);
     }
 
-    return undef;
+    return;
 }
 
 sub is_volume_in_use {
@@ -548,7 +741,7 @@ sub is_volume_in_use {
     my $path = PVE::Storage::path($storecfg, $volid);
 
     my $scan_config = sub {
-       my ($cref, $snapname) = @_;
+       my ($cref) = @_;
 
        foreach my $key (keys %$cref) {
            my $value = $cref->{$key};
@@ -564,7 +757,7 @@ sub is_volume_in_use {
                    next if !$storeid;
                    my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
                    next if !$scfg;
-                   return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
+                   return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file});
                }
            }
        }
@@ -576,8 +769,8 @@ sub is_volume_in_use {
 
     undef $skip_drive;
 
-    foreach my $snapname (keys %{$conf->{snapshots}}) {
-       return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
+    for my $snap (values %{$conf->{snapshots}}) {
+       return 1 if $scan_config->($snap);
     }
 
     return 0;
@@ -585,14 +778,107 @@ sub is_volume_in_use {
 
 sub resolve_first_disk {
     my ($conf, $cdrom) = @_;
-    my @disks = valid_drive_names();
+    my @disks = valid_drive_names_for_boot();
     foreach my $ds (@disks) {
        next if !$conf->{$ds};
        my $disk = parse_drive($ds, $conf->{$ds});
        next if drive_is_cdrom($disk) xor $cdrom;
        return $ds;
     }
-    return undef;
+    return;
 }
 
+sub scsi_inquiry {
+    my($fh, $noerr) = @_;
+
+    my $SG_IO = 0x2285;
+    my $SG_GET_VERSION_NUM = 0x2282;
+
+    my $versionbuf = "\x00" x 8;
+    my $ret = ioctl($fh, $SG_GET_VERSION_NUM, $versionbuf);
+    if (!$ret) {
+       die "scsi ioctl SG_GET_VERSION_NUM failoed - $!\n" if !$noerr;
+       return;
+    }
+    my $version = unpack("I", $versionbuf);
+    if ($version < 30000) {
+       die "scsi generic interface too old\n"  if !$noerr;
+       return;
+    }
+
+    my $buf = "\x00" x 36;
+    my $sensebuf = "\x00" x 8;
+    my $cmd = pack("C x3 C x1", 0x12, 36);
+
+    # see /usr/include/scsi/sg.h
+    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";
+
+    my $packet = pack(
+       $sg_io_hdr_t, ord('S'), -3, length($cmd), length($sensebuf), 0, length($buf), $buf, $cmd, $sensebuf, 6000
+    );
+
+    $ret = ioctl($fh, $SG_IO, $packet);
+    if (!$ret) {
+       die "scsi ioctl SG_IO failed - $!\n" if !$noerr;
+       return;
+    }
+
+    my @res = unpack($sg_io_hdr_t, $packet);
+    if ($res[17] || $res[18]) {
+       die "scsi ioctl SG_IO status error - $!\n" if !$noerr;
+       return;
+    }
+
+    my $res = {};
+    $res->@{qw(type removable vendor product revision)} = unpack("C C x6 A8 A16 A4", $buf);
+
+    $res->{removable} = $res->{removable} & 128 ? 1 : 0;
+    $res->{type} &= 0x1F;
+
+    return $res;
+}
+
+sub path_is_scsi {
+    my ($path) = @_;
+
+    my $fh = IO::File->new("+<$path") || return;
+    my $res = scsi_inquiry($fh, 1);
+    close($fh);
+
+    return $res;
+}
+
+sub get_scsi_device_type {
+    my ($drive, $storecfg, $machine_version) = @_;
+
+    my $devicetype = 'hd';
+    my $path = '';
+    if (drive_is_cdrom($drive) || drive_is_cloudinit($drive)) {
+       $devicetype = 'cd';
+    } else {
+       if ($drive->{file} =~ m|^/|) {
+           $path = $drive->{file};
+           if (my $info = path_is_scsi($path)) {
+               if ($info->{type} == 0 && $drive->{scsiblock}) {
+                   $devicetype = 'block';
+               } elsif ($info->{type} == 1) { # tape
+                   $devicetype = 'generic';
+               }
+           }
+       } elsif ($drive->{file} =~ $NEW_DISK_RE){
+           # special syntax cannot be parsed to path
+           return $devicetype;
+       } else {
+           $path = PVE::Storage::path($storecfg, $drive->{file});
+       }
+
+       # for compatibility only, we prefer scsi-hd (#2408, #2355, #2380)
+       if ($path =~ m/^iscsi\:\/\// &&
+           !PVE::QemuServer::Helpers::min_version($machine_version, 4, 1)) {
+           $devicetype = 'generic';
+       }
+    }
+
+    return $devicetype;
+}
 1;