]> git.proxmox.com Git - pve-storage.git/blame - src/PVE/Storage/ESXiPlugin.pm
esxi import: add "longhorn" to possible Windows Server 2008 OS types
[pve-storage.git] / src / PVE / Storage / ESXiPlugin.pm
CommitLineData
a1710367
WB
1package PVE::Storage::ESXiPlugin;
2
3use strict;
4use warnings;
5
6use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
70204915 7use File::Path qw(mkpath remove_tree);
a1710367 8use JSON qw(from_json);
70204915 9use Net::IP;
a1710367 10use POSIX ();
a1710367
WB
11
12use PVE::Network;
13use PVE::Systemd;
14use PVE::Tools qw(file_get_contents file_set_contents run_command);
15
16use base qw(PVE::Storage::Plugin);
17
dde640ed
TL
18my $ESXI_LIST_VMS = '/usr/libexec/pve-esxi-import-tools/listvms.py';
19my $ESXI_FUSE_TOOL = '/usr/libexec/pve-esxi-import-tools/esxi-folder-fuse';
a1710367
WB
20my $ESXI_PRIV_DIR = '/etc/pve/priv/import/esxi';
21
22#
23# Configuration
24#
25
26sub type {
27 return 'esxi';
28}
29
30sub plugindata {
31 return {
da513e26 32 content => [ { import => 1 }, { import => 1 }],
a1710367
WB
33 format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
34 };
35}
36
37sub properties {
2ff6f99c
WB
38 return {
39 'skip-cert-verification' => {
40 description => 'Disable TLS certificate verification, only enable on fully trusted networks!',
41 type => 'boolean',
42 default => 'false',
43 },
44 };
a1710367
WB
45}
46
47sub options {
48 return {
49 nodes => { optional => 1 },
50 shared => { optional => 1 },
51 disable => { optional => 1 },
52 content => { optional => 1 },
53 # FIXME: bwlimit => { optional => 1 },
54 server => {},
55 username => {},
56 password => { optional => 1},
2ff6f99c 57 'skip-cert-verification' => { optional => 1},
70204915 58 port => { optional => 1 },
a1710367
WB
59 };
60}
61
62sub esxi_cred_file_name {
63 my ($storeid) = @_;
64 return "/etc/pve/priv/storage/${storeid}.pw";
65}
66
67sub esxi_delete_credentials {
68 my ($storeid) = @_;
69
70 if (my $cred_file = get_cred_file($storeid)) {
71 unlink($cred_file) or warn "removing esxi credientials '$cred_file' failed: $!\n";
72 }
73}
74
75sub esxi_set_credentials {
76 my ($password, $storeid) = @_;
77
78 my $cred_file = esxi_cred_file_name($storeid);
79 mkdir "/etc/pve/priv/storage";
80
81 PVE::Tools::file_set_contents($cred_file, $password);
82
83 return $cred_file;
84}
85
86sub get_cred_file {
87 my ($storeid) = @_;
88
89 my $cred_file = esxi_cred_file_name($storeid);
90
91 if (-e $cred_file) {
92 return $cred_file;
93 }
94 return undef;
95}
96
97#
98# Dealing with the esxi API.
99#
100
101my sub run_path : prototype($) {
102 my ($storeid) = @_;
103 return "/run/pve/import/esxi/$storeid";
104}
105
106# "public" because it is needed by the VMX package
107sub mount_dir : prototype($) {
108 my ($storeid) = @_;
109 return run_path($storeid) . "/mnt";
110}
111
112my sub check_esxi_import_package : prototype() {
113 die "pve-esxi-import-tools package not installed, cannot proceed\n"
114 if !-e $ESXI_LIST_VMS;
115}
116
99ace9d4
WB
117my sub is_old : prototype($) {
118 my ($file) = @_;
119 my $mtime = (CORE::stat($file))[9];
897662ba 120 return !defined($mtime) || ($mtime + 30) < CORE::time();
99ace9d4
WB
121}
122
a1710367
WB
123sub get_manifest : prototype($$$;$) {
124 my ($class, $storeid, $scfg, $force_query) = @_;
125
126 my $rundir = run_path($storeid);
127 my $manifest_file = "$rundir/manifest.json";
128
99ace9d4
WB
129 $force_query ||= is_old($manifest_file);
130
a1710367
WB
131 if (!$force_query && -e $manifest_file) {
132 return PVE::Storage::ESXiPlugin::Manifest->new(
133 file_get_contents($manifest_file),
134 );
135 }
136
137 check_esxi_import_package();
138
2ff6f99c
WB
139 my @extra_params;
140 push @extra_params, '--skip-cert-verification' if $scfg->{'skip-cert-verification'};
70204915
WB
141 if (my $port = $scfg->{port}) {
142 push @extra_params, '--port', $port;
143 }
a1710367
WB
144 my $host = $scfg->{server};
145 my $user = $scfg->{username};
146 my $pwfile = esxi_cred_file_name($storeid);
147 my $json = '';
06a28968
WB
148 my $errmsg = '';
149 eval {
150 run_command(
151 [$ESXI_LIST_VMS, @extra_params, $host, $user, $pwfile],
152 outfunc => sub { $json .= $_[0] . "\n" },
153 errfunc => sub { $errmsg .= $_[0] . "\n" },
154 );
155 };
156 if ($@) {
157 # propagate listvms error output if any, otherwise use the error from run_command
158 die $errmsg || $@;
159 }
a1710367
WB
160
161 my $result = PVE::Storage::ESXiPlugin::Manifest->new($json);
162 mkpath($rundir);
163 file_set_contents($manifest_file, $json);
164
165 return $result;
166}
167
168my sub scope_name_base : prototype($) {
169 my ($storeid) = @_;
170 return "pve-esxi-fuse-" . PVE::Systemd::escape_unit($storeid);
171}
172
173my sub is_mounted : prototype($) {
174 my ($storeid) = @_;
175
176 my $scope_name_base = scope_name_base($storeid);
177 return PVE::Systemd::is_unit_active($scope_name_base . '.scope');
178}
179
180sub esxi_mount : prototype($$$;$) {
181 my ($class, $storeid, $scfg, $force_requery) = @_;
182
183 return if !$force_requery && is_mounted($storeid);
184
185 $class->get_manifest($storeid, $scfg, $force_requery);
186
187 my $rundir = run_path($storeid);
188 my $manifest_file = "$rundir/manifest.json";
189 my $mount_dir = mount_dir($storeid);
190 if (!mkdir($mount_dir)) {
191 die "mkdir failed on $mount_dir $!\n" if !$!{EEXIST};
192 }
193
194 my $scope_name_base = scope_name_base($storeid);
195 my $user = $scfg->{username};
196 my $host = $scfg->{server};
197 my $pwfile = esxi_cred_file_name($storeid);
198
70204915
WB
199 my $hostport = $host;
200 $hostport = "[$hostport]" if Net::IP::ip_is_ipv6($host);
201 if (my $port = $scfg->{port}) {
202 $hostport .= ":$port";
203 }
204
a1710367
WB
205 pipe(my $rd, my $wr) or die "failed to create pipe: $!\n";
206
207 my $pid = fork();
208 die "fork failed: $!\n" if !defined($pid);
209 if (!$pid) {
210 eval {
211 undef $rd;
212 POSIX::setsid();
213 PVE::Systemd::enter_systemd_scope(
214 $scope_name_base,
215 "Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
216 );
217
2ff6f99c
WB
218 my @extra_params;
219 push @extra_params, '--skip-cert-verification' if $scfg->{'skip-cert-verification'};
220
a1710367
WB
221 my $flags = fcntl($wr, F_GETFD, 0)
222 // die "failed to get file descriptor flags: $!\n";
223 fcntl($wr, F_SETFD, $flags & ~FD_CLOEXEC)
224 // die "failed to remove CLOEXEC flag from fd: $!\n";
225 # FIXME: use the user/group options!
dde640ed
TL
226 exec {$ESXI_FUSE_TOOL}
227 $ESXI_FUSE_TOOL,
2ff6f99c 228 @extra_params,
9bb651ef
WB
229 '--change-user', 'nobody',
230 '--change-group', 'nogroup',
a1710367
WB
231 '-o', 'allow_other',
232 '--ready-fd', fileno($wr),
233 '--user', $user,
234 '--password-file', $pwfile,
70204915 235 $hostport,
a1710367
WB
236 $manifest_file,
237 $mount_dir;
238 die "exec failed: $!\n";
239 };
240 if (my $err = $@) {
241 print {$wr} "ERROR: $err";
242 }
243 POSIX::_exit(1);
244 };
245 undef $wr;
246
247 my $result = do { local $/ = undef; <$rd> };
248 if ($result =~ /^ERROR: (.*)$/) {
249 die "$1\n";
250 }
251
252 if (waitpid($pid, POSIX::WNOHANG) == $pid) {
253 die "failed to spawn fuse mount, process exited with status $?\n";
254 }
255}
256
257sub esxi_unmount : prototype($$$) {
258 my ($class, $storeid, $scfg) = @_;
259
260 my $scope_name_base = scope_name_base($storeid);
261 my $scope = "${scope_name_base}.scope";
262 my $mount_dir = mount_dir($storeid);
263
264 my %silence_std_outs = (outfunc => sub {}, errfunc => sub {});
265 eval { run_command(['/bin/systemctl', 'reset-failed', $scope], %silence_std_outs) };
266 eval { run_command(['/bin/systemctl', 'stop', $scope], %silence_std_outs) };
267 run_command(['/bin/umount', $mount_dir]);
268}
269
a1710367
WB
270# Split a path into (datacenter, datastore, path)
271sub split_path : prototype($) {
272 my ($path) = @_;
273 if ($path =~ m!^([^/]+)/([^/]+)/(.+)$!) {
274 return ($1, $2, $3);
275 }
276 return;
277}
278
4f50a578 279sub get_import_metadata : prototype($$$$$) {
a1710367
WB
280 my ($class, $scfg, $volname, $storeid) = @_;
281
282 if ($volname !~ m!^([^/]+)/.*\.vmx$!) {
283 die "volume '$volname' does not look like an importable vm config\n";
284 }
285
286 my $vmx_path = $class->path($scfg, $volname, $storeid, undef);
287 if (!is_mounted($storeid)) {
288 die "storage '$storeid' is not activated\n";
289 }
290
291 my $manifest = $class->get_manifest($storeid, $scfg, 0);
292 my $contents = file_get_contents($vmx_path);
4f50a578 293 my $vmx = PVE::Storage::ESXiPlugin::VMX->parse(
a1710367
WB
294 $storeid,
295 $scfg,
296 $volname,
297 $contents,
298 $manifest,
299 );
4f50a578 300 return $vmx->get_create_args();
a1710367
WB
301}
302
303# Returns a size in bytes, this is a helper for already-mounted files.
304sub query_vmdk_size : prototype($;$) {
305 my ($filename, $timeout) = @_;
306
307 my $json = eval {
308 my $json = '';
309 run_command(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
310 timeout => $timeout,
311 outfunc => sub { $json .= $_[0]; },
312 errfunc => sub { warn "$_[0]\n"; }
313 );
314 from_json($json)
315 };
316 warn $@ if $@;
317
318 return int($json->{'virtual-size'});
319}
320
321#
322# Storage API implementation
323#
324
325sub on_add_hook {
326 my ($class, $storeid, $scfg, %sensitive) = @_;
327
328 my $password = $sensitive{password};
329 die "missing password\n" if !defined($password);
330 esxi_set_credentials($password, $storeid);
331
332 return;
333}
334
335sub on_update_hook {
336 my ($class, $storeid, $scfg, %sensitive) = @_;
337
0a2f75ff
TL
338 # FIXME: allow to actually determine this, e.g., through new $changed hash passed to the hook
339 my $connection_detail_changed = 1;
340
341 if (exists($sensitive{password})) {
342 $connection_detail_changed = 1;
343 if (defined($sensitive{password})) {
344 esxi_set_credentials($sensitive{password}, $storeid);
345 } else {
346 esxi_delete_credentials($storeid);
347 }
348 }
a1710367 349
0a2f75ff
TL
350 if ($connection_detail_changed) {
351 # best-effort deactivate storage so that it can get re-mounted with updated params
352 eval { $class->deactivate_storage($storeid, $scfg) };
353 warn $@ if $@;
a1710367
WB
354 }
355
356 return;
357}
358
359sub on_delete_hook {
360 my ($class, $storeid, $scfg) = @_;
361
72418e2f
TL
362 eval { $class->deactivate_storage($storeid, $scfg) };
363 warn $@ if $@;
364
a1710367
WB
365 esxi_delete_credentials($storeid);
366
367 return;
368}
369
370sub activate_storage {
371 my ($class, $storeid, $scfg, $cache) = @_;
372
373 $class->esxi_mount($storeid, $scfg, 0);
374}
375
376sub deactivate_storage {
377 my ($class, $storeid, $scfg, $cache) = @_;
378
379 $class->esxi_unmount($storeid, $scfg);
68f3ec3f
TL
380
381 my $rundir = run_path($storeid);
382 remove_tree($rundir); # best-effort, ignore errors for now
383
a1710367
WB
384}
385
386sub activate_volume {
387 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
388
389 # FIXME: maybe check if it exists?
390}
391
392sub deactivate_volume {
393 my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
394
395 return 1;
396}
397
398sub check_connection {
399 my ($class, $storeid, $scfg) = @_;
400
70204915
WB
401 my $port = $scfg->{port} || 443;
402 return PVE::Network::tcp_ping($scfg->{server}, $port, 2);
a1710367
WB
403}
404
405sub status {
406 my ($class, $storeid, $scfg, $cache) = @_;
407
d09ed4bf
TL
408 my $active = is_mounted($storeid) ? 1 : 0;
409
410 return (0, 0, 0, $active);
a1710367
WB
411}
412
413sub parse_volname {
414 my ($class, $volname) = @_;
415
416 # it doesn't really make sense tbh, we can't return an owner, the format
417 # may be a 'vmx' (config), the paths are arbitrary...
418
419 die "failed to parse volname '$volname'\n"
420 if $volname !~ m!^([^/]+)/([^/]+)/(.+)$!;
421
422 return ('import', $volname) if $volname =~ /\.vmx$/;
423
424 my $format = 'raw';
425 $format = 'vmdk' if $volname =~ /\.vmdk/;
426 return ('images', $volname, 0, undef, undef, undef, $format);
427}
428
429sub list_images {
430 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
431
432 return [];
433}
434
435sub list_volumes {
436 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
437
438 return if !grep { $_ eq 'import' } @$content_types;
439
440 my $data = $class->get_manifest($storeid, $scfg, 0);
441
442 my $res = [];
443 for my $dc_name (keys $data->%*) {
444 my $dc = $data->{$dc_name};
445 my $vms = $dc->{vms};
446 for my $vm_name (keys $vms->%*) {
447 my $vm = $vms->{$vm_name};
448 my $ds_name = $vm->{config}->{datastore};
449 my $path = $vm->{config}->{path};
450 push @$res, {
451 content => 'import',
452 format => 'vmx',
453 name => $vm_name,
454 volid => "$storeid:$dc_name/$ds_name/$path",
455 size => 0,
456 };
457 }
458 }
459
460 return $res;
461}
462
463sub clone_image {
464 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
465
466 die "cloning images is not supported for $class\n";
467}
468
469sub create_base {
470 my ($class, $storeid, $scfg, $volname) = @_;
471
472 die "creating base images is not supported for $class\n";
473}
474
475sub path {
476 my ($class, $scfg, $volname, $storeid, $snapname) = @_;
477
478 die "storage '$class' does not support snapshots\n" if defined $snapname;
479
480 # FIXME: activate/mount:
481 return mount_dir($storeid) . '/' . $volname;
482}
483
484sub alloc_image {
485 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
486
487 die "creating images is not supported for $class\n";
488}
489
490sub free_image {
491 my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
492
493 die "deleting images is not supported for $class\n";
494}
495
496sub rename_volume {
497 my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
498
499 die "renaming volumes is not supported for $class\n";
500}
501
502sub volume_export_formats {
503 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
504
505 # FIXME: maybe we can support raw+size via `qemu-img dd`?
506
507 die "exporting not supported for $class\n";
508}
509
510sub volume_export {
511 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
512
513 # FIXME: maybe we can support raw+size via `qemu-img dd`?
514
515 die "exporting not supported for $class\n";
516}
517
518sub volume_import_formats {
519 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
520
521 die "importing not supported for $class\n";
522}
523
524sub volume_import {
525 my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
526
527 die "importing not supported for $class\n";
528}
529
530sub volume_resize {
531 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
532
533 die "resizing volumes is not supported for $class\n";
534}
535
536sub volume_size_info {
537 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
538
539 return 0 if $volname =~ /\.vmx$/;
540
541 my $filename = $class->path($scfg, $volname, $storeid, undef);
542 return PVE::Storage::Plugin::file_size_info($filename, $timeout);
543}
544
545sub volume_snapshot {
546 my ($class, $scfg, $storeid, $volname, $snap) = @_;
547
548 die "creating snapshots is not supported for $class\n";
549}
550
551sub volume_snapshot_delete {
552 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
553
554 die "deleting snapshots is not supported for $class\n";
555}
556sub volume_snapshot_info {
557
558 my ($class, $scfg, $storeid, $volname) = @_;
559
560 die "getting snapshot information is not supported for $class";
561}
562
563sub volume_rollback_is_possible {
564 my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
565
566 return 0;
567}
568
569sub volume_has_feature {
570 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
571
572 return undef if defined($snapname) || $volname =~ /\.vmx$/;
573 return 1 if $feature eq 'copy';
574 return undef;
575}
576
577sub get_subdir {
578 my ($class, $scfg, $vtype) = @_;
579
580 die "no subdirectories available for storage $class\n";
581}
582
583package PVE::Storage::ESXiPlugin::Manifest;
584
585use strict;
586use warnings;
587
588use JSON qw(from_json);
589
590sub new : prototype($$) {
591 my ($class, $data) = @_;
592
593 my $json = from_json($data);
594
595 return bless $json, $class;
596}
597
598sub datacenter_for_vm {
599 my ($self, $vm) = @_;
600
601 for my $dc_name (sort keys %$self) {
602 my $dc = $self->{$dc_name};
603 return $dc_name if exists($dc->{vms}->{$vm});
604 }
605
606 return;
607}
608
609sub datastore_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 if (defined(my $vm = $dc->{vms}->{$vm})) {
617 return $vm->{config}->{datastore};
618 }
619 }
620
621 return;
622}
623
624sub resolve_path {
625 my ($self, $path) = @_;
626
627 if ($path !~ m|^/|) {
628 return wantarray ? (undef, undef, $path) : $path;
629 }
630
631 for my $dc_name (sort keys %$self) {
632 my $dc = $self->{$dc_name};
633
634 my $datastores = $dc->{datastores};
635
636 for my $ds_name (keys %$datastores) {
637 my $ds_path = $datastores->{$ds_name};
638 if (substr($path, 0, length($ds_path)) eq $ds_path) {
639 my $relpath = substr($path, length($ds_path));
640 return wantarray ? ($dc_name, $ds_name, $relpath) : $relpath;
641 }
642 }
643 }
644
645 return;
646}
647
648sub config_path_for_vm {
649 my ($self, $vm, $datacenter) = @_;
650
651 my @dc_names = defined($datacenter) ? ($datacenter) : keys %$self;
652 for my $dc_name (@dc_names) {
653 my $dc = $self->{$dc_name}
654 or die "no such datacenter '$datacenter'\n";
655
656 my $vm = $dc->{vms}->{$vm}
657 or next;
658
659 my $cfg = $vm->{config};
660 if (my (undef, $ds_name, $path) = $self->resolve_path($cfg->{path})) {
661 $ds_name //= $cfg->{datastore};
662 return ($dc_name, $ds_name, $path);
663 }
664
665 die "failed to resolve path for vm '$vm' "
666 ."($dc_name, $cfg->{datastore}, $cfg->{path})\n";
667 }
668
669 die "no such vm '$vm'\n";
670}
671
672# Since paths in the vmx file are relative to the vmx file itself, this helper
673# provides a way to resolve paths which are relative based on the config file
674# path, while also resolving absolute paths without the vm config.
a1710367
WB
675sub resolve_path_relative_to {
676 my ($self, $vmx_path, $path) = @_;
677
678 if ($path =~ m|^/|) {
679 if (my ($disk_dc, $disk_ds, $disk_path) = $self->resolve_path($path)) {
680 return "$disk_dc/$disk_ds/$disk_path";
681 }
682 die "failed to resolve path '$path'\n";
683 }
684
685 my ($rel_dc, $rel_ds, $rel_path) = PVE::Storage::ESXiPlugin::split_path($vmx_path)
686 or die "bad path '$vmx_path'\n";
687 $rel_path =~ s|/[^/]+$||;
688
689 return "$rel_dc/$rel_ds/$rel_path/$path";
690}
691
aba709f2
WB
692# Imports happen by the volume id which is a path to a VMX file.
693# In order to find the vm's power state and disk capacity info, we need to find the
694# VM the vmx file belongs to.
695sub vm_for_vmx_path {
696 my ($self, $vmx_path) = @_;
697
698 my ($dc_name, $ds_name, $path) = PVE::Storage::ESXiPlugin::split_path($vmx_path);
699 if (my $dc = $self->{$dc_name}) {
700 my $vms = $dc->{vms};
701 for my $vm_name (keys %$vms) {
702 my $vm = $vms->{$vm_name};
703 my $cfg_info = $vm->{config};
704 if ($cfg_info->{datastore} eq $ds_name && $cfg_info->{path} eq $path) {
705 return $vm;
706 }
707 }
708 }
709 return;
710}
711
a1710367
WB
712package PVE::Storage::ESXiPlugin::VMX;
713
714use strict;
715use warnings;
716use feature 'fc';
717
718# FIXME: see if vmx files can actually have escape sequences in their quoted values?
719my sub unquote : prototype($) {
720 my ($value) = @_;
721 $value =~ s/^\"(.*)\"$/$1/s
722 or $value =~ s/^\'(.*)\'$/$1/s;
723 return $value;
724}
725
726sub parse : prototype($$$$$$) {
727 my ($class, $storeid, $scfg, $vmx_path, $vmxdata, $manifest) = @_;
728
729 my $conf = {};
730
731 for my $line (split(/\n/, $vmxdata)) {
732 $line =~ s/^\s+//;
733 $line =~ s/\s+$//;
734 next if $line !~ /^(\S+)\s*=\s*(.+)$/;
735 my ($key, $value) = ($1, $2);
736
737 $value = unquote($value);
738
739 $conf->{$key} = $value;
740 }
741
742 $conf->{'pve.storeid'} = $storeid;
743 $conf->{'pve.storage.config'} = $scfg;
744 $conf->{'pve.vmx.path'} = $vmx_path;
745 $conf->{'pve.manifest'} = $manifest;
746
747 return bless $conf, $class;
748}
749
750sub storeid { $_[0]->{'pve.storeid'} }
751sub scfg { $_[0]->{'pve.storage.config'} }
752sub vmx_path { $_[0]->{'pve.vmx.path'} }
753sub manifest { $_[0]->{'pve.manifest'} }
754
755# (Also used for the fileName config key...)
756sub is_disk_entry : prototype($) {
757 my ($id) = @_;
758 if ($id =~ /^(scsi|ide|sata|nvme)(\d+:\d+)(:?\.fileName)?$/) {
759 return ($1, $2);
760 }
761 return;
762}
763
764sub is_cdrom {
765 my ($self, $bus, $slot) = @_;
766 if (my $type = $self->{"${bus}${slot}.deviceType"}) {
767 return $type =~ /cdrom/;
768 }
769 return;
770}
771
772sub for_each_disk {
773 my ($self, $code) = @_;
774
775 for my $key (sort keys %$self) {
776 my ($bus, $slot) = is_disk_entry($key)
777 or next;
778 my $kind = $self->is_cdrom($bus, $slot) ? 'cdrom' : 'disk';
779
780 my $file = $self->{$key};
781
782 my ($maj, $min) = split(/:/, $slot, 2);
783 my $vdev = $self->{"${bus}${maj}.virtualDev"}; # may of course be undef...
784
785 $code->($bus, $slot, $file, $vdev, $kind);
786 }
787
788 return;
789}
790
791sub for_each_netdev {
792 my ($self, $code) = @_;
793
794 my $found_devs = {};
795 for my $key (keys %$self) {
796 next if $key !~ /^ethernet(\d+)\.(.+)$/;
797 my ($slot, $opt) = ($1, $2);
798
799 my $dev = ($found_devs->{$slot} //= {});
800 $dev->{$opt} = $self->{$key};
801 }
802
803 for my $id (sort keys %$found_devs) {
804 my $dev = $found_devs->{$id};
805
806 next if ($dev->{present} // '') ne 'TRUE';
807
808 my $ty = $dev->{addressType};
809 my $mac = $dev->{address};
b29a8c35 810 if ($ty && fc($ty) =~ /^(static|generated|vpx)$/) {
a1710367
WB
811 $mac = $dev->{generatedAddress} // $mac;
812 }
813
814 $code->($id, $dev, $mac);
815 }
816
817 return;
818}
819
5e934933
WB
820sub for_each_serial {
821 my ($self, $code) = @_;
822
823 my $found_serials = {};
824 for my $key (sort keys %$self) {
825 next if $key !~ /^serial(\d+)\.(.+)$/;
826 my ($slot, $opt) = ($1, $2);
827 my $serial = ($found_serials->{$1} //= {});
828 $serial->{$opt} = $self->{$key};
829 }
830
831 for my $id (sort { $a <=> $b } keys %$found_serials) {
832 my $serial = $found_serials->{$id};
833
834 next if ($serial->{present} // '') ne 'TRUE';
835
836 $code->($id, $serial);
837 }
838
839 return;
840}
841
a1710367
WB
842sub firmware {
843 my ($self) = @_;
844 my $fw = $self->{firmware};
845 return 'efi' if $fw && fc($fw) eq fc('efi');
846 return 'bios';
847}
848
849# This is in MB
850sub memory {
851 my ($self) = @_;
852
853 return $self->{memSize};
854}
855
856# CPU info is stored as a maximum ('numvcpus') and a core-per-socket count.
857# We return a (cores, sockets) tuple the way want it for PVE.
858sub cpu_info {
859 my ($self) = @_;
860
861 my $cps = int($self->{'cpuid.coresPerSocket'} // 1);
862 my $max = int($self->{numvcpus} // $cps);
863
864 return ($cps, ($max / $cps));
865}
866
867# FIXME: Test all possible values esxi creates?
868sub is_windows {
869 my ($self) = @_;
870
871 my $guest = $self->{guestOS} // return;
872 return 1 if $guest =~ /^win/i;
873 return;
874}
875
688cc11a 876my %guest_types_windows = (
10facd37 877 'dos' => 'other',
b1d0effc 878 'longhorn' => 'w2k8',
10facd37
TL
879 'winNetBusiness' => 'w2k3',
880 'windows9' => 'win10',
a1710367 881 'windows9-64' => 'win10',
10facd37 882 'windows9srv' => 'win10',
f72d3e12 883 'windows9srv-64' => 'win10',
a1710367
WB
884 'windows11-64' => 'win11',
885 'windows12-64' => 'win11', # FIXME / win12?
10facd37
TL
886 'win2000AdvServ' => 'w2k',
887 'win2000Pro' => 'w2k',
888 'win2000Serv' => 'w2k',
889 'win31' => 'other',
890 'windows7' => 'win7',
a1710367 891 'windows7-64' => 'win7',
10facd37 892 'windows8' => 'win8',
a1710367 893 'windows8-64' => 'win8',
10facd37
TL
894 'win95' => 'other',
895 'win98' => 'other',
896 'winNT' => 'wxp', # ?
897 'winNetEnterprise' => 'w2k3',
a1710367 898 'winNetEnterprise-64' => 'w2k3',
10facd37 899 'winNetDatacenter' => 'w2k3',
a1710367 900 'winNetDatacenter-64' => 'w2k3',
10facd37 901 'winNetStandard' => 'w2k3',
a1710367 902 'winNetStandard-64' => 'w2k3',
10facd37
TL
903 'winNetWeb' => 'w2k3',
904 'winLonghorn' => 'w2k8',
a1710367
WB
905 'winLonghorn-64' => 'w2k8',
906 'windows7Server-64' => 'w2k8',
907 'windows8Server-64' => 'win8',
908 'windows9Server-64' => 'win10',
909 'windows2019srv-64' => 'win10',
910 'windows2019srvNext-64' => 'win11',
911 'windows2022srvNext-64' => 'win11', # FIXME / win12?
10facd37 912 'winVista' => 'wvista',
a1710367 913 'winVista-64' => 'wvista',
10facd37 914 'winXPPro' => 'wxp',
a1710367
WB
915 'winXPPro-64' => 'wxp',
916);
917
688cc11a
GG
918my %guest_types_other = (
919 'freeBSD11' => 'other',
920 'freeBSD11-64' => 'other',
921 'freeBSD12' => 'other',
922 'freeBSD12-64' => 'other',
923 'freeBSD13' => 'other',
924 'freeBSD13-64' => 'other',
925 'freeBSD14' => 'other',
926 'freeBSD14-64' => 'other',
927 'freeBSD' => 'other',
928 'freeBSD-64' => 'other',
929 'os2' => 'other',
930 'netware5' => 'other',
931 'netware6' => 'other',
932 'solaris10' => 'solaris',
933 'solaris10-64' => 'solaris',
934 'solaris11-64' => 'solaris',
935 'other' => 'other',
936 'other-64' => 'other',
937 'openserver5' => 'other',
938 'openserver6' => 'other',
939 'unixware7' => 'other',
940 'eComStation' => 'other',
941 'eComStation2' => 'other',
942 'solaris8' => 'solaris',
943 'solaris9' => 'solaris',
944 'vmkernel' => 'other',
945 'vmkernel5' => 'other',
946 'vmkernel6' => 'other',
947 'vmkernel65' => 'other',
948 'vmkernel7' => 'other',
949 'vmkernel8' => 'other',
950);
951
a1710367
WB
952# Best effort translation from vmware guest os type to pve.
953# Returns a tuple: `(pve-type, is_windows)`
954sub guest_type {
955 my ($self) = @_;
a1710367 956 if (defined(my $guest = $self->{guestOS})) {
688cc11a
GG
957 if (defined(my $known_windows = $guest_types_windows{$guest})) {
958 return ($known_windows, 1);
959 } elsif (defined(my $known_other = $guest_types_other{$guest})) {
960 return ($known_other, 0);
a1710367
WB
961 }
962 # This covers all the 'Mac OS' types AFAICT
963 return ('other', 0) if $guest =~ /^darwin/;
964 }
965
966 # otherwise we'll just go with l26 defaults because why not...
967 return ('l26', 0);
968}
969
970sub smbios1_uuid {
971 my ($self) = @_;
972
973 my $uuid = $self->{'uuid.bios'};
974
975 return if !defined($uuid);
976
977 # vmware stores space separated bytes and has 1 dash in the middle...
978 $uuid =~ s/[^0-9a-fA-f]//g;
979
980 if ($uuid =~ /^
981 ([0-9a-fA-F]{8})
982 ([0-9a-fA-F]{4})
983 ([0-9a-fA-F]{4})
984 ([0-9a-fA-F]{4})
985 ([0-9a-fA-F]{12})
986 $/x)
987 {
988 return "$1-$2-$3-$4-$5";
989 }
990 return;
991}
992
993# This builds arguments for the `create` api call for this config.
994sub get_create_args {
728c08b2 995 my ($self) = @_;
a1710367
WB
996
997 my $storeid = $self->storeid;
998 my $manifest = $self->manifest;
11281005 999 my $vminfo = $manifest->vm_for_vmx_path($self->vmx_path);
a1710367
WB
1000
1001 my $create_args = {};
728c08b2
WB
1002 my $create_disks = {};
1003 my $create_net = {};
203d73ba 1004 my $warnings = [];
a1710367 1005
1a94dceb 1006 # NOTE: all types must be added to the return schema of the import-metadata API endpoint
203d73ba 1007 my $warn = sub {
1a94dceb
TL
1008 my ($type, %properties) = @_;
1009 push @$warnings, { type => $type, %properties };
203d73ba
WB
1010 };
1011
a1710367
WB
1012 my ($cores, $sockets) = $self->cpu_info();
1013 $create_args->{cores} = $cores if $cores != 1;
1014 $create_args->{sockets} = $sockets if $sockets != 1;
1015
1016 my $firmware = $self->firmware;
1017 if ($firmware eq 'efi') {
1018 $create_args->{bios} = 'ovmf';
728c08b2 1019 $create_disks->{efidisk0} = 1;
0f940f10 1020 $warn->('efi-state-lost', key => "bios", value => "ovmf");
a1710367
WB
1021 } else {
1022 $create_args->{bios} = 'seabios';
1023 }
1024
1025 my $memory = $self->memory;
1026 $create_args->{memory} = $memory;
1027
1028 my $default_scsihw;
1029 my $scsihw;
1030 my $set_scsihw = sub {
1031 if (defined($scsihw) && $scsihw ne $_[0]) {
1032 warn "multiple different SCSI hardware types are not supported\n";
1033 return;
1034 }
1035 $scsihw = $_[0];
1036 };
1037
1038 my ($ostype, $is_windows) = $self->guest_type();
1039 $create_args->{ostype} //= $ostype if defined($ostype);
1040 if ($ostype eq 'l26') {
1041 $default_scsihw = 'virtio-scsi-single';
1042 }
1043
1044 $self->for_each_netdev(sub {
1045 my ($id, $dev, $mac) = @_;
1046 $mac //= '';
1047 my $model = $dev->{virtualDev} // 'vmxnet3';
1048
728c08b2
WB
1049 my $param = { model => $model };
1050 $param->{macaddr} = $mac if length($mac);
1051 $create_net->{"net$id"} = $param;
a1710367
WB
1052 });
1053
1054 my %counts = ( scsi => 0, sata => 0, ide => 0 );
1055
a1710367
WB
1056 my $boot_order = '';
1057
1058 # we deal with nvme disks in a 2nd go-around since we currently don't
1059 # support nvme disks and instead just add them as additional scsi
1060 # disks.
1061 my @nvmes;
1062 my $add_disk = sub {
1063 my ($bus, $slot, $file, $devtype, $kind, $do_nvmes) = @_;
1064
1065 my $vmbus = $bus;
1066 if ($do_nvmes) {
1067 $bus = 'scsi';
1068 } elsif ($bus eq 'nvme') {
1069 push @nvmes, [$slot, $file, $devtype, $kind];
1070 return;
1071 }
1072
1073 my $path = eval { $manifest->resolve_path_relative_to($self->vmx_path, $file) };
1074 return if !defined($path);
1075
a3271a47
WB
1076 if ($devtype) {
1077 if ($devtype =~ /^lsi/i) {
1078 $set_scsihw->('lsi');
1079 } elsif ($devtype eq 'pvscsi') {
1080 $set_scsihw->('pvscsi'); # same name in pve
1081 }
a1710367
WB
1082 }
1083
10b58740
WB
1084 my $disk_capacity;
1085 if (defined(my $diskinfo = $vminfo->{disks})) {
1086 my ($dc, $ds, $rel_path) = PVE::Storage::ESXiPlugin::split_path($path);
1087 for my $disk ($diskinfo->@*) {
1088 if ($disk->{datastore} eq $ds && $disk->{path} eq $rel_path) {
1089 $disk_capacity = $disk->{capacity};
1090 last;
1091 }
1092 }
1093 }
1094
a1710367 1095 my $count = $counts{$bus}++;
763a2292
WB
1096 if ($kind eq 'cdrom') {
1097 # We currently do not pass cdroms through via the esxi storage.
1098 # Users should adapt import these from the storages directly/manually.
1099 $create_args->{"${bus}${count}"} = "none,media=cdrom";
1a94dceb
TL
1100 # CD-ROM image will not get imported
1101 $warn->('cdrom-image-ignored', key => "${bus}${count}", value => "$storeid:$path");
763a2292 1102 } else {
10b58740
WB
1103 $create_disks->{"${bus}${count}"} = {
1104 volid => "$storeid:$path",
1105 defined($disk_capacity) ? (size => $disk_capacity) : (),
1106 };
763a2292 1107 }
a1710367
WB
1108
1109 $boot_order .= ';' if length($boot_order);
1110 $boot_order .= $bus.$count;
1111 };
1112 $self->for_each_disk($add_disk);
203d73ba 1113 if (@nvmes) {
203d73ba
WB
1114 for my $nvme (@nvmes) {
1115 my ($slot, $file, $devtype, $kind) = @$nvme;
1a94dceb
TL
1116 $warn->('nvme-unsupported', key => "nvme${slot}", value => "$file");
1117 $add_disk->('scsi', $slot, $file, $devtype, $kind, 1);
203d73ba 1118 }
a1710367
WB
1119 }
1120
1121 $scsihw //= $default_scsihw;
203d73ba
WB
1122 if ($firmware eq 'efi') {
1123 if (!defined($scsihw) || $scsihw =~ /^lsi/) {
1124 if ($is_windows) {
1125 $scsihw = 'pvscsi';
1126 } else {
1127 $scsihw = 'virtio-scsi-single';
1128 }
1a94dceb
TL
1129 # OVMF is built without LSI drivers, scsi hardware was set to $scsihw
1130 $warn->('ovmf-with-lsi-unsupported', key => 'scsihw', value => "$scsihw");
a1710367
WB
1131 }
1132 }
c9bece69 1133 $create_args->{scsihw} = $scsihw if defined($scsihw);
a1710367
WB
1134
1135 $create_args->{boot} = "order=$boot_order";
1136
1137 if (defined(my $smbios1_uuid = $self->smbios1_uuid())) {
1138 $create_args->{smbios1} = "uuid=$smbios1_uuid";
1139 }
1140
1141 if (defined(my $name = $self->{displayName})) {
1142 # name in pve is a 'dns-name', so... clean it
1143 $name =~ s/\s/-/g;
1144 $name =~ s/[^a-zA-Z0-9\-.]//g;
1145 $name =~ s/^[.-]+//;
1146 $name =~ s/[.-]+$//;
1147 $create_args->{name} = $name if length($name);
1148 }
1149
5e934933
WB
1150 my $serid = 0;
1151 $self->for_each_serial(sub {
1152 my ($id, $serial) = @_;
1153 # currently we only support 'socket' type serials anyway
1a94dceb 1154 $warn->('serial-port-socket-only', key => "serial$serid");
5e934933
WB
1155 $create_args->{"serial$serid"} = 'socket';
1156 ++$serid;
1157 });
1158
11281005
WB
1159 $warn->('guest-is-running') if defined($vminfo) && ($vminfo->{power}//'') ne 'poweredOff';
1160
763a2292
WB
1161 return {
1162 type => 'vm',
fd16d984 1163 source => 'esxi',
763a2292 1164 'create-args' => $create_args,
728c08b2
WB
1165 disks => $create_disks,
1166 net => $create_net,
203d73ba 1167 warnings => $warnings,
763a2292 1168 };
a1710367
WB
1169}
1170
11711;