1 package PVE
::Storage
::ESXiPlugin
;
6 use Fcntl
qw(F_GETFD F_SETFD FD_CLOEXEC);
7 use JSON
qw(from_json);
9 use File
::Path
qw(mkpath remove_tree);
13 use PVE
::Tools
qw(file_get_contents file_set_contents run_command);
15 use base
qw(PVE::Storage::Plugin);
17 my $ESXI_LIST_VMS = '/usr/libexec/pve-esxi-import-tools/listvms.py';
18 my $ESXI_FUSE_TOOL = '/usr/libexec/pve-esxi-import-tools/esxi-folder-fuse';
19 my $ESXI_PRIV_DIR = '/etc/pve/priv/import/esxi';
31 content
=> [ { import
=> 1 }, { import
=> 1 }],
32 format
=> [ { raw
=> 1, qcow2
=> 1, vmdk
=> 1 } , 'raw' ],
38 'skip-cert-verification' => {
39 description
=> 'Disable TLS certificate verification, only enable on fully trusted networks!',
48 nodes
=> { optional
=> 1 },
49 shared
=> { optional
=> 1 },
50 disable
=> { optional
=> 1 },
51 content
=> { optional
=> 1 },
52 # FIXME: bwlimit => { optional => 1 },
55 password
=> { optional
=> 1},
56 'skip-cert-verification' => { optional
=> 1},
60 sub esxi_cred_file_name
{
62 return "/etc/pve/priv/storage/${storeid}.pw";
65 sub esxi_delete_credentials
{
68 if (my $cred_file = get_cred_file
($storeid)) {
69 unlink($cred_file) or warn "removing esxi credientials '$cred_file' failed: $!\n";
73 sub esxi_set_credentials
{
74 my ($password, $storeid) = @_;
76 my $cred_file = esxi_cred_file_name
($storeid);
77 mkdir "/etc/pve/priv/storage";
79 PVE
::Tools
::file_set_contents
($cred_file, $password);
87 my $cred_file = esxi_cred_file_name
($storeid);
96 # Dealing with the esxi API.
99 my sub run_path
: prototype($) {
101 return "/run/pve/import/esxi/$storeid";
104 # "public" because it is needed by the VMX package
105 sub mount_dir
: prototype($) {
107 return run_path
($storeid) . "/mnt";
110 my sub check_esxi_import_package
: prototype() {
111 die "pve-esxi-import-tools package not installed, cannot proceed\n"
112 if !-e
$ESXI_LIST_VMS;
115 my sub is_old
: prototype($) {
117 my $mtime = (CORE
::stat($file))[9];
118 return !defined($mtime) || ($mtime + 60) < CORE
::time();
121 sub get_manifest
: prototype($$$;$) {
122 my ($class, $storeid, $scfg, $force_query) = @_;
124 my $rundir = run_path
($storeid);
125 my $manifest_file = "$rundir/manifest.json";
127 $force_query ||= is_old
($manifest_file);
129 if (!$force_query && -e
$manifest_file) {
130 return PVE
::Storage
::ESXiPlugin
::Manifest-
>new(
131 file_get_contents
($manifest_file),
135 check_esxi_import_package
();
138 push @extra_params, '--skip-cert-verification' if $scfg->{'skip-cert-verification'};
139 my $host = $scfg->{server
};
140 my $user = $scfg->{username
};
141 my $pwfile = esxi_cred_file_name
($storeid);
144 [$ESXI_LIST_VMS, @extra_params, $host, $user, $pwfile],
145 outfunc
=> sub { $json .= $_[0] . "\n" },
148 my $result = PVE
::Storage
::ESXiPlugin
::Manifest-
>new($json);
150 file_set_contents
($manifest_file, $json);
155 my sub scope_name_base
: prototype($) {
157 return "pve-esxi-fuse-" . PVE
::Systemd
::escape_unit
($storeid);
160 my sub is_mounted
: prototype($) {
163 my $scope_name_base = scope_name_base
($storeid);
164 return PVE
::Systemd
::is_unit_active
($scope_name_base . '.scope');
167 sub esxi_mount
: prototype($$$;$) {
168 my ($class, $storeid, $scfg, $force_requery) = @_;
170 return if !$force_requery && is_mounted
($storeid);
172 $class->get_manifest($storeid, $scfg, $force_requery);
174 my $rundir = run_path
($storeid);
175 my $manifest_file = "$rundir/manifest.json";
176 my $mount_dir = mount_dir
($storeid);
177 if (!mkdir($mount_dir)) {
178 die "mkdir failed on $mount_dir $!\n" if !$!{EEXIST
};
181 my $scope_name_base = scope_name_base
($storeid);
182 my $user = $scfg->{username
};
183 my $host = $scfg->{server
};
184 my $pwfile = esxi_cred_file_name
($storeid);
186 pipe(my $rd, my $wr) or die "failed to create pipe: $!\n";
189 die "fork failed: $!\n" if !defined($pid);
194 PVE
::Systemd
::enter_systemd_scope
(
196 "Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
200 push @extra_params, '--skip-cert-verification' if $scfg->{'skip-cert-verification'};
202 my $flags = fcntl($wr, F_GETFD
, 0)
203 // die "failed to get file descriptor flags: $!\n";
204 fcntl($wr, F_SETFD
, $flags & ~FD_CLOEXEC
)
205 // die "failed to remove CLOEXEC flag from fd: $!\n";
206 # FIXME: use the user/group options!
207 exec {$ESXI_FUSE_TOOL}
211 '--ready-fd', fileno($wr),
213 '--password-file', $pwfile,
217 die "exec failed: $!\n";
220 print {$wr} "ERROR: $err";
226 my $result = do { local $/ = undef; <$rd> };
227 if ($result =~ /^ERROR: (.*)$/) {
231 if (waitpid($pid, POSIX
::WNOHANG
) == $pid) {
232 die "failed to spawn fuse mount, process exited with status $?\n";
236 sub esxi_unmount
: prototype($$$) {
237 my ($class, $storeid, $scfg) = @_;
239 my $scope_name_base = scope_name_base
($storeid);
240 my $scope = "${scope_name_base}.scope";
241 my $mount_dir = mount_dir
($storeid);
243 my %silence_std_outs = (outfunc
=> sub {}, errfunc
=> sub {});
244 eval { run_command
(['/bin/systemctl', 'reset-failed', $scope], %silence_std_outs) };
245 eval { run_command
(['/bin/systemctl', 'stop', $scope], %silence_std_outs) };
246 run_command
(['/bin/umount', $mount_dir]);
249 # Split a path into (datacenter, datastore, path)
250 sub split_path
: prototype($) {
252 if ($path =~ m!^([^/]+)/([^/]+)/(.+)$!) {
258 sub get_import_metadata
: prototype($$$$$) {
259 my ($class, $scfg, $volname, $storeid) = @_;
261 if ($volname !~ m!^([^/]+)/.*\.vmx$!) {
262 die "volume '$volname' does not look like an importable vm config\n";
265 my $vmx_path = $class->path($scfg, $volname, $storeid, undef);
266 if (!is_mounted
($storeid)) {
267 die "storage '$storeid' is not activated\n";
270 my $manifest = $class->get_manifest($storeid, $scfg, 0);
271 my $contents = file_get_contents
($vmx_path);
272 my $vmx = PVE
::Storage
::ESXiPlugin
::VMX-
>parse(
279 return $vmx->get_create_args();
282 # Returns a size in bytes, this is a helper for already-mounted files.
283 sub query_vmdk_size
: prototype($;$) {
284 my ($filename, $timeout) = @_;
288 run_command
(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
290 outfunc
=> sub { $json .= $_[0]; },
291 errfunc
=> sub { warn "$_[0]\n"; }
297 return int($json->{'virtual-size'});
301 # Storage API implementation
305 my ($class, $storeid, $scfg, %sensitive) = @_;
307 my $password = $sensitive{password
};
308 die "missing password\n" if !defined($password);
309 esxi_set_credentials
($password, $storeid);
315 my ($class, $storeid, $scfg, %sensitive) = @_;
317 return if !exists($sensitive{password
});
319 if (defined($sensitive{password
})) {
320 esxi_set_credentials
($sensitive{password
}, $storeid);
322 esxi_delete_credentials
($storeid);
329 my ($class, $storeid, $scfg) = @_;
331 eval { $class->deactivate_storage($storeid, $scfg) };
334 esxi_delete_credentials
($storeid);
339 sub activate_storage
{
340 my ($class, $storeid, $scfg, $cache) = @_;
342 $class->esxi_mount($storeid, $scfg, 0);
345 sub deactivate_storage
{
346 my ($class, $storeid, $scfg, $cache) = @_;
348 $class->esxi_unmount($storeid, $scfg);
351 sub activate_volume
{
352 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
354 # FIXME: maybe check if it exists?
357 sub deactivate_volume
{
358 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
363 sub check_connection
{
364 my ($class, $storeid, $scfg) = @_;
366 return PVE
::Network
::tcp_ping
($scfg->{server
}, 443, 2);
370 my ($class, $storeid, $scfg, $cache) = @_;
376 my ($class, $volname) = @_;
378 # it doesn't really make sense tbh, we can't return an owner, the format
379 # may be a 'vmx' (config), the paths are arbitrary...
381 die "failed to parse volname '$volname'\n"
382 if $volname !~ m!^([^/]+)/([^/]+)/(.+)$!;
384 return ('import', $volname) if $volname =~ /\.vmx$/;
387 $format = 'vmdk' if $volname =~ /\.vmdk/;
388 return ('images', $volname, 0, undef, undef, undef, $format);
392 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
398 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
400 return if !grep { $_ eq 'import' } @$content_types;
402 my $data = $class->get_manifest($storeid, $scfg, 0);
405 for my $dc_name (keys $data->%*) {
406 my $dc = $data->{$dc_name};
407 my $vms = $dc->{vms
};
408 for my $vm_name (keys $vms->%*) {
409 my $vm = $vms->{$vm_name};
410 my $ds_name = $vm->{config
}->{datastore
};
411 my $path = $vm->{config
}->{path
};
416 volid
=> "$storeid:$dc_name/$ds_name/$path",
426 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
428 die "cloning images is not supported for $class\n";
432 my ($class, $storeid, $scfg, $volname) = @_;
434 die "creating base images is not supported for $class\n";
438 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
440 die "storage '$class' does not support snapshots\n" if defined $snapname;
442 # FIXME: activate/mount:
443 return mount_dir
($storeid) . '/' . $volname;
447 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
449 die "creating images is not supported for $class\n";
453 my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
455 die "deleting images is not supported for $class\n";
459 my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
461 die "renaming volumes is not supported for $class\n";
464 sub volume_export_formats
{
465 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
467 # FIXME: maybe we can support raw+size via `qemu-img dd`?
469 die "exporting not supported for $class\n";
473 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
475 # FIXME: maybe we can support raw+size via `qemu-img dd`?
477 die "exporting not supported for $class\n";
480 sub volume_import_formats
{
481 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
483 die "importing not supported for $class\n";
487 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
489 die "importing not supported for $class\n";
493 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
495 die "resizing volumes is not supported for $class\n";
498 sub volume_size_info
{
499 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
501 return 0 if $volname =~ /\.vmx$/;
503 my $filename = $class->path($scfg, $volname, $storeid, undef);
504 return PVE
::Storage
::Plugin
::file_size_info
($filename, $timeout);
507 sub volume_snapshot
{
508 my ($class, $scfg, $storeid, $volname, $snap) = @_;
510 die "creating snapshots is not supported for $class\n";
513 sub volume_snapshot_delete
{
514 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
516 die "deleting snapshots is not supported for $class\n";
518 sub volume_snapshot_info
{
520 my ($class, $scfg, $storeid, $volname) = @_;
522 die "getting snapshot information is not supported for $class";
525 sub volume_rollback_is_possible
{
526 my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
531 sub volume_has_feature
{
532 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
534 return undef if defined($snapname) || $volname =~ /\.vmx$/;
535 return 1 if $feature eq 'copy';
540 my ($class, $scfg, $vtype) = @_;
542 die "no subdirectories available for storage $class\n";
545 package PVE
::Storage
::ESXiPlugin
::Manifest
;
550 use JSON
qw(from_json);
552 sub new
: prototype($$) {
553 my ($class, $data) = @_;
555 my $json = from_json
($data);
557 return bless $json, $class;
560 sub datacenter_for_vm
{
561 my ($self, $vm) = @_;
563 for my $dc_name (sort keys %$self) {
564 my $dc = $self->{$dc_name};
565 return $dc_name if exists($dc->{vms
}->{$vm});
571 sub datastore_for_vm
{
572 my ($self, $vm, $datacenter) = @_;
574 my @dc_names = defined($datacenter) ?
($datacenter) : keys %$self;
575 for my $dc_name (@dc_names) {
576 my $dc = $self->{$dc_name}
577 or die "no such datacenter '$datacenter'\n";
578 if (defined(my $vm = $dc->{vms
}->{$vm})) {
579 return $vm->{config
}->{datastore
};
587 my ($self, $path) = @_;
589 if ($path !~ m
|^/|) {
590 return wantarray ?
(undef, undef, $path) : $path;
593 for my $dc_name (sort keys %$self) {
594 my $dc = $self->{$dc_name};
596 my $datastores = $dc->{datastores
};
598 for my $ds_name (keys %$datastores) {
599 my $ds_path = $datastores->{$ds_name};
600 if (substr($path, 0, length($ds_path)) eq $ds_path) {
601 my $relpath = substr($path, length($ds_path));
602 return wantarray ?
($dc_name, $ds_name, $relpath) : $relpath;
610 sub config_path_for_vm
{
611 my ($self, $vm, $datacenter) = @_;
613 my @dc_names = defined($datacenter) ?
($datacenter) : keys %$self;
614 for my $dc_name (@dc_names) {
615 my $dc = $self->{$dc_name}
616 or die "no such datacenter '$datacenter'\n";
618 my $vm = $dc->{vms
}->{$vm}
621 my $cfg = $vm->{config
};
622 if (my (undef, $ds_name, $path) = $self->resolve_path($cfg->{path
})) {
623 $ds_name //= $cfg->{datastore
};
624 return ($dc_name, $ds_name, $path);
627 die "failed to resolve path for vm '$vm' "
628 ."($dc_name, $cfg->{datastore}, $cfg->{path})\n";
631 die "no such vm '$vm'\n";
634 # Since paths in the vmx file are relative to the vmx file itself, this helper
635 # provides a way to resolve paths which are relative based on the config file
636 # path, while also resolving absolute paths without the vm config.
637 sub resolve_path_relative_to
{
638 my ($self, $vmx_path, $path) = @_;
640 if ($path =~ m
|^/|) {
641 if (my ($disk_dc, $disk_ds, $disk_path) = $self->resolve_path($path)) {
642 return "$disk_dc/$disk_ds/$disk_path";
644 die "failed to resolve path '$path'\n";
647 my ($rel_dc, $rel_ds, $rel_path) = PVE
::Storage
::ESXiPlugin
::split_path
($vmx_path)
648 or die "bad path '$vmx_path'\n";
649 $rel_path =~ s
|/[^/]+$||;
651 return "$rel_dc/$rel_ds/$rel_path/$path";
654 # Imports happen by the volume id which is a path to a VMX file.
655 # In order to find the vm's power state and disk capacity info, we need to find the
656 # VM the vmx file belongs to.
657 sub vm_for_vmx_path
{
658 my ($self, $vmx_path) = @_;
660 my ($dc_name, $ds_name, $path) = PVE
::Storage
::ESXiPlugin
::split_path
($vmx_path);
661 if (my $dc = $self->{$dc_name}) {
662 my $vms = $dc->{vms
};
663 for my $vm_name (keys %$vms) {
664 my $vm = $vms->{$vm_name};
665 my $cfg_info = $vm->{config
};
666 if ($cfg_info->{datastore
} eq $ds_name && $cfg_info->{path
} eq $path) {
674 package PVE
::Storage
::ESXiPlugin
::VMX
;
680 # FIXME: see if vmx files can actually have escape sequences in their quoted values?
681 my sub unquote
: prototype($) {
683 $value =~ s/^\"(.*)\"$/$1/s
684 or $value =~ s/^\'(.*)\'$/$1/s;
688 sub parse
: prototype($$$$$$) {
689 my ($class, $storeid, $scfg, $vmx_path, $vmxdata, $manifest) = @_;
693 for my $line (split(/\n/, $vmxdata)) {
696 next if $line !~ /^(\S+)\s*=\s*(.+)$/;
697 my ($key, $value) = ($1, $2);
699 $value = unquote
($value);
701 $conf->{$key} = $value;
704 $conf->{'pve.storeid'} = $storeid;
705 $conf->{'pve.storage.config'} = $scfg;
706 $conf->{'pve.vmx.path'} = $vmx_path;
707 $conf->{'pve.manifest'} = $manifest;
709 return bless $conf, $class;
712 sub storeid
{ $_[0]->{'pve.storeid'} }
713 sub scfg
{ $_[0]->{'pve.storage.config'} }
714 sub vmx_path
{ $_[0]->{'pve.vmx.path'} }
715 sub manifest
{ $_[0]->{'pve.manifest'} }
717 # (Also used for the fileName config key...)
718 sub is_disk_entry
: prototype($) {
720 if ($id =~ /^(scsi|ide|sata|nvme)(\d+:\d+)(:?\.fileName)?$/) {
727 my ($self, $bus, $slot) = @_;
728 if (my $type = $self->{"${bus}${slot}.deviceType"}) {
729 return $type =~ /cdrom/;
735 my ($self, $code) = @_;
737 for my $key (sort keys %$self) {
738 my ($bus, $slot) = is_disk_entry
($key)
740 my $kind = $self->is_cdrom($bus, $slot) ?
'cdrom' : 'disk';
742 my $file = $self->{$key};
744 my ($maj, $min) = split(/:/, $slot, 2);
745 my $vdev = $self->{"${bus}${maj}.virtualDev"}; # may of course be undef...
747 $code->($bus, $slot, $file, $vdev, $kind);
753 sub for_each_netdev
{
754 my ($self, $code) = @_;
757 for my $key (keys %$self) {
758 next if $key !~ /^ethernet(\d+)\.(.+)$/;
759 my ($slot, $opt) = ($1, $2);
761 my $dev = ($found_devs->{$slot} //= {});
762 $dev->{$opt} = $self->{$key};
765 for my $id (sort keys %$found_devs) {
766 my $dev = $found_devs->{$id};
768 next if ($dev->{present
} // '') ne 'TRUE';
770 my $ty = $dev->{addressType
};
771 my $mac = $dev->{address
};
772 if ($ty && fc
($ty) eq fc
('generated')) {
773 $mac = $dev->{generatedAddress
} // $mac;
776 $code->($id, $dev, $mac);
782 sub for_each_serial
{
783 my ($self, $code) = @_;
785 my $found_serials = {};
786 for my $key (sort keys %$self) {
787 next if $key !~ /^serial(\d+)\.(.+)$/;
788 my ($slot, $opt) = ($1, $2);
789 my $serial = ($found_serials->{$1} //= {});
790 $serial->{$opt} = $self->{$key};
793 for my $id (sort { $a <=> $b } keys %$found_serials) {
794 my $serial = $found_serials->{$id};
796 next if ($serial->{present
} // '') ne 'TRUE';
798 $code->($id, $serial);
806 my $fw = $self->{firmware
};
807 return 'efi' if $fw && fc
($fw) eq fc
('efi');
815 return $self->{memSize
};
818 # CPU info is stored as a maximum ('numvcpus') and a core-per-socket count.
819 # We return a (cores, sockets) tuple the way want it for PVE.
823 my $cps = int($self->{'cpuid.coresPerSocket'} // 1);
824 my $max = int($self->{numvcpus
} // $cps);
826 return ($cps, ($max / $cps));
829 # FIXME: Test all possible values esxi creates?
833 my $guest = $self->{guestOS
} // return;
834 return 1 if $guest =~ /^win/i;
840 winNetBusiness
=> 'w2k3',
842 'windows9-64' => 'win10',
843 'windows11-64' => 'win11',
844 'windows12-64' => 'win11', # FIXME / win12?
845 win2000AdvServ
=> 'w2k',
847 win2000Serv
=> 'w2k',
850 'windows7-64' => 'win7',
852 'windows8-64' => 'win8',
856 winNetEnterprise
=> 'w2k3',
857 'winNetEnterprise-64' => 'w2k3',
858 winNetDatacenter
=> 'w2k3',
859 'winNetDatacenter-64' => 'w2k3',
860 winNetStandard
=> 'w2k3',
861 'winNetStandard-64' => 'w2k3',
863 winLonghorn
=> 'w2k8',
864 'winLonghorn-64' => 'w2k8',
865 'windows7Server-64' => 'w2k8',
866 'windows8Server-64' => 'win8',
867 'windows9Server-64' => 'win10',
868 'windows2019srv-64' => 'win10',
869 'windows2019srvNext-64' => 'win11',
870 'windows2022srvNext-64' => 'win11', # FIXME / win12?
871 winVista
=> 'wvista',
872 'winVista-64' => 'wvista',
874 'winXPPro-64' => 'wxp',
877 # Best effort translation from vmware guest os type to pve.
878 # Returns a tuple: `(pve-type, is_windows)`
882 if (defined(my $guest = $self->{guestOS
})) {
883 if (defined(my $known = $guest_types{$guest})) {
886 # This covers all the 'Mac OS' types AFAICT
887 return ('other', 0) if $guest =~ /^darwin/;
890 # otherwise we'll just go with l26 defaults because why not...
897 my $uuid = $self->{'uuid.bios'};
899 return if !defined($uuid);
901 # vmware stores space separated bytes and has 1 dash in the middle...
902 $uuid =~ s/[^0-9a-fA-f]//g;
912 return "$1-$2-$3-$4-$5";
917 # This builds arguments for the `create` api call for this config.
918 sub get_create_args
{
921 my $storeid = $self->storeid;
922 my $manifest = $self->manifest;
923 my $vminfo = $manifest->vm_for_vmx_path($self->vmx_path);
925 my $create_args = {};
926 my $create_disks = {};
930 # NOTE: all types must be added to the return schema of the import-metadata API endpoint
932 my ($type, %properties) = @_;
933 push @$warnings, { type
=> $type, %properties };
936 my ($cores, $sockets) = $self->cpu_info();
937 $create_args->{cores
} = $cores if $cores != 1;
938 $create_args->{sockets
} = $sockets if $sockets != 1;
940 my $firmware = $self->firmware;
941 if ($firmware eq 'efi') {
942 $create_args->{bios
} = 'ovmf';
943 $create_disks->{efidisk0
} = 1;
945 $create_args->{bios
} = 'seabios';
948 my $memory = $self->memory;
949 $create_args->{memory
} = $memory;
953 my $set_scsihw = sub {
954 if (defined($scsihw) && $scsihw ne $_[0]) {
955 warn "multiple different SCSI hardware types are not supported\n";
961 my ($ostype, $is_windows) = $self->guest_type();
962 $create_args->{ostype
} //= $ostype if defined($ostype);
963 if ($ostype eq 'l26') {
964 $default_scsihw = 'virtio-scsi-single';
967 $self->for_each_netdev(sub {
968 my ($id, $dev, $mac) = @_;
970 my $model = $dev->{virtualDev
} // 'vmxnet3';
972 my $param = { model
=> $model };
973 $param->{macaddr
} = $mac if length($mac);
974 $create_net->{"net$id"} = $param;
977 my %counts = ( scsi
=> 0, sata
=> 0, ide
=> 0 );
979 my $mntdir = PVE
::Storage
::ESXiPlugin
::mount_dir
($storeid);
983 # we deal with nvme disks in a 2nd go-around since we currently don't
984 # support nvme disks and instead just add them as additional scsi
988 my ($bus, $slot, $file, $devtype, $kind, $do_nvmes) = @_;
993 } elsif ($bus eq 'nvme') {
994 push @nvmes, [$slot, $file, $devtype, $kind];
998 my $path = eval { $manifest->resolve_path_relative_to($self->vmx_path, $file) };
999 return if !defined($path);
1001 # my $fullpath = "$mntdir/$path";
1002 # return if !-e $fullpath;
1005 if ($devtype =~ /^lsi/i) {
1006 $set_scsihw->('lsi');
1007 } elsif ($devtype eq 'pvscsi') {
1008 $set_scsihw->('pvscsi'); # same name in pve
1013 if (defined(my $diskinfo = $vminfo->{disks
})) {
1014 my ($dc, $ds, $rel_path) = PVE
::Storage
::ESXiPlugin
::split_path
($path);
1015 for my $disk ($diskinfo->@*) {
1016 if ($disk->{datastore
} eq $ds && $disk->{path
} eq $rel_path) {
1017 $disk_capacity = $disk->{capacity
};
1023 my $count = $counts{$bus}++;
1024 if ($kind eq 'cdrom') {
1025 # We currently do not pass cdroms through via the esxi storage.
1026 # Users should adapt import these from the storages directly/manually.
1027 $create_args->{"${bus}${count}"} = "none,media=cdrom";
1028 # CD-ROM image will not get imported
1029 $warn->('cdrom-image-ignored', key
=> "${bus}${count}", value
=> "$storeid:$path");
1031 $create_disks->{"${bus}${count}"} = {
1032 volid
=> "$storeid:$path",
1033 defined($disk_capacity) ?
(size
=> $disk_capacity) : (),
1037 $boot_order .= ';' if length($boot_order);
1038 $boot_order .= $bus.$count;
1040 $self->for_each_disk($add_disk);
1042 for my $nvme (@nvmes) {
1043 my ($slot, $file, $devtype, $kind) = @$nvme;
1044 $warn->('nvme-unsupported', key
=> "nvme${slot}", value
=> "$file");
1045 $add_disk->('scsi', $slot, $file, $devtype, $kind, 1);
1049 $scsihw //= $default_scsihw;
1050 if ($firmware eq 'efi') {
1051 if (!defined($scsihw) || $scsihw =~ /^lsi/) {
1055 $scsihw = 'virtio-scsi-single';
1057 # OVMF is built without LSI drivers, scsi hardware was set to $scsihw
1058 $warn->('ovmf-with-lsi-unsupported', key
=> 'scsihw', value
=> "$scsihw");
1061 $create_args->{scsihw
} = $scsihw;
1063 $create_args->{boot
} = "order=$boot_order";
1065 if (defined(my $smbios1_uuid = $self->smbios1_uuid())) {
1066 $create_args->{smbios1
} = "uuid=$smbios1_uuid";
1069 if (defined(my $name = $self->{displayName
})) {
1070 # name in pve is a 'dns-name', so... clean it
1072 $name =~ s/[^a-zA-Z0-9\-.]//g;
1073 $name =~ s/^[.-]+//;
1074 $name =~ s/[.-]+$//;
1075 $create_args->{name
} = $name if length($name);
1079 $self->for_each_serial(sub {
1080 my ($id, $serial) = @_;
1081 # currently we only support 'socket' type serials anyway
1082 $warn->('serial-port-socket-only', key
=> "serial$serid");
1083 $create_args->{"serial$serid"} = 'socket';
1087 $warn->('guest-is-running') if defined($vminfo) && ($vminfo->{power
}//'') ne 'poweredOff';
1092 'create-args' => $create_args,
1093 disks
=> $create_disks,
1095 warnings
=> $warnings,