Skip to content

Instantly share code, notes, and snippets.

@lucanastasio
Last active August 11, 2023 22:17
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lucanastasio/03618be000df7969726d932f395f63e3 to your computer and use it in GitHub Desktop.
Save lucanastasio/03618be000df7969726d932f395f63e3 to your computer and use it in GitHub Desktop.
Proxmox passthrough saver hook script
#!/usr/bin/perl
# Copyright (c) 2021 Luca Anastasio
# anastasio<dot>lu<at>gmail<dot>com
# Hookscript to avoid taking PCI passed through devices from running VMs
# also attaches them to their placeholders after guests are stopped
# You can set this via qm with
# qm set 100 --hookscript local:snippets/hookscript.pl
use strict;
use warnings;
my $dryRun = 0;
print "GUEST HOOK: " . join(' ', @ARGV). "\n";
my $vmid = shift;
my $phase = shift;
my $placeholder = "gpu-placeholder-vm-";
sub get_vm_pci {
my @retstr = qx(qm config $_[0] --current | grep hostpci | awk '{print substr(\$2,1,5)}');
chomp(@retstr);
return @retstr;
}
sub get_vm_name {
my $retstr = qx(qm config $_[0] | grep name | awk '{print \$2}');
chomp($retstr);
return $retstr;
}
sub get_vm_run {
my @retstr = qx(qm list | grep running | awk '{print \$1}');
chomp(@retstr);
return @retstr;
}
sub get_vm_hold {
my @retstr = qx(qm list | grep $placeholder | grep stopped | awk '{print \$1}');
chomp(@retstr);
return @retstr;
}
sub get_pci_id {
my $retstr = qx(lspci -s $_[0].0 -n | awk '{print \$3}');
chomp($retstr);
return $retstr;
}
sub get_pci_from_id {
my @retstr = qx(lspci -n | grep $_[0] | awk '{print substr(\$1,1,5)}');
chomp(@retstr);
return @retstr;
}
sub replace_vm_pci {
# old PCI, new PCI
my $orig = qx(qm config $vmid | grep \": $_[0],\");
my $hostpci = substr($orig, 0, index($orig, ':'));
my $newconf = substr($orig, index($orig, ' ')+1);
$newconf =~ s/$_[0]/$_[1]/g;
my $cmd = "rm /var/lock/qemu-server/lock-$vmid.conf && qm set $vmid --$hostpci $newconf";
print("Changing configuration with command: $cmd\n");
my $ret = 0;
$ret = system($cmd) if ($dryRun == 0);
if ($ret != 0) {
print("Error editing configuration.\n");
exit($ret);
}
}
if ($phase eq 'pre-start') {
my $start_vm = 1;
print("VM $vmid pre-start check.\n");
# get PCI devices needed by the desired VM
my @vm_pci = get_vm_pci($vmid);
# only execute if that VM actually needs PCI devices
if (scalar(@vm_pci) > 0) {
print("VM $vmid requires PCI devices: ", join(" ",@vm_pci), "\n");
# get running VMs list
my @vm_run = get_vm_run();
if (scalar(@vm_run) > 0) {
my @vm_to_stop = ();
print("Running VMs: ", join(" ",@vm_run), "\n");
# iterate over each required PCI device
for my $vm_pci_i (@vm_pci) {
print("Searching for VMs using PCI device $vm_pci_i.\n");
for my $vm_run_i (@vm_run) {
my $pci_found = 0;
# get running VM PCI devices and name
my @vm_run_pci = get_vm_pci($vm_run_i);
if (scalar(@vm_run_pci) > 0) {
my $vm_run_name = get_vm_name($vm_run_i);
print("VM $vm_run_i \"$vm_run_name\" is using PCI devices: ", join(" ",@vm_run_pci), "\n");
for my $vm_run_pci_i (@vm_run_pci) {
if ($vm_run_pci_i eq $vm_pci_i) {
if (index($vm_run_name, $placeholder) != -1) {
print("Placeholder VM $vm_run_i will be stopped.\n");
push(@vm_to_stop, $vm_run_i);
$pci_found = 1;
last;
}
else {
print("PCI device $vm_run_pci_i required by VM $vm_run_i \"$vm_run_name\", searching for alternatives.\n");
my $pci_id = get_pci_id($vm_pci_i);
my @pci_alt = get_pci_from_id($pci_id);
my $alt_found = 0;
if (scalar(@pci_alt) > 1) {
for my $pci_alt_i (@pci_alt) {
my $same_as_req = 0;
for my $vm_pci_alt_i (@vm_pci) {
$same_as_req = ($vm_pci_alt_i eq $pci_alt_i);
last if ($same_as_req);
}
if (not $same_as_req) {
print("Found alternative PCI device $pci_alt_i, searching for VMs using it.\n");
my $alt_stop = 0;
for my $vm_alt_i (@vm_run) {
if ($vm_alt_i ne $vm_run_i) {
my @vm_alt_pci = get_vm_pci($vm_alt_i);
my $vm_alt_name = get_vm_name($vm_alt_i);
print("VM $vm_alt_i \"$vm_alt_name\" is using PCI devices: ", join(" ",@vm_alt_pci), "\n");
for my $vm_alt_pci_i (@vm_alt_pci) {
if ($vm_alt_pci_i eq $pci_alt_i) {
if (index($vm_alt_name, $placeholder) != -1) {
print("Alternative available, placeholder VM $vm_alt_i will be stopped.\n");
replace_vm_pci($vm_pci_i, $pci_alt_i);
push(@vm_to_stop, $vm_alt_i);
$alt_found = 1;
$pci_found = 1;
$start_vm = 0;
last;
}
else {
print("Alternative PCI device $pci_alt_i not available, in use by VM $vm_alt_i.\n");
$alt_stop = 1;
last;
}
}
}
}
last if ($alt_stop == 1 || $alt_found == 1);
}
if ($alt_found == 0 && $alt_stop == 0) {
print("Alternative available, PCI device $pci_alt_i not in use.\n");
replace_vm_pci($vm_pci_i, $pci_alt_i);
$alt_found = 1;
$pci_found = 1;
$start_vm = 0;
}
}
last if ($alt_found == 1);
}
}
if ($alt_found == 0) {
print("No alternatives found for PCI device $vm_run_pci_i, aborting.\n");
exit(1);
}
}
}
else {
print("PCI device $vm_run_pci_i skipped.\n");
}
}
}
else {
print("VM $vm_run_i is not using PCI devices.\n");
}
last if ($pci_found == 1);
}
}
if (scalar(@vm_to_stop) > 0) {
for my $vm_to_stop_i (@vm_to_stop) {
my $cmd = "qm stop $vm_to_stop_i --skiplock";
print("Stopping placeholder VM $vm_to_stop_i with command: $cmd\n");
my $ret = 0;
$ret = system($cmd) if ($dryRun == 0);
if ($ret != 0) {
print("Error $ret stopping placeholder VM $vm_to_stop_i, aborting.\n");
exit($ret);
}
}
}
else {
print("No VMs to be stopped.\n");
}
}
else {
print("No running VMs, starting normally.\n");
}
}
else {
print("VM $vmid does not require PCI devices.\n")
}
if ($start_vm == 1) {
print("Starting VM $vmid.\n");
}
else {
print("Configuration changed, cannot start immediately, please start again.\n");
exit(1);
}
}
elsif ($phase eq 'post-start') {
print "VM $vmid started successfully.\n";
}
elsif ($phase eq 'pre-stop') {
print "VM $vmid will be stopped.\n";
}
elsif ($phase eq 'post-stop') {
print("VM $vmid post-stop check.\n");
my $vm_name = get_vm_name($vmid);
if (index($vm_name, $placeholder) != -1) {
print("Placeholder VM $vmid stopped successfully.\n");
}
else {
my @vm_pci = get_vm_pci($vmid);
if (scalar(@vm_pci) > 0) {
print("VM $vmid was using PCI devices: ", join(" ",@vm_pci), "\n");
my @holder = get_vm_hold();
my @vm_to_start = ();
print("Available placeholders: ", join(" ",@holder), "\n");
for my $vm_pci_i (@vm_pci) {
print("Searching for PCI device $vm_pci_i placeholder.\n");
for my $holder_i (@holder) {
my @holder_pci = get_vm_pci($holder_i);
if ($holder_pci[0] eq $vm_pci_i) {
print("Placeholder VM $holder_i will be started.\n");
push(@vm_to_start, $holder_i);
last;
}
}
}
if (scalar(@vm_to_start) > 0) {
for my $vm_to_start_i (@vm_to_start) {
my $cmd = "qm start $vm_to_start_i --skiplock";
print("Starting placeholder VM $vm_to_start_i with command: $cmd\n");
my $ret = 0;
$ret = system($cmd) if ($dryRun == 0);
if ($ret != 0) {
print("Error $ret starting placeholder VM $vm_to_start_i.\n");
exit($ret);
}
else {
print("Placeholder VM $vm_to_start_i started successfully.\n");
}
}
}
if (scalar(@vm_to_start) < scalar(@vm_pci)) {
print("Error: number of placeholders found does not match number of PCI devices.\n");
exit(1);
}
}
else {
print("VM $vmid was not using PCI devices.\n");
}
}
} else {
die "Got unknown phase '$phase'\n";
}
exit(0);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment