]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
usb: Fix local resource check of Spice USB devices
[qemu-server.git] / PVE / QemuServer.pm
index 568dc5c6f1e9227a246dbd7a76343922f2a42fdd..33bf966737ab2748f68c97fccf42181f3d57a6f0 100644 (file)
@@ -33,7 +33,7 @@ use PVE::QemuConfig;
 use PVE::QMPClient;
 use PVE::RPCEnvironment;
 use PVE::GuestHelpers;
-use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr);
+use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port);
 use PVE::QemuServer::Memory;
 use PVE::QemuServer::USB qw(parse_usb_device);
 use PVE::QemuServer::Cloudinit;
@@ -167,7 +167,21 @@ my $cpu_vendor_list = {
     max => 'default',
 };
 
-my $cpu_flag = qr/[+-](pcid|spec-ctrl|ibpb|ssbd|virt-ssbd|amd-ssbd|amd-no-ssb|pdpe1gb|md-clear)/;
+my @supported_cpu_flags = (
+    'pcid',
+    'spec-ctrl',
+    'ibpb',
+    'ssbd',
+    'virt-ssbd',
+    'amd-ssbd',
+    'amd-no-ssb',
+    'pdpe1gb',
+    'md-clear',
+    'hv-tlbflush',
+    'hv-evmcs',
+    'aes'
+);
+my $cpu_flag = qr/[+-](@{[join('|', @supported_cpu_flags)]})/;
 
 my $cpu_fmt = {
     cputype => {
@@ -193,7 +207,7 @@ my $cpu_fmt = {
     flags => {
        description => "List of additional CPU flags separated by ';'."
                     . " Use '+FLAG' to enable, '-FLAG' to disable a flag."
-                    . " Currently supported flags: 'pcid', 'spec-ctrl', 'ibpb', 'ssbd', 'virt-ssbd', 'amd-ssbd', 'amd-no-ssb', 'pdpe1gb', 'md-clear'.",
+                    . " Currently supported flags: @{[join(', ', @supported_cpu_flags)]}.",
        format_description => '+FLAG[;-FLAG...]',
        type => 'string',
        pattern => qr/$cpu_flag(;$cpu_flag)*/,
@@ -282,6 +296,22 @@ my $audio_fmt = {
     },
 };
 
+my $spice_enhancements_fmt = {
+    foldersharing => {
+       type => 'boolean',
+       optional => 1,
+       default => '0',
+       description =>  "Enable folder sharing via SPICE. Needs Spice-WebDAV daemon installed in the VM."
+    },
+    videostreaming =>  {
+       type => 'string',
+       enum => ['off', 'all', 'filter'],
+       default => 'off',
+       optional => 1,
+       description => "Enable video streaming. Uses compression for detected video streams."
+    },
+};
+
 my $confdesc = {
     onboot => {
        optional => 1,
@@ -659,6 +689,12 @@ EODESCR
        description => "Configure a audio device, useful in combination with QXL/Spice.",
        optional => 1
     },
+    spice_enhancements => {
+       type => 'string',
+       format => $spice_enhancements_fmt,
+       description => "Configure additional enhancements for SPICE.",
+       optional => 1
+    },
 };
 
 my $cicustom_fmt = {
@@ -755,7 +791,7 @@ my $MAX_SATA_DISKS = 6;
 my $MAX_USB_DEVICES = 5;
 my $MAX_NETS = 32;
 my $MAX_UNUSED_DISKS = 256;
-my $MAX_HOSTPCI_DEVICES = 4;
+my $MAX_HOSTPCI_DEVICES = 16;
 my $MAX_SERIAL_PORTS = 4;
 my $MAX_PARALLEL_PORTS = 3;
 my $MAX_NUMA = 8;
@@ -1458,25 +1494,33 @@ sub kvm_version {
     return $kvm_api_version;
 }
 
-my $kvm_user_version;
+my $kvm_user_version = {};
+my $kvm_mtime = {};
 
 sub kvm_user_version {
+    my ($binary) = @_;
 
-    return $kvm_user_version if $kvm_user_version;
+    $binary //= get_command_for_arch(get_host_arch()); # get the native arch by default
+    my $st = stat($binary);
 
-    $kvm_user_version = 'unknown';
+    my $cachedmtime = $kvm_mtime->{$binary} // -1;
+    return $kvm_user_version->{$binary} if $kvm_user_version->{$binary} &&
+       $cachedmtime == $st->mtime;
+
+    $kvm_user_version->{$binary} = 'unknown';
+    $kvm_mtime->{$binary} = $st->mtime;
 
     my $code = sub {
        my $line = shift;
        if ($line =~ m/^QEMU( PC)? emulator version (\d+\.\d+(\.\d+)?)(\.\d+)?[,\s]/) {
-           $kvm_user_version = $2;
+           $kvm_user_version->{$binary} = $2;
        }
     };
 
-    eval { run_command("kvm -version", outfunc => $code); };
+    eval { run_command([$binary, '--version'], outfunc => $code); };
     warn $@ if $@;
 
-    return $kvm_user_version;
+    return $kvm_user_version->{$binary};
 
 }
 
@@ -2881,7 +2925,7 @@ sub check_local_resources {
     push @loc_res, "ivshmem" if $conf->{ivshmem};
 
     foreach my $k (keys %$conf) {
-       next if $k =~ m/^usb/ && ($conf->{$k} eq 'spice');
+       next if $k =~ m/^usb/ && ($conf->{$k} =~ m/^spice(?![^,])/);
        # sockets are safe: they will recreated be on the target side post-migrate
        next if $k =~ m/^serial/ && ($conf->{$k} eq 'socket');
        push @loc_res, $k if $k =~ m/^(usb|hostpci|serial|parallel)\d+$/;
@@ -3413,6 +3457,24 @@ sub conf_has_serial {
     return 0;
 }
 
+sub conf_has_audio {
+    my ($conf, $id) = @_;
+
+    $id //= 0;
+    my $audio = $conf->{"audio$id"};
+    return undef if !defined($audio);
+
+    my $audioproperties = PVE::JSONSchema::parse_property_string($audio_fmt, $audio);
+    my $audiodriver = $audioproperties->{driver} // 'spice';
+
+    return {
+       dev => $audioproperties->{device},
+       dev_id => "audiodev$id",
+       backend => $audiodriver,
+       backend_id => "$audiodriver-backend${id}",
+    };
+}
+
 sub vga_conf_has_spice {
     my ($vga) = @_;
 
@@ -3535,13 +3597,14 @@ sub config_to_command {
     my $devices = [];
     my $pciaddr = '';
     my $bridges = {};
-    my $kvmver = kvm_user_version();
     my $vernum = 0; # unknown
     my $ostype = $conf->{ostype};
     my $winversion = windows_version($ostype);
     my $kvm = $conf->{kvm};
 
     my ($arch, $machine_type) = get_basic_machine_info($conf, $forcemachine);
+    my $kvm_binary = get_command_for_arch($arch);
+    my $kvmver = kvm_user_version($kvm_binary);
     $kvm //= 1 if is_native($arch);
 
     if ($kvm) {
@@ -3567,7 +3630,7 @@ sub config_to_command {
     my $cpuunits = defined($conf->{cpuunits}) ?
             $conf->{cpuunits} : $defaults->{cpuunits};
 
-    push @$cmd, get_command_for_arch($arch);
+    push @$cmd, $kvm_binary;
 
     push @$cmd, '-id', $vmid;
 
@@ -3698,67 +3761,66 @@ sub config_to_command {
 
     # host pci devices
     for (my $i = 0; $i < $MAX_HOSTPCI_DEVICES; $i++)  {
-       my $d = parse_hostpci($conf->{"hostpci$i"});
+       my $id = "hostpci$i";
+       my $d = parse_hostpci($conf->{$id});
        next if !$d;
 
-       my $pcie = $d->{pcie};
-       if ($pcie) {
+       if (my $pcie = $d->{pcie}) {
            die "q35 machine model is not enabled" if !$q35;
            # win7 wants to have the pcie devices directly on the pcie bus
            # instead of in the root port
            if ($winversion == 7) {
-               $pciaddr = print_pcie_addr("hostpci${i}bus0");
+               $pciaddr = print_pcie_addr("${id}bus0");
            } else {
-               $pciaddr = print_pcie_addr("hostpci$i");
+               # add more root ports if needed, 4 are present by default
+               # by pve-q35 cfgs, rest added here on demand.
+               if ($i > 3) {
+                   push @$devices, '-device', print_pcie_root_port($i);
+               }
+               $pciaddr = print_pcie_addr($id);
            }
        } else {
-           $pciaddr = print_pci_addr("hostpci$i", $bridges, $arch, $machine_type);
+           $pciaddr = print_pci_addr($id, $bridges, $arch, $machine_type);
        }
 
-       my $rombar = defined($d->{rombar}) && !$d->{rombar} ? ',rombar=0' : '';
-       my $romfile = $d->{romfile};
-
        my $xvga = '';
        if ($d->{'x-vga'}) {
-           $xvga = ',x-vga=on';
+           $xvga = ',x-vga=on' if !($conf->{bios} && $conf->{bios} eq 'ovmf');
            $kvm_off = 1;
            $vga->{type} = 'none' if !defined($conf->{vga});
            $gpu_passthrough = 1;
-
-           if ($conf->{bios} && $conf->{bios} eq 'ovmf') {
-               $xvga = "";
-           }
        }
+
        my $pcidevices = $d->{pciid};
        my $multifunction = 1 if @$pcidevices > 1;
+
        my $sysfspath;
        if ($d->{mdev} && scalar(@$pcidevices) == 1) {
-           my $id = $pcidevices->[0]->{id};
+           my $pci_id = $pcidevices->[0]->{id};
            my $uuid = PVE::SysFSTools::generate_mdev_uuid($vmid, $i);
-           $sysfspath = "/sys/bus/pci/devices/0000:$id/$uuid";
+           $sysfspath = "/sys/bus/pci/devices/0000:$pci_id/$uuid";
        } elsif ($d->{mdev}) {
-           warn "ignoring mediated device with multifunction device\n";
+           warn "ignoring mediated device '$id' with multifunction device\n";
        }
 
        my $j=0;
-        foreach my $pcidevice (@$pcidevices) {
-
-           my $id = "hostpci$i";
-           $id .= ".$j" if $multifunction;
-           my $addr = $pciaddr;
-           $addr .= ".$j" if $multifunction;
+       foreach my $pcidevice (@$pcidevices) {
            my $devicestr = "vfio-pci";
+
            if ($sysfspath) {
                $devicestr .= ",sysfsdev=$sysfspath";
            } else {
                $devicestr .= ",host=$pcidevice->{id}";
            }
-           $devicestr .= ",id=$id$addr";
 
-           if($j == 0){
-               $devicestr .= "$rombar$xvga";
+           my $mf_addr = $multifunction ? ".$j" : '';
+           $devicestr .= ",id=${id}${mf_addr}${pciaddr}${mf_addr}";
+
+           if ($j == 0) {
+               $devicestr .= ',rombar=0' if defined($d->{rombar}) && !$d->{rombar};
+               $devicestr .= "$xvga";
                $devicestr .= ",multifunction=on" if $multifunction;
-               $devicestr .= ",romfile=/usr/share/kvm/$romfile" if $romfile;
+               $devicestr .= ",romfile=/usr/share/kvm/$d->{romfile}" if $d->{romfile};
            }
 
            push @$devices, '-device', $devicestr;
@@ -3767,7 +3829,10 @@ sub config_to_command {
     }
 
     # usb devices
-    my @usbdevices = PVE::QemuServer::USB::get_usb_devices($conf, $usbdesc->{format}, $MAX_USB_DEVICES);
+    my $usb_dev_features = {};
+    $usb_dev_features->{spice_usb3} = 1 if qemu_machine_feature_enabled($machine_type, $kvmver, 4, 1);
+
+    my @usbdevices = PVE::QemuServer::USB::get_usb_devices($conf, $usbdesc->{format}, $MAX_USB_DEVICES, $usb_dev_features);
     push @$devices, @usbdevices if @usbdevices;
     # serial devices
     for (my $i = 0; $i < $MAX_SERIAL_PORTS; $i++)  {
@@ -3801,23 +3866,22 @@ sub config_to_command {
        }
     }
 
-    if ($conf->{audio0}) {
-       my $audioproperties = PVE::JSONSchema::parse_property_string($audio_fmt, $conf->{audio0});
-       my $audiodevice = $audioproperties->{device};
-       my $audiodriver = $audioproperties->{driver} // 'spice';
+    if (my $audio = conf_has_audio($conf)) {
+
        my $audiopciaddr = print_pci_addr("audio0", $bridges, $arch, $machine_type);
 
-       if ($audiodevice eq 'AC97') {
-           push @$devices, '-device', "AC97,id=sound0${audiopciaddr}";
-       } elsif ($audiodevice =~ /intel\-hda$/) {
-           push @$devices, '-device', "${audiodevice},id=sound5${audiopciaddr}";
-           push @$devices, '-device', "hda-micro,id=sound5-codec0,bus=sound5.0,cad=0";
-           push @$devices, '-device', "hda-duplex,id=sound5-codec1,bus=sound5.0,cad=1";
+       my $id = $audio->{dev_id};
+       if ($audio->{dev} eq 'AC97') {
+           push @$devices, '-device', "AC97,id=${id}${audiopciaddr}";
+       } elsif ($audio->{dev} =~ /intel\-hda$/) {
+           push @$devices, '-device', "$audio->{dev},id=${id}${audiopciaddr}";
+           push @$devices, '-device', "hda-micro,id=${id}-codec0,bus=${id}.0,cad=0";
+           push @$devices, '-device', "hda-duplex,id=${id}-codec1,bus=${id}.0,cad=1";
        } else {
-           die "unkown audio device '$audiodevice', implement me!";
+           die "unkown audio device '$audio->{dev}', implement me!";
        }
 
-       push @$devices, '-audiodev', "${audiodriver},id=${audiodriver}-driver";
+       push @$devices, '-audiodev', "$audio->{backend},id=$audio->{backend_id}";
     }
 
     my $sockets = 1;
@@ -3953,11 +4017,20 @@ sub config_to_command {
        my $localhost = PVE::Network::addr_to_ip($nodeaddrs[0]->{addr});
        $spice_port = PVE::Tools::next_spice_port($pfamily, $localhost);
 
-       push @$devices, '-spice', "tls-port=${spice_port},addr=$localhost,tls-ciphers=HIGH,seamless-migration=on";
+       my $spice_enhancement = PVE::JSONSchema::parse_property_string($spice_enhancements_fmt, $conf->{spice_enhancements} // '');
+       if ($spice_enhancement->{foldersharing}) {
+           push @$devices, '-chardev', "spiceport,id=foldershare,name=org.spice-space.webdav.0";
+           push @$devices, '-device', "virtserialport,chardev=foldershare,name=org.spice-space.webdav.0";
+       }
+
+       my $spice_opts = "tls-port=${spice_port},addr=$localhost,tls-ciphers=HIGH,seamless-migration=on";
+       $spice_opts .= ",streaming-video=$spice_enhancement->{videostreaming}" if $spice_enhancement->{videostreaming};
+       push @$devices, '-spice', "$spice_opts";
 
        push @$devices, '-device', "virtio-serial,id=spice$pciaddr";
        push @$devices, '-chardev', "spicevmc,id=vdagent,name=vdagent";
        push @$devices, '-device', "virtserialport,chardev=vdagent,name=com.redhat.spice.0";
+
     }
 
     # enable balloon by default, unless explicitly disabled
@@ -5270,6 +5343,10 @@ sub vm_start {
 
        die "VM $vmid already running\n" if check_running($vmid, undef, $migratedfrom);
 
+       # clean up leftover reboot request files
+       eval { clear_reboot_request($vmid); };
+       warn $@ if $@;
+
        if (!$statefile && scalar(keys %{$conf->{pending}})) {
            vmconfig_apply_pending($vmid, $conf, $storecfg);
            $conf = PVE::QemuConfig->load_config($vmid); # update/reload
@@ -5694,85 +5771,42 @@ sub vm_stop_cleanup {
     warn $@ if $@; # avoid errors - just warn
 }
 
-# Note: use $nockeck to skip tests if VM configuration file exists.
-# We need that when migration VMs to other nodes (files already moved)
-# Note: we set $keepActive in vzdump stop mode - volumes need to stay active
-sub vm_stop {
-    my ($storecfg, $vmid, $skiplock, $nocheck, $timeout, $shutdown, $force, $keepActive, $migratedfrom) = @_;
-
-    $force = 1 if !defined($force) && !$shutdown;
-
-    if ($migratedfrom){
-       my $pid = check_running($vmid, $nocheck, $migratedfrom);
-       kill 15, $pid if $pid;
-       my $conf = PVE::QemuConfig->load_config($vmid, $migratedfrom);
-       vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 0);
-       return;
-    }
-
-    PVE::QemuConfig->lock_config($vmid, sub {
+# call only in locked context
+sub _do_vm_stop {
+    my ($storecfg, $vmid, $skiplock, $nocheck, $timeout, $shutdown, $force, $keepActive) = @_;
 
-       my $pid = check_running($vmid, $nocheck);
-       return if !$pid;
+    my $pid = check_running($vmid, $nocheck);
+    return if !$pid;
 
-       my $conf;
-       if (!$nocheck) {
-           $conf = PVE::QemuConfig->load_config($vmid);
-           PVE::QemuConfig->check_lock($conf) if !$skiplock;
-           if (!defined($timeout) && $shutdown && $conf->{startup}) {
-               my $opts = PVE::JSONSchema::pve_parse_startup_order($conf->{startup});
-               $timeout = $opts->{down} if $opts->{down};
-           }
-           PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-stop');
+    my $conf;
+    if (!$nocheck) {
+       $conf = PVE::QemuConfig->load_config($vmid);
+       PVE::QemuConfig->check_lock($conf) if !$skiplock;
+       if (!defined($timeout) && $shutdown && $conf->{startup}) {
+           my $opts = PVE::JSONSchema::pve_parse_startup_order($conf->{startup});
+           $timeout = $opts->{down} if $opts->{down};
        }
+       PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'pre-stop');
+    }
 
-       eval {
-           if ($shutdown) {
-               if (defined($conf) && parse_guest_agent($conf)->{enabled}) {
-                   vm_qmp_command($vmid, {
+    eval {
+       if ($shutdown) {
+           if (defined($conf) && parse_guest_agent($conf)->{enabled}) {
+               vm_qmp_command($vmid, {
                        execute => "guest-shutdown",
                        arguments => { timeout => $timeout }
                    }, $nocheck);
-               } else {
-                   vm_qmp_command($vmid, { execute => "system_powerdown" }, $nocheck);
-               }
            } else {
-               vm_qmp_command($vmid, { execute => "quit" }, $nocheck);
-           }
-       };
-       my $err = $@;
-
-       if (!$err) {
-           $timeout = 60 if !defined($timeout);
-
-           my $count = 0;
-           while (($count < $timeout) && check_running($vmid, $nocheck)) {
-               $count++;
-               sleep 1;
-           }
-
-           if ($count >= $timeout) {
-               if ($force) {
-                   warn "VM still running - terminating now with SIGTERM\n";
-                   kill 15, $pid;
-               } else {
-                   die "VM quit/powerdown failed - got timeout\n";
-               }
-           } else {
-               vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 1) if $conf;
-               return;
+               vm_qmp_command($vmid, { execute => "system_powerdown" }, $nocheck);
            }
        } else {
-           if ($force) {
-               warn "VM quit/powerdown failed - terminating now with SIGTERM\n";
-               kill 15, $pid;
-           } else {
-               die "VM quit/powerdown failed\n";
-           }
+           vm_qmp_command($vmid, { execute => "quit" }, $nocheck);
        }
+    };
+    my $err = $@;
 
-       # wait again
-       $timeout = 10;
+    if (!$err) {
+       $timeout = 60 if !defined($timeout);
 
        my $count = 0;
        while (($count < $timeout) && check_running($vmid, $nocheck)) {
@@ -5781,12 +5815,77 @@ sub vm_stop {
        }
 
        if ($count >= $timeout) {
-           warn "VM still running - terminating now with SIGKILL\n";
-           kill 9, $pid;
-           sleep 1;
+           if ($force) {
+               warn "VM still running - terminating now with SIGTERM\n";
+               kill 15, $pid;
+           } else {
+               die "VM quit/powerdown failed - got timeout\n";
+           }
+       } else {
+           vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 1) if $conf;
+           return;
+       }
+    } else {
+       if ($force) {
+           warn "VM quit/powerdown failed - terminating now with SIGTERM\n";
+           kill 15, $pid;
+       } else {
+           die "VM quit/powerdown failed\n";
        }
+    }
+
+    # wait again
+    $timeout = 10;
+
+    my $count = 0;
+    while (($count < $timeout) && check_running($vmid, $nocheck)) {
+       $count++;
+       sleep 1;
+    }
+
+    if ($count >= $timeout) {
+       warn "VM still running - terminating now with SIGKILL\n";
+       kill 9, $pid;
+       sleep 1;
+    }
+
+    vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 1) if $conf;
+}
+
+# Note: use $nocheck to skip tests if VM configuration file exists.
+# We need that when migration VMs to other nodes (files already moved)
+# Note: we set $keepActive in vzdump stop mode - volumes need to stay active
+sub vm_stop {
+    my ($storecfg, $vmid, $skiplock, $nocheck, $timeout, $shutdown, $force, $keepActive, $migratedfrom) = @_;
+
+    $force = 1 if !defined($force) && !$shutdown;
+
+    if ($migratedfrom){
+       my $pid = check_running($vmid, $nocheck, $migratedfrom);
+       kill 15, $pid if $pid;
+       my $conf = PVE::QemuConfig->load_config($vmid, $migratedfrom);
+       vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 0);
+       return;
+    }
+
+    PVE::QemuConfig->lock_config($vmid, sub {
+       _do_vm_stop($storecfg, $vmid, $skiplock, $nocheck, $timeout, $shutdown, $force, $keepActive);
+   });
+}
+
+sub vm_reboot {
+    my ($vmid, $timeout) = @_;
+
+    PVE::QemuConfig->lock_config($vmid, sub {
+
+       # only reboot if running, as qmeventd starts it again on a stop event
+       return if !check_running($vmid);
+
+       create_reboot_request($vmid);
+
+       my $storecfg = PVE::Storage::config();
+       _do_vm_stop($storecfg, $vmid, undef, undef, $timeout, 1);
 
-       vm_stop_cleanup($storecfg, $vmid, $conf, $keepActive, 1) if $conf;
    });
 }
 
@@ -7139,9 +7238,9 @@ sub qemu_machine_feature_enabled {
 }
 
 sub qemu_machine_pxe {
-    my ($vmid, $conf, $machine) = @_;
+    my ($vmid, $conf) = @_;
 
-    $machine =  PVE::QemuServer::get_current_qemu_machine($vmid) if !$machine;
+    my $machine =  PVE::QemuServer::get_current_qemu_machine($vmid);
 
     if ($conf->{machine} && $conf->{machine} =~ m/\.pxe$/) {
        $machine .= '.pxe';
@@ -7257,10 +7356,7 @@ sub add_hyperv_enlightenments {
        }
 
        if (qemu_machine_feature_enabled ($machine_type, $kvmver, 3, 1)) {
-           push @$cpuFlags , 'hv_tlbflush';
            push @$cpuFlags , 'hv_ipi';
-           # FIXME: AMD does not supports this currently, only add with special flag??
-           #push @$cpuFlags , 'hv_evmcs';
        }
     }
 }
@@ -7333,6 +7429,25 @@ sub nbd_stop {
     vm_mon_cmd($vmid, 'nbd-server-stop');
 }
 
+sub create_reboot_request {
+    my ($vmid) = @_;
+    open(my $fh, '>', "/run/qemu-server/$vmid.reboot")
+       or die "failed to create reboot trigger file: $!\n";
+    close($fh);
+}
+
+sub clear_reboot_request {
+    my ($vmid) = @_;
+    my $path = "/run/qemu-server/$vmid.reboot";
+    my $res = 0;
+
+    $res = unlink($path);
+    die "could not remove reboot request for $vmid: $!"
+       if !$res && $! != POSIX::ENOENT;
+
+    return $res;
+}
+
 # bash completion helper
 
 sub complete_backup_archives {