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' ],
42 nodes
=> { optional
=> 1 },
43 shared
=> { optional
=> 1 },
44 disable
=> { optional
=> 1 },
45 content
=> { optional
=> 1 },
46 # FIXME: bwlimit => { optional => 1 },
49 password
=> { optional
=> 1},
53 sub esxi_cred_file_name
{
55 return "/etc/pve/priv/storage/${storeid}.pw";
58 sub esxi_delete_credentials
{
61 if (my $cred_file = get_cred_file
($storeid)) {
62 unlink($cred_file) or warn "removing esxi credientials '$cred_file' failed: $!\n";
66 sub esxi_set_credentials
{
67 my ($password, $storeid) = @_;
69 my $cred_file = esxi_cred_file_name
($storeid);
70 mkdir "/etc/pve/priv/storage";
72 PVE
::Tools
::file_set_contents
($cred_file, $password);
80 my $cred_file = esxi_cred_file_name
($storeid);
89 # Dealing with the esxi API.
92 my sub run_path
: prototype($) {
94 return "/run/pve/import/esxi/$storeid";
97 # "public" because it is needed by the VMX package
98 sub mount_dir
: prototype($) {
100 return run_path
($storeid) . "/mnt";
103 my sub check_esxi_import_package
: prototype() {
104 die "pve-esxi-import-tools package not installed, cannot proceed\n"
105 if !-e
$ESXI_LIST_VMS;
108 sub get_manifest
: prototype($$$;$) {
109 my ($class, $storeid, $scfg, $force_query) = @_;
111 my $rundir = run_path
($storeid);
112 my $manifest_file = "$rundir/manifest.json";
114 if (!$force_query && -e
$manifest_file) {
115 return PVE
::Storage
::ESXiPlugin
::Manifest-
>new(
116 file_get_contents
($manifest_file),
120 check_esxi_import_package
();
122 my $host = $scfg->{server
};
123 my $user = $scfg->{username
};
124 my $pwfile = esxi_cred_file_name
($storeid);
127 [$ESXI_LIST_VMS, $host, $user, $pwfile],
128 outfunc
=> sub { $json .= $_[0] . "\n" },
131 my $result = PVE
::Storage
::ESXiPlugin
::Manifest-
>new($json);
133 file_set_contents
($manifest_file, $json);
138 my sub scope_name_base
: prototype($) {
140 return "pve-esxi-fuse-" . PVE
::Systemd
::escape_unit
($storeid);
143 my sub is_mounted
: prototype($) {
146 my $scope_name_base = scope_name_base
($storeid);
147 return PVE
::Systemd
::is_unit_active
($scope_name_base . '.scope');
150 sub esxi_mount
: prototype($$$;$) {
151 my ($class, $storeid, $scfg, $force_requery) = @_;
153 return if !$force_requery && is_mounted
($storeid);
155 $class->get_manifest($storeid, $scfg, $force_requery);
157 my $rundir = run_path
($storeid);
158 my $manifest_file = "$rundir/manifest.json";
159 my $mount_dir = mount_dir
($storeid);
160 if (!mkdir($mount_dir)) {
161 die "mkdir failed on $mount_dir $!\n" if !$!{EEXIST
};
164 my $scope_name_base = scope_name_base
($storeid);
165 my $user = $scfg->{username
};
166 my $host = $scfg->{server
};
167 my $pwfile = esxi_cred_file_name
($storeid);
169 pipe(my $rd, my $wr) or die "failed to create pipe: $!\n";
172 die "fork failed: $!\n" if !defined($pid);
177 PVE
::Systemd
::enter_systemd_scope
(
179 "Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
182 my $flags = fcntl($wr, F_GETFD
, 0)
183 // die "failed to get file descriptor flags: $!\n";
184 fcntl($wr, F_SETFD
, $flags & ~FD_CLOEXEC
)
185 // die "failed to remove CLOEXEC flag from fd: $!\n";
186 # FIXME: use the user/group options!
187 exec {$ESXI_FUSE_TOOL}
190 '--ready-fd', fileno($wr),
192 '--password-file', $pwfile,
196 die "exec failed: $!\n";
199 print {$wr} "ERROR: $err";
205 my $result = do { local $/ = undef; <$rd> };
206 if ($result =~ /^ERROR: (.*)$/) {
210 if (waitpid($pid, POSIX
::WNOHANG
) == $pid) {
211 die "failed to spawn fuse mount, process exited with status $?\n";
215 sub esxi_unmount
: prototype($$$) {
216 my ($class, $storeid, $scfg) = @_;
218 my $scope_name_base = scope_name_base
($storeid);
219 my $scope = "${scope_name_base}.scope";
220 my $mount_dir = mount_dir
($storeid);
222 my %silence_std_outs = (outfunc
=> sub {}, errfunc
=> sub {});
223 eval { run_command
(['/bin/systemctl', 'reset-failed', $scope], %silence_std_outs) };
224 eval { run_command
(['/bin/systemctl', 'stop', $scope], %silence_std_outs) };
225 run_command
(['/bin/umount', $mount_dir]);
228 my sub get_raw_vmx
: prototype($$$$%) {
229 my ($class, $storeid, $scfg, $vm, %opts) = @_;
231 my ($datacenter, $mount, $force_requery) = @opts{qw(datacenter mount force-requery)};
232 my $mntdir = mount_dir
($storeid);
233 my $manifest = $class->get_manifest($storeid, $scfg, $force_requery);
235 $datacenter //= $manifest->datacenter_for_vm($vm);
236 die "no such VM\n" if !defined($datacenter);
238 my $dc = $manifest->{$datacenter}
239 or die "no such datacenter\n";
240 my $info = $dc->{vms
}->{$vm}
241 or die "no such vm\n";
242 my ($datastore, $path) = $info->{config
}->@{qw(datastore path)};
244 if ($mount && !is_mounted
($storeid)) {
245 $class->esxi_mount($storeid, $scfg, $force_requery);
248 my $contents = file_get_contents
("$mntdir/$datacenter/$datastore/$path");
249 return wantarray ?
($datacenter, $contents) : $contents;
252 # Split a path into (datacenter, datastore, path)
253 sub split_path
: prototype($) {
255 if ($path =~ m!^([^/]+)/([^/]+)/(.+)$!) {
261 sub get_import_metadata
: prototype($$$$) {
262 my ($class, $scfg, $volname, $storeid) = @_;
264 if ($volname !~ m!^([^/]+)/.*\.vmx$!) {
265 die "volume '$volname' does not look like an importable vm config\n";
268 my $vmx_path = $class->path($scfg, $volname, $storeid, undef);
269 if (!is_mounted
($storeid)) {
270 die "storage '$storeid' is not activated\n";
273 my $manifest = $class->get_manifest($storeid, $scfg, 0);
274 my $contents = file_get_contents
($vmx_path);
275 return PVE
::Storage
::ESXiPlugin
::VMX-
>parse(
284 # Returns a size in bytes, this is a helper for already-mounted files.
285 sub query_vmdk_size
: prototype($;$) {
286 my ($filename, $timeout) = @_;
290 run_command
(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
292 outfunc
=> sub { $json .= $_[0]; },
293 errfunc
=> sub { warn "$_[0]\n"; }
299 return int($json->{'virtual-size'});
303 # Storage API implementation
307 my ($class, $storeid, $scfg, %sensitive) = @_;
309 my $password = $sensitive{password
};
310 die "missing password\n" if !defined($password);
311 esxi_set_credentials
($password, $storeid);
317 my ($class, $storeid, $scfg, %sensitive) = @_;
319 return if !exists($sensitive{password
});
321 if (defined($sensitive{password
})) {
322 esxi_set_credentials
($sensitive{password
}, $storeid);
324 esxi_delete_credentials
($storeid);
331 my ($class, $storeid, $scfg) = @_;
333 esxi_delete_credentials
($storeid);
338 sub activate_storage
{
339 my ($class, $storeid, $scfg, $cache) = @_;
341 $class->esxi_mount($storeid, $scfg, 0);
344 sub deactivate_storage
{
345 my ($class, $storeid, $scfg, $cache) = @_;
347 $class->esxi_unmount($storeid, $scfg);
350 sub activate_volume
{
351 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
353 # FIXME: maybe check if it exists?
356 sub deactivate_volume
{
357 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
362 sub check_connection
{
363 my ($class, $storeid, $scfg) = @_;
365 return PVE
::Network
::tcp_ping
($scfg->{server
}, 443, 2);
369 my ($class, $storeid, $scfg, $cache) = @_;
375 my ($class, $volname) = @_;
377 # it doesn't really make sense tbh, we can't return an owner, the format
378 # may be a 'vmx' (config), the paths are arbitrary...
380 die "failed to parse volname '$volname'\n"
381 if $volname !~ m!^([^/]+)/([^/]+)/(.+)$!;
383 return ('import', $volname) if $volname =~ /\.vmx$/;
386 $format = 'vmdk' if $volname =~ /\.vmdk/;
387 return ('images', $volname, 0, undef, undef, undef, $format);
391 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
397 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
399 return if !grep { $_ eq 'import' } @$content_types;
401 my $data = $class->get_manifest($storeid, $scfg, 0);
404 for my $dc_name (keys $data->%*) {
405 my $dc = $data->{$dc_name};
406 my $vms = $dc->{vms
};
407 for my $vm_name (keys $vms->%*) {
408 my $vm = $vms->{$vm_name};
409 my $ds_name = $vm->{config
}->{datastore
};
410 my $path = $vm->{config
}->{path
};
415 volid
=> "$storeid:$dc_name/$ds_name/$path",
425 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
427 die "cloning images is not supported for $class\n";
431 my ($class, $storeid, $scfg, $volname) = @_;
433 die "creating base images is not supported for $class\n";
437 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
439 die "storage '$class' does not support snapshots\n" if defined $snapname;
441 # FIXME: activate/mount:
442 return mount_dir
($storeid) . '/' . $volname;
446 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
448 die "creating images is not supported for $class\n";
452 my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
454 die "deleting images is not supported for $class\n";
458 my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
460 die "renaming volumes is not supported for $class\n";
463 sub volume_export_formats
{
464 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
466 # FIXME: maybe we can support raw+size via `qemu-img dd`?
468 die "exporting not supported for $class\n";
472 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
474 # FIXME: maybe we can support raw+size via `qemu-img dd`?
476 die "exporting not supported for $class\n";
479 sub volume_import_formats
{
480 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
482 die "importing not supported for $class\n";
486 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
488 die "importing not supported for $class\n";
492 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
494 die "resizing volumes is not supported for $class\n";
497 sub volume_size_info
{
498 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
500 return 0 if $volname =~ /\.vmx$/;
502 my $filename = $class->path($scfg, $volname, $storeid, undef);
503 return PVE
::Storage
::Plugin
::file_size_info
($filename, $timeout);
506 sub volume_snapshot
{
507 my ($class, $scfg, $storeid, $volname, $snap) = @_;
509 die "creating snapshots is not supported for $class\n";
512 sub volume_snapshot_delete
{
513 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
515 die "deleting snapshots is not supported for $class\n";
517 sub volume_snapshot_info
{
519 my ($class, $scfg, $storeid, $volname) = @_;
521 die "getting snapshot information is not supported for $class";
524 sub volume_rollback_is_possible
{
525 my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
530 sub volume_has_feature
{
531 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
533 return undef if defined($snapname) || $volname =~ /\.vmx$/;
534 return 1 if $feature eq 'copy';
539 my ($class, $scfg, $vtype) = @_;
541 die "no subdirectories available for storage $class\n";
544 package PVE
::Storage
::ESXiPlugin
::Manifest
;
549 use JSON
qw(from_json);
551 sub new
: prototype($$) {
552 my ($class, $data) = @_;
554 my $json = from_json
($data);
556 return bless $json, $class;
559 sub datacenter_for_vm
{
560 my ($self, $vm) = @_;
562 for my $dc_name (sort keys %$self) {
563 my $dc = $self->{$dc_name};
564 return $dc_name if exists($dc->{vms
}->{$vm});
570 sub datastore_for_vm
{
571 my ($self, $vm, $datacenter) = @_;
573 my @dc_names = defined($datacenter) ?
($datacenter) : keys %$self;
574 for my $dc_name (@dc_names) {
575 my $dc = $self->{$dc_name}
576 or die "no such datacenter '$datacenter'\n";
577 if (defined(my $vm = $dc->{vms
}->{$vm})) {
578 return $vm->{config
}->{datastore
};
586 my ($self, $path) = @_;
588 if ($path !~ m
|^/|) {
589 return wantarray ?
(undef, undef, $path) : $path;
592 for my $dc_name (sort keys %$self) {
593 my $dc = $self->{$dc_name};
595 my $datastores = $dc->{datastores
};
597 for my $ds_name (keys %$datastores) {
598 my $ds_path = $datastores->{$ds_name};
599 if (substr($path, 0, length($ds_path)) eq $ds_path) {
600 my $relpath = substr($path, length($ds_path));
601 return wantarray ?
($dc_name, $ds_name, $relpath) : $relpath;
609 sub config_path_for_vm
{
610 my ($self, $vm, $datacenter) = @_;
612 my @dc_names = defined($datacenter) ?
($datacenter) : keys %$self;
613 for my $dc_name (@dc_names) {
614 my $dc = $self->{$dc_name}
615 or die "no such datacenter '$datacenter'\n";
617 my $vm = $dc->{vms
}->{$vm}
620 my $cfg = $vm->{config
};
621 if (my (undef, $ds_name, $path) = $self->resolve_path($cfg->{path
})) {
622 $ds_name //= $cfg->{datastore
};
623 return ($dc_name, $ds_name, $path);
626 die "failed to resolve path for vm '$vm' "
627 ."($dc_name, $cfg->{datastore}, $cfg->{path})\n";
630 die "no such vm '$vm'\n";
633 # Since paths in the vmx file are relative to the vmx file itself, this helper
634 # provides a way to resolve paths which are relative based on the config file
635 # path, while also resolving absolute paths without the vm config.
636 sub resolve_path_for_vm
{
637 my ($self, $vm, $path, $datacenter) = @_;
639 if ($path =~ m
|^/|) {
640 if (my ($disk_dc, $disk_ds, $disk_path) = $self->resolve_path($path)) {
641 return "$disk_dc/$disk_ds/$disk_path";
643 die "failed to resolve path '$path' for vm '$vm'\n";
646 my ($cfg_dc, $cfg_ds, $cfg_path) = $self->config_path_for_vm($vm, $datacenter)
647 or die "failed to resolve vm config path for '$vm'\n";
648 $cfg_path =~ s
|/[^/]+$||;
650 return "$cfg_dc/$cfg_ds/$cfg_path/$path";
653 sub resolve_path_relative_to
{
654 my ($self, $vmx_path, $path) = @_;
656 if ($path =~ m
|^/|) {
657 if (my ($disk_dc, $disk_ds, $disk_path) = $self->resolve_path($path)) {
658 return "$disk_dc/$disk_ds/$disk_path";
660 die "failed to resolve path '$path'\n";
663 my ($rel_dc, $rel_ds, $rel_path) = PVE
::Storage
::ESXiPlugin
::split_path
($vmx_path)
664 or die "bad path '$vmx_path'\n";
665 $rel_path =~ s
|/[^/]+$||;
667 return "$rel_dc/$rel_ds/$rel_path/$path";
670 package PVE
::Storage
::ESXiPlugin
::VMX
;
676 # FIXME: see if vmx files can actually have escape sequences in their quoted values?
677 my sub unquote
: prototype($) {
679 $value =~ s/^\"(.*)\"$/$1/s
680 or $value =~ s/^\'(.*)\'$/$1/s;
684 sub parse
: prototype($$$$$$) {
685 my ($class, $storeid, $scfg, $vmx_path, $vmxdata, $manifest) = @_;
689 for my $line (split(/\n/, $vmxdata)) {
692 next if $line !~ /^(\S+)\s*=\s*(.+)$/;
693 my ($key, $value) = ($1, $2);
695 $value = unquote
($value);
697 $conf->{$key} = $value;
700 $conf->{'pve.storeid'} = $storeid;
701 $conf->{'pve.storage.config'} = $scfg;
702 $conf->{'pve.vmx.path'} = $vmx_path;
703 $conf->{'pve.manifest'} = $manifest;
705 return bless $conf, $class;
708 sub storeid
{ $_[0]->{'pve.storeid'} }
709 sub scfg
{ $_[0]->{'pve.storage.config'} }
710 sub vmx_path
{ $_[0]->{'pve.vmx.path'} }
711 sub manifest
{ $_[0]->{'pve.manifest'} }
713 # (Also used for the fileName config key...)
714 sub is_disk_entry
: prototype($) {
716 if ($id =~ /^(scsi|ide|sata|nvme)(\d+:\d+)(:?\.fileName)?$/) {
723 my ($self, $bus, $slot) = @_;
724 if (my $type = $self->{"${bus}${slot}.deviceType"}) {
725 return $type =~ /cdrom/;
731 my ($self, $code) = @_;
733 for my $key (sort keys %$self) {
734 my ($bus, $slot) = is_disk_entry
($key)
736 my $kind = $self->is_cdrom($bus, $slot) ?
'cdrom' : 'disk';
738 my $file = $self->{$key};
740 my ($maj, $min) = split(/:/, $slot, 2);
741 my $vdev = $self->{"${bus}${maj}.virtualDev"}; # may of course be undef...
743 $code->($bus, $slot, $file, $vdev, $kind);
749 sub for_each_netdev
{
750 my ($self, $code) = @_;
753 for my $key (keys %$self) {
754 next if $key !~ /^ethernet(\d+)\.(.+)$/;
755 my ($slot, $opt) = ($1, $2);
757 my $dev = ($found_devs->{$slot} //= {});
758 $dev->{$opt} = $self->{$key};
761 for my $id (sort keys %$found_devs) {
762 my $dev = $found_devs->{$id};
764 next if ($dev->{present
} // '') ne 'TRUE';
766 my $ty = $dev->{addressType
};
767 my $mac = $dev->{address
};
768 if ($ty && fc
($ty) eq fc
('generated')) {
769 $mac = $dev->{generatedAddress
} // $mac;
772 $code->($id, $dev, $mac);
778 sub for_each_serial
{
779 my ($self, $code) = @_;
781 my $found_serials = {};
782 for my $key (sort keys %$self) {
783 next if $key !~ /^serial(\d+)\.(.+)$/;
784 my ($slot, $opt) = ($1, $2);
785 my $serial = ($found_serials->{$1} //= {});
786 $serial->{$opt} = $self->{$key};
789 for my $id (sort { $a <=> $b } keys %$found_serials) {
790 my $serial = $found_serials->{$id};
792 next if ($serial->{present
} // '') ne 'TRUE';
794 $code->($id, $serial);
802 my $fw = $self->{firmware
};
803 return 'efi' if $fw && fc
($fw) eq fc
('efi');
811 return $self->{memSize
};
814 # CPU info is stored as a maximum ('numvcpus') and a core-per-socket count.
815 # We return a (cores, sockets) tuple the way want it for PVE.
819 my $cps = int($self->{'cpuid.coresPerSocket'} // 1);
820 my $max = int($self->{numvcpus
} // $cps);
822 return ($cps, ($max / $cps));
825 # FIXME: Test all possible values esxi creates?
829 my $guest = $self->{guestOS
} // return;
830 return 1 if $guest =~ /^win/i;
836 winNetBusiness
=> 'w2k3',
838 'windows9-64' => 'win10',
839 'windows11-64' => 'win11',
840 'windows12-64' => 'win11', # FIXME / win12?
841 win2000AdvServ
=> 'w2k',
843 win2000Serv
=> 'w2k',
846 'windows7-64' => 'win7',
848 'windows8-64' => 'win8',
852 winNetEnterprise
=> 'w2k3',
853 'winNetEnterprise-64' => 'w2k3',
854 winNetDatacenter
=> 'w2k3',
855 'winNetDatacenter-64' => 'w2k3',
856 winNetStandard
=> 'w2k3',
857 'winNetStandard-64' => 'w2k3',
859 winLonghorn
=> 'w2k8',
860 'winLonghorn-64' => 'w2k8',
861 'windows7Server-64' => 'w2k8',
862 'windows8Server-64' => 'win8',
863 'windows9Server-64' => 'win10',
864 'windows2019srv-64' => 'win10',
865 'windows2019srvNext-64' => 'win11',
866 'windows2022srvNext-64' => 'win11', # FIXME / win12?
867 winVista
=> 'wvista',
868 'winVista-64' => 'wvista',
870 'winXPPro-64' => 'wxp',
873 # Best effort translation from vmware guest os type to pve.
874 # Returns a tuple: `(pve-type, is_windows)`
878 if (defined(my $guest = $self->{guestOS
})) {
879 if (defined(my $known = $guest_types{$guest})) {
882 # This covers all the 'Mac OS' types AFAICT
883 return ('other', 0) if $guest =~ /^darwin/;
886 # otherwise we'll just go with l26 defaults because why not...
893 my $uuid = $self->{'uuid.bios'};
895 return if !defined($uuid);
897 # vmware stores space separated bytes and has 1 dash in the middle...
898 $uuid =~ s/[^0-9a-fA-f]//g;
908 return "$1-$2-$3-$4-$5";
913 # This builds arguments for the `create` api call for this config.
914 sub get_create_args
{
917 my $storeid = $self->storeid;
918 my $manifest = $self->manifest;
920 my $create_args = {};
921 my $create_disks = {};
924 my $ignored_volumes = {};
927 push @$warnings, { message
=> $_[0] };
930 my ($cores, $sockets) = $self->cpu_info();
931 $create_args->{cores
} = $cores if $cores != 1;
932 $create_args->{sockets
} = $sockets if $sockets != 1;
934 my $firmware = $self->firmware;
935 if ($firmware eq 'efi') {
936 $create_args->{bios
} = 'ovmf';
937 $create_disks->{efidisk0
} = 1;
939 $create_args->{bios
} = 'seabios';
942 my $memory = $self->memory;
943 $create_args->{memory
} = $memory;
947 my $set_scsihw = sub {
948 if (defined($scsihw) && $scsihw ne $_[0]) {
949 warn "multiple different SCSI hardware types are not supported\n";
955 my ($ostype, $is_windows) = $self->guest_type();
956 $create_args->{ostype
} //= $ostype if defined($ostype);
957 if ($ostype eq 'l26') {
958 $default_scsihw = 'virtio-scsi-single';
961 $self->for_each_netdev(sub {
962 my ($id, $dev, $mac) = @_;
964 my $model = $dev->{virtualDev
} // 'vmxnet3';
966 my $param = { model
=> $model };
967 $param->{macaddr
} = $mac if length($mac);
968 $create_net->{"net$id"} = $param;
971 my %counts = ( scsi
=> 0, sata
=> 0, ide
=> 0 );
973 my $mntdir = PVE
::Storage
::ESXiPlugin
::mount_dir
($storeid);
977 # we deal with nvme disks in a 2nd go-around since we currently don't
978 # support nvme disks and instead just add them as additional scsi
982 my ($bus, $slot, $file, $devtype, $kind, $do_nvmes) = @_;
987 } elsif ($bus eq 'nvme') {
988 push @nvmes, [$slot, $file, $devtype, $kind];
992 my $path = eval { $manifest->resolve_path_relative_to($self->vmx_path, $file) };
993 return if !defined($path);
995 # my $fullpath = "$mntdir/$path";
996 # return if !-e $fullpath;
999 if ($devtype =~ /^lsi/i) {
1000 $set_scsihw->('lsi');
1001 } elsif ($devtype eq 'pvscsi') {
1002 $set_scsihw->('pvscsi'); # same name in pve
1006 my $count = $counts{$bus}++;
1007 if ($kind eq 'cdrom') {
1008 # We currently do not pass cdroms through via the esxi storage.
1009 # Users should adapt import these from the storages directly/manually.
1010 $create_args->{"${bus}${count}"} = "none,media=cdrom";
1011 $ignored_volumes->{"${bus}${count}"} = "$storeid:$path";
1013 $create_disks->{"${bus}${count}"} = "$storeid:$path";
1016 $boot_order .= ';' if length($boot_order);
1017 $boot_order .= $bus.$count;
1019 $self->for_each_disk($add_disk);
1021 $warn->("PVE currently does not support NVMe guest disks, they are converted to SCSI");
1022 for my $nvme (@nvmes) {
1023 my ($slot, $file, $devtype, $kind) = @$nvme;
1024 $add_disk->('nvme', $slot, $file, $devtype, $kind, 1);
1028 $scsihw //= $default_scsihw;
1029 if ($firmware eq 'efi') {
1030 if (!defined($scsihw) || $scsihw =~ /^lsi/) {
1034 $scsihw = 'virtio-scsi-single';
1036 $warn->("OVMF is built without LSI drivers, scsi hardware was set to $scsihw");
1039 $create_args->{scsihw
} = $scsihw;
1041 $create_args->{boot
} = "order=$boot_order";
1043 if (defined(my $smbios1_uuid = $self->smbios1_uuid())) {
1044 $create_args->{smbios1
} = "uuid=$smbios1_uuid";
1047 if (defined(my $name = $self->{displayName
})) {
1048 # name in pve is a 'dns-name', so... clean it
1050 $name =~ s/[^a-zA-Z0-9\-.]//g;
1051 $name =~ s/^[.-]+//;
1052 $name =~ s/[.-]+$//;
1053 $create_args->{name
} = $name if length($name);
1057 $self->for_each_serial(sub {
1058 my ($id, $serial) = @_;
1059 # currently we only support 'socket' type serials anyway
1060 $warn->("serial ports are currently all mapped to sockets") if $serid == 0;
1061 $create_args->{"serial$serid"} = 'socket';
1068 'create-args' => $create_args,
1069 disks
=> $create_disks,
1071 'ignored-volumes' => $ignored_volumes,
1072 warnings
=> $warnings,