]> git.proxmox.com Git - pve-storage.git/blob - src/PVE/Storage/ESXiPlugin.pm
c4795c4c48f8b35f62e71f3db6ff01e08b3ffa38
[pve-storage.git] / src / PVE / Storage / ESXiPlugin.pm
1 package PVE::Storage::ESXiPlugin;
2
3 use strict;
4 use warnings;
5
6 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
7 use JSON qw(from_json);
8 use POSIX ();
9 use File::Path qw(mkpath remove_tree);
10
11 use PVE::Network;
12 use PVE::Systemd;
13 use PVE::Tools qw(file_get_contents file_set_contents run_command);
14
15 use base qw(PVE::Storage::Plugin);
16
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';
20
21 #
22 # Configuration
23 #
24
25 sub type {
26 return 'esxi';
27 }
28
29 sub plugindata {
30 return {
31 content => [ { import => 1 }, { import => 1 }],
32 format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
33 };
34 }
35
36 sub properties {
37 return {
38 'skip-cert-verification' => {
39 description => 'Disable TLS certificate verification, only enable on fully trusted networks!',
40 type => 'boolean',
41 default => 'false',
42 },
43 };
44 }
45
46 sub options {
47 return {
48 nodes => { optional => 1 },
49 shared => { optional => 1 },
50 disable => { optional => 1 },
51 content => { optional => 1 },
52 # FIXME: bwlimit => { optional => 1 },
53 server => {},
54 username => {},
55 password => { optional => 1},
56 'skip-cert-verification' => { optional => 1},
57 };
58 }
59
60 sub esxi_cred_file_name {
61 my ($storeid) = @_;
62 return "/etc/pve/priv/storage/${storeid}.pw";
63 }
64
65 sub esxi_delete_credentials {
66 my ($storeid) = @_;
67
68 if (my $cred_file = get_cred_file($storeid)) {
69 unlink($cred_file) or warn "removing esxi credientials '$cred_file' failed: $!\n";
70 }
71 }
72
73 sub esxi_set_credentials {
74 my ($password, $storeid) = @_;
75
76 my $cred_file = esxi_cred_file_name($storeid);
77 mkdir "/etc/pve/priv/storage";
78
79 PVE::Tools::file_set_contents($cred_file, $password);
80
81 return $cred_file;
82 }
83
84 sub get_cred_file {
85 my ($storeid) = @_;
86
87 my $cred_file = esxi_cred_file_name($storeid);
88
89 if (-e $cred_file) {
90 return $cred_file;
91 }
92 return undef;
93 }
94
95 #
96 # Dealing with the esxi API.
97 #
98
99 my sub run_path : prototype($) {
100 my ($storeid) = @_;
101 return "/run/pve/import/esxi/$storeid";
102 }
103
104 # "public" because it is needed by the VMX package
105 sub mount_dir : prototype($) {
106 my ($storeid) = @_;
107 return run_path($storeid) . "/mnt";
108 }
109
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;
113 }
114
115 my sub is_old : prototype($) {
116 my ($file) = @_;
117 my $mtime = (CORE::stat($file))[9];
118 return !defined($mtime) || ($mtime + 60) < CORE::time();
119 }
120
121 sub get_manifest : prototype($$$;$) {
122 my ($class, $storeid, $scfg, $force_query) = @_;
123
124 my $rundir = run_path($storeid);
125 my $manifest_file = "$rundir/manifest.json";
126
127 $force_query ||= is_old($manifest_file);
128
129 if (!$force_query && -e $manifest_file) {
130 return PVE::Storage::ESXiPlugin::Manifest->new(
131 file_get_contents($manifest_file),
132 );
133 }
134
135 check_esxi_import_package();
136
137 my @extra_params;
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);
142 my $json = '';
143 run_command(
144 [$ESXI_LIST_VMS, @extra_params, $host, $user, $pwfile],
145 outfunc => sub { $json .= $_[0] . "\n" },
146 );
147
148 my $result = PVE::Storage::ESXiPlugin::Manifest->new($json);
149 mkpath($rundir);
150 file_set_contents($manifest_file, $json);
151
152 return $result;
153 }
154
155 my sub scope_name_base : prototype($) {
156 my ($storeid) = @_;
157 return "pve-esxi-fuse-" . PVE::Systemd::escape_unit($storeid);
158 }
159
160 my sub is_mounted : prototype($) {
161 my ($storeid) = @_;
162
163 my $scope_name_base = scope_name_base($storeid);
164 return PVE::Systemd::is_unit_active($scope_name_base . '.scope');
165 }
166
167 sub esxi_mount : prototype($$$;$) {
168 my ($class, $storeid, $scfg, $force_requery) = @_;
169
170 return if !$force_requery && is_mounted($storeid);
171
172 $class->get_manifest($storeid, $scfg, $force_requery);
173
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};
179 }
180
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);
185
186 pipe(my $rd, my $wr) or die "failed to create pipe: $!\n";
187
188 my $pid = fork();
189 die "fork failed: $!\n" if !defined($pid);
190 if (!$pid) {
191 eval {
192 undef $rd;
193 POSIX::setsid();
194 PVE::Systemd::enter_systemd_scope(
195 $scope_name_base,
196 "Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
197 );
198
199 my @extra_params;
200 push @extra_params, '--skip-cert-verification' if $scfg->{'skip-cert-verification'};
201
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}
208 $ESXI_FUSE_TOOL,
209 @extra_params,
210 '-o', 'allow_other',
211 '--ready-fd', fileno($wr),
212 '--user', $user,
213 '--password-file', $pwfile,
214 $host,
215 $manifest_file,
216 $mount_dir;
217 die "exec failed: $!\n";
218 };
219 if (my $err = $@) {
220 print {$wr} "ERROR: $err";
221 }
222 POSIX::_exit(1);
223 };
224 undef $wr;
225
226 my $result = do { local $/ = undef; <$rd> };
227 if ($result =~ /^ERROR: (.*)$/) {
228 die "$1\n";
229 }
230
231 if (waitpid($pid, POSIX::WNOHANG) == $pid) {
232 die "failed to spawn fuse mount, process exited with status $?\n";
233 }
234 }
235
236 sub esxi_unmount : prototype($$$) {
237 my ($class, $storeid, $scfg) = @_;
238
239 my $scope_name_base = scope_name_base($storeid);
240 my $scope = "${scope_name_base}.scope";
241 my $mount_dir = mount_dir($storeid);
242
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]);
247 }
248
249 # Split a path into (datacenter, datastore, path)
250 sub split_path : prototype($) {
251 my ($path) = @_;
252 if ($path =~ m!^([^/]+)/([^/]+)/(.+)$!) {
253 return ($1, $2, $3);
254 }
255 return;
256 }
257
258 sub get_import_metadata : prototype($$$$$) {
259 my ($class, $scfg, $volname, $storeid) = @_;
260
261 if ($volname !~ m!^([^/]+)/.*\.vmx$!) {
262 die "volume '$volname' does not look like an importable vm config\n";
263 }
264
265 my $vmx_path = $class->path($scfg, $volname, $storeid, undef);
266 if (!is_mounted($storeid)) {
267 die "storage '$storeid' is not activated\n";
268 }
269
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(
273 $storeid,
274 $scfg,
275 $volname,
276 $contents,
277 $manifest,
278 );
279 return $vmx->get_create_args();
280 }
281
282 # Returns a size in bytes, this is a helper for already-mounted files.
283 sub query_vmdk_size : prototype($;$) {
284 my ($filename, $timeout) = @_;
285
286 my $json = eval {
287 my $json = '';
288 run_command(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
289 timeout => $timeout,
290 outfunc => sub { $json .= $_[0]; },
291 errfunc => sub { warn "$_[0]\n"; }
292 );
293 from_json($json)
294 };
295 warn $@ if $@;
296
297 return int($json->{'virtual-size'});
298 }
299
300 #
301 # Storage API implementation
302 #
303
304 sub on_add_hook {
305 my ($class, $storeid, $scfg, %sensitive) = @_;
306
307 my $password = $sensitive{password};
308 die "missing password\n" if !defined($password);
309 esxi_set_credentials($password, $storeid);
310
311 return;
312 }
313
314 sub on_update_hook {
315 my ($class, $storeid, $scfg, %sensitive) = @_;
316
317 return if !exists($sensitive{password});
318
319 if (defined($sensitive{password})) {
320 esxi_set_credentials($sensitive{password}, $storeid);
321 } else {
322 esxi_delete_credentials($storeid);
323 }
324
325 return;
326 }
327
328 sub on_delete_hook {
329 my ($class, $storeid, $scfg) = @_;
330
331 eval { $class->deactivate_storage($storeid, $scfg) };
332 warn $@ if $@;
333
334 esxi_delete_credentials($storeid);
335
336 return;
337 }
338
339 sub activate_storage {
340 my ($class, $storeid, $scfg, $cache) = @_;
341
342 $class->esxi_mount($storeid, $scfg, 0);
343 }
344
345 sub deactivate_storage {
346 my ($class, $storeid, $scfg, $cache) = @_;
347
348 $class->esxi_unmount($storeid, $scfg);
349 }
350
351 sub activate_volume {
352 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
353
354 # FIXME: maybe check if it exists?
355 }
356
357 sub deactivate_volume {
358 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
359
360 return 1;
361 }
362
363 sub check_connection {
364 my ($class, $storeid, $scfg) = @_;
365
366 return PVE::Network::tcp_ping($scfg->{server}, 443, 2);
367 }
368
369 sub status {
370 my ($class, $storeid, $scfg, $cache) = @_;
371
372 return (0, 0, 0, 0);
373 }
374
375 sub parse_volname {
376 my ($class, $volname) = @_;
377
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...
380
381 die "failed to parse volname '$volname'\n"
382 if $volname !~ m!^([^/]+)/([^/]+)/(.+)$!;
383
384 return ('import', $volname) if $volname =~ /\.vmx$/;
385
386 my $format = 'raw';
387 $format = 'vmdk' if $volname =~ /\.vmdk/;
388 return ('images', $volname, 0, undef, undef, undef, $format);
389 }
390
391 sub list_images {
392 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
393
394 return [];
395 }
396
397 sub list_volumes {
398 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
399
400 return if !grep { $_ eq 'import' } @$content_types;
401
402 my $data = $class->get_manifest($storeid, $scfg, 0);
403
404 my $res = [];
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};
412 push @$res, {
413 content => 'import',
414 format => 'vmx',
415 name => $vm_name,
416 volid => "$storeid:$dc_name/$ds_name/$path",
417 size => 0,
418 };
419 }
420 }
421
422 return $res;
423 }
424
425 sub clone_image {
426 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
427
428 die "cloning images is not supported for $class\n";
429 }
430
431 sub create_base {
432 my ($class, $storeid, $scfg, $volname) = @_;
433
434 die "creating base images is not supported for $class\n";
435 }
436
437 sub path {
438 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
439
440 die "storage '$class' does not support snapshots\n" if defined $snapname;
441
442 # FIXME: activate/mount:
443 return mount_dir($storeid) . '/' . $volname;
444 }
445
446 sub alloc_image {
447 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
448
449 die "creating images is not supported for $class\n";
450 }
451
452 sub free_image {
453 my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
454
455 die "deleting images is not supported for $class\n";
456 }
457
458 sub rename_volume {
459 my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
460
461 die "renaming volumes is not supported for $class\n";
462 }
463
464 sub volume_export_formats {
465 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
466
467 # FIXME: maybe we can support raw+size via `qemu-img dd`?
468
469 die "exporting not supported for $class\n";
470 }
471
472 sub volume_export {
473 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
474
475 # FIXME: maybe we can support raw+size via `qemu-img dd`?
476
477 die "exporting not supported for $class\n";
478 }
479
480 sub volume_import_formats {
481 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
482
483 die "importing not supported for $class\n";
484 }
485
486 sub volume_import {
487 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
488
489 die "importing not supported for $class\n";
490 }
491
492 sub volume_resize {
493 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
494
495 die "resizing volumes is not supported for $class\n";
496 }
497
498 sub volume_size_info {
499 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
500
501 return 0 if $volname =~ /\.vmx$/;
502
503 my $filename = $class->path($scfg, $volname, $storeid, undef);
504 return PVE::Storage::Plugin::file_size_info($filename, $timeout);
505 }
506
507 sub volume_snapshot {
508 my ($class, $scfg, $storeid, $volname, $snap) = @_;
509
510 die "creating snapshots is not supported for $class\n";
511 }
512
513 sub volume_snapshot_delete {
514 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
515
516 die "deleting snapshots is not supported for $class\n";
517 }
518 sub volume_snapshot_info {
519
520 my ($class, $scfg, $storeid, $volname) = @_;
521
522 die "getting snapshot information is not supported for $class";
523 }
524
525 sub volume_rollback_is_possible {
526 my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
527
528 return 0;
529 }
530
531 sub volume_has_feature {
532 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
533
534 return undef if defined($snapname) || $volname =~ /\.vmx$/;
535 return 1 if $feature eq 'copy';
536 return undef;
537 }
538
539 sub get_subdir {
540 my ($class, $scfg, $vtype) = @_;
541
542 die "no subdirectories available for storage $class\n";
543 }
544
545 package PVE::Storage::ESXiPlugin::Manifest;
546
547 use strict;
548 use warnings;
549
550 use JSON qw(from_json);
551
552 sub new : prototype($$) {
553 my ($class, $data) = @_;
554
555 my $json = from_json($data);
556
557 return bless $json, $class;
558 }
559
560 sub datacenter_for_vm {
561 my ($self, $vm) = @_;
562
563 for my $dc_name (sort keys %$self) {
564 my $dc = $self->{$dc_name};
565 return $dc_name if exists($dc->{vms}->{$vm});
566 }
567
568 return;
569 }
570
571 sub datastore_for_vm {
572 my ($self, $vm, $datacenter) = @_;
573
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};
580 }
581 }
582
583 return;
584 }
585
586 sub resolve_path {
587 my ($self, $path) = @_;
588
589 if ($path !~ m|^/|) {
590 return wantarray ? (undef, undef, $path) : $path;
591 }
592
593 for my $dc_name (sort keys %$self) {
594 my $dc = $self->{$dc_name};
595
596 my $datastores = $dc->{datastores};
597
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;
603 }
604 }
605 }
606
607 return;
608 }
609
610 sub config_path_for_vm {
611 my ($self, $vm, $datacenter) = @_;
612
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";
617
618 my $vm = $dc->{vms}->{$vm}
619 or next;
620
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);
625 }
626
627 die "failed to resolve path for vm '$vm' "
628 ."($dc_name, $cfg->{datastore}, $cfg->{path})\n";
629 }
630
631 die "no such vm '$vm'\n";
632 }
633
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) = @_;
639
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";
643 }
644 die "failed to resolve path '$path'\n";
645 }
646
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|/[^/]+$||;
650
651 return "$rel_dc/$rel_ds/$rel_path/$path";
652 }
653
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) = @_;
659
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) {
667 return $vm;
668 }
669 }
670 }
671 return;
672 }
673
674 package PVE::Storage::ESXiPlugin::VMX;
675
676 use strict;
677 use warnings;
678 use feature 'fc';
679
680 # FIXME: see if vmx files can actually have escape sequences in their quoted values?
681 my sub unquote : prototype($) {
682 my ($value) = @_;
683 $value =~ s/^\"(.*)\"$/$1/s
684 or $value =~ s/^\'(.*)\'$/$1/s;
685 return $value;
686 }
687
688 sub parse : prototype($$$$$$) {
689 my ($class, $storeid, $scfg, $vmx_path, $vmxdata, $manifest) = @_;
690
691 my $conf = {};
692
693 for my $line (split(/\n/, $vmxdata)) {
694 $line =~ s/^\s+//;
695 $line =~ s/\s+$//;
696 next if $line !~ /^(\S+)\s*=\s*(.+)$/;
697 my ($key, $value) = ($1, $2);
698
699 $value = unquote($value);
700
701 $conf->{$key} = $value;
702 }
703
704 $conf->{'pve.storeid'} = $storeid;
705 $conf->{'pve.storage.config'} = $scfg;
706 $conf->{'pve.vmx.path'} = $vmx_path;
707 $conf->{'pve.manifest'} = $manifest;
708
709 return bless $conf, $class;
710 }
711
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'} }
716
717 # (Also used for the fileName config key...)
718 sub is_disk_entry : prototype($) {
719 my ($id) = @_;
720 if ($id =~ /^(scsi|ide|sata|nvme)(\d+:\d+)(:?\.fileName)?$/) {
721 return ($1, $2);
722 }
723 return;
724 }
725
726 sub is_cdrom {
727 my ($self, $bus, $slot) = @_;
728 if (my $type = $self->{"${bus}${slot}.deviceType"}) {
729 return $type =~ /cdrom/;
730 }
731 return;
732 }
733
734 sub for_each_disk {
735 my ($self, $code) = @_;
736
737 for my $key (sort keys %$self) {
738 my ($bus, $slot) = is_disk_entry($key)
739 or next;
740 my $kind = $self->is_cdrom($bus, $slot) ? 'cdrom' : 'disk';
741
742 my $file = $self->{$key};
743
744 my ($maj, $min) = split(/:/, $slot, 2);
745 my $vdev = $self->{"${bus}${maj}.virtualDev"}; # may of course be undef...
746
747 $code->($bus, $slot, $file, $vdev, $kind);
748 }
749
750 return;
751 }
752
753 sub for_each_netdev {
754 my ($self, $code) = @_;
755
756 my $found_devs = {};
757 for my $key (keys %$self) {
758 next if $key !~ /^ethernet(\d+)\.(.+)$/;
759 my ($slot, $opt) = ($1, $2);
760
761 my $dev = ($found_devs->{$slot} //= {});
762 $dev->{$opt} = $self->{$key};
763 }
764
765 for my $id (sort keys %$found_devs) {
766 my $dev = $found_devs->{$id};
767
768 next if ($dev->{present} // '') ne 'TRUE';
769
770 my $ty = $dev->{addressType};
771 my $mac = $dev->{address};
772 if ($ty && fc($ty) eq fc('generated')) {
773 $mac = $dev->{generatedAddress} // $mac;
774 }
775
776 $code->($id, $dev, $mac);
777 }
778
779 return;
780 }
781
782 sub for_each_serial {
783 my ($self, $code) = @_;
784
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};
791 }
792
793 for my $id (sort { $a <=> $b } keys %$found_serials) {
794 my $serial = $found_serials->{$id};
795
796 next if ($serial->{present} // '') ne 'TRUE';
797
798 $code->($id, $serial);
799 }
800
801 return;
802 }
803
804 sub firmware {
805 my ($self) = @_;
806 my $fw = $self->{firmware};
807 return 'efi' if $fw && fc($fw) eq fc('efi');
808 return 'bios';
809 }
810
811 # This is in MB
812 sub memory {
813 my ($self) = @_;
814
815 return $self->{memSize};
816 }
817
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.
820 sub cpu_info {
821 my ($self) = @_;
822
823 my $cps = int($self->{'cpuid.coresPerSocket'} // 1);
824 my $max = int($self->{numvcpus} // $cps);
825
826 return ($cps, ($max / $cps));
827 }
828
829 # FIXME: Test all possible values esxi creates?
830 sub is_windows {
831 my ($self) = @_;
832
833 my $guest = $self->{guestOS} // return;
834 return 1 if $guest =~ /^win/i;
835 return;
836 }
837
838 my %guest_types = (
839 dos => 'other',
840 winNetBusiness => 'w2k3',
841 windows9 => 'win10',
842 'windows9-64' => 'win10',
843 'windows11-64' => 'win11',
844 'windows12-64' => 'win11', # FIXME / win12?
845 win2000AdvServ => 'w2k',
846 win2000Pro => 'w2k',
847 win2000Serv => 'w2k',
848 win31 => 'other',
849 windows7 => 'win7',
850 'windows7-64' => 'win7',
851 windows8 => 'win8',
852 'windows8-64' => 'win8',
853 win95 => 'other',
854 win98 => 'other',
855 winNT => 'wxp', # ?
856 winNetEnterprise => 'w2k3',
857 'winNetEnterprise-64' => 'w2k3',
858 winNetDatacenter => 'w2k3',
859 'winNetDatacenter-64' => 'w2k3',
860 winNetStandard => 'w2k3',
861 'winNetStandard-64' => 'w2k3',
862 winNetWeb => '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',
873 winXPPro => 'wxp',
874 'winXPPro-64' => 'wxp',
875 );
876
877 # Best effort translation from vmware guest os type to pve.
878 # Returns a tuple: `(pve-type, is_windows)`
879 sub guest_type {
880 my ($self) = @_;
881
882 if (defined(my $guest = $self->{guestOS})) {
883 if (defined(my $known = $guest_types{$guest})) {
884 return ($known, 1);
885 }
886 # This covers all the 'Mac OS' types AFAICT
887 return ('other', 0) if $guest =~ /^darwin/;
888 }
889
890 # otherwise we'll just go with l26 defaults because why not...
891 return ('l26', 0);
892 }
893
894 sub smbios1_uuid {
895 my ($self) = @_;
896
897 my $uuid = $self->{'uuid.bios'};
898
899 return if !defined($uuid);
900
901 # vmware stores space separated bytes and has 1 dash in the middle...
902 $uuid =~ s/[^0-9a-fA-f]//g;
903
904 if ($uuid =~ /^
905 ([0-9a-fA-F]{8})
906 ([0-9a-fA-F]{4})
907 ([0-9a-fA-F]{4})
908 ([0-9a-fA-F]{4})
909 ([0-9a-fA-F]{12})
910 $/x)
911 {
912 return "$1-$2-$3-$4-$5";
913 }
914 return;
915 }
916
917 # This builds arguments for the `create` api call for this config.
918 sub get_create_args {
919 my ($self) = @_;
920
921 my $storeid = $self->storeid;
922 my $manifest = $self->manifest;
923 my $vminfo = $manifest->vm_for_vmx_path($self->vmx_path);
924
925 my $create_args = {};
926 my $create_disks = {};
927 my $create_net = {};
928 my $warnings = [];
929
930 # NOTE: all types must be added to the return schema of the import-metadata API endpoint
931 my $warn = sub {
932 my ($type, %properties) = @_;
933 push @$warnings, { type => $type, %properties };
934 };
935
936 my ($cores, $sockets) = $self->cpu_info();
937 $create_args->{cores} = $cores if $cores != 1;
938 $create_args->{sockets} = $sockets if $sockets != 1;
939
940 my $firmware = $self->firmware;
941 if ($firmware eq 'efi') {
942 $create_args->{bios} = 'ovmf';
943 $create_disks->{efidisk0} = 1;
944 } else {
945 $create_args->{bios} = 'seabios';
946 }
947
948 my $memory = $self->memory;
949 $create_args->{memory} = $memory;
950
951 my $default_scsihw;
952 my $scsihw;
953 my $set_scsihw = sub {
954 if (defined($scsihw) && $scsihw ne $_[0]) {
955 warn "multiple different SCSI hardware types are not supported\n";
956 return;
957 }
958 $scsihw = $_[0];
959 };
960
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';
965 }
966
967 $self->for_each_netdev(sub {
968 my ($id, $dev, $mac) = @_;
969 $mac //= '';
970 my $model = $dev->{virtualDev} // 'vmxnet3';
971
972 my $param = { model => $model };
973 $param->{macaddr} = $mac if length($mac);
974 $create_net->{"net$id"} = $param;
975 });
976
977 my %counts = ( scsi => 0, sata => 0, ide => 0 );
978
979 my $mntdir = PVE::Storage::ESXiPlugin::mount_dir($storeid);
980
981 my $boot_order = '';
982
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
985 # disks.
986 my @nvmes;
987 my $add_disk = sub {
988 my ($bus, $slot, $file, $devtype, $kind, $do_nvmes) = @_;
989
990 my $vmbus = $bus;
991 if ($do_nvmes) {
992 $bus = 'scsi';
993 } elsif ($bus eq 'nvme') {
994 push @nvmes, [$slot, $file, $devtype, $kind];
995 return;
996 }
997
998 my $path = eval { $manifest->resolve_path_relative_to($self->vmx_path, $file) };
999 return if !defined($path);
1000
1001 # my $fullpath = "$mntdir/$path";
1002 # return if !-e $fullpath;
1003
1004 if ($devtype) {
1005 if ($devtype =~ /^lsi/i) {
1006 $set_scsihw->('lsi');
1007 } elsif ($devtype eq 'pvscsi') {
1008 $set_scsihw->('pvscsi'); # same name in pve
1009 }
1010 }
1011
1012 my $disk_capacity;
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};
1018 last;
1019 }
1020 }
1021 }
1022
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");
1030 } else {
1031 $create_disks->{"${bus}${count}"} = {
1032 volid => "$storeid:$path",
1033 defined($disk_capacity) ? (size => $disk_capacity) : (),
1034 };
1035 }
1036
1037 $boot_order .= ';' if length($boot_order);
1038 $boot_order .= $bus.$count;
1039 };
1040 $self->for_each_disk($add_disk);
1041 if (@nvmes) {
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);
1046 }
1047 }
1048
1049 $scsihw //= $default_scsihw;
1050 if ($firmware eq 'efi') {
1051 if (!defined($scsihw) || $scsihw =~ /^lsi/) {
1052 if ($is_windows) {
1053 $scsihw = 'pvscsi';
1054 } else {
1055 $scsihw = 'virtio-scsi-single';
1056 }
1057 # OVMF is built without LSI drivers, scsi hardware was set to $scsihw
1058 $warn->('ovmf-with-lsi-unsupported', key => 'scsihw', value => "$scsihw");
1059 }
1060 }
1061 $create_args->{scsihw} = $scsihw;
1062
1063 $create_args->{boot} = "order=$boot_order";
1064
1065 if (defined(my $smbios1_uuid = $self->smbios1_uuid())) {
1066 $create_args->{smbios1} = "uuid=$smbios1_uuid";
1067 }
1068
1069 if (defined(my $name = $self->{displayName})) {
1070 # name in pve is a 'dns-name', so... clean it
1071 $name =~ s/\s/-/g;
1072 $name =~ s/[^a-zA-Z0-9\-.]//g;
1073 $name =~ s/^[.-]+//;
1074 $name =~ s/[.-]+$//;
1075 $create_args->{name} = $name if length($name);
1076 }
1077
1078 my $serid = 0;
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';
1084 ++$serid;
1085 });
1086
1087 $warn->('guest-is-running') if defined($vminfo) && ($vminfo->{power}//'') ne 'poweredOff';
1088
1089 return {
1090 type => 'vm',
1091 source => 'esxi',
1092 'create-args' => $create_args,
1093 disks => $create_disks,
1094 net => $create_net,
1095 warnings => $warnings,
1096 };
1097 }
1098
1099 1;