usbhotplug

unRAID Automatic USB Hotplugging (Libvirt)

A long time ago, I figured out how to have an unRAID server detect a hotplugged USB device and automatically associate this device with a VM. I wasn't ever planning on revisiting this issue as unRAID was supposed to implement this as a feature in a future release. Well, after several years and numerous unRAID upgrades, not only am I still waiting for this feature, but the solution I was using stopped working.

There have been a couple of good souls that have written plugins that allow you to manually add a USB device to a running VM, but it requires that you login to the unRAID management GUI everytime you want this done. While this isn't so bad when you need to plug in an occassional USB thumb drive, it becomes a much bigger hassle when you throw a USB switch and multiple devices into the mix.

For those who are unaware, a USB switch is like a KVM...not the KVM related to VM's, but the Keyboard/Video/Mouse kind. The only difference is it lacks the video portion. However, like a KVM switch, it allows you to hookup any USB device to multiple PC's at once, but only one PC can see the device at a time. Anytime you cycle the switch away from a PC, the USB device, like say a keyboard or mouse, will act as if its been unplugged. With the currently available plugins, this means you'd always need a second PC handy to access the unRAID GUI and reassociate your keyboard and mouse. Like I said before, major hassle.

Another complication when using a USB switch, at least in my case, is everytime you cycle the USB switch away from the unRAID server, multiple add and remove events are detected. I'm not quite sure why this is, but while monitoring udev, Linux's device manager, I would see my mouse and keyboard rapidly attach and detach several times in less than a second. This can wreck havoc on a script that relies on these events to launch itself and it's one of the reasons why my previous solution stopped working.

Don't fret, as after much trial and error, I believe I have a working yet, somewhat convulated way to automatically attach and detach your USB devices to your VM's. I developed this method after stumbling upon this helpful post, Automatic hotplugging of USB devices for libvirt-managed VMs at kicherer.org. If you're using libvirt on a non-unRAID server while hotplugging a single USB device directly into the VM host, their guide may be better suited to you.

*UPDATE(3/20/20): This guide has been updated and tested to work with later versions of unRAID 6.8 & 6.9.

Synopsis

Preparing for the Fix

The first thing we need to do is obtain the ID's of the device or devices you'd like to have hotplugged to your VM. You can do this by gaining console access to your unRAID server. The easiest way to achieve this is by opening a web browser and logging into the unRAID management GUI.

Identify Your USB Device

After logging in, click on the Terminal icon at the top of the page.

unRAID Terminal Link

*TIP: For those more hardcore or if you're running an older version of unRAID missing this option, you can ssh to your unRAID server as root with your favorite SSH client.

Inside the terminal window, run the following command to identify your USB device.

lsusb

In the output, locate the USB device and note the Bus & Device ID #'s.

Next run the following, to get the Vendor ID, Model ID & Product ID of your USB device substituting the Bus & Device ID #'s from the last command. For example, if your Bus ID is 001 and your Device ID is 005 your command will be udevadm info /dev/bus/usb/001/005 | egrep "ID_VENDOR=|ID_MODEL=|PRODUCT".

udevadm info /dev/bus/usb/BusID/DeviceID | egrep "ID_VENDOR=|ID_MODEL=|PRODUCT"

Example USB ID's

ID_VENDOR = 0000
ID_MODEL = aaaa
PRODUCT = 000/aaa/111

Remember these values as we'll be needing them in the upcoming sections.

Create the Libvirt Config

We're now going to create a Libvirt (QEMU Management Library) XML Config file called usb-0000-aaaa.xml in the /etc/libvirt/qemu/USB directory which will define the USB device for the VM. Replace the 0's with the Vendor ID and the a's with the Model ID from the last section.

*NOTE: While technically the name of the XML file can be anything, keeping this naming scheme is necessary for the scripts we'll be creating later on in this guide.

nano /etc/libvirt/qemu/USB/usb-0000-aaaa.xml

Add the following in the XML while making sure to replace the vendor id and the product id values after the "0x" with the 4-digit vendor id and model id values from the last section.

<hostdev mode='subsystem' type='usb'>
  <source>
    <vendor id='0x0000'/>
    <product id='0xaaaa'/>
  </source>
</hostdev>

udev Rules...Not!

In this section, we'll be creating a list of rules which udev will execute whenever the USB device is added or removed from the server. In our case, the rules will launch a call script which will in turn execute another script that will attach or detach the USB device to our VM. The reason for this convoluted process is udev doesn't play nice with complicated, long-running processes. It's fine running a simple command that starts and ends quickly, but it doesn't work with more advanced processes.

Create the config file by running the following

nano /boot/config/90-libvirt-usb.rules

*NOTE: Unlike the previous XML file, the name of this file does matter. The "90" at the beginning of the file tells udev the order in which it should apply its rules. The rest of the name, excluding the .rules file extension, can be changed to your preference.

*TIP: For those not using unRAID, you should create this file in /etc/udev/rules.d. The only reason we're creating the config file in the /boot/config directory is unRAID wipes all user created files from rules.d upon every reboot. To combat this, we're going to create a script later in this guide which copies back this file during bootup.

Now paste the following text into the 90-libvirt-usb.rules config file.

ACTION=="add", \
	ENV{PRODUCT}=="000/aaa/111", \
	RUN+="/root/callusbhotplug.sh 'add' 'VM-NAME' '0000' 'aaaa'"
ACTION=="remove", \
	ENV{PRODUCT}=="000/aaa/111", \
	RUN+="/root/callusbhotplug.sh 'remove' 'VM-NAME' '0000' 'aaaa'"

*NOTE: udev requires all of its rules to be written on one line. The backslash at the end of each line allows you to circumvent this rule. This method doesn't work if you include a commented line in the middle of the command, so don't do that.

Make sure to change the ENV{PRODUCT} value to the Product ID you wrote down earlier.

You'll also need to modify the two RUN+ lines which executes our call script. Change VM-NAME to the name of the VM you want to attach the USB device to and change all of the 0000's & aaaa's to your Vendor & Model ID. This should sound familiar as we've already done this at least 50 times already.

*TIP: If you don't remember the exact name of your VM, you can either check the unRAID management GUI or run the following command within the unRAID console, virsh list --all.

To quickly summarize what's actually going on here, we're creating two different udev rules. One for when the USB device is added and one for when it is removed. Both rules launch the same call script and pass along the udev action (add/remove) and the VM name, USB Vendor ID & USB Model ID.

With all of our configs and rules created, we now move on to the scripting portion of the guide which will do all the heavy lifting for us.

Hotplugging Part III: The Scripting

First up we'll be creating the call script which we've referenced in the udev rules at the end of the previous section.

Calling All Scripts

nano /boot/config/callusbhotplug.sh

Copy and paste the following into the script.

#!/bin/bash
/root/usbhotplug.sh $1 $2 $3 $4 & disown

This script passes the arguments that are sent from our udev rules to the script which will handle the USB hotplugging and then disowns the process. As stated previously, udev can't handle anything more than a quick commmand so it's important to disavow all knowledge of the mission...I mean process after it is launched from a udev rule.

Now that the call script is in place, we need to create the thing its calling.

Creating the Hotplug Script

Instead of posting the entire script, I created a GitHub repository you can download the script from in case there are any future changes.

Download the script using the large link directly above and move the script to the /boot/config directory.

wget https://raw.githubusercontent.com/labsrc/Libvirt-USB-Hotplugging/master/usbhotplug.sh
mv usbhotplug.sh /boot/config

For those interested in the nitty gritty, the script creates a log of all actions in /root and also creates individual time stamped files anytime it is run for a USB device. This helps to prevent the script from inadvertantly firing multiple times which is what I experienced when using a USB switch to add and remove my keyboard & mouse.

The script also removes any previous instances of the USB device from the VM before attaching it. This is to prevent the VM from continually adding multiple instances of the same USB device and eventually hitting the VM's limit if your udev rule fails to trigger when the USB device is removed. Keep in mind, this will cause issues if you were planning on hotplugging multiple copies of the exact same USB device into your unRAID server.

Surviving the Reboot

For those of you who have already ventured beyond the constraints of unRAID's WebGUI, you probably know that just about everything you create via command-line magically disappears when the server reboots. Fortunately, the creators of unRAID provided a way to make sure everything you customize can be automatically re-added during startup including the udev rules we've created. The key to our files having everlasting life is unRAID's Go Script.

Go Go Gadget Go Script!

The Go Script, located at /boot/config/go, is really just a bash script that runs after unRAID mounts its drives. We'll be using this script to copy the udev rules & the two scripts we just created to their proper locations.

nano /boot/config/go

Now add the following into the Go script and save.

#!/bin/bash
cp /boot/config/90-libvirt-usb.rules /etc/udev/rules.d
chmod 644 /etc/udev/rules.d/90-libvirt-usb.rules
cp /boot/config/usbhotplug.sh /root
chmod 700 /root/usbhotplug.sh
cp /boot/config/callusbhotplug.sh /root
chmod 700 /root/callusbhotplug.sh

The first two lines of this script copies our udev rules to /etc/udev/rules.d and change the permissions on the file.

The next 4 lines copies our two scripts to /root and allows us to run them. This added step is needed because scripts were blocked from running from the /boot/config directory in later versions of unRAID including the latest release, 6.9.

Now with everything in place, go ahead and reboot your unRAID server.

Testing Phase

With your unRAID server now rebooted, make sure your VM is powered on. Now go ahead and plug in your USB device into the unRAID server or, as in my case, toggle your USB switch to the unRAID server. If everything worked as it should, your USB device should now be attached to your VM. If this is the first time attaching the USB device to the VM, you may have to load device drivers just like you would for a physical PC.

You can check if the USB device successfully attached to the VM by inputting the following into the unRAID console.

virsh dumpxml VM-Name | grep vendorID -A1

Change VM-Name to the name of your VM and make sure any letters in the vendorID are typed in as lowercase. If the output is blank, make a sad face and skip to the next section.

If that worked, wait a few seconds, unplug the USB device, wait a few more seconds and plug it back in. If my script did it's job, you should see the USB device attached to the VM again.

If any part of the test failed, it's time to put on your troubleshooting caps.

What To Do If You Failed

The first thing you should do is stop blaming me. I'm just some random lifeform on the internet giving out free advice. The next thing you should do is check the /root directory. I coded the script to create log files in case of different types of failures. If you don't see any newly created files in the /root directory, that means the script never ran and we most likely have a udev rules issue. Start off by doublechecking all the files we've created for syntax errors.

If you make changes to the udev rules file, 90-libvirt-usb.rules, you'll need to run the following command to have your changes take effect otherwise it requires a reboot.

udevadm control --reload-rules && udevadm trigger

If you've already doublechecked for any typos, it's time to make sure your server is actually generating udev events whenever the USB device is attached or removed from the server.

Inside the unRAID console, you can monitor all udev events by running the following.

udevadm monitor

While this command is running, physically plug the USB device into the server and you should see a bunch of output within the console if the USB device is recognized. If the console remains blank, you most likely have a hardware issue.

If, however, you do see output, confirm there are lines that contain the USB device's Vendor & Model ID's. Check this against the ID's you've been using in your configs to make sure they match.

If that checks out, you can try manually attaching the USB device to the VM.

virsh detach-device VM-Name /etc/libvirt/qemu/USB/usb-vendorID-productID.xml

Change VM-Name to the name of your VM and change vendorID & productID to the corresponding ID's of your USB device.

Now check to see if the VM sees the device attached.

virsh dumpxml VM-Name | grep vendorID -A1

If this still outputs nothing, there's probably a typo within the usb xml file we created in /etc/libvirt/qemu/USB/ or you're using the wrong USB ID.

You can also use the dmesg command to see if there are any errors when running the scripts we created.

dmesg

Hopefully these troubleshooting steps should get things up and running for you. If not, feel free to comment down below and I'll be happy to hand out more free interweb advice.

Please Share Me, I'm Lonely

15 Responses to unRAID Automatic USB Hotplugging (Libvirt)

  1. Hi
    I am trying to install my Conbee II stick in ta a linux VM on UNRAID . I see your solution, however i am unable to create the
    nano /etc/libvirt/qemu/USB/usb-0000-aaaa.xml as it states no directory exist. When i check the /etc/libvirt/qemu there is no USB folder hence the problem i think , but i new to Linux etc ?
    The steps previously are all fine ready to continue.

    Any advise would be appreciated.

  2. I’m on 6.9, and following this so I can get my realtek wifi antenna to work with a linux vm.
    I perform the first step:
    $lsusb
    Bus 003 Device 002: ID 0bda:b812 Realtek Semiconductor Corp. 802.11ac NIC

    The second step I get:

    E: ID_MODEL=802.11ac_NIC
    E: ID_VENDOR=Realtek
    E: PRODUCT=bda/b812/210

    Now, I’m going to assume that the only hex codes I have here are in the first step where I get the bus/device codes… So those are what I should be using for the vendor IDs for the xml?

    It would be these from the lsusb?

    Thanks! Been beating myself up over this for a few weeks, as to why I can’t get the usb pass thru to work, and why it’s not automatically showing up on the VMs I’m using on UnRaid.

    • hrm… no edit, and no html codes allowed in posts..
      vendor id=’0x0bda’/
      product id=’0xb812’/
      pretend there are brackets on that… it’s missing from the line right above where I asked “It would be these from the lsusb command”

  3. Does this script work with UNRAID 6.9.x releases? IMHO UNRAID is missing a trick here with auto USB hot plug , saves having to pass through an entire USB controller.

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      I haven’t tested it on 6.9 yet as it hasn’t officially launched yet. If you’ve already loaded the RC or one of the beta’s and would like to try it out that would be great. Otherwise, I can post an update when 6.9 is released.

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      I finally upgraded to unRAID 6.9.x and I did need to make some changes to the configs/scripts to get it to work. The two major takeaways for 6.9.x is that you can’t run scripts directly from /boot/config. They need to be copied to the /root directory first with the go script, but this issue also occurred with later versions of 6.8 as well. The other thing is that I was no longer able to launch scripts with a preceding “sh” command. I needed to remove that for the scripts to be able to fire.

      I will be updating this guide to reflect these changes.

  4. Avatar germanus4711
    germanus4711 says:

    Curious as I am, I had to optimize the script a bit to reduce the retries to detach a device. So I added some conditions that occur on 6.8.3 which denote that the device in question is not attached.

    I am not sure what the best way to post my changes is, so let me ask for forgiveness after I posted the changed script.

    —– /boot/config/usbhotplug.sh —-
    #!/bin/bash
    # run last part of the pipeline in the main shell process (ksh like behavior)
    shopt -s lastpipe

    # command line arguments
    action=$1
    # quote argument string to allow spaces, e.g. Windows 10
    vmname=”$2″
    vendorid=$3
    productid=$4

    # locally used vars
    sleepon=false
    timecheckfile=”/root/usb-${vendorid}-${productid}”
    #touch -a /root/runcheck

    # for debugging: uncomment next line to clear log for every run
    # echo -n > /root/usbhotplug.log

    # Check if timecheck file exists
    if [ -f “$timecheckfile” ]; then
    lastruntime=$(stat -c %X “${timecheckfile}”)
    else
    lastruntime=0
    fi

    # Compare Last Time Device was Added/Removed
    currenttime=$(date +%s)
    timecheck=$(( currenttime – lastruntime ))
    echo ${timecheck} >> /root/usbtimelog

    # Check if Device is already attached to VM
    devcheck=$(/usr/sbin/virsh dumpxml “${vmname}” | grep -A1 -e “” | grep -c -e “”)

    # Remove All Instances of Device
    removeDevice () {
    # max retires to detach device
    maxloop=5

    while [[ $devcheck -gt 0 ]]; do
    if [ $maxloop -gt 0 ]; then
    /usr/sbin/virsh detach-device “${vmname}” /etc/libvirt/qemu/USB/usb-“${vendorid}”-“${productid}”.xml 2>&1 |\
    while read -r line; do
    echo “[$(date ‘+%Y-%m-%d %H:%M:%S’)] $line” >> /root/usbhotplug.log
    if [[ “$line” = *”device not found”*
    || “$line” = *”Device detached successfully”* ]]; then
    maxloop=0
    break
    else
    (( –maxloop ))
    sleep 2
    fi
    done
    else
    break
    fi
    done
    }

    # Add Device Function
    addDevice () {
    /usr/sbin/virsh attach-device “${vmname}” /etc/libvirt/qemu/USB/usb-“${vendorid}”-“${productid}”.xml –current 2>&1 |\
    while read -r line; do
    echo “[$(date ‘+%Y-%m-%d %H:%M:%S’)] $line” >> /root/usbhotplug.log
    done
    }

    # Main Code
    if [ “${action}” == ‘add’ ]; then
    if [ ${timecheck} -gt 2 ]; then
    touch -a “${timecheckfile}”
    removeDevice
    addDevice
    sleepon=true
    else
    touch -a /root/usbattachnotrun
    exit 0
    fi
    elif [ “${action}” == ‘remove’ ]; then
    if [ ${timecheck} -gt 2 ]; then
    touch -a “${timecheckfile}”
    removeDevice
    else
    touch -a /root/usbdetachnotrun
    exit 0
    fi
    else
    echo “Incorrect or Missing Argument”
    exit 1
    fi

    if [ ${sleepon} = true ]; then
    sleep 2
    exit 1
    fi
    —- end script —-

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      I’m glad you were able to modify and improve upon the script. When I have some free time, I’ll review your changes and update the original.

      • Avatar germanus4711
        germanus4711 says:

        I have created a script for the rules and xml file generation. The bash script scans a given usb id and creates the required files.

        Some parts are quite experimental and have not been tested yet. Especially the part of the address tags in the xml and managed attribute. At the moment the files are created in a local subdirectory of the user, NOT the unraid server boot directory.

        I am still trying to connect 2+ devices with the same vendor id and model id to unraid. Until now I have not found a way to declare the MAJOR and MINOR attributes in libvirt. The tag might work if the device id’s would not change ever so often.

        BTW my devices are Saitek Flight Instrument Panels. Here a small cut from my cockpit hardware:
        Bus 011 Device 017: ID 06a3:a2ae Saitek PLC Pro Flight Instrument Panel
        Bus 011 Device 018: ID 06a3:a2ae Saitek PLC Pro Flight Instrument Panel

        Please do have a look at the bitbucket repo and let me know what you think.

        https://bitbucket.org/germanus4711/usb/src/master/

        ./gen_usb.sh -h will print a usage text

  5. Avatar germanus4711
    germanus4711 says:

    Next I ran into the situation that when adding a new Windows VM, Unraid automatically adds a version number with a space to the name.
    Example: “Windows 10″ and yes, I do agree that spaces are evil in any kind of names digested by machines.

    This results into /boot/config/usbhotplug.sh failing as the VM-NAME parameter would be split in to two arguments and causes nasty side effects.
    The simple fix is to quote the argument in the shell script. Example: vmname=”$2”

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      Thanks for pointing out that oversight on my part. I’m so used to living in the Linux world I didn’t take into account spaces being in the VM name. I went ahead and updated the script to reflect your suggested change, but haven’t had a chance to test it out yet.

  6. Avatar germanus4711
    germanus4711 says:

    Thank you for the great post.

    I am using Unraid 6.8.3 202-03-05 and I had some minor tweaks to do when following the guide.

    In the section “Get USB Vendor, Model & Product ID’s”, I used:
    udevadm info /dev/bus/usb/BusID/DeviceID | egrep “ID_VENDOR_ID=|ID_MODEL_ID=|PRODUCT”

    this will return the required hex numbers for the next steps. The original version shows the vendor and device alphanumerical names.

    In the section “Udev rules… (Tip)”, in the command ‘virsh list –all’ , the single dash is now a double dash. Example: virsh list –all
    Same in the section “What to do if…” a double dash should be used for ‘udevadm control -–reload’

    • Avatar Postmin (Admin)
      Postmin (Admin) says:

      Thanks for taking the time to read, register & comment. I’m currently on Unraid 6.8.3 too and the original command shows the hex values on my server. Not sure why it’s showing different values on your server. Maybe different vendors populate it with different values?

      As far as the single dash goes, my website’s backend was automatically converting double-dashes into a single long dash. I made a change and it’s no longer doing that. Thanks for pointing it out to me.