diff --git a/src/PVE/GuestImport/OVF.pm b/src/PVE/GuestImport/OVF.pm index 70df604..a228cd2 100644 --- a/src/PVE/GuestImport/OVF.pm +++ b/src/PVE/GuestImport/OVF.pm @@ -369,24 +369,6 @@ ovf:Item[rasd:InstanceID='%s']/rasd:ResourceType", $controller_id, }; } - # the address is only unique per controller, so disks on different controllers of the same - # type can end up with the same address, move those to the next free one of that bus - my $used_addresses = { map { $_->{disk_address} => 1 } @disks }; - my $assigned_addresses = {}; - for my $disk (@disks) { - if ( - $assigned_addresses->{ $disk->{disk_address} } - && $disk->{disk_address} =~ m/^([a-z]+)\d+$/ - ) { - my $bus = $1; - my $index = 0; - $index++ while $used_addresses->{"$bus$index"}; - $disk->{disk_address} = "$bus$index"; - $used_addresses->{ $disk->{disk_address} } = 1; - } - $assigned_addresses->{ $disk->{disk_address} } = 1; - } - my $nic_id = dtmf_name_to_id('Ethernet Adapter'); my $xpath_find_nics = "/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${nic_id}]"; diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm new file mode 100644 index 0000000..80c4a03 --- /dev/null +++ b/src/PVE/Storage/DirPlugin.pm @@ -0,0 +1,326 @@ +package PVE::Storage::DirPlugin; + +use strict; +use warnings; + +use Cwd; +use Encode qw(decode encode); +use File::Path; +use File::Spec; +use IO::File; +use POSIX; + +use PVE::Storage::Plugin; +use PVE::GuestImport::OVF; +use PVE::JSONSchema qw(get_standard_option); + +use base qw(PVE::Storage::Plugin); + +# Configuration + +sub type { + return 'dir'; +} + +sub plugindata { + return { + content => [ + { + images => 1, + rootdir => 1, + vztmpl => 1, + iso => 1, + backup => 1, + snippets => 1, + none => 1, + import => 1, + }, + { images => 1, rootdir => 1 }, + ], + format => [{ raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 }, 'raw'], + 'sensitive-properties' => {}, + }; +} + +sub properties { + return { + path => { + description => "File system path.", + type => 'string', + format => 'pve-storage-path', + }, + mkdir => { + description => + "Create the directory if it doesn't exist and populate it with default sub-dirs." + . " NOTE: Deprecated, use the 'create-base-path' and 'create-subdirs' options instead.", + type => 'boolean', + default => 'yes', + }, + 'create-base-path' => { + description => "Create the base directory if it doesn't exist.", + type => 'boolean', + default => 'yes', + }, + 'create-subdirs' => { + description => "Populate the directory with the default structure.", + type => 'boolean', + default => 'yes', + }, + is_mountpoint => { + description => "Assume the given path is an externally managed mountpoint " + . "and consider the storage offline if it is not mounted. " + . "Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.", + type => 'string', + default => 'no', + }, + bwlimit => get_standard_option('bwlimit'), + }; +} + +sub options { + return { + path => { fixed => 1 }, + 'content-dirs' => { optional => 1 }, + nodes => { optional => 1 }, + shared => { optional => 1 }, + disable => { optional => 1 }, + 'prune-backups' => { optional => 1 }, + 'max-protected-backups' => { optional => 1 }, + content => { optional => 1 }, + format => { optional => 1 }, + mkdir => { optional => 1 }, + 'create-base-path' => { optional => 1 }, + 'create-subdirs' => { optional => 1 }, + is_mountpoint => { optional => 1 }, + bwlimit => { optional => 1 }, + preallocation => { optional => 1 }, + 'snapshot-as-volume-chain' => { optional => 1, fixed => 1 }, + }; +} + +# Storage implementation +# + +# NOTE: should ProcFSTools::is_mounted accept an optional cache like this? +sub path_is_mounted { + my ($mountpoint, $mountdata) = @_; + + $mountpoint = Cwd::realpath($mountpoint); # symlinks + return 0 if !defined($mountpoint); # path does not exist + + $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata; + return 1 if grep { $_->[1] eq $mountpoint } @$mountdata; + return undef; +} + +sub parse_is_mountpoint { + my ($scfg) = @_; + my $is_mp = $scfg->{is_mountpoint}; + return undef if !defined $is_mp; + if (defined(my $bool = PVE::JSONSchema::parse_boolean($is_mp))) { + return $bool ? $scfg->{path} : undef; + } + return $is_mp; # contains a path +} + +# FIXME move into 'get_volume_attribute' when removing 'get_volume_notes' +my $get_volume_notes_impl = sub { + my ($class, $scfg, $storeid, $volname, $timeout) = @_; + + my ($vtype) = $class->parse_volname($volname); + return if $vtype ne 'backup'; + + my $path = $class->filesystem_path($scfg, $volname); + $path .= $class->SUPER::NOTES_EXT; + + if (-f $path) { + my $data = PVE::Tools::file_get_contents($path); + return eval { decode('UTF-8', $data, 1) } // $data; + } + + return ''; +}; + +# FIXME remove on the next APIAGE reset. +# Deprecated, use get_volume_attribute instead. +sub get_volume_notes { + my ($class, $scfg, $storeid, $volname, $timeout) = @_; + return $get_volume_notes_impl->($class, $scfg, $storeid, $volname, $timeout); +} + +# FIXME move into 'update_volume_attribute' when removing 'update_volume_notes' +my $update_volume_notes_impl = sub { + my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_; + + my ($vtype) = $class->parse_volname($volname); + die "only backups can have notes\n" if $vtype ne 'backup'; + + my $path = $class->filesystem_path($scfg, $volname); + $path .= $class->SUPER::NOTES_EXT; + + if (defined($notes) && $notes ne '') { + my $encoded = encode('UTF-8', $notes); + PVE::Tools::file_set_contents($path, $encoded); + } else { + unlink $path or $! == ENOENT or die "could not delete notes - $!\n"; + } + return; +}; + +# FIXME remove on the next APIAGE reset. +# Deprecated, use update_volume_attribute instead. +sub update_volume_notes { + my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_; + return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $notes, $timeout); +} + +sub get_volume_attribute { + my ($class, $scfg, $storeid, $volname, $attribute) = @_; + + if ($attribute eq 'notes') { + return $get_volume_notes_impl->($class, $scfg, $storeid, $volname); + } + + my ($vtype) = $class->parse_volname($volname); + return if $vtype ne 'backup'; + + if ($attribute eq 'protected') { + my $path = $class->filesystem_path($scfg, $volname); + return -e PVE::Storage::protection_file_path($path) ? 1 : 0; + } + + return; +} + +sub update_volume_attribute { + my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_; + + if ($attribute eq 'notes') { + return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $value); + } + + my ($vtype) = $class->parse_volname($volname); + die "only backups support attribute '$attribute'\n" if $vtype ne 'backup'; + + if ($attribute eq 'protected') { + my $path = $class->filesystem_path($scfg, $volname); + my $protection_path = PVE::Storage::protection_file_path($path); + + return if !((-e $protection_path) xor $value); # protection status already correct + + if ($value) { + my $fh = IO::File->new($protection_path, O_CREAT, 0644) + or die "unable to create protection file '$protection_path' - $!\n"; + close($fh); + } else { + unlink $protection_path + or $! == ENOENT + or die "could not delete protection file '$protection_path' - $!\n"; + } + + return; + } + + die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n"; +} + +sub status { + my ($class, $storeid, $scfg, $cache) = @_; + + if (defined(my $mp = parse_is_mountpoint($scfg))) { + $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts() + if !$cache->{mountdata}; + + return undef if !path_is_mounted($mp, $cache->{mountdata}); + } + + return $class->SUPER::status($storeid, $scfg, $cache); +} + +sub activate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + + my $path = $scfg->{path}; + + my $mp = parse_is_mountpoint($scfg); + if (defined($mp) && !path_is_mounted($mp, $cache->{mountdata})) { + die "unable to activate storage '$storeid' - " + . "directory is expected to be a mount point but is not mounted: '$mp'\n"; + } + + $class->config_aware_base_mkdir($scfg, $path); + $class->SUPER::activate_storage($storeid, $scfg, $cache); +} + +sub check_config { + my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_; + my $opts = + PVE::SectionConfig::check_config($self, $sectionId, $config, $create, $skipSchemaCheck); + return $opts if !$create; + if ($opts->{path} !~ m|^/[-/a-zA-Z0-9_.@]+$|) { + die "illegal path for directory storage: $opts->{path}\n"; + } + # remove trailing slashes from path + $opts->{path} = File::Spec->canonpath($opts->{path}); + return $opts; +} + +sub get_import_metadata { + my ($class, $scfg, $volname, $storeid) = @_; + + my ($vtype, $name, undef, undef, undef, undef, $fmt) = $class->parse_volname($volname); + die "invalid content type '$vtype'\n" if $vtype ne 'import'; + die "invalid format\n" if $fmt ne 'ova' && $fmt ne 'ovf'; + + # NOTE: all types of warnings must be added to the return schema of the import-metadata API endpoint + my $warnings = []; + + my $isOva = 0; + if ($fmt =~ m/^ova/) { + $isOva = 1; + push @$warnings, { type => 'ova-needs-extracting' }; + } + my $path = $class->path($scfg, $volname, $storeid, undef); + my $res = PVE::GuestImport::OVF::parse_ovf($path, $isOva); + my $disks = {}; + for my $disk ($res->{disks}->@*) { + my $id = $disk->{disk_address}; + my $size = $disk->{virtual_size}; + my $path = $disk->{relative_path}; + my $volid; + if ($isOva) { + $volid = "$storeid:$volname/$path"; + } else { + $volid = "$storeid:import/$path",; + } + $disks->{$id} = { + volid => $volid, + defined($size) ? (size => $size) : (), + }; + } + + if (defined($res->{qm}->{bios}) && $res->{qm}->{bios} eq 'ovmf') { + $disks->{efidisk0} = 1; + push @$warnings, { type => 'efi-state-lost', key => 'bios', value => 'ovmf' }; + } + + return { + type => 'vm', + source => $volname, + 'create-args' => $res->{qm}, + 'disks' => $disks, + warnings => $warnings, + net => $res->{net}, + }; +} + +sub volume_qemu_snapshot_method { + my ($class, $storeid, $scfg, $volname) = @_; + + my $format = ($class->parse_volname($volname))[6]; + return 'storage' if $format ne 'qcow2'; + + return $scfg->{'snapshot-as-volume-chain'} ? 'mixed' : 'qemu'; +} + +1; diff --git a/src/test/run_ovf_tests.pl b/src/test/run_ovf_tests.pl index 8d89bc1..23f0aa6 100644 --- a/src/test/run_ovf_tests.pl +++ b/src/test/run_ovf_tests.pl @@ -38,15 +38,6 @@ if (my $err = $@) { ok('parse win10 no default rasd NS'); } -my $two_controllers = - eval { PVE::GuestImport::OVF::parse_ovf("$test_manifests/two-scsi-controllers.ovf") }; -if (my $err = $@) { - fail('parse two scsi controllers'); - warn("error: $err\n"); -} else { - ok('parse two scsi controllers'); -} - my $xee = eval { PVE::GuestImport::OVF::parse_ovf("$test_manifests/XEE.ovf") }; if (my $err = $@) { ok("parsing XEE.ovf failed as expected"); @@ -104,17 +95,6 @@ is( 'single disk vm (no default rasd NS) has the correct size', ); -is_deeply( - [map { $_->{disk_address} } $two_controllers->{disks}->@*], - ['scsi0', 'scsi2', 'scsi1'], - 'disks with the same address on different controllers get unique addresses', -); -is( - $two_controllers->{disks}->[1]->{backing_file}, - "$test_manifests/Win10-Liz-disk1.vmdk", - 'disk on the second controller has the correct backing device', -); - print "testing nics\n"; is($win2008->{net}->{net0}->{model}, 'e1000', 'win2008 has correct nic model'); is($win10->{net}->{net0}->{model}, 'e1000e', 'win10 has correct nic model'); @@ -129,12 +109,6 @@ is($win2008->{qm}->{memory}, '2048', 'win2008 VM memory is correct'); is($win2008->{qm}->{cores}, '1', 'win2008 VM cores are correct'); is($win2008->{qm}->{ostype}, 'win7', 'win2008 VM ostype is correcty'); -is( - $two_controllers->{qm}->{boot}, - 'order=scsi0;scsi2;scsi1', - 'VM with two scsi controllers boot is correct', -); - is($win10->{qm}->{boot}, 'order=scsi0', 'win10 VM boot is correct'); is($win10->{qm}->{name}, 'Win10-Liz', 'win10 VM name is correct'); is($win10->{qm}->{memory}, '6144', 'win10 VM memory is correct');