]>
Commit | Line | Data |
---|---|---|
a1710367 WB |
1 | package PVE::Storage::ESXiPlugin; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); | |
70204915 | 7 | use File::Path qw(mkpath remove_tree); |
a1710367 | 8 | use JSON qw(from_json); |
70204915 | 9 | use Net::IP; |
a1710367 | 10 | use POSIX (); |
a1710367 WB |
11 | |
12 | use PVE::Network; | |
13 | use PVE::Systemd; | |
14 | use PVE::Tools qw(file_get_contents file_set_contents run_command); | |
15 | ||
16 | use base qw(PVE::Storage::Plugin); | |
17 | ||
dde640ed TL |
18 | my $ESXI_LIST_VMS = '/usr/libexec/pve-esxi-import-tools/listvms.py'; |
19 | my $ESXI_FUSE_TOOL = '/usr/libexec/pve-esxi-import-tools/esxi-folder-fuse'; | |
a1710367 WB |
20 | my $ESXI_PRIV_DIR = '/etc/pve/priv/import/esxi'; |
21 | ||
22 | # | |
23 | # Configuration | |
24 | # | |
25 | ||
26 | sub type { | |
27 | return 'esxi'; | |
28 | } | |
29 | ||
30 | sub plugindata { | |
31 | return { | |
da513e26 | 32 | content => [ { import => 1 }, { import => 1 }], |
a1710367 WB |
33 | format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ], |
34 | }; | |
35 | } | |
36 | ||
37 | sub 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 | ||
47 | sub 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 | ||
62 | sub esxi_cred_file_name { | |
63 | my ($storeid) = @_; | |
64 | return "/etc/pve/priv/storage/${storeid}.pw"; | |
65 | } | |
66 | ||
67 | sub 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 | ||
75 | sub 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 | ||
86 | sub 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 | ||
101 | my 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 | |
107 | sub mount_dir : prototype($) { | |
108 | my ($storeid) = @_; | |
109 | return run_path($storeid) . "/mnt"; | |
110 | } | |
111 | ||
112 | my 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 |
117 | my 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 |
123 | sub 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 | ||
168 | my sub scope_name_base : prototype($) { | |
169 | my ($storeid) = @_; | |
170 | return "pve-esxi-fuse-" . PVE::Systemd::escape_unit($storeid); | |
171 | } | |
172 | ||
173 | my 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 | ||
180 | sub 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 | ||
257 | sub 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) |
271 | sub split_path : prototype($) { | |
272 | my ($path) = @_; | |
273 | if ($path =~ m!^([^/]+)/([^/]+)/(.+)$!) { | |
274 | return ($1, $2, $3); | |
275 | } | |
276 | return; | |
277 | } | |
278 | ||
4f50a578 | 279 | sub 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. | |
304 | sub 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 | ||
325 | sub 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 | ||
335 | sub 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 | ||
359 | sub 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 | ||
370 | sub activate_storage { | |
371 | my ($class, $storeid, $scfg, $cache) = @_; | |
372 | ||
373 | $class->esxi_mount($storeid, $scfg, 0); | |
374 | } | |
375 | ||
376 | sub 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 | ||
386 | sub activate_volume { | |
387 | my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; | |
388 | ||
389 | # FIXME: maybe check if it exists? | |
390 | } | |
391 | ||
392 | sub deactivate_volume { | |
393 | my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; | |
394 | ||
395 | return 1; | |
396 | } | |
397 | ||
398 | sub 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 | ||
405 | sub 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 | ||
413 | sub 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 | ||
429 | sub list_images { | |
430 | my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_; | |
431 | ||
432 | return []; | |
433 | } | |
434 | ||
435 | sub 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 | ||
463 | sub clone_image { | |
464 | my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; | |
465 | ||
466 | die "cloning images is not supported for $class\n"; | |
467 | } | |
468 | ||
469 | sub create_base { | |
470 | my ($class, $storeid, $scfg, $volname) = @_; | |
471 | ||
472 | die "creating base images is not supported for $class\n"; | |
473 | } | |
474 | ||
475 | sub 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 | ||
484 | sub alloc_image { | |
485 | my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_; | |
486 | ||
487 | die "creating images is not supported for $class\n"; | |
488 | } | |
489 | ||
490 | sub free_image { | |
491 | my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_; | |
492 | ||
493 | die "deleting images is not supported for $class\n"; | |
494 | } | |
495 | ||
496 | sub 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 | ||
502 | sub 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 | ||
510 | sub 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 | ||
518 | sub 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 | ||
524 | sub 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 | ||
530 | sub volume_resize { | |
531 | my ($class, $scfg, $storeid, $volname, $size, $running) = @_; | |
532 | ||
533 | die "resizing volumes is not supported for $class\n"; | |
534 | } | |
535 | ||
536 | sub 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 | ||
545 | sub volume_snapshot { | |
546 | my ($class, $scfg, $storeid, $volname, $snap) = @_; | |
547 | ||
548 | die "creating snapshots is not supported for $class\n"; | |
549 | } | |
550 | ||
551 | sub volume_snapshot_delete { | |
552 | my ($class, $scfg, $storeid, $volname, $snap, $running) = @_; | |
553 | ||
554 | die "deleting snapshots is not supported for $class\n"; | |
555 | } | |
556 | sub volume_snapshot_info { | |
557 | ||
558 | my ($class, $scfg, $storeid, $volname) = @_; | |
559 | ||
560 | die "getting snapshot information is not supported for $class"; | |
561 | } | |
562 | ||
563 | sub volume_rollback_is_possible { | |
564 | my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_; | |
565 | ||
566 | return 0; | |
567 | } | |
568 | ||
569 | sub 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 | ||
577 | sub get_subdir { | |
578 | my ($class, $scfg, $vtype) = @_; | |
579 | ||
580 | die "no subdirectories available for storage $class\n"; | |
581 | } | |
582 | ||
583 | package PVE::Storage::ESXiPlugin::Manifest; | |
584 | ||
585 | use strict; | |
586 | use warnings; | |
587 | ||
588 | use JSON qw(from_json); | |
589 | ||
590 | sub new : prototype($$) { | |
591 | my ($class, $data) = @_; | |
592 | ||
593 | my $json = from_json($data); | |
594 | ||
595 | return bless $json, $class; | |
596 | } | |
597 | ||
598 | sub 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 | ||
609 | sub 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 | ||
624 | sub 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 | ||
648 | sub 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 |
675 | sub 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. | |
695 | sub 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 |
712 | package PVE::Storage::ESXiPlugin::VMX; |
713 | ||
714 | use strict; | |
715 | use warnings; | |
716 | use feature 'fc'; | |
717 | ||
718 | # FIXME: see if vmx files can actually have escape sequences in their quoted values? | |
719 | my sub unquote : prototype($) { | |
720 | my ($value) = @_; | |
721 | $value =~ s/^\"(.*)\"$/$1/s | |
722 | or $value =~ s/^\'(.*)\'$/$1/s; | |
723 | return $value; | |
724 | } | |
725 | ||
726 | sub 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 | ||
750 | sub storeid { $_[0]->{'pve.storeid'} } | |
751 | sub scfg { $_[0]->{'pve.storage.config'} } | |
752 | sub vmx_path { $_[0]->{'pve.vmx.path'} } | |
753 | sub manifest { $_[0]->{'pve.manifest'} } | |
754 | ||
755 | # (Also used for the fileName config key...) | |
756 | sub 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 | ||
764 | sub is_cdrom { | |
765 | my ($self, $bus, $slot) = @_; | |
766 | if (my $type = $self->{"${bus}${slot}.deviceType"}) { | |
767 | return $type =~ /cdrom/; | |
768 | } | |
769 | return; | |
770 | } | |
771 | ||
772 | sub 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 | ||
791 | sub 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 |
820 | sub 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 |
842 | sub 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 | |
850 | sub 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. | |
858 | sub 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? | |
868 | sub is_windows { | |
869 | my ($self) = @_; | |
870 | ||
871 | my $guest = $self->{guestOS} // return; | |
872 | return 1 if $guest =~ /^win/i; | |
873 | return; | |
874 | } | |
875 | ||
688cc11a | 876 | my %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 |
918 | my %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)` | |
954 | sub 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 | ||
970 | sub 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. | |
994 | sub 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 | ||
1171 | 1; |