use strict;
use warnings;
+use Storable qw(dclone);
+
+use IO::File;
+
use PVE::Storage;
use PVE::JSONSchema qw(get_standard_option);
drive_is_cloudinit
drive_is_cdrom
drive_is_read_only
+get_scsi_devicetype
parse_drive
print_drive
);
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' },
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',
}
);
+my %readonly_fmt = (
+ ro => {
+ type => 'boolean',
+ description => "Whether the drive is read-only.",
+ optional => 1,
+ },
+);
+
my %scsiblock_fmt = (
scsiblock => {
type => 'boolean',
};
PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
-my $ALLOCATION_SYNTAX_DESC =
- "Use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.";
-
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) . "). " .
- $ALLOCATION_SYNTAX_DESC,
+ 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 = {
optional => 1,
type => 'string', format => $scsi_fmt,
- description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . "). " .
- $ALLOCATION_SYNTAX_DESC,
+ description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
};
PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
my $satadesc = {
optional => 1,
type => 'string', format => $sata_fmt,
- description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). "). " .
- $ALLOCATION_SYNTAX_DESC,
+ description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
};
PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
my $virtio_fmt = {
%drivedesc_base,
%iothread_fmt,
+ %readonly_fmt,
};
my $virtiodesc = {
optional => 1,
type => 'string', format => $virtio_fmt,
- description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . "). " .
- $ALLOCATION_SYNTAX_DESC,
+ description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
};
PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
+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' },
file => {
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. " .
- $ALLOCATION_SYNTAX_DESC . " Note that SIZE_IN_GiB is ignored here " .
- "and that the default EFI vars are copied to the volume instead.",
+ description => "Configure a disk for storing EFI vars.",
};
PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
my $tpmstate_desc = {
optional => 1,
type => 'string', format => $tpmstate_fmt,
- description => "Configure a Disk for storing TPM state. " .
- $ALLOCATION_SYNTAX_DESC . " Note that SIZE_IN_GiB is ignored here " .
- "and that the default size of 4 MiB will always be used instead. The " .
- "format is also fixed to 'raw'.",
+ description => "Configure a Disk for storing TPM state. The format is fixed to 'raw'.",
};
use constant TPMSTATE_DISK_SIZE => 4 * 1024 * 1024;
%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 = {
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 {
'tpmstate0');
}
+sub valid_drive_names_with_unused {
+ return (valid_drive_names(), map {"unused$_"} (0 .. ($MAX_UNUSED_DISKS - 1)));
+}
+
sub is_valid_drivename {
my $dev = shift;
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 {
# [,iothread=on][,serial=serial][,model=model]
sub parse_drive {
- my ($key, $data) = @_;
+ my ($key, $data, $with_alloc) = @_;
my ($interface, $index);
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;
}
- my $desc = $drivedesc_hash->{$key}->{format};
+ my $desc = $desc_hash->{$key}->{format};
my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
return if !$res;
$res->{interface} = $interface;
}
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 get_bootdisks {
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});
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_devicetype {
+ 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;