I've worked on it and found a solution. it is no problem to manual add VLAN ids to the slave interfaces of a bridge, for example:
host ~$ sudo bridge vlan add vid 26 dev vnet13 pvid 26 untagged
host ~$ sudo bridge vlan add vid 30 dev vnet13
host ~$ sudo bridge vlan add vid 50 dev vnet14
host ~$ $ sudo bridge vlan
port vlan-id
enp1s0 10
26
30
50
vnet13 26 PVID Egress Untagged
30
vnet14 50
The problem is to do it automatically on startup of the virtual machine. Fortunately libvirt supports libvirt hook scripts. I will use the hook script for qemu and do this in three steps.
Step 1: define which VLAN-ID is attached to what interface
For this we have an extra element <metadata> in Domain XML format for custom metadata. We can simply add the information to the static configuration of a domain (virtual machine):
host ~$ virsh edit guest-vm
--- snip ---
<metadata>
<my:home xmlns:my="http://hoeft-online.de/libvirt">
<my:iface pvid="26">
<my:vlan untagged="yes">26</my:vlan>
<my:vlan>50</my:vlan>
<my:vlan untagged="no">30</my:vlan>
</my:iface>
<my:iface>
<my:vlan untagged="yes">50</my:vlan>
<my:vlan>10</my:vlan>
</my:iface>
</my:home>
</metadata>
--- snap ---
As given by the documentation I have to use my own custom namespace <my:home xmlns:my="http://hoeft-online.de/libvirt">
. Once created it is easier to work within the namespace:
host ~$ virsh metadata guest-vm http://hoeft-online.de/libvirt [--edit --key my]
<home>
<iface pvid="26">
<vlan untagged="yes">26</vlan>
<vlan>50</vlan>
<vlan untagged="no">30</vlan>
</iface>
<iface>
<vlan untagged="yes">50</vlan>
<vlan>10</vlan>
</iface>
</home>
Step 2: get information on startup from the runtime XML-config of the domain
The hook script gets information on its standard input. This is the XML-config of the running VM. We can also get it with virsh dumpxml guest-vm
when the VM is running. I use XSLT with xmlstarlet to get needed information with a xsl-stylesheet. I can test with:
host ~$ virsh dumpxml guest-vm | xmlstarlet transform /etc/libvirt/hooks/qemu.xsl | xmlstarlet format
Here is the stylesheet:
host ~$ cat /etc/libvirt/hooks/qemu.xsl
<?xml version="1.0" encoding="UTF-8"?>
<!-- This stylesheet processes the live XML configuration output from a virtual
machine managed by libvirt. It transforms the custom metadata information
together with attached interfaces and returns a normalized XML with VLAN
ids attached to the interface. For further information look at the
README file.
Author: 2021-01-26 - Ingo Höft (Ingo@Hoeft-online.de)
Licence: GPLv3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://hoeft-online.de/libvirt" exclude-result-prefixes="my">
<xsl:output omit-xml-declaration="yes" indent="no"
encoding="utf-8" media-type="text/xml"/>
<xsl:strip-space elements="*"/>
<xsl:template match="text()|@*"/>
<xsl:template match="/domain">
<meta>
<xsl:apply-templates/>
</meta>
</xsl:template>
<xsl:template match='*'>
<xsl:for-each select='interface[@type="bridge"]/target'>
<iface>
<xsl:variable name="_index" select="position()" />
<xsl:attribute name="pvid">
<xsl:choose>
<xsl:when test="/*/metadata/my:home/my:iface[$_index]/@pvid != ''">
<xsl:value-of select="/*/metadata/my:home/my:iface[$_index]/@pvid"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>0</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:value-of select="@dev"/>
<xsl:for-each select="/*/metadata/my:home/my:iface[$_index]/my:vlan">
<vlan>
<xsl:attribute name="untagged">
<xsl:choose>
<xsl:when test="@untagged='yes'">
<xsl:text>yes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>no</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:value-of select="."/>
</vlan>
</xsl:for-each>
</iface>
</xsl:for-each>
</xsl:template>
<!-- vim: set sts=2 sw=2 et autoindent nowrap: -->
</xsl:stylesheet>
Step 3: set VLAN-ID to the dynamic virtual network interface vnet*
With information from the stylesheet we can now setup the network interface with a hook script. Make it executable.
harley$ cat /etc/libvirt/hooks/qemu
#!/usr/bin/bash
# /etc/libvirt/hooks/qemu
# Docs: https://www.libvirt.org/hooks.html
# Author: 2021-01-26 - Ingo Höft (Ingo@Hoeft-online.de)
# Licence: GPLv3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
# If you save a modified hook script then do 'sudo systemctl restart libvirtd'.
# This script adds VLAN support to interfaces of libvirt guests on start up.
# For more details look at the README file.
# Most work is done with the powerful XML transformation of the XML
# configuration of the guest on stdin with qemu.xsl to get a normalized
# meta information into $META, for example like this
# (we need it to understand the script):
#
# <?xml version="1.0"?>
# <meta>
# <iface pvid="26">vnet1
# <vlan untagged="yes">26</vlan>
# <vlan untagged="no">50</vlan>
# <vlan untagged="no">30</vlan>
# </iface>
# <iface pvid="30">vnet2
# <vlan untagged="yes">30</vlan>
# <vlan untagged="no">50</vlan>
# </iface>
# <iface pvid="0">vnet3</iface>
# </meta>
# for DEBUG uncomment/comment next three lines
#exec 0< start-vdeb11-base02.xml # for DEBUG: read testfile to stdin
#BRIDGE="/usr/bin/echo"
BRIDGE="/usr/sbin/bridge"
# and call it with ./qemu "dummy-vm" "start" "begin" "-"
XSLFILE="/etc/libvirt/hooks/qemu.xsl"
XMLPROG="/usr/bin/xmlstarlet"
#GUEST=$1 # name of guest being started
OPERATION=$2
SUB_OPERATION=$3
EXTRA_PARM=$4
#echo 'DEBUG: entering qemu.hook' >&2
case "$OPERATION" in
prepare)
;;
start)
if [[ "$SUB_OPERATION" != "begin" ]] || [[ "$EXTRA_PARM" != "-" ]]; then
echo "Error: Unhandled parameter \$3='$SUB_OPERATION' or \$4='$EXTRA_PARM' to $0 \$1 \$2 \$3 \$4" >&2
exit 1
fi
if [[ ! -x "$XMLPROG" ]]; then
echo "Error: $XMLPROG is not executable" >&2
exit 1
fi
#cat - >/var/log/libvirt/start-"$1".xml; exit 1 # get live xml for DEBUG
META=$("$XMLPROG" tr "$XSLFILE" -)
#echo "DEBUG: using hook start with $META" >&2
# loop through interfaces
IFACE_COUNT=0
while true; do
((++IFACE_COUNT))
IFACE=$(echo "$META" | "$XMLPROG" sel -t -c "/meta/iface[$IFACE_COUNT]/text()")
if [[ -z "$IFACE" ]]; then
# finished, no more interfaces available
exit 0
fi
"$BRIDGE" link set dev "$IFACE" flood off
# loop through vlans on one interface
VLAN_COUNT=0
while true; do
((++VLAN_COUNT))
VLAN=$(echo "$META" | "$XMLPROG" sel -t -v "/meta/iface[$IFACE_COUNT]/vlan[$VLAN_COUNT]/text()")
if [[ -z "$VLAN" ]]; then
# finished, no more vlans available, process next interface
break
else
UNTAGGED=$(echo "$META" | "$XMLPROG" sel -t -v "/meta/iface[$IFACE_COUNT]/vlan[$VLAN_COUNT]/@untagged")
if [[ "$UNTAGGED" == "yes" ]]; then
"$BRIDGE" vlan add vid "$VLAN" dev "$IFACE" pvid "$VLAN" untagged
else
"$BRIDGE" vlan add vid "$VLAN" dev "$IFACE"
fi
fi
done
done
;;
started)
;;
stopped)
;;
release)
;;
migrate)
;;
restore)
;;
reconnect)
;;
attach)
;;
*)
echo "Error: qemu hook called with unexpected options $*" >&2
exit 1
;;
esac
# vim: set sts=4 sw=4 et autoindent nowrap: