Mapping GoControl HUSBZB-1 USB Hub ZigBee/Z-Wave devices inside a systemd nspawn container
As I'm moving away from Docker the last thing I needed to move was my Home Assistant instance in to a systemd nspawn container instance. For the most part this has been pretty easy, however I needed a slightly more advanced setup than my other containers. I need to be able to map my GoControl HUSBZB-1 USB Hub's ZigBee and Z-Wave devices in to the container.
Identifying the device
The first thing I needed to do was define the name I wanted to use in the
container. My current Docker setup uses the /dev/ttyUSB0
and
/dev/ttyUSB1
devices directly. I know you can give them better names
via udev
so let's do that!
First I needed to identify the device and grab some useful static identifiers.
user@host:~$ udevadm info -a /dev/ttyUSB0 looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.0/ttyUSB0/tty/ttyUSB0': SUBSYSTEM=="tty" DRIVER=="" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.0/ttyUSB0': KERNELS=="ttyUSB0" SUBSYSTEMS=="usb-serial" DRIVERS=="cp210x" ATTRS{port_number}=="0" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.0': KERNELS=="2-4:1.0" SUBSYSTEMS=="usb" DRIVERS=="cp210x" ... ATTRS{interface}=="HubZ Z-Wave Com Port" ... user@host:~$ udevadm info -a /dev/ttyUSB1 looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.1/ttyUSB1/tty/ttyUSB1': KERNEL=="ttyUSB1" SUBSYSTEM=="tty" DRIVER=="" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.1/ttyUSB1': KERNELS=="ttyUSB1" SUBSYSTEMS=="usb-serial" DRIVERS=="cp210x" ATTRS{port_number}=="0" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.1': KERNELS=="2-4:1.1" SUBSYSTEMS=="usb" DRIVERS=="cp210x" ... ATTRS{interface}=="HubZ ZigBee Com Port" ... user@host:~$
Based on some very
helpful
posts, I was able to figure out the pieces I needed to define my udev
rules.
Creating a udev.rules
file in
/etc/udev/rules.d/99-gocontrol.rules
I added the following.
SUBSYSTEM=="tty", ATTRS{interface}=="HubZ Z-Wave Com Port", SYMLINK+="zwave", MODE="660", GROUP="1500905492" SUBSYSTEM=="tty", ATTRS{interface}=="HubZ ZigBee Com Port", SYMLINK+="zigbee", MODE="660", GROUP="1500905492"
I then reloaded the rules and triggered udev
to rescan devices.
user@host:~$ sudo udevadm control --reload-rules user@host:~$ sudo udevadm trigger user@host:~$ ls -l /dev/ttyUSB* crw-rw---- 1 root 1500905492 188, 0 Sep 22 23:23 /dev/ttyUSB0 crw-rw---- 1 root 1500905492 188, 1 Sep 22 23:23 /dev/ttyUSB1 user@host:~$ ls -l /dev/z* crw-rw-rw- 1 root root 1, 5 Sep 22 23:18 /dev/zero lrwxrwxrwx 1 root root 7 Sep 22 23:18 /dev/zigbee -> ttyUSB1 lrwxrwxrwx 1 root root 7 Sep 22 23:18 /dev/zwave -> ttyUSB0 user@host:~$
Yay! The devices have an updated group ID and the symlinks match the
device names in a more reliable way. Note the symlinks are still owned by
root
since symlinks themselves cannot have permissions assigned, they
just point to another file in the file system, they can, however have an
owner and group assigned, but in this case it's inconsequential. Now that
the devices have been created, I need to map them inside the container.
Binding the devices to the container
To bind the devices in the container, I need to configure both the
machine's .nspawn
file in /etc/systemd/nspawn
as well as the
service startup file .service
. I'm using systemd's nspawn's template
file, so I take advantage of the override logic systemd provides rather
than create a standalone template file. This lets any changes provided by
the system to provide the base while I augment it with the couple
settings I need.
The first thing I need to do is map the device in
/etc/systemd/nspawn/home-assistant.nspawn
. (The name of the file is
the same name of the container, you'll see it again in the service file
as well.)
# /etc/systemd/nspawn/home-assistant.nspawn [Files] Bind=/dev/zigbee:/dev/zigbee Bind=/dev/zwave:/dev/zwave
The Bind=
directive in the .nspawn
file will the host's file on the left of the
:
to the container's path on the right. In this case, it's a direct
mapping of /dev/zigbee
from the host to the container.
The default container template systemd uses to launch a container is
correctly restrictive when it comes to device access, so we need to
override the configuration to allow our devices through. If the container
is running, you can run sudo systemctl edit systemd-nspawn@home-assistant.service
and systemd will take care of creating the correct path and override file
for you. However, if the container is not running, the service will not
exist and the command will fail.
// if the machine is running, you can edit the override with this user@host:~$ sudo systemctl edit systemd-nspawn@home-assistant.service // or, if the machine is not running, you need to create the directory // and override file yourself. user@host:~$ sudo install -d -m 0755 -o root -g root /etc/systemd/system/systemd-nspawn@home-assistant.service.d user@host:~$ cd /etc/systemd/system/systemd-nspawn@home-assistant.service.d user@host:/etc/systemd/system/systemd-nspawn@home-assistant.service.d$ sudoedit override.conf
We need to add two DeviceAllow=
directives to let the devices map
inside the container.
# /etc/systemd/system/systemd-nspawn@home-assistant.servce.d/override.conf [Service] DeviceAllow=/dev/zigbee rwm DeviceAllow=/dev/zwave rwm
The DeviceAllow=
directive grants access to the device based on the second string
provided, in our case rwm
. The rwm
allows (r)ead access, (w)rite
access, and the ability to (m)ake the node.
Verifying the devices show up in the container
All that is left is to restart the container, get a shell, and verify the device listing.
user@host:~$ machinectl poweroff home-assistant user@host:~$ machinectl start home-assistant user@host:~$ machinectl shell home-assistant root@home-assistant:~$ ls -l /dev/ttyUSB* ls: cannot access '/dev/ttyUSB*': No such file or directory root@home-assistant:~$ ls -l /dev/z* crw-rw-rw- 1 root root 1, 5 Sep 22 23:43 /dev/zero crw-rw---- 1 nobody dialout 188, 1 Sep 22 23:23 /dev/zigbee crw-rw---- 1 nobody dialout 188, 0 Sep 22 23:23 /dev/zwave
Boom! That's it, we're done! Eagle-eyed observers will notice that the
owner is nobody
and not root
. This is the flip side of the
private-user-coin. On the host, root
owns the device and
1500905492
is the group. Inside the container, the opposite is true,
the owner is the special nobody
wildcard since it can'tresolve the
real root owner (because the root account is actually 1500905472
inside the container) but the group is properly matched to dialout
since the gid in the container is 20, and hey would you look at that,
1500905492
is 20 higher than 1500905472
!
If you look a little closer, the original /dev/ttyUSB0
and
/dev/ttyUSB1
devices are not in the container, and that's OK! Instead
of simply binding the symlink (as what usually happens in a bind) the
nspawn process created new device nodes for us, with the correct 188, 0
and 188, 1
device identifiers.