[pve-devel] [PATCH storage] esxi: detect correct os type in 'other' family

Gabriel Goller g.goller at proxmox.com
Wed Mar 20 16:42:37 CET 2024


This patch introduces the conversion table for all possible OS Types
that are in the VMWare 'other' family and sets the pve counterpart.
Our default OS Type is 'linux', so including mappings to 'other' makes
sense.

Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
---
 src/PVE/Storage/ESXiPlugin.pm     |   43 +-
 src/PVE/Storage/ESXiPlugin.pm.tdy | 1216 +++++++++++++++++++++++++++++
 2 files changed, 1255 insertions(+), 4 deletions(-)
 create mode 100644 src/PVE/Storage/ESXiPlugin.pm.tdy

diff --git a/src/PVE/Storage/ESXiPlugin.pm b/src/PVE/Storage/ESXiPlugin.pm
index b36cea8..6e80caa 100644
--- a/src/PVE/Storage/ESXiPlugin.pm
+++ b/src/PVE/Storage/ESXiPlugin.pm
@@ -849,7 +849,7 @@ sub is_windows {
     return;
 }
 
-my %guest_types = (
+my %guest_types_windows = (
     dos                     => 'other',
     winNetBusiness          => 'w2k3',
     windows9                => 'win10',
@@ -888,14 +888,49 @@ my %guest_types = (
     'winXPPro-64'           => 'wxp',
 );
 
+my %guest_types_other = (
+    'freeBSD11'    => 'other',
+    'freeBSD11-64' => 'other',
+    'freeBSD12'    => 'other',
+    'freeBSD12-64' => 'other',
+    'freeBSD13'    => 'other',
+    'freeBSD13-64' => 'other',
+    'freeBSD14'    => 'other',
+    'freeBSD14-64' => 'other',
+    'freeBSD'      => 'other',
+    'freeBSD-64'   => 'other',
+    'os2'          => 'other',
+    'netware5'     => 'other',
+    'netware6'     => 'other',
+    'solaris10'    => 'solaris',
+    'solaris10-64' => 'solaris',
+    'solaris11-64' => 'solaris',
+    'other'        => 'other',
+    'other-64'     => 'other',
+    'openserver5'  => 'other',
+    'openserver6'  => 'other',
+    'unixware7'    => 'other',
+    'eComStation'  => 'other',
+    'eComStation2' => 'other',
+    'solaris8'     => 'solaris',
+    'solaris9'     => 'solaris',
+    'vmkernel'     => 'other',
+    'vmkernel5'    => 'other',
+    'vmkernel6'    => 'other',
+    'vmkernel65'   => 'other',
+    'vmkernel7'    => 'other',
+    'vmkernel8'    => 'other',
+);
+
 # Best effort translation from vmware guest os type to pve.
 # Returns a tuple: `(pve-type, is_windows)`
 sub guest_type {
     my ($self) = @_;
-
     if (defined(my $guest = $self->{guestOS})) {
-	if (defined(my $known = $guest_types{$guest})) {
-	    return ($known, 1);
+	if (defined(my $known_windows = $guest_types_windows{$guest})) {
+	    return ($known_windows, 1);
+	} elsif (defined(my $known_other = $guest_types_other{$guest})) {
+	    return ($known_other, 0);
 	}
 	# This covers all the 'Mac OS' types AFAICT
 	return ('other', 0) if $guest =~ /^darwin/;
diff --git a/src/PVE/Storage/ESXiPlugin.pm.tdy b/src/PVE/Storage/ESXiPlugin.pm.tdy
new file mode 100644
index 0000000..2a08986
--- /dev/null
+++ b/src/PVE/Storage/ESXiPlugin.pm.tdy
@@ -0,0 +1,1216 @@
+package PVE::Storage::ESXiPlugin;
+
+use strict;
+use warnings;
+
+use Fcntl      qw(F_GETFD F_SETFD FD_CLOEXEC);
+use JSON       qw(from_json);
+use POSIX      ();
+use File::Path qw(mkpath remove_tree);
+
+use PVE::Network;
+use PVE::Systemd;
+use PVE::Tools qw(file_get_contents file_set_contents run_command);
+
+use base qw(PVE::Storage::Plugin);
+
+my $ESXI_LIST_VMS  = '/usr/libexec/pve-esxi-import-tools/listvms.py';
+my $ESXI_FUSE_TOOL = '/usr/libexec/pve-esxi-import-tools/esxi-folder-fuse';
+my $ESXI_PRIV_DIR  = '/etc/pve/priv/import/esxi';
+
+#
+# Configuration
+#
+
+sub type {
+    return 'esxi';
+}
+
+sub plugindata {
+    return {
+        content => [ { import => 1 },                        { import => 1 } ],
+        format  => [ { raw    => 1, qcow2 => 1, vmdk => 1 }, 'raw' ],
+    };
+}
+
+sub properties {
+    return {
+        'skip-cert-verification' => {
+            description =>
+'Disable TLS certificate verification, only enable on fully trusted networks!',
+            type    => 'boolean',
+            default => 'false',
+        },
+    };
+}
+
+sub options {
+    return {
+        nodes   => { optional => 1 },
+        shared  => { optional => 1 },
+        disable => { optional => 1 },
+        content => { optional => 1 },
+
+        # FIXME: bwlimit => { optional => 1 },
+        server                   => {},
+        username                 => {},
+        password                 => { optional => 1 },
+        'skip-cert-verification' => { optional => 1 },
+    };
+}
+
+sub esxi_cred_file_name {
+    my ($storeid) = @_;
+    return "/etc/pve/priv/storage/${storeid}.pw";
+}
+
+sub esxi_delete_credentials {
+    my ($storeid) = @_;
+
+    if ( my $cred_file = get_cred_file($storeid) ) {
+        unlink($cred_file)
+          or warn "removing esxi credientials '$cred_file' failed: $!\n";
+    }
+}
+
+sub esxi_set_credentials {
+    my ( $password, $storeid ) = @_;
+
+    my $cred_file = esxi_cred_file_name($storeid);
+    mkdir "/etc/pve/priv/storage";
+
+    PVE::Tools::file_set_contents( $cred_file, $password );
+
+    return $cred_file;
+}
+
+sub get_cred_file {
+    my ($storeid) = @_;
+
+    my $cred_file = esxi_cred_file_name($storeid);
+
+    if ( -e $cred_file ) {
+        return $cred_file;
+    }
+    return undef;
+}
+
+#
+# Dealing with the esxi API.
+#
+
+my sub run_path : prototype($) {
+    my ($storeid) = @_;
+    return "/run/pve/import/esxi/$storeid";
+}
+
+# "public" because it is needed by the VMX package
+sub mount_dir : prototype($) {
+    my ($storeid) = @_;
+    return run_path($storeid) . "/mnt";
+}
+
+my sub check_esxi_import_package : prototype() {
+    die "pve-esxi-import-tools package not installed, cannot proceed\n"
+      if !-e $ESXI_LIST_VMS;
+}
+
+my sub is_old : prototype($) {
+    my ($file) = @_;
+    my $mtime = ( CORE::stat($file) )[9];
+    return !defined($mtime) || ( $mtime + 60 ) < CORE::time();
+}
+
+sub get_manifest : prototype($$$;$) {
+    my ( $class, $storeid, $scfg, $force_query ) = @_;
+
+    my $rundir        = run_path($storeid);
+    my $manifest_file = "$rundir/manifest.json";
+
+    $force_query ||= is_old($manifest_file);
+
+    if ( !$force_query && -e $manifest_file ) {
+        return PVE::Storage::ESXiPlugin::Manifest->new(
+            file_get_contents($manifest_file), );
+    }
+
+    check_esxi_import_package();
+
+    my @extra_params;
+    push @extra_params, '--skip-cert-verification'
+      if $scfg->{'skip-cert-verification'};
+    my $host   = $scfg->{server};
+    my $user   = $scfg->{username};
+    my $pwfile = esxi_cred_file_name($storeid);
+    my $json   = '';
+    run_command( [ $ESXI_LIST_VMS, @extra_params, $host, $user, $pwfile ],
+        outfunc => sub { $json .= $_[0] . "\n" }, );
+
+    my $result = PVE::Storage::ESXiPlugin::Manifest->new($json);
+    mkpath($rundir);
+    file_set_contents( $manifest_file, $json );
+
+    return $result;
+}
+
+my sub scope_name_base : prototype($) {
+    my ($storeid) = @_;
+    return "pve-esxi-fuse-" . PVE::Systemd::escape_unit($storeid);
+}
+
+my sub is_mounted : prototype($) {
+    my ($storeid) = @_;
+
+    my $scope_name_base = scope_name_base($storeid);
+    return PVE::Systemd::is_unit_active( $scope_name_base . '.scope' );
+}
+
+sub esxi_mount : prototype($$$;$) {
+    my ( $class, $storeid, $scfg, $force_requery ) = @_;
+
+    return if !$force_requery && is_mounted($storeid);
+
+    $class->get_manifest( $storeid, $scfg, $force_requery );
+
+    my $rundir        = run_path($storeid);
+    my $manifest_file = "$rundir/manifest.json";
+    my $mount_dir     = mount_dir($storeid);
+    if ( !mkdir($mount_dir) ) {
+        die "mkdir failed on $mount_dir $!\n" if !$!{EEXIST};
+    }
+
+    my $scope_name_base = scope_name_base($storeid);
+    my $user            = $scfg->{username};
+    my $host            = $scfg->{server};
+    my $pwfile          = esxi_cred_file_name($storeid);
+
+    pipe( my $rd, my $wr ) or die "failed to create pipe: $!\n";
+
+    my $pid = fork();
+    die "fork failed: $!\n" if !defined($pid);
+    if ( !$pid ) {
+        eval {
+            undef $rd;
+            POSIX::setsid();
+            PVE::Systemd::enter_systemd_scope(
+                $scope_name_base,
+"Proxmox VE FUSE mount for ESXi storage $storeid (server $host)",
+            );
+
+            my @extra_params;
+            push @extra_params, '--skip-cert-verification'
+              if $scfg->{'skip-cert-verification'};
+
+            my $flags = fcntl( $wr, F_GETFD, 0 )
+              // die "failed to get file descriptor flags: $!\n";
+            fcntl( $wr, F_SETFD, $flags & ~FD_CLOEXEC )
+              // die "failed to remove CLOEXEC flag from fd: $!\n";
+
+            # FIXME: use the user/group options!
+            exec {$ESXI_FUSE_TOOL}
+              $ESXI_FUSE_TOOL,
+              @extra_params,
+              '-o',              'allow_other',
+              '--ready-fd',      fileno($wr),
+              '--user',          $user,
+              '--password-file', $pwfile,
+              $host,
+              $manifest_file,
+              $mount_dir;
+            die "exec failed: $!\n";
+        };
+        if ( my $err = $@ ) {
+            print {$wr} "ERROR: $err";
+        }
+        POSIX::_exit(1);
+    }
+    undef $wr;
+
+    my $result = do { local $/ = undef; <$rd> };
+    if ( $result =~ /^ERROR: (.*)$/ ) {
+        die "$1\n";
+    }
+
+    if ( waitpid( $pid, POSIX::WNOHANG ) == $pid ) {
+        die "failed to spawn fuse mount, process exited with status $?\n";
+    }
+}
+
+sub esxi_unmount : prototype($$$) {
+    my ( $class, $storeid, $scfg ) = @_;
+
+    my $scope_name_base = scope_name_base($storeid);
+    my $scope           = "${scope_name_base}.scope";
+    my $mount_dir       = mount_dir($storeid);
+
+    my %silence_std_outs = ( outfunc => sub { }, errfunc => sub { } );
+    eval {
+        run_command( [ '/bin/systemctl', 'reset-failed', $scope ],
+            %silence_std_outs );
+    };
+    eval {
+        run_command( [ '/bin/systemctl', 'stop', $scope ], %silence_std_outs );
+    };
+    run_command( [ '/bin/umount', $mount_dir ] );
+}
+
+# Split a path into (datacenter, datastore, path)
+sub split_path : prototype($) {
+    my ($path) = @_;
+    if ( $path =~ m!^([^/]+)/([^/]+)/(.+)$! ) {
+        return ( $1, $2, $3 );
+    }
+    return;
+}
+
+sub get_import_metadata : prototype($$$$$) {
+    my ( $class, $scfg, $volname, $storeid ) = @_;
+
+    if ( $volname !~ m!^([^/]+)/.*\.vmx$! ) {
+        die "volume '$volname' does not look like an importable vm config\n";
+    }
+
+    my $vmx_path = $class->path( $scfg, $volname, $storeid, undef );
+    if ( !is_mounted($storeid) ) {
+        die "storage '$storeid' is not activated\n";
+    }
+
+    my $manifest = $class->get_manifest( $storeid, $scfg, 0 );
+    my $contents = file_get_contents($vmx_path);
+    my $vmx = PVE::Storage::ESXiPlugin::VMX->parse( $storeid, $scfg, $volname,
+        $contents, $manifest, );
+    return $vmx->get_create_args();
+}
+
+# Returns a size in bytes, this is a helper for already-mounted files.
+sub query_vmdk_size : prototype($;$) {
+    my ( $filename, $timeout ) = @_;
+
+    my $json = eval {
+        my $json = '';
+        run_command(
+            [ '/usr/bin/qemu-img', 'info', '--output=json', $filename ],
+            timeout => $timeout,
+            outfunc => sub { $json .= $_[0]; },
+            errfunc => sub { warn "$_[0]\n"; }
+        );
+        from_json($json);
+    };
+    warn $@ if $@;
+
+    return int( $json->{'virtual-size'} );
+}
+
+#
+# Storage API implementation
+#
+
+sub on_add_hook {
+    my ( $class, $storeid, $scfg, %sensitive ) = @_;
+
+    my $password = $sensitive{password};
+    die "missing password\n" if !defined($password);
+    esxi_set_credentials( $password, $storeid );
+
+    return;
+}
+
+sub on_update_hook {
+    my ( $class, $storeid, $scfg, %sensitive ) = @_;
+
+# FIXME: allow to actually determine this, e.g., through new $changed hash passed to the hook
+    my $connection_detail_changed = 1;
+
+    if ( exists( $sensitive{password} ) ) {
+        $connection_detail_changed = 1;
+        if ( defined( $sensitive{password} ) ) {
+            esxi_set_credentials( $sensitive{password}, $storeid );
+        }
+        else {
+            esxi_delete_credentials($storeid);
+        }
+    }
+
+    if ($connection_detail_changed) {
+
+# best-effort deactivate storage so that it can get re-mounted with updated params
+        eval { $class->deactivate_storage( $storeid, $scfg ) };
+        warn $@ if $@;
+    }
+
+    return;
+}
+
+sub on_delete_hook {
+    my ( $class, $storeid, $scfg ) = @_;
+
+    eval { $class->deactivate_storage( $storeid, $scfg ) };
+    warn $@ if $@;
+
+    esxi_delete_credentials($storeid);
+
+    return;
+}
+
+sub activate_storage {
+    my ( $class, $storeid, $scfg, $cache ) = @_;
+
+    $class->esxi_mount( $storeid, $scfg, 0 );
+}
+
+sub deactivate_storage {
+    my ( $class, $storeid, $scfg, $cache ) = @_;
+
+    $class->esxi_unmount( $storeid, $scfg );
+
+    my $rundir = run_path($storeid);
+    remove_tree($rundir);    # best-effort, ignore errors for now
+
+}
+
+sub activate_volume {
+    my ( $class, $storeid, $scfg, $volname, $snapname, $cache ) = @_;
+
+    # FIXME: maybe check if it exists?
+}
+
+sub deactivate_volume {
+    my ( $class, $storeid, $scfg, $volname, $snapname, $cache ) = @_;
+
+    return 1;
+}
+
+sub check_connection {
+    my ( $class, $storeid, $scfg ) = @_;
+
+    return PVE::Network::tcp_ping( $scfg->{server}, 443, 2 );
+}
+
+sub status {
+    my ( $class, $storeid, $scfg, $cache ) = @_;
+
+    return ( 0, 0, 0, 0 );
+}
+
+sub parse_volname {
+    my ( $class, $volname ) = @_;
+
+    # it doesn't really make sense tbh, we can't return an owner, the format
+    # may be a 'vmx' (config), the paths are arbitrary...
+
+    die "failed to parse volname '$volname'\n"
+      if $volname !~ m!^([^/]+)/([^/]+)/(.+)$!;
+
+    return ( 'import', $volname ) if $volname =~ /\.vmx$/;
+
+    my $format = 'raw';
+    $format = 'vmdk' if $volname =~ /\.vmdk/;
+    return ( 'images', $volname, 0, undef, undef, undef, $format );
+}
+
+sub list_images {
+    my ( $class, $storeid, $scfg, $vmid, $vollist, $cache ) = @_;
+
+    return [];
+}
+
+sub list_volumes {
+    my ( $class, $storeid, $scfg, $vmid, $content_types ) = @_;
+
+    return if !grep { $_ eq 'import' } @$content_types;
+
+    my $data = $class->get_manifest( $storeid, $scfg, 0 );
+
+    my $res = [];
+    for my $dc_name ( keys $data->%* ) {
+        my $dc  = $data->{$dc_name};
+        my $vms = $dc->{vms};
+        for my $vm_name ( keys $vms->%* ) {
+            my $vm      = $vms->{$vm_name};
+            my $ds_name = $vm->{config}->{datastore};
+            my $path    = $vm->{config}->{path};
+            push @$res,
+              {
+                content => 'import',
+                format  => 'vmx',
+                name    => $vm_name,
+                volid   => "$storeid:$dc_name/$ds_name/$path",
+                size    => 0,
+              };
+        }
+    }
+
+    return $res;
+}
+
+sub clone_image {
+    my ( $class, $scfg, $storeid, $volname, $vmid, $snap ) = @_;
+
+    die "cloning images is not supported for $class\n";
+}
+
+sub create_base {
+    my ( $class, $storeid, $scfg, $volname ) = @_;
+
+    die "creating base images is not supported for $class\n";
+}
+
+sub path {
+    my ( $class, $scfg, $volname, $storeid, $snapname ) = @_;
+
+    die "storage '$class' does not support snapshots\n" if defined $snapname;
+
+    # FIXME: activate/mount:
+    return mount_dir($storeid) . '/' . $volname;
+}
+
+sub alloc_image {
+    my ( $class, $storeid, $scfg, $vmid, $fmt, $name, $size ) = @_;
+
+    die "creating images is not supported for $class\n";
+}
+
+sub free_image {
+    my ( $class, $storeid, $scfg, $volname, $isBase, $format ) = @_;
+
+    die "deleting images is not supported for $class\n";
+}
+
+sub rename_volume {
+    my ( $class, $scfg, $storeid, $source_volname, $target_vmid,
+        $target_volname )
+      = @_;
+
+    die "renaming volumes is not supported for $class\n";
+}
+
+sub volume_export_formats {
+    my ( $class, $scfg, $storeid, $volname, $snapshot, $base_snapshot,
+        $with_snapshots )
+      = @_;
+
+    # FIXME: maybe we can support raw+size via `qemu-img dd`?
+
+    die "exporting not supported for $class\n";
+}
+
+sub volume_export {
+    my (
+        $class,    $scfg,          $storeid,
+        $fh,       $volname,       $format,
+        $snapshot, $base_snapshot, $with_snapshots
+    ) = @_;
+
+    # FIXME: maybe we can support raw+size via `qemu-img dd`?
+
+    die "exporting not supported for $class\n";
+}
+
+sub volume_import_formats {
+    my ( $class, $scfg, $storeid, $volname, $snapshot, $base_snapshot,
+        $with_snapshots )
+      = @_;
+
+    die "importing not supported for $class\n";
+}
+
+sub volume_import {
+    my (
+        $class,          $scfg,   $storeid,  $fh,
+        $volname,        $format, $snapshot, $base_snapshot,
+        $with_snapshots, $allow_rename
+    ) = @_;
+
+    die "importing not supported for $class\n";
+}
+
+sub volume_resize {
+    my ( $class, $scfg, $storeid, $volname, $size, $running ) = @_;
+
+    die "resizing volumes is not supported for $class\n";
+}
+
+sub volume_size_info {
+    my ( $class, $scfg, $storeid, $volname, $timeout ) = @_;
+
+    return 0 if $volname =~ /\.vmx$/;
+
+    my $filename = $class->path( $scfg, $volname, $storeid, undef );
+    return PVE::Storage::Plugin::file_size_info( $filename, $timeout );
+}
+
+sub volume_snapshot {
+    my ( $class, $scfg, $storeid, $volname, $snap ) = @_;
+
+    die "creating snapshots is not supported for $class\n";
+}
+
+sub volume_snapshot_delete {
+    my ( $class, $scfg, $storeid, $volname, $snap, $running ) = @_;
+
+    die "deleting snapshots is not supported for $class\n";
+}
+
+sub volume_snapshot_info {
+
+    my ( $class, $scfg, $storeid, $volname ) = @_;
+
+    die "getting snapshot information is not supported for $class";
+}
+
+sub volume_rollback_is_possible {
+    my ( $class, $scfg, $storeid, $volname, $snap, $blockers ) = @_;
+
+    return 0;
+}
+
+sub volume_has_feature {
+    my (
+        $class,   $scfg,     $feature, $storeid,
+        $volname, $snapname, $running, $opts
+    ) = @_;
+
+    return undef if defined($snapname) || $volname =~ /\.vmx$/;
+    return 1     if $feature eq 'copy';
+    return undef;
+}
+
+sub get_subdir {
+    my ( $class, $scfg, $vtype ) = @_;
+
+    die "no subdirectories available for storage $class\n";
+}
+
+package PVE::Storage::ESXiPlugin::Manifest;
+
+use strict;
+use warnings;
+
+use JSON qw(from_json);
+
+sub new : prototype($$) {
+    my ( $class, $data ) = @_;
+
+    my $json = from_json($data);
+
+    return bless $json, $class;
+}
+
+sub datacenter_for_vm {
+    my ( $self, $vm ) = @_;
+
+    for my $dc_name ( sort keys %$self ) {
+        my $dc = $self->{$dc_name};
+        return $dc_name if exists( $dc->{vms}->{$vm} );
+    }
+
+    return;
+}
+
+sub datastore_for_vm {
+    my ( $self, $vm, $datacenter ) = @_;
+
+    my @dc_names = defined($datacenter) ? ($datacenter) : keys %$self;
+    for my $dc_name (@dc_names) {
+        my $dc = $self->{$dc_name}
+          or die "no such datacenter '$datacenter'\n";
+        if ( defined( my $vm = $dc->{vms}->{$vm} ) ) {
+            return $vm->{config}->{datastore};
+        }
+    }
+
+    return;
+}
+
+sub resolve_path {
+    my ( $self, $path ) = @_;
+
+    if ( $path !~ m|^/| ) {
+        return wantarray ? ( undef, undef, $path ) : $path;
+    }
+
+    for my $dc_name ( sort keys %$self ) {
+        my $dc = $self->{$dc_name};
+
+        my $datastores = $dc->{datastores};
+
+        for my $ds_name ( keys %$datastores ) {
+            my $ds_path = $datastores->{$ds_name};
+            if ( substr( $path, 0, length($ds_path) ) eq $ds_path ) {
+                my $relpath = substr( $path, length($ds_path) );
+                return wantarray ? ( $dc_name, $ds_name, $relpath ) : $relpath;
+            }
+        }
+    }
+
+    return;
+}
+
+sub config_path_for_vm {
+    my ( $self, $vm, $datacenter ) = @_;
+
+    my @dc_names = defined($datacenter) ? ($datacenter) : keys %$self;
+    for my $dc_name (@dc_names) {
+        my $dc = $self->{$dc_name}
+          or die "no such datacenter '$datacenter'\n";
+
+        my $vm = $dc->{vms}->{$vm}
+          or next;
+
+        my $cfg = $vm->{config};
+        if ( my ( undef, $ds_name, $path ) =
+            $self->resolve_path( $cfg->{path} ) )
+        {
+            $ds_name //= $cfg->{datastore};
+            return ( $dc_name, $ds_name, $path );
+        }
+
+        die "failed to resolve path for vm '$vm' "
+          . "($dc_name, $cfg->{datastore}, $cfg->{path})\n";
+    }
+
+    die "no such vm '$vm'\n";
+}
+
+# Since paths in the vmx file are relative to the vmx file itself, this helper
+# provides a way to resolve paths which are relative based on the config file
+# path, while also resolving absolute paths without the vm config.
+sub resolve_path_relative_to {
+    my ( $self, $vmx_path, $path ) = @_;
+
+    if ( $path =~ m|^/| ) {
+        if ( my ( $disk_dc, $disk_ds, $disk_path ) =
+            $self->resolve_path($path) )
+        {
+            return "$disk_dc/$disk_ds/$disk_path";
+        }
+        die "failed to resolve path '$path'\n";
+    }
+
+    my ( $rel_dc, $rel_ds, $rel_path ) =
+      PVE::Storage::ESXiPlugin::split_path($vmx_path)
+      or die "bad path '$vmx_path'\n";
+    $rel_path =~ s|/[^/]+$||;
+
+    return "$rel_dc/$rel_ds/$rel_path/$path";
+}
+
+# Imports happen by the volume id which is a path to a VMX file.
+# In order to find the vm's power state and disk capacity info, we need to find the
+# VM the vmx file belongs to.
+sub vm_for_vmx_path {
+    my ( $self, $vmx_path ) = @_;
+
+    my ( $dc_name, $ds_name, $path ) =
+      PVE::Storage::ESXiPlugin::split_path($vmx_path);
+    if ( my $dc = $self->{$dc_name} ) {
+        my $vms = $dc->{vms};
+        for my $vm_name ( keys %$vms ) {
+            my $vm       = $vms->{$vm_name};
+            my $cfg_info = $vm->{config};
+            if (   $cfg_info->{datastore} eq $ds_name
+                && $cfg_info->{path} eq $path )
+            {
+                return $vm;
+            }
+        }
+    }
+    return;
+}
+
+package PVE::Storage::ESXiPlugin::VMX;
+
+use strict;
+use warnings;
+use feature 'fc';
+
+# FIXME: see if vmx files can actually have escape sequences in their quoted values?
+my sub unquote : prototype($) {
+    my ($value) = @_;
+    $value =~ s/^\"(.*)\"$/$1/s
+      or $value =~ s/^\'(.*)\'$/$1/s;
+    return $value;
+}
+
+sub parse : prototype($$$$$$) {
+    my ( $class, $storeid, $scfg, $vmx_path, $vmxdata, $manifest ) = @_;
+
+    my $conf = {};
+
+    for my $line ( split( /\n/, $vmxdata ) ) {
+        $line         =~ s/^\s+//;
+        $line         =~ s/\s+$//;
+        next if $line !~ /^(\S+)\s*=\s*(.+)$/;
+        my ( $key, $value ) = ( $1, $2 );
+
+        $value = unquote($value);
+
+        $conf->{$key} = $value;
+    }
+
+    $conf->{'pve.storeid'}        = $storeid;
+    $conf->{'pve.storage.config'} = $scfg;
+    $conf->{'pve.vmx.path'}       = $vmx_path;
+    $conf->{'pve.manifest'}       = $manifest;
+
+    return bless $conf, $class;
+}
+
+sub storeid  { $_[0]->{'pve.storeid'} }
+sub scfg     { $_[0]->{'pve.storage.config'} }
+sub vmx_path { $_[0]->{'pve.vmx.path'} }
+sub manifest { $_[0]->{'pve.manifest'} }
+
+# (Also used for the fileName config key...)
+sub is_disk_entry : prototype($) {
+    my ($id) = @_;
+    if ( $id =~ /^(scsi|ide|sata|nvme)(\d+:\d+)(:?\.fileName)?$/ ) {
+        return ( $1, $2 );
+    }
+    return;
+}
+
+sub is_cdrom {
+    my ( $self, $bus, $slot ) = @_;
+    if ( my $type = $self->{"${bus}${slot}.deviceType"} ) {
+        return $type =~ /cdrom/;
+    }
+    return;
+}
+
+sub for_each_disk {
+    my ( $self, $code ) = @_;
+
+    for my $key ( sort keys %$self ) {
+        my ( $bus, $slot ) = is_disk_entry($key)
+          or next;
+        my $kind = $self->is_cdrom( $bus, $slot ) ? 'cdrom' : 'disk';
+
+        my $file = $self->{$key};
+
+        my ( $maj, $min ) = split( /:/, $slot, 2 );
+        my $vdev =
+          $self->{"${bus}${maj}.virtualDev"};    # may of course be undef...
+
+        $code->( $bus, $slot, $file, $vdev, $kind );
+    }
+
+    return;
+}
+
+sub for_each_netdev {
+    my ( $self, $code ) = @_;
+
+    my $found_devs = {};
+    for my $key ( keys %$self ) {
+        next if $key !~ /^ethernet(\d+)\.(.+)$/;
+        my ( $slot, $opt ) = ( $1, $2 );
+
+        my $dev = ( $found_devs->{$slot} //= {} );
+        $dev->{$opt} = $self->{$key};
+    }
+
+    for my $id ( sort keys %$found_devs ) {
+        my $dev = $found_devs->{$id};
+
+        next if ( $dev->{present} // '' ) ne 'TRUE';
+
+        my $ty  = $dev->{addressType};
+        my $mac = $dev->{address};
+        if ( $ty && fc($ty) eq fc('generated') ) {
+            $mac = $dev->{generatedAddress} // $mac;
+        }
+
+        $code->( $id, $dev, $mac );
+    }
+
+    return;
+}
+
+sub for_each_serial {
+    my ( $self, $code ) = @_;
+
+    my $found_serials = {};
+    for my $key ( sort keys %$self ) {
+        next if $key !~ /^serial(\d+)\.(.+)$/;
+        my ( $slot, $opt ) = ( $1, $2 );
+        my $serial = ( $found_serials->{$1} //= {} );
+        $serial->{$opt} = $self->{$key};
+    }
+
+    for my $id ( sort { $a <=> $b } keys %$found_serials ) {
+        my $serial = $found_serials->{$id};
+
+        next if ( $serial->{present} // '' ) ne 'TRUE';
+
+        $code->( $id, $serial );
+    }
+
+    return;
+}
+
+sub firmware {
+    my ($self) = @_;
+    my $fw = $self->{firmware};
+    return 'efi' if $fw && fc($fw) eq fc('efi');
+    return 'bios';
+}
+
+# This is in MB
+sub memory {
+    my ($self) = @_;
+
+    return $self->{memSize};
+}
+
+# CPU info is stored as a maximum ('numvcpus') and a core-per-socket count.
+# We return a (cores, sockets) tuple the way want it for PVE.
+sub cpu_info {
+    my ($self) = @_;
+
+    my $cps = int( $self->{'cpuid.coresPerSocket'} // 1 );
+    my $max = int( $self->{numvcpus}               // $cps );
+
+    return ( $cps, ( $max / $cps ) );
+}
+
+# FIXME: Test all possible values esxi creates?
+sub is_windows {
+    my ($self) = @_;
+
+    my $guest = $self->{guestOS} // return;
+    return 1 if $guest =~ /^win/i;
+    return;
+}
+
+my %guest_types_windows = (
+    dos                     => 'other',
+    winNetBusiness          => 'w2k3',
+    windows9                => 'win10',
+    'windows9-64'           => 'win10',
+    'windows11-64'          => 'win11',
+    'windows12-64'          => 'win11',    # FIXME / win12?
+    win2000AdvServ          => 'w2k',
+    win2000Pro              => 'w2k',
+    win2000Serv             => 'w2k',
+    win31                   => 'other',
+    windows7                => 'win7',
+    'windows7-64'           => 'win7',
+    windows8                => 'win8',
+    'windows8-64'           => 'win8',
+    win95                   => 'other',
+    win98                   => 'other',
+    winNT                   => 'wxp',      # ?
+    winNetEnterprise        => 'w2k3',
+    'winNetEnterprise-64'   => 'w2k3',
+    winNetDatacenter        => 'w2k3',
+    'winNetDatacenter-64'   => 'w2k3',
+    winNetStandard          => 'w2k3',
+    'winNetStandard-64'     => 'w2k3',
+    winNetWeb               => 'w2k3',
+    winLonghorn             => 'w2k8',
+    'winLonghorn-64'        => 'w2k8',
+    'windows7Server-64'     => 'w2k8',
+    'windows8Server-64'     => 'win8',
+    'windows9Server-64'     => 'win10',
+    'windows2019srv-64'     => 'win10',
+    'windows2019srvNext-64' => 'win11',
+    'windows2022srvNext-64' => 'win11',    # FIXME / win12?
+    winVista                => 'wvista',
+    'winVista-64'           => 'wvista',
+    winXPPro                => 'wxp',
+    'winXPPro-64'           => 'wxp',
+);
+
+my %guest_types_other = (
+    'freeBSD11'    => 'other',
+    'freeBSD11-64' => 'other',
+    'freeBSD12'    => 'other',
+    'freeBSD12-64' => 'other',
+    'freeBSD13'    => 'other',
+    'freeBSD13-64' => 'other',
+    'freeBSD14'    => 'other',
+    'freeBSD14-64' => 'other',
+    'freeBSD'      => 'other',
+    'freeBSD-64'   => 'other',
+    'os2'          => 'other',
+    'netware5'     => 'other',
+    'netware6'     => 'other',
+    'solaris10'    => 'solaris',
+    'solaris10-64' => 'solaris',
+    'solaris11-64' => 'solaris',
+    'other'        => 'other',
+    'other-64'     => 'other',
+    'openserver5'  => 'other',
+    'openserver6'  => 'other',
+    'unixware7'    => 'other',
+    'eComStation'  => 'other',
+    'eComStation2' => 'other',
+    'solaris8'     => 'solaris',
+    'solaris9'     => 'solaris',
+    'vmkernel'     => 'other',
+    'vmkernel5'    => 'other',
+    'vmkernel6'    => 'other',
+    'vmkernel65'   => 'other',
+    'vmkernel7'    => 'other',
+    'vmkernel8'    => 'other',
+);
+
+# Best effort translation from vmware guest os type to pve.
+# Returns a tuple: `(pve-type, is_windows)`
+sub guest_type {
+    my ($self) = @_;
+    if ( defined( my $guest = $self->{guestOS} ) ) {
+        if ( defined( my $known_windows = $guest_types_windows{$guest} ) ) {
+            return ( $known_windows, 1 );
+        }
+        elsif ( defined( my $known_other = $guest_types_other{$guest} ) ) {
+            return ( $known_other, 0 );
+        }
+
+        # This covers all the 'Mac OS' types AFAICT
+        return ( 'other', 0 ) if $guest =~ /^darwin/;
+    }
+
+    # otherwise we'll just go with l26 defaults because why not...
+    return ( 'l26', 0 );
+}
+
+sub smbios1_uuid {
+    my ($self) = @_;
+
+    my $uuid = $self->{'uuid.bios'};
+
+    return if !defined($uuid);
+
+    # vmware stores space separated bytes and has 1 dash in the middle...
+    $uuid =~ s/[^0-9a-fA-f]//g;
+
+    if (
+        $uuid =~ /^
+	([0-9a-fA-F]{8})
+	([0-9a-fA-F]{4})
+	([0-9a-fA-F]{4})
+	([0-9a-fA-F]{4})
+	([0-9a-fA-F]{12})
+	$/x
+      )
+    {
+        return "$1-$2-$3-$4-$5";
+    }
+    return;
+}
+
+# This builds arguments for the `create` api call for this config.
+sub get_create_args {
+    my ($self) = @_;
+
+    my $storeid  = $self->storeid;
+    my $manifest = $self->manifest;
+    my $vminfo   = $manifest->vm_for_vmx_path( $self->vmx_path );
+
+    my $create_args  = {};
+    my $create_disks = {};
+    my $create_net   = {};
+    my $warnings     = [];
+
+# NOTE: all types must be added to the return schema of the import-metadata API endpoint
+    my $warn = sub {
+        my ( $type, %properties ) = @_;
+        push @$warnings, { type => $type, %properties };
+    };
+
+    my ( $cores, $sockets ) = $self->cpu_info();
+    $create_args->{cores}   = $cores   if $cores != 1;
+    $create_args->{sockets} = $sockets if $sockets != 1;
+
+    my $firmware = $self->firmware;
+    if ( $firmware eq 'efi' ) {
+        $create_args->{bios}      = 'ovmf';
+        $create_disks->{efidisk0} = 1;
+        $warn->( 'efi-state-lost', key => "bios", value => "ovmf" );
+    }
+    else {
+        $create_args->{bios} = 'seabios';
+    }
+
+    my $memory = $self->memory;
+    $create_args->{memory} = $memory;
+
+    my $default_scsihw;
+    my $scsihw;
+    my $set_scsihw = sub {
+        if ( defined($scsihw) && $scsihw ne $_[0] ) {
+            warn "multiple different SCSI hardware types are not supported\n";
+            return;
+        }
+        $scsihw = $_[0];
+    };
+
+    my ( $ostype, $is_windows ) = $self->guest_type();
+    $create_args->{ostype} //= $ostype if defined($ostype);
+    if ( $ostype eq 'l26' ) {
+        $default_scsihw = 'virtio-scsi-single';
+    }
+
+    $self->for_each_netdev(
+        sub {
+            my ( $id, $dev, $mac ) = @_;
+            $mac //= '';
+            my $model = $dev->{virtualDev} // 'vmxnet3';
+
+            my $param = { model => $model };
+            $param->{macaddr}       = $mac if length($mac);
+            $create_net->{"net$id"} = $param;
+        }
+    );
+
+    my %counts = ( scsi => 0, sata => 0, ide => 0 );
+
+    my $mntdir = PVE::Storage::ESXiPlugin::mount_dir($storeid);
+
+    my $boot_order = '';
+
+    # we deal with nvme disks in a 2nd go-around since we currently don't
+    # support nvme disks and instead just add them as additional scsi
+    # disks.
+    my @nvmes;
+    my $add_disk = sub {
+        my ( $bus, $slot, $file, $devtype, $kind, $do_nvmes ) = @_;
+
+        my $vmbus = $bus;
+        if ($do_nvmes) {
+            $bus = 'scsi';
+        }
+        elsif ( $bus eq 'nvme' ) {
+            push @nvmes, [ $slot, $file, $devtype, $kind ];
+            return;
+        }
+
+        my $path =
+          eval { $manifest->resolve_path_relative_to( $self->vmx_path, $file ) };
+        return if !defined($path);
+
+        # my $fullpath = "$mntdir/$path";
+        # return if !-e $fullpath;
+
+        if ($devtype) {
+            if ( $devtype =~ /^lsi/i ) {
+                $set_scsihw->('lsi');
+            }
+            elsif ( $devtype eq 'pvscsi' ) {
+                $set_scsihw->('pvscsi');    # same name in pve
+            }
+        }
+
+        my $disk_capacity;
+        if ( defined( my $diskinfo = $vminfo->{disks} ) ) {
+            my ( $dc, $ds, $rel_path ) =
+              PVE::Storage::ESXiPlugin::split_path($path);
+            for my $disk ( $diskinfo->@* ) {
+                if ( $disk->{datastore} eq $ds && $disk->{path} eq $rel_path ) {
+                    $disk_capacity = $disk->{capacity};
+                    last;
+                }
+            }
+        }
+
+        my $count = $counts{$bus}++;
+        if ( $kind eq 'cdrom' ) {
+
+          # We currently do not pass cdroms through via the esxi storage.
+          # Users should adapt import these from the storages directly/manually.
+            $create_args->{"${bus}${count}"} = "none,media=cdrom";
+
+            # CD-ROM image will not get imported
+            $warn->(
+                'cdrom-image-ignored',
+                key   => "${bus}${count}",
+                value => "$storeid:$path"
+            );
+        }
+        else {
+            $create_disks->{"${bus}${count}"} = {
+                volid => "$storeid:$path",
+                defined($disk_capacity) ? ( size => $disk_capacity ) : (),
+            };
+        }
+
+        $boot_order .= ';' if length($boot_order);
+        $boot_order .= $bus . $count;
+    };
+    $self->for_each_disk($add_disk);
+    if (@nvmes) {
+        for my $nvme (@nvmes) {
+            my ( $slot, $file, $devtype, $kind ) = @$nvme;
+            $warn->(
+                'nvme-unsupported',
+                key   => "nvme${slot}",
+                value => "$file"
+            );
+            $add_disk->( 'scsi', $slot, $file, $devtype, $kind, 1 );
+        }
+    }
+
+    $scsihw //= $default_scsihw;
+    if ( $firmware eq 'efi' ) {
+        if ( !defined($scsihw) || $scsihw =~ /^lsi/ ) {
+            if ($is_windows) {
+                $scsihw = 'pvscsi';
+            }
+            else {
+                $scsihw = 'virtio-scsi-single';
+            }
+
+           # OVMF is built without LSI drivers, scsi hardware was set to $scsihw
+            $warn->(
+                'ovmf-with-lsi-unsupported',
+                key   => 'scsihw',
+                value => "$scsihw"
+            );
+        }
+    }
+    $create_args->{scsihw} = $scsihw if defined($scsihw);
+
+    $create_args->{boot} = "order=$boot_order";
+
+    if ( defined( my $smbios1_uuid = $self->smbios1_uuid() ) ) {
+        $create_args->{smbios1} = "uuid=$smbios1_uuid";
+    }
+
+    if ( defined( my $name = $self->{displayName} ) ) {
+
+        # name in pve is a 'dns-name', so... clean it
+        $name =~ s/\s/-/g;
+        $name =~ s/[^a-zA-Z0-9\-.]//g;
+        $name =~ s/^[.-]+//;
+        $name =~ s/[.-]+$//;
+        $create_args->{name} = $name if length($name);
+    }
+
+    my $serid = 0;
+    $self->for_each_serial(
+        sub {
+            my ( $id, $serial ) = @_;
+
+            # currently we only support 'socket' type serials anyway
+            $warn->( 'serial-port-socket-only', key => "serial$serid" );
+            $create_args->{"serial$serid"} = 'socket';
+            ++$serid;
+        }
+    );
+
+    $warn->('guest-is-running')
+      if defined($vminfo) && ( $vminfo->{power} // '' ) ne 'poweredOff';
+
+    return {
+        type          => 'vm',
+        source        => 'esxi',
+        'create-args' => $create_args,
+        disks         => $create_disks,
+        net           => $create_net,
+        warnings      => $warnings,
+    };
+}
+
+1;
-- 
2.43.0





More information about the pve-devel mailing list