to ensure that the next access actually uses the updated config for accessing the remote ESXi, for both manifest API and FUSE mount. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
1114 lines
27 KiB
Perl
1114 lines
27 KiB
Perl
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 = (
|
|
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',
|
|
);
|
|
|
|
# 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);
|
|
}
|
|
# 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;
|
|
} 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;
|
|
|
|
$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;
|