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