]> git.ipfire.org Git - thirdparty/lldpd.git/commitdiff
tests: replace integration test by py.test+namespace tests
authorVincent Bernat <vincent@bernat.im>
Tue, 1 Mar 2016 19:01:23 +0000 (20:01 +0100)
committerVincent Bernat <vincent@bernat.im>
Sun, 13 Mar 2016 23:14:37 +0000 (00:14 +0100)
Relying on namespaces enable us to quickly run isolated instances of
lldpd without the need of virtual machines. Since the startup time is
quite fast (despite having to wait for lldpd to be "ready"), we can use
a classic unittest framework like py.test to run tests and get
appropriate reports. Tests can be run in parallel to overcome the
slowness induced by all those `time.sleep(2)`.

20 files changed:
README.md
tests/Makefile.am
tests/R1.expected [deleted file]
tests/integration-tests.in [deleted file]
tests/integration/.gitignore [new file with mode: 0644]
tests/integration/README.md [new file with mode: 0644]
tests/integration/conftest.py [new file with mode: 0644]
tests/integration/fixtures/namespaces.py [new file with mode: 0644]
tests/integration/fixtures/network.py [new file with mode: 0644]
tests/integration/fixtures/programs.py [new file with mode: 0644]
tests/integration/requirements.txt [new file with mode: 0644]
tests/integration/test_basic.py [new file with mode: 0644]
tests/integration/test_custom.py [new file with mode: 0644]
tests/integration/test_dot1.py [new file with mode: 0644]
tests/integration/test_dot3.py [new file with mode: 0644]
tests/integration/test_interfaces.py [new file with mode: 0644]
tests/integration/test_lldpcli.py [new file with mode: 0644]
tests/integration/test_med.py [new file with mode: 0644]
tests/integration/test_protocols.py [new file with mode: 0644]
tests/lldpcli.conf

index ddd30647e509e94eca393fb49dc8bb23e79e4e2c..ffcf79534d4d928ec20150649977ba635c24607b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -249,11 +249,8 @@ that:
     afl-fuzz -i inputs -o outputs ./decode @@
 
 There is a general test suite with `make check`. It's also possible to
-run integration tests with `make integration-tests && sh
-./integration-tests`. Those are not very flexible and may or may not
-work depending on your platform. Also check the content of
-`tests/lldpcli.conf`. It's a configuration file that should cover all
-commands present in lldpcli.
+run integration tests. They need [py.test](http://pytest.org/latest/)
+and rely on Linux containers to be executed.
 
 Embedding
 ---------
index e8c41fc017d3ffa928905eb3a5bcf0ca7533626f..b06d7f81a8b099cf37c612fab896aad7c0478b05 100644 (file)
@@ -52,9 +52,3 @@ decode_SOURCES = decode.c \
 endif
 
 MOSTLYCLEANFILES = *.pcap
-
-TEMPLATES  = integration-tests
-EXTRA_DIST = integration-tests.in R1.expected
-CLEANFILES = $(TEMPLATES)
-integration-tests: integration-tests.in
-include $(top_srcdir)/edit.am
diff --git a/tests/R1.expected b/tests/R1.expected
deleted file mode 100644 (file)
index bb05a23..0000000
+++ /dev/null
@@ -1,497 +0,0 @@
-[∗] Waiting for commands.
-[∗] Execute command libtool execute src/daemon/lldpd -M 1 -L $PWD/src/client/lldpcli.
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface1, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, on
-  Port:        
-    PortID:       mac 50:54:b3:3c:2c:36
-    PortDescr:    iface1
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-Interface:    iface1, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, on
-  Port:        
-    PortID:       mac 50:54:07:79:57:69
-    PortDescr:    iface1
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-Interface:    iface2, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, on
-  Port:        
-    PortID:       ifname iface2
-    PortDescr:    SecondInterface
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-Interface:    iface3, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, on
-  Port:        
-    PortID:       mac 50:54:5e:ed:e2:df
-    PortDescr:    iface3
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-Interface:    iface4, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, on
-  Port:        
-    PortID:       mac 50:54:30:01:78:dd
-    PortDescr:    iface2
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-Interface:    iface5, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, on
-  Port:        
-    PortID:       mac 50:54:ef:f4:f5:bf
-    PortDescr:    iface3
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface2.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface2, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, on
-  Port:        
-    PortID:       ifname iface2
-    PortDescr:    SecondInterface
-  VLAN:         450 iface2.450
-  VLAN:         451 iface2.451
-  VLAN:         452 iface2.452
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface2.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface2, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, on
-  Port:        
-    PortID:       ifname iface2
-    PortDescr:    SecondInterface
-  VLAN:         450 iface2.450
-  VLAN:         452 iface2.452
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface4.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface4, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, off
-  Port:        
-    PortID:       mac 50:54:30:01:78:dd
-    PortDescr:    iface2
-    Port is aggregated. PortAggregID: 6
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface5.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface5, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, off
-  Port:        
-    PortID:       mac 50:54:ef:f4:f5:bf
-    PortDescr:    iface3
-    Port is aggregated. PortAggregID: 6
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface4.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface4, via: LLDP, RID: 2, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:07:79:57:69
-    SysName:      R3
-    SysDescr:
-    MgmtIP:       192.0.2.17
-    MgmtIP:       2001:db8::cafe:17
-    Capability:   Bridge, off
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Tel, on
-    Capability:   Station, off
-  Port:        
-    PortID:       mac 50:54:30:01:78:dd
-    PortDescr:    iface2
-    Port is aggregated. PortAggregID: 6
-  VLAN:         453 bond0.453
-  LLDP-MED:    
-    Device Type:  Communication Device Endpoint (Class III)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface3.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface3, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, on
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, off
-  Port:        
-    PortID:       mac 50:54:5e:ed:e2:df
-    PortDescr:    iface3
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface2.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface2, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, on
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, off
-  Port:        
-    PortID:       ifname iface2
-    PortDescr:    SecondInterface
-    MDI Power:    supported: yes, enabled: yes, pair control: yes
-      Device type:  PSE
-      Power pairs:  spare
-      Class:        class 3
-  VLAN:         450 iface2.450
-  VLAN:         452 iface2.452
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    LLDP-MED Location Identification: Type: Coordinates, Geoid: WGS84
-      Latitude:     48.58666N
-      Longitude:    2.2013E
-      Altitude:     m 117.46
-    LLDP-MED Location Identification: Type: ELIN
-      ECS ELIN:     911
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
-  UnknownTLVs: 
-    TLV:          OUI: 33,44,55, SubType: 44, Len: 5 45,45,45,45,45
--------------------------------------------------------------------------------
-[∗] End of command: 0.
-[∗] Execute command libtool execute src/client/lldpcli show neighbor detail ports iface2.
--------------------------------------------------------------------------------
-LLDP neighbors:
--------------------------------------------------------------------------------
-Interface:    iface2, via: LLDP, RID: 1, Time: 0 day
-  Chassis:     
-    ChassisID:    mac 50:54:b3:3c:2c:36
-    SysName:      R2
-    SysDescr:
-    MgmtIP:       192.0.2.16
-    MgmtIP:       2001:db8::cafe:16
-    Capability:   Bridge, on
-    Capability:   Router, off
-    Capability:   Wlan, off
-    Capability:   Station, off
-  Port:        
-    PortID:       ifname iface2
-    PortDescr:    SecondInterface
-    MDI Power:    supported: yes, enabled: yes, pair control: yes
-      Device type:  PSE
-      Power pairs:  spare
-      Class:        class 3
-  VLAN:         450 iface2.450
-  VLAN:         452 iface2.452
-  LLDP-MED:    
-    Device Type:  Media Endpoint (Class II)
-    Capability:   Capabilities
-    Capability:   Policy
-    Capability:   Location
-    Capability:   MDI/PSE
-    Capability:   MDI/PD
-    Capability:   Inventory
-    LLDP-MED Location Identification: Type: Coordinates, Geoid: WGS84
-      Latitude:     48.58666N
-      Longitude:    2.2013E
-      Altitude:     m 117.46
-    LLDP-MED Location Identification: Type: ELIN
-      ECS ELIN:     911
-    Inventory:   
-      Hardware Revision: pc-i440fx
-      Software Revision: 
-      Firmware Revision: 
-      Manufacturer: QEMU
-      Model:        Standard PC (i440FX + PIIX, 1996
-  UnknownTLVs: 
-    TLV:          OUI: 33,44,55, SubType: 44, Len: 5 45,45,45,45,45
--------------------------------------------------------------------------------
-[∗] End of command: 0.
diff --git a/tests/integration-tests.in b/tests/integration-tests.in
deleted file mode 100755 (executable)
index 54d28b3..0000000
+++ /dev/null
@@ -1,504 +0,0 @@
-#!/bin/sh
-
-# Integration tests for lldpd. Those tests only run on Linux. They
-# spawn several VM and connect them together, then check lldpcli
-# results to see if everything is in order.
-
-# lldpd should be configure with:
-#  ../configure --localstatedir=/var --sysconfdir=/etc --prefix=/usr CFLAGS="-O0 -g"
-
-LABNAME=lldpd
-
-set -e
-
-log_begin_msg () {
-    echo "[…] $1... "
-}
-log_ok_msg () {
-    echo "[✔] $1."
-}
-log_warn_msg () {
-    echo "[⚡] $1!"
-}
-log_error_msg () {
-    echo "[✘] $1!"
-    exit 1
-}
-log_info_msg () {
-    echo "[∗] $1."
-}
-
-check_kernel() {
-    log_begin_msg "Checking kernel version"
-    [ -f "$KERNEL" ] || log_error_msg "Unable to find kernel $KERNEL"
-    [ -r "$KERNEL" ] || log_error_msg "Kernel $KERNEL is not readable.\n    Try \`setfacl -m u:$USER:r $KERNEL'"
-
-    # A recent version of `file` is able to extract the
-    # information. Since it is not widely available, let use some hack
-    # method.
-    VERSION=$(cat <<EOF |
-cat
-gunzip  \\\037\\\213\\\010 xy
-unxz    \\\3757zXZ\\\000   abcde
-bunzip2 BZh                xy
-unlzma  \\\135\\\0\\\0\\\0 xxx
-EOF
-              while read cmd sig1 sig2; do
-                  case $sig1,$sig2 in
-                      ,) poss="0:_" ;;
-                      *) poss=$(tr "${sig1}\n${sig2}" "\n${sig2}=" < "$KERNEL" | grep -abo "^${sig2}" || true) ;;
-                  esac
-                  [ -n "$poss" ] || continue
-                  for pos in $poss; do
-                      pos=${pos%%:*}
-                      tail -c+$pos "$KERNEL" | $cmd 2> /dev/null | strings -20 | \
-                          grep ^Linux.version | head -1
-                  done
-              done | head -1)
-
-    [ -n "$VERSION" ] || log_error_msg "Unable to determine version for $KERNEL"
-    VERSION="${VERSION#Linux version }"
-    VERSION="${VERSION%% *}"
-    log_ok_msg "Found kernel $VERSION"
-
-    log_begin_msg "Check kernel configuration"
-    CONFIG="$(dirname $KERNEL)/config-$VERSION"
-    [ -f "$CONFIG" ] || log_error_msg "Unable to find configuration file $CONFIG"
-    cat <<EOF | while read el; do
-9P_FS=[ym]
-NET_9P=[ym]
-NET_9P_VIRTIO=[ym]
-VIRTIO=[ym]
-VIRTIO_PCI=[ym]
-SERIAL_8250=y
-SERIAL_8250_CONSOLE=y
-TMPFS=y
-BLK_DEV_INITRD=y
-DEVTMPFS=[ym]
-EOF
-        grep -qx "CONFIG_$el" $CONFIG || log_error_msg "Kernel not configured with CONFIG_$el"
-    done
-
-    log_begin_msg "Search for modules"
-    for dir in "$(dirname $KERNEL)/lib/modules/$VERSION" "/lib/modules/$VERSION"; do
-        [ -d $dir ] || continue
-        MODULES="$dir"
-        break
-    done
-    if [ -z "$MODULES" ]; then
-        log_warn_msg "Unable to find module directory"
-    else
-        log_ok_msg "Modules are in $MODULES"
-    fi
-}
-
-check_dependencies() {
-    log_begin_msg "Checking if dependencies are present"
-    for exec in \
-        busybox \
-        qemu-system-x86_64 \
-        vde_switch \
-        ip \
-        brctl; do
-        which $exec 2> /dev/null > /dev/null || log_error_msg "$exec is not installed"
-    done
-    log_ok_msg "All dependencies are met"
-}
-
-setup_tmp () {
-    TMP=$(mktemp -d)
-    trap "ret=$? ; cleanup" EXIT
-    log_info_msg "TMP is $TMP"
-}
-
-# Setup a VDE switch
-setup_switch() {
-    nb=$1 ; shift
-    log_begin_msg "Setup switch $nb"
-    cat <<EOF > "$TMP/switch-$nb.conf"
-plugin/add /usr/lib/vde2/plugins/pdump.so
-pdump/filename $TMP/switch-$nb.pcap
-pdump/buffered 0
-pdump/active 1
-EOF
-    vde_switch \
-        --sock "$TMP/switch-$nb.sock" --mgmt "$TMP/switch-management-$nb.sock" \
-        --rcfile "$TMP/switch-$nb.conf" --hub --daemon --pidfile "$TMP/switch-$nb.pid"
-    # Management socket can be used with:
-    #    socat - UNIX-CONNECT:"$TMP/switch-management-$nb.sock"
-    log_ok_msg "Switch $nb started"
-}
-
-setup_initrd () {
-    log_begin_msg "Build initrd"
-    DESTDIR=$TMP/initrd
-    mkdir -p $DESTDIR
-
-    # Copy busybox and eventually insmod
-    bins="busybox"
-    busybox --list | grep -qFx insmod || bins="$bins insmod"
-    for bin in $bins; do
-        install -D "$(which $bin)" ${DESTDIR}/bin/$bin
-        for x in $(ldd "$(which $bin)" 2> /dev/null | sed -e '
-               /\//!d;
-               /linux-gate/d;
-               /=>/ {s/.*=>[[:blank:]]*\([^[:blank:]]*\).*/\1/};
-               s/[[:blank:]]*\([^[:blank:]]*\) (.*)/\1/' 2>/dev/null); do
-            [ -f "${DESTDIR}/$x" ] || install -D "$x" "${DESTDIR}/$x"
-        done
-    done
-
-    # Configure busybox
-    for applet in $(${DESTDIR}/bin/busybox --list); do
-        ln -s busybox ${DESTDIR}/bin/${applet}
-    done
-
-    # Add modules
-    [ -z "$MODULES" ] || {
-        modules="9pnet_virtio 9p virtio_pci $UNION"
-        for mod in $modules; do
-            modprobe --all --set-version="${VERSION}" -d ${MODULES}/../../.. \
-                --ignore-install --quiet --show-depends $mod > /dev/null || {
-                log_warn_msg "Unable to find module $mod"
-                log_begin_msg "Continue building initrd"
-            }
-            modprobe --all --set-version="${VERSION}" -d ${MODULES}/../../.. \
-                --ignore-install --quiet --show-depends $mod |
-            while read prefix kmod options ; do
-                [ "${prefix}" = "insmod" ] || continue
-                grep -qFw "$kmod" ${DESTDIR}/modules 2> /dev/null || {
-                    install -D "$kmod" "${DESTDIR}/${kmod}"
-                    echo $prefix $kmod $options >> ${DESTDIR}/modules
-                }
-            done
-        done
-    }
-
-    # Copy this program
-    cp "$PROGNAME" ${DESTDIR}/init
-
-    # Create /tmp
-    mkdir ${DESTDIR}/tmp
-
-    # Build initrd
-    (cd "${DESTDIR}" && find . | cpio --quiet -R 0:0 -o -H newc | gzip > $TMP/initrd.gz)
-
-    log_ok_msg "initrd built in $TMP/initrd.gz"
-}
-
-random_mac () {
-    # But, not random in fact
-    name=$1
-    net=$2
-    mac=$(echo $name-$net | sha1sum | \
-        awk '{print "50:54:" substr($1,0,2) ":" substr($1, 2, 2) ":" substr($1, 4, 2) ":" substr($1, 6, 2)}')
-    echo $mac
-}
-
-start_vm () {
-    name=$1
-    shift
-
-    netargs=""
-    saveifs="$IFS"
-    IFS=,
-    for net in $NET; do
-        mac=$(random_mac $name $net)
-        netargs="$netargs -net nic,model=virtio,macaddr=$mac,vlan=$net"
-        netargs="$netargs -net vde,sock=$TMP/switch-$net.sock,vlan=$net"
-    done
-    IFS="$saveifs"
-
-    log_info_msg "Start VM $name"
-    # /root is mounted with version 9p2000.u to allow access to /dev,
-    # /sys and to mount new partitions over them. This is not the case
-    # for 9p2000.L.
-    qemu-system-x86_64 \
-        -enable-kvm \
-        -nodefconfig -nodefaults \
-        -display none \
-        -m ${MEM:-128M} \
-        \
-        -chardev file,id=charserial0,path=$name.console \
-        -device isa-serial,chardev=charserial0,id=serial0 \
-        \
-        -fsdev local,security_model=passthrough,id=fsdev-root,path=${ROOT} \
-        -device virtio-9p-pci,id=fs-root,fsdev=fsdev-root,mount_tag=rootshare \
-        -fsdev local,security_model=none,id=fsdev-lab,path=@top_builddir@ \
-        -device virtio-9p-pci,id=fs-lab,fsdev=fsdev-lab,mount_tag=labshare \
-        -fsdev local,security_model=none,id=fsdev-tmp,path=${TMP} \
-        -device virtio-9p-pci,id=fs-tmp,fsdev=fsdev-tmp,mount_tag=tmpshare \
-        -fsdev local,security_model=none,id=fsdev-modules,path=${MODULES}/..,readonly \
-        -device virtio-9p-pci,id=fs-modules,fsdev=fsdev-modules,mount_tag=moduleshare \
-        \
-        -kernel $KERNEL \
-        -no-reboot \
-        -initrd $TMP/initrd.gz \
-        -append "uts=$name console=ttyS0 panic=1 TERM=$TERM quiet" \
-        $netargs \
-        $@ &
-    echo $! > "$TMP/vm-$name.pid"
-}
-
-run() {
-    r=$1
-    shift
-    log_info_msg "$r: execute $*"
-    printf "%s\n" "$*" > $TMP/${r}.command
-    n=0
-    while [ -f $TMP/${r}.command ]; do
-        sleep 0.1
-        n=$((n + 1))
-        [ $n -le 150 ] || {
-            log_error_msg "Timeout while executing command on $r"
-        }
-    done
-}
-
-process_commands() {
-    cd /mnt/lab
-    log_info_msg "Waiting for commands"
-    cmd=/tmp/lab/${uts}.command
-    set +e
-    while true; do
-        while [ ! -f $cmd ]; do
-            sleep 1
-        done
-        log_info_msg "Execute command $(head -1 $cmd)"
-        sh $cmd
-        log_info_msg "End of command: $?"
-        rm $cmd
-    done
-}
-
-start_tests() {
-    # Set the environment
-    run R2 ip link set dev iface2 alias "SecondInterface"
-    # Start lldpd on each VM
-    run R1 ./libtool execute src/daemon/lldpd -M 1 -L \$PWD/src/client/lldpcli
-    run R2 ./libtool execute src/daemon/lldpd -M 2 -L \$PWD/src/client/lldpcli
-    run R3 ./libtool execute src/daemon/lldpd -M 3 -L \$PWD/src/client/lldpcli
-    sleep 2
-    # Query neighbors
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail
-    run R2 ./libtool execute src/client/lldpcli show neighbor detail
-    run R3 ./libtool execute src/client/lldpcli show neighbor detail
-    # Add some VLAN
-    run R2 ip link add link iface2 name iface2.450 type vlan id 450
-    run R2 ip link set up dev iface2.450
-    run R2 ip link add link iface2 name iface2.451 type vlan id 451
-    run R2 ip link set up dev iface2.451
-    run R2 ip link add link iface2 name iface2.452 type vlan id 452
-    run R2 ip link set up dev iface2.452
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface2
-    # Remove one
-    run R2 ip link del iface2.451
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface2
-    # Add a bond
-    run R3 ip link set down dev iface2
-    run R3 ip link set down dev iface3
-    run R3 ip link set iface2 master bond0
-    run R3 ip link set iface3 master bond0
-    run R3 ip link set up dev bond0
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface4
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface5
-    # Add a VLAN on top of bond
-    run R3 ip link add link bond0 name bond0.453 type vlan id 453
-    run R3 ip link set up dev bond0.453
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface4
-    # Add a bridge
-    run R2 brctl addbr br0
-    run R2 brctl addif br0 iface3
-    run R2 ip link set up dev br0
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface3
-    # Modify some TLV
-    conf="./libtool execute src/client/lldpcli configure ports iface2"
-    run R2 $conf lldp custom-tlv oui 33,44,55 subtype 44 oui-info 45,45,45,45,45
-    run R2 $conf med location elin 911
-    run R2 $conf med location coordinate latitude 48.58667N longitude 2.2014E altitude 117.47 m datum WGS84
-    run R2 $conf med power pd source pse priority high value 5000
-    run R2 $conf dot3 power pse supported enabled paircontrol powerpairs spare class class-3
-    sleep 2
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface2
-    # Configuration should stay when port go down and up
-    run R2 ip link set down dev iface2
-    sleep 2
-    run R2 ip link set up dev iface2
-    sleep 5
-    run R1 ./libtool execute src/client/lldpcli show neighbor detail ports iface2
-}
-
-cleanup() {
-    set +e
-    for pid in $TMP/*.pid; do
-        kill -15 -$(cat $pid) 2> /dev/null || true
-    done
-    sleep 1
-    for pid in $TMP/*.pid; do
-        kill -9 -$(cat $pid) 2> /dev/null || true
-    done
-    rm -rf $TMP
-}
-
-# FSM
-export STATE=${STATE:-BEGIN}
-case $$,$STATE in
-    1,BEGIN)
-        # In initrd
-        log_info_msg "initrd started"
-        hostname ${uts}
-        export PATH=/usr/local/bin:/usr/bin:/bin:/sbin:/usr/local/sbin:/usr/sbin
-        export HOME=/root
-
-        [ ! -f /modules ] || {
-            log_info_msg "Loading modules"
-            . /modules
-        }
-
-        log_begin_msg "Setup root file system"
-        mount -n -t tmpfs tmpfs /tmp -o rw
-        mkdir /tmp/target
-        mkdir /tmp/target/ro
-        mkdir /tmp/target/overlay
-        mount -n -t 9p    rootshare /tmp/target/overlay -o trans=virtio,version=9p2000.u,ro
-        mount -n -t proc  proc /tmp/target/overlay/proc
-        mount -n -t sysfs sys  /tmp/target/overlay/sys
-        log_ok_msg "Root file system setup"
-
-        log_begin_msg "Clean /tmp and /run"
-        for fs in /run /var/run /var/tmp /var/log /tmp /mnt; do
-            if [ -d /tmp/target/overlay$fs ] && [ ! -h /tmp/target/overlay$fs ]; then
-                mount -t tmpfs tmpfs /tmp/target/overlay$fs -o rw,nosuid,nodev
-            fi
-        done
-        log_ok_msg "/tmp, /run and others are clean"
-
-        log_begin_msg "Mount /lib/modules"
-        mount -t 9p moduleshare /tmp/target/overlay/lib/modules -o trans=virtio,version=9p2000.L,access=0,ro || \
-            log_error_msg "Unable to mount /lib/modules"
-        log_ok_msg "/root and /lib/modules mounted"
-
-        log_begin_msg "Mount /mnt/lab"
-        mkdir /tmp/target/overlay/mnt/lab
-        mount -t 9p labshare /tmp/target/overlay/mnt/lab -o trans=virtio,version=9p2000.L,access=any,rw || \
-            log_error_msg "Unable to mount /mnt/lab"
-        log_ok_msg "/mnt/lab mounted"
-
-        log_begin_msg "Mount /tmp/lab"
-        mkdir /tmp/target/overlay/tmp/lab
-        mount -t 9p tmpshare /tmp/target/overlay/tmp/lab -o trans=virtio,version=9p2000.L,access=any,rw || \
-            log_error_msg "Unable to mount /tmp/lab"
-        log_ok_msg "/tmp/lab mounted"
-
-        log_info_msg "Change root"
-        export STATE=CHROOTED
-        exec chroot /tmp/target/overlay /mnt/lab/tests/integration-tests
-        ;;
-
-    1,CHROOTED)
-        log_begin_msg "Starting udev"
-        udev_log=err
-        mount -n -o size=10M,mode=0755 -t devtmpfs devtmpfs /dev
-        udevadm info --cleanup-db
-        for udev in /lib/systemd/systemd-udevd /usr/lib/systemd/systemd-udevd $(command -v udevd 2> /dev/null); do
-            [ ! -x $udev ] || break
-        done
-        $udev --daemon
-        udevadm trigger --action=add
-        udevadm settle
-        log_ok_msg "udev started"
-
-        log_info_msg "Setup interfaces"
-        modprobe dummy 2>/dev/null || true
-        modprobe bonding 2>/dev/null || true
-        sleep 0.5               # Some interfaces may take some time to appear
-        # Rename all interfaces to "predictable" and "non-colliding"
-        # name. We don't have if we have eth* or ens* interfaces. Let
-        # take a totally different naming convention.
-        nb=1
-        for iface in $(echo /sys/bus/virtio/drivers/virtio_net/*/net/*); do
-            ip link set name iface$nb dev ${iface##*/}
-            nb=$((nb + 1))
-        done
-        for intf in /sys/class/net/*; do
-            intf=$(basename $intf)
-            ip a l dev $intf 2> /dev/null >/dev/null || continue
-            ip link set up dev $intf
-        done
-
-        log_info_msg "Setup IP addresses"
-        case $uts in
-            R1)
-                ip -4 addr add 192.0.2.15/24 dev iface1
-                ip -6 addr add 2001:db8::cafe:15/64 dev iface1
-                ;;
-            R2)
-                ip -4 addr add 192.0.2.16/24 dev iface1
-                ip -6 addr add 2001:db8::cafe:16/64 dev iface1
-                ;;
-            R3)
-                ip -4 addr add 192.0.2.17/24 dev iface1
-                ip -6 addr add 2001:db8::cafe:17/64 dev iface1
-                ;;
-        esac
-        rtmon file /mnt/lab/tests/${uts}.rtmon &
-        process_commands 2>&1 | tee /mnt/lab/tests/${uts}.output
-        ;;
-
-    *,BEGIN)
-        # Initial state
-        [ $(id -u) != 0 ] || {
-            log_error_msg "You should not run this as root"
-            exit 1
-        }
-        PROGNAME="$(readlink -f "$0")"
-        PROGARGS="$@"
-        ROOT="$(readlink -f "${ROOT:-/}")" # Root filesystem
-        KERNEL="$(readlink -f "${1:-/boot/vmlinuz-$(uname -r)}")" # Kernel
-        PATH="$PATH":/usr/local/sbin:/usr/sbin:/sbin
-        [ $# -lt 1 ] || shift
-        chmod +x "$PROGNAME"
-
-        check_kernel
-        check_dependencies
-        setup_tmp
-        setup_initrd
-
-        setup_switch 1
-        setup_switch 2
-        setup_switch 3
-        setup_switch 4
-        setup_switch 5
-        sleep 0.3
-
-        NET=1,2,3,4,5 start_vm  R1
-        NET=1,2,3     start_vm  R2
-        NET=1,4,5     start_vm  R3
-
-        start_tests
-
-        sed \
-            -e 's/^\(Interface:.*, Time: 0 day\).*/\1/' \
-            -e 's/^\(    SysDescr:\).*/\1/' \
-            -e 's/^\(      Hardware Revision: pc-i440fx\).*/\1/' \
-            -e 's/^\(      Software Revision: \).*/\1/' \
-            -e 's/^\(      Firmware Revision: \).*/\1/' \
-            -e 's/command \.\/libtool /command libtool /' \
-            R1.output > R1.output.redacted
-        diff -u @srcdir@/R1.expected R1.output.redacted || \
-            log_error_msg "Unexpected differences"
-
-        log_info_msg "End of tests"
-        ;;
-esac
-
-# Local Variables:
-# mode: sh
-# indent-tabs-mode: nil
-# sh-basic-offset: 4
-# End:
diff --git a/tests/integration/.gitignore b/tests/integration/.gitignore
new file mode 100644 (file)
index 0000000..36200cc
--- /dev/null
@@ -0,0 +1,6 @@
+# Python
+__pycache__
+*.pyc
+
+# py.test
+/.cache
diff --git a/tests/integration/README.md b/tests/integration/README.md
new file mode 100644 (file)
index 0000000..90940b0
--- /dev/null
@@ -0,0 +1,21 @@
+lldpd integration tests
+=======================
+
+To run those tests, you need Python 3.
+
+    $ virtualenv -p /usr/bin/python3 venv
+    $ . venv/bin/activate
+    $ pip install -r requirements.txt
+
+The tests rely on namespace support. Therefore, they only work on
+Linux. At least a 3.11 kernel is needed. While it would have been
+convenient to rely on a user namespace to avoid to run tests as root,
+there are restrictions that makes that difficult, notably we can only
+map one user to root and we have to map the current user and the
+_lldpd user.
+
+Then, tests can be run with:
+
+    $ sudo $(which py.test) -vv -n 10 --boxed
+
+Add an additional `-v` to get even more traces.
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
new file mode 100644 (file)
index 0000000..7468e3f
--- /dev/null
@@ -0,0 +1,15 @@
+import pytest
+from fixtures.programs import *
+from fixtures.namespaces import *
+from fixtures.network import *
+
+
+@pytest.yield_fixture(autouse=True, scope='session')
+def root():
+    """Ensure we are somewhat root."""
+    # We could do a user namespace but there are too many
+    # restrictions: we cannot do arbitrary user mapping and therefore,
+    # this doesn't play well with privilege separation and the use of
+    # _lldpd. Just do a plain namespace.
+    with Namespace('pid', 'net', 'mnt', 'ipc', 'uts'):
+        yield
diff --git a/tests/integration/fixtures/namespaces.py b/tests/integration/fixtures/namespaces.py
new file mode 100644 (file)
index 0000000..6257df2
--- /dev/null
@@ -0,0 +1,170 @@
+import contextlib
+import ctypes
+import errno
+import os
+import pyroute2
+import pytest
+import signal
+
+# All allowed namespace types
+NAMESPACE_FLAGS = dict(mnt=0x00020000,
+                       uts=0x04000000,
+                       ipc=0x08000000,
+                       user=0x10000000,
+                       pid=0x20000000,
+                       net=0x40000000)
+STACKSIZE = 1024*1024
+
+libc = ctypes.CDLL('libc.so.6', use_errno=True)
+
+
+@contextlib.contextmanager
+def keep_directory():
+    """Restore the current directory on exit."""
+    pwd = os.getcwd()
+    try:
+        yield
+    finally:
+        os.chdir(pwd)
+
+
+class Namespace(object):
+    """Combine several namespaces into one.
+
+    This gets a list of namespace types to create and combine into one. The
+    combined namespace can be used as a context manager to enter all the
+    created namespaces and exit them at the end.
+    """
+
+    def __init__(self, *namespaces):
+        self.namespaces = namespaces
+        for ns in namespaces:
+            assert ns in NAMESPACE_FLAGS
+
+        # Get a pipe to signal the future child to exit
+        self.pipe = os.pipe()
+
+        # First, create a child in the given namespaces
+        child = ctypes.CFUNCTYPE(ctypes.c_int)(self.child)
+        child_stack = ctypes.create_string_buffer(STACKSIZE)
+        child_stack_pointer = ctypes.c_void_p(
+            ctypes.cast(child_stack,
+                        ctypes.c_void_p).value + STACKSIZE)
+        flags = signal.SIGCHLD
+        for ns in namespaces:
+            flags |= NAMESPACE_FLAGS[ns]
+        pid = libc.clone(child, child_stack_pointer, flags)
+        if pid == -1:
+            e = ctypes.get_errno()
+            raise OSError(e, os.strerror(e))
+
+        # If a user namespace, map UID 0 to the current one
+        if 'user' in namespaces:
+            uid_map = '0 {} 1'.format(os.getuid())
+            gid_map = '0 {} 1'.format(os.getgid())
+            with open('/proc/{}/uid_map'.format(pid), 'w') as f:
+                f.write(uid_map)
+            with open('/proc/{}/setgroups'.format(pid), 'w') as f:
+                f.write('deny')
+            with open('/proc/{}/gid_map'.format(pid), 'w') as f:
+                f.write(gid_map)
+
+        # Retrieve a file descriptor to this new namespace
+        self.next = [os.open('/proc/{}/ns/{}'.format(pid, x),
+                             os.O_RDONLY) for x in namespaces]
+
+        # Keep a file descriptor to our old namespaces
+        self.previous = [os.open('/proc/self/ns/{}'.format(x),
+                                 os.O_RDONLY) for x in namespaces]
+
+        # Tell the child all is done and let it die
+        os.close(self.pipe[0])
+        if 'pid' not in namespaces:
+            os.close(self.pipe[1])
+            os.waitpid(pid, 0)
+
+    def child(self):
+        """Cloned child.
+
+        Just be here until our parent extract the file descriptor from
+        us.
+
+        """
+        os.close(self.pipe[1])
+
+        # For a network namespace, enable lo
+        if 'net' in self.namespaces:
+            ipr = pyroute2.IPRoute()
+            lo = ipr.link_lookup(ifname='lo')[0]
+            ipr.link('set', index=lo, state='up')
+        # For a mount namespace, make it private
+        if 'mnt' in self.namespaces:
+            libc.mount(b"none", b"/", None,
+                       # MS_REC | MS_PRIVATE
+                       16384 | (1 << 18),
+                       None)
+
+        while True:
+            try:
+                os.read(self.pipe[0], 1)
+            except OSError as e:
+                if e.errno in [errno.EAGAIN, errno.EINTR]:
+                    continue
+            break
+
+        os._exit(0)
+
+    def fd(self, namespace):
+        """Return the file descriptor associated to a namespace"""
+        assert namespace in self.namespaces
+        return self.next[self.namespaces.index(namespace)]
+
+    def __enter__(self):
+        with keep_directory():
+            for n in self.next:
+                if libc.setns(n, 0) == -1:
+                    ns = self.namespaces[self.next.index(n)]  # NOQA
+                    e = ctypes.get_errno()
+                    raise OSError(e, os.strerror(e))
+
+    def __exit__(self, *exc):
+        with keep_directory():
+            err = None
+            for p in reversed(self.previous):
+                if libc.setns(p, 0) == -1 and err is None:
+                    ns = self.namespaces[self.previous.index(p)]  # NOQA
+                    e = ctypes.get_errno()
+                    err = OSError(e, os.strerror(e))
+            if err:
+                raise err
+
+    def __repr__(self):
+        return 'Namespace({})'.format(", ".join(self.namespaces))
+
+
+class NamespaceFactory(object):
+    """Dynamically create namespaces as they are created.
+
+    Those namespaces are namespaces for IPC, net, mount and UTS. PID
+    is a bit special as we have to keep a process for that. We don't
+    do that to ensure that everything is cleaned
+    automatically. Therefore, the child process is killed as soon as
+    we got a file descriptor to the namespace. We don't use a user
+    namespace either because we are unlikely to be able to exit it.
+
+    """
+
+    def __init__(self):
+        self.namespaces = {}
+
+    def __call__(self, ns):
+        """Return a namespace. Create it if it doesn't exist."""
+        if ns in self.namespaces:
+            return self.namespaces[ns]
+        self.namespaces[ns] = Namespace('ipc', 'net', 'mnt', 'uts')
+        return self.namespaces[ns]
+
+
+@pytest.fixture
+def namespaces():
+    return NamespaceFactory()
diff --git a/tests/integration/fixtures/network.py b/tests/integration/fixtures/network.py
new file mode 100644 (file)
index 0000000..34fef84
--- /dev/null
@@ -0,0 +1,124 @@
+import pytest
+import pyroute2
+import struct
+from .namespaces import Namespace
+
+
+def int_to_mac(c):
+    """Turn an int into a MAC address."""
+    return ":".join(('{:02x}',)*6).format(
+        *struct.unpack('BBBBBB', c.to_bytes(6, byteorder='big')))
+
+
+class LinksFactory(object):
+    """A factory for veth pair of interfaces and other L2 stuff.
+
+    Each veth interfaces will get named ethX with X strictly
+    increasing at each call.
+
+    """
+
+    def __init__(self):
+        # We create all those links in a dedicated namespace to avoid
+        # conflict with other namespaces.
+        self.ns = Namespace('net')
+        self.count = 0
+
+    def __call__(self, *args):
+        return self.veth(*args)
+
+    def veth(self, ns1, ns2):
+        """Create a veth pair between two namespaces."""
+        with self.ns:
+            # First, create a link
+            first = 'eth{}'.format(self.count)
+            second = 'eth{}'.format(self.count + 1)
+            ipr = pyroute2.IPRoute()
+            ipr.link_create(ifname=first,
+                            peer=second,
+                            kind='veth')
+            idx = [ipr.link_lookup(ifname=x)[0]
+                   for x in (first, second)]
+
+            # Set an easy to remember MAC address
+            ipr.link('set', index=idx[0],
+                     address=int_to_mac(self.count + 1))
+            ipr.link('set', index=idx[1],
+                     address=int_to_mac(self.count + 2))
+
+            # Then, move each to the target namespace
+            ipr.link('set', index=idx[0], net_ns_fd=ns1.fd('net'))
+            ipr.link('set', index=idx[1], net_ns_fd=ns2.fd('net'))
+
+            # And put them up
+            with ns1:
+                ipr = pyroute2.IPRoute()
+                ipr.link('set', index=idx[0], state='up')
+            with ns2:
+                ipr = pyroute2.IPRoute()
+                ipr.link('set', index=idx[1], state='up')
+
+            self.count += 2
+
+    def bridge(self, name, *ifaces):
+        """Create a bridge."""
+        ipr = pyroute2.IPRoute()
+        # Create the bridge
+        ipr.link_create(ifname=name,
+                        kind='bridge')
+        idx = ipr.link_lookup(ifname=name)[0]
+        # Attach interfaces
+        for iface in ifaces:
+            port = ipr.link_lookup(ifname=iface)[0]
+            ipr.link('set', index=port, master=idx)
+        # Put the bridge up
+        ipr.link('set', index=idx, state='up')
+        return idx
+
+    def bond(self, name, *ifaces):
+        """Create a bond."""
+        ipr = pyroute2.IPRoute()
+        # Create the bond
+        ipr.link_create(ifname=name,
+                        kind='bond')
+        idx = ipr.link_lookup(ifname=name)[0]
+        # Attach interfaces
+        for iface in ifaces:
+            slave = ipr.link_lookup(ifname=iface)[0]
+            ipr.link('set', index=slave, state='down')
+            ipr.link('set', index=slave, master=idx)
+        # Put the bond up
+        ipr.link('set', index=idx, state='up')
+        return idx
+
+    def vlan(self, name, id, iface):
+        """Create a VLAN."""
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname=iface)[0]
+        ipr.link_create(ifname=name,
+                        kind='vlan',
+                        vlan_id=id,
+                        link=idx)
+        idx = ipr.link_lookup(ifname=name)[0]
+        ipr.link('set', index=idx, state='up')
+        return idx
+
+    def up(self, name):
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname=name)[0]
+        ipr.link('set', index=idx, state='up')
+
+    def down(self, name):
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname=name)[0]
+        ipr.link('set', index=idx, state='down')
+
+    def remove(self, name):
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname=name)[0]
+        ipr.link_remove(idx)
+
+
+@pytest.fixture
+def links():
+    return LinksFactory()
diff --git a/tests/integration/fixtures/programs.py b/tests/integration/fixtures/programs.py
new file mode 100644 (file)
index 0000000..37e5fad
--- /dev/null
@@ -0,0 +1,357 @@
+import pytest
+import glob
+import os
+import pwd
+import grp
+import re
+import signal
+import subprocess
+import multiprocessing
+import uuid
+import time
+from collections import namedtuple
+
+
+def most_recent(*args):
+    """Return the most recent files matching one of the provided glob
+    expression."""
+    candidates = [l
+                  for location in args
+                  for l in glob.glob(location)]
+    candidates.sort(key=lambda x: os.stat(x).st_mtime)
+    assert len(candidates) > 0
+    return candidates[0]
+
+lldpcli_location = most_recent('../../src/client/lldpcli',
+                               '../../*/src/client/lldpcli')
+lldpd_location = most_recent('../../src/daemon/lldpd',
+                             '../../*/src/daemon/lldpd')
+
+
+def _replace_file(tmpdir, target, content):
+    tmpname = str(uuid.uuid1())
+    with tmpdir.join(tmpname).open("w") as tmp:
+        tmp.write(content)
+        subprocess.check_call(["mount", "-n", "--bind",
+                               str(tmpdir.join(tmpname)),
+                               target])
+
+
+@pytest.fixture
+def replace_file(tmpdir):
+    """Replace a file by another content by bind-mounting on it."""
+    return lambda target, content: _replace_file(tmpdir, target, content)
+
+
+def format_process_output(program, args, result):
+    """Return a string representing the result of a process."""
+    return "\n".join([
+        'P: {} {}'.format(program, " ".join(args)),
+        'C: {}'.format(os.getcwd()),
+        '\n'.join(['O: {}'.format(l)
+                   for l in result.stdout.decode(
+                           'ascii', 'ignore').strip().split('\n')]),
+        '\n'.join(['E: {}'.format(l)
+                   for l in result.stderr.decode(
+                           'ascii', 'ignore').strip().split('\n')]),
+        'S: {}'.format(result.returncode),
+        ''])
+
+
+class LldpdFactory(object):
+    """Factory for lldpd. When invoked, lldpd will configure the current
+    namespace to be in a reproducible environment and spawn itself in
+    the background. On termination, output will be logged to temporary
+    file.
+    """
+    def __init__(self, tmpdir, config):
+        """Create a new wrapped program."""
+        tmpdir.join('lldpd-outputs').ensure(dir=True)
+        self.tmpdir = tmpdir
+        self.config = config
+        self.pids = []
+        self.threads = []
+        self.counter = 0
+
+    def __call__(self, *args, sleep=2, silent=False):
+        self.counter += 1
+        self.setup_namespace("ns-{}".format(self.counter))
+        args = (self.config.option.verbose > 2 and "-dddd" or "-dd",
+                "-L",
+                lldpcli_location,
+                "-u",
+                str(self.tmpdir.join("ns", "lldpd.socket"))) + args
+        p = subprocess.Popen((lldpd_location,) + args,
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        self.pids.append(p.pid)
+        t = multiprocessing.Process(target=self.run, args=(p, args, silent))
+        self.threads.append(t)
+        t.start()
+        time.sleep(sleep)
+        return t
+
+    def run(self, p, args, silent):
+        stdout, stderr = p.communicate()
+        self.pids.remove(p.pid)
+        if not silent:
+            o = format_process_output("lldpd",
+                                      args,
+                                      namedtuple('ProcessResult',
+                                                 ['returncode',
+                                                  'stdout',
+                                                  'stderr'])(
+                                                      p.returncode,
+                                                      stdout,
+                                                      stderr))
+            self.tmpdir.join('lldpd-outputs', '{}-{}'.format(
+                os.getpid(),
+                p.pid)).write(o)
+
+    def killall(self):
+        for p in self.pids[:]:
+            os.kill(p, signal.SIGTERM)
+        for t in self.threads:
+            if t.is_alive():
+                t.join(1)
+        for p in self.pids[:]:
+            os.kill(p, signal.SIGKILL)
+        for t in self.threads:
+            if t.is_alive():
+                t.join(1)
+
+    def setup_namespace(self, name):
+        # Setup privsep. While not enforced, we assume we are running in a
+        # throwaway mount namespace.
+        tmpdir = self.tmpdir
+        if self.config.lldpd.privsep.enabled:
+            # Chroot
+            chroot = self.config.lldpd.privsep.chroot
+            if os.path.isdir(chroot):
+                subprocess.check_call(["mount", "-n",
+                                       "-t", "tmpfs",
+                                       "tmpfs", chroot])
+            else:
+                parent = os.path.abspath(os.path.join(chroot, os.pardir))
+                assert os.path.isdir(parent)
+                subprocess.check_call(["mount", "-n",
+                                       "-t", "tmpfs",
+                                       "tmpfs", parent])
+            # User/group
+            user = self.config.lldpd.privsep.user
+            group = self.config.lldpd.privsep.group
+            try:
+                pwd.getpwnam(user)
+                grp.getgrnam(group)
+            except KeyError:
+                passwd = ""
+                for l in open("/etc/passwd", "r").readlines():
+                    if not l.startswith("{}:".format(user)):
+                        passwd += l
+                passwd += "{}:x:39861:39861::{}:/bin/false\n".format(
+                    user, chroot)
+                group = ""
+                for l in open("/etc/group", "r").readlines():
+                    if not l.startswith("{}:".format(group)):
+                        group += l
+                group += "{}:x:39861:\n".format(group)
+                _replace_file(tmpdir, "/etc/passwd", passwd)
+                _replace_file(tmpdir, "/etc/group", group)
+
+        # Also setup the "namespace-dependant" directory
+        tmpdir.join("ns").ensure(dir=True)
+        subprocess.check_call(["mount", "-n", "--make-slave", "--make-private",
+                               "-t", "tmpfs",
+                               "tmpfs", str(tmpdir.join("ns"))])
+
+        # We also need a proper /etc/os-release
+        _replace_file(tmpdir, "/etc/os-release",
+                     """PRETTY_NAME="Spectacular GNU/Linux 2016"
+NAME="Spectacular GNU/Linux"
+ID=spectacular
+HOME_URL="https://www.example.com/spectacular"
+SUPPORT_URL="https://www.example.com/spectacular/support"
+BUG_REPORT_URL="https://www.example.com/spectacular/bugs"
+""")
+
+        # We also need a proper name
+        subprocess.check_call(["hostname", name])
+
+        # And we need to ensure name resolution is sane
+        _replace_file(tmpdir, "/etc/hosts",
+                     """
+127.0.0.1 localhost.localdomain localhost
+127.0.1.1 {name}.example.com {name}
+::1       ip6-localhost ip6-loopback
+""".format(name=name))
+        _replace_file(tmpdir, "/etc/nsswitch.conf",
+                     """
+passwd: files
+group: files
+shadow: files
+hosts: files
+networks: files
+protocols: files
+services: files
+""")
+
+        # Remove any config
+        path = os.path.join(self.config.lldpd.confdir, "lldpd.conf")
+        if os.path.isfile(path):
+            _replace_file(tmpdir, path, "")
+        path = os.path.join(self.config.lldpd.confdir, "lldpd.d")
+        if os.path.isdir(path):
+            subprocess.check_call(["mount", "-n", "-t", "tmpfs",
+                                   "tmpfs", path])
+
+
+@pytest.fixture()
+def lldpd(request, tmpdir):
+    """Execute ``lldpd``."""
+    p = LldpdFactory(tmpdir, request.config)
+    request.addfinalizer(p.killall)
+    return p
+
+
+@pytest.fixture()
+def lldpd1(lldpd, links, namespaces):
+    """Shortcut for a first lldpd daemon."""
+    links(namespaces(1), namespaces(2))
+    with namespaces(1):
+        lldpd()
+
+
+@pytest.fixture()
+def lldpcli(request, tmpdir):
+    """Execute ``lldpcli``."""
+    socketdir = tmpdir.join("ns", "lldpd.socket")
+    count = [0]
+
+    def run(*args):
+        cargs = ("-u", str(socketdir)) + args
+        p = subprocess.Popen((lldpcli_location,) + cargs,
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        stdout, stderr = p.communicate(timeout=30)
+        result = namedtuple('ProcessResult',
+                            ['returncode', 'stdout', 'stderr'])(
+                                p.returncode, stdout, stderr)
+        request.node.add_report_section(
+            'run', 'lldpcli output {}'.format(count[0]),
+            format_process_output("lldpcli", cargs, result))
+        count[0] += 1
+        # When keyvalue is requested, return a formatted result
+        if args[:2] == ("-f", "keyvalue"):
+            assert result.returncode == 0
+            out = {}
+            for k, v in [l.split('=', 2)
+                         for l in result.stdout.decode('ascii').split("\n")
+                         if '=' in l]:
+                if k in out:
+                    out[k] += [v]
+                else:
+                    out[k] = [v]
+            for k in out:
+                if len(out[k]) == 1:
+                    out[k] = out[k][0]
+            return out
+        # Otherwise, return the named tuple
+        return result
+    return run
+
+
+def pytest_runtest_makereport(item, call):
+    """Collect outputs written to tmpdir and put them in report."""
+    # Only do that after tests are run, but not on teardown (too late)
+    if call.when != 'call':
+        return
+    # We can't wait for teardown, kill any running lldpd daemon right
+    # now. Otherwise, we won't get any output.
+    if "lldpd" in item.fixturenames and "lldpd" in item.funcargs:
+        lldpd = item.funcargs["lldpd"]
+        lldpd.killall()
+    if "tmpdir" in item.fixturenames and "tmpdir" in item.funcargs:
+        tmpdir = item.funcargs["tmpdir"]
+        if tmpdir.join('lldpd-outputs').check(dir=1):
+            for path in tmpdir.join('lldpd-outputs').visit():
+                item.add_report_section(
+                    call.when,
+                    'lldpd {}'.format(path.basename),
+                    path.read())
+
+
+def pytest_configure(config):
+    """Put lldpd/lldpcli configuration into the config object."""
+    output = subprocess.check_output([lldpcli_location, "-vv"])
+    output = output.decode('ascii')
+    config.lldpcli = namedtuple(
+        'lldpcli',
+        ['version',
+         'outputs'])(
+             re.search(
+                 r"^lldpcli (.*)$", output,
+                 re.MULTILINE).group(1),
+             re.search(
+                 r"^Additional output formats:\s+(.*)$",
+                 output,
+                 re.MULTILINE).group(1).split(", "))
+    output = subprocess.check_output([lldpd_location, "-vv"])
+    output = output.decode('ascii')
+    if {"enabled": True,
+        "disabled": False}[re.search(r"^Privilege separation:\s+(.*)$",
+                                     output, re.MULTILINE).group(1)]:
+        privsep = namedtuple('privsep',
+                             ['user',
+                              'group',
+                              'chroot',
+                              'enabled'])(
+                                  re.search(
+                                      r"^Privilege separation user:\s+(.*)$",
+                                      output,
+                                      re.MULTILINE).group(1),
+                                  re.search(
+                                      r"^Privilege separation group:\s+(.*)$",
+                                      output,
+                                      re.MULTILINE).group(1),
+                                  re.search(
+                                      r"^Privilege separation chroot:\s(.*)$",
+                                      output,
+                                      re.MULTILINE).group(1),
+                                  True)
+    else:
+        privsep = namedtuple('privsep',
+                             ['enabled'])(False)
+    config.lldpd = namedtuple('lldpd',
+                              ['features',
+                               'protocols',
+                               'confdir',
+                               'snmp',
+                               'privsep',
+                               'version'])(
+                                   re.search(
+                                       r"^Additional LLDP features:\s+(.*)$",
+                                       output,
+                                       re.MULTILINE).group(1).split(", "),
+                                   re.search(
+                                       r"^Additional protocols:\s+(.*)$",
+                                       output,
+                                       re.MULTILINE).group(1).split(", "),
+                                   re.search(
+                                       r"^Configuration directory:\s+(.*)$",
+                                       output, re.MULTILINE).group(1),
+                                   {"yes": True,
+                                    "no": False}[re.search(
+                                        r"^SNMP support:\s+(.*)$",
+                                        output,
+                                        re.MULTILINE).group(1)],
+                                   privsep,
+                                   re.search(r"^lldpd (.*)$",
+                                             output, re.MULTILINE).group(1))
+
+
+def pytest_report_header(config):
+    """Report lldpd/lldpcli version and configuration."""
+    print('lldpd: {} {}'.format(config.lldpd.version,
+                                ", ".join(config.lldpd.protocols +
+                                          config.lldpd.features)))
+    print('lldpcli: {} {}'.format(config.lldpcli.version,
+                                  ", ".join(config.lldpcli.outputs)))
diff --git a/tests/integration/requirements.txt b/tests/integration/requirements.txt
new file mode 100644 (file)
index 0000000..85d0278
--- /dev/null
@@ -0,0 +1,8 @@
+apipkg==1.4
+execnet==1.4.1
+pkg-resources==0.0.0
+py==1.4.31
+pyroute2==0.3.16
+pytest==2.9.0
+pytest-xdist==1.14
+wheel==0.29.0
diff --git a/tests/integration/test_basic.py b/tests/integration/test_basic.py
new file mode 100644 (file)
index 0000000..bc32fe4
--- /dev/null
@@ -0,0 +1,89 @@
+import time
+import pytest
+import pyroute2
+
+
+def test_one_neighbor(lldpd1, lldpd, lldpcli, namespaces):
+    with namespaces(2):
+        lldpd()
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out['lldp.eth0.age'].startswith('0 day, 00:00:')
+        assert out['lldp.eth0.chassis.descr'].startswith(
+            "Spectacular GNU/Linux 2016 Linux")
+        assert 'lldp.eth0.chassis.Router.enabled' in out
+        assert 'lldp.eth0.chassis.Station.enabled' in out
+        del out['lldp.eth0.age']
+        del out['lldp.eth0.chassis.descr']
+        del out['lldp.eth0.chassis.Router.enabled']
+        del out['lldp.eth0.chassis.Station.enabled']
+        assert out == {"lldp.eth0.via": "LLDP",
+                       "lldp.eth0.rid": "1",
+                       "lldp.eth0.chassis.mac": "00:00:00:00:00:02",
+                       "lldp.eth0.chassis.name": "ns-2.example.com",
+                       "lldp.eth0.chassis.mgmt-ip": "fe80::200:ff:fe00:2",
+                       "lldp.eth0.chassis.Bridge.enabled": "off",
+                       "lldp.eth0.chassis.Wlan.enabled": "off",
+                       "lldp.eth0.port.mac": "00:00:00:00:00:02",
+                       "lldp.eth0.port.descr": "eth1"}
+
+
+@pytest.mark.parametrize("neighbors", (5, 10, 50))
+def test_several_neighbors(lldpd, lldpcli, links, namespaces, neighbors):
+    for i in range(2, neighbors + 1):
+        links(namespaces(1), namespaces(i))
+    for i in range(1, neighbors + 1):
+        with namespaces(i):
+            lldpd(sleep=(i == 1 and 2 or 0),
+                  silent=True)
+    time.sleep(2)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        for i in range(2, neighbors + 1):
+            assert out['lldp.eth{}.chassis.name'.format((i - 2)*2)] == \
+                'ns-{}.example.com'.format(i)
+
+
+def test_overrided_description(lldpd1, lldpd, lldpcli, namespaces):
+    with namespaces(2):
+        lldpd("-S", "Modified description")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out['lldp.eth0.chassis.descr'] == "Modified description"
+
+
+def test_hide_kernel(lldpd1, lldpd, lldpcli, namespaces):
+    with namespaces(2):
+        lldpd("-k")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out["lldp.eth0.chassis.descr"] == \
+            "Spectacular GNU/Linux 2016"
+
+
+def test_listen_only(lldpd1, lldpd, lldpcli, namespaces):
+    with namespaces(2):
+        lldpd("-r")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out == {}
+
+
+def test_forced_management_address(lldpd1, lldpd, lldpcli, namespaces):
+    with namespaces(2):
+        lldpd("-m", "2001:db8::47")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out["lldp.eth0.chassis.mgmt-ip"] == "2001:db8::47"
+
+
+def test_management_address(lldpd1, lldpd, lldpcli, links, namespaces):
+    with namespaces(2):
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname="eth1")[0]
+        ipr.addr('add', index=idx, address="192.168.14.2", mask=24)
+        ipr.addr('add', index=idx, address="172.25.21.47", mask=24)
+        lldpd("-m", "172.25.*")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors")
+        assert out["lldp.eth0.chassis.mgmt-ip"] == "172.25.21.47"
diff --git a/tests/integration/test_custom.py b/tests/integration/test_custom.py
new file mode 100644 (file)
index 0000000..8b2d5d5
--- /dev/null
@@ -0,0 +1,70 @@
+import pytest
+import shlex
+import time
+
+
+@pytest.mark.skipif('Custom TLV' not in pytest.config.lldpd.features,
+                    reason="Custom TLV not supported")
+@pytest.mark.parametrize("commands, expected", [
+    (["oui 33,44,55 subtype 44"],
+     {'unknown-tlv.oui': '33,44,55',
+      'unknown-tlv.subtype': '44',
+      'unknown-tlv.len': '0'}),
+    (["oui 33,44,55 subtype 44 oui-info 45,45,45,45,45"],
+     {'unknown-tlv.oui': '33,44,55',
+      'unknown-tlv.subtype': '44',
+      'unknown-tlv.len': '5',
+      'unknown-tlv': '45,45,45,45,45'}),
+    (["oui 33,44,55 subtype 44 oui-info 45,45,45,45,45",
+      "add oui 33,44,55 subtype 44 oui-info 55,55,55,55,55",
+      "add oui 33,44,55 subtype 55 oui-info 65,65,65,65,65"],
+     {'unknown-tlv.oui': ['33,44,55', '33,44,55', '33,44,55'],
+      'unknown-tlv.subtype': ['44', '44', '55'],
+      'unknown-tlv.len': ['5', '5', '5'],
+      'unknown-tlv': ['45,45,45,45,45',
+                      '55,55,55,55,55',
+                      '65,65,65,65,65']}),
+    (["oui 33,44,55 subtype 44 oui-info 45,45,45,45,45",
+      "add oui 33,44,55 subtype 55 oui-info 65,65,65,65,65",
+      "replace oui 33,44,55 subtype 44 oui-info 66,66,66,66,66"],
+     {'unknown-tlv.oui': ['33,44,55', '33,44,55'],
+      'unknown-tlv.subtype': ['55', '44'],
+      'unknown-tlv.len': ['5', '5'],
+      'unknown-tlv': ['65,65,65,65,65',
+                      '66,66,66,66,66']}),
+    (["add oui 33,44,55 subtype 55 oui-info 65,65,65,65,65",
+      "replace oui 33,44,55 subtype 44 oui-info 66,66,66,66,66"],
+     {'unknown-tlv.oui': ['33,44,55', '33,44,55'],
+      'unknown-tlv.subtype': ['55', '44'],
+      'unknown-tlv.len': ['5', '5'],
+      'unknown-tlv': ['65,65,65,65,65',
+                      '66,66,66,66,66']}),
+    (["oui 33,44,55 subtype 44 oui-info 45,45,45,45,45",
+      "add oui 33,44,55 subtype 55 oui-info 55,55,55,55,55",
+      "-oui 33,44,55 subtype 55"],
+     {'unknown-tlv.oui': '33,44,55',
+      'unknown-tlv.subtype': '44',
+      'unknown-tlv.len': '5',
+      'unknown-tlv': '45,45,45,45,45'}),
+    (["oui 33,44,55 subtype 44 oui-info 45,45,45,45,45",
+      "add oui 33,44,55 subtype 55 oui-info 65,65,65,65,65",
+      "-"],
+     {})])
+def test_custom_tlv(lldpd1, lldpd, lldpcli, namespaces,
+                    commands, expected):
+    with namespaces(2):
+        lldpd()
+        for command in commands:
+            result = lldpcli(
+                *shlex.split("{}configure lldp custom-tlv {}".format(
+                    command.startswith("-") and "un" or "",
+                    command.lstrip("-"))))
+            assert result.returncode == 0
+        time.sleep(2)
+    with namespaces(1):
+        pfx = "lldp.eth0.unknown-tlvs."
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        out = {k[len(pfx):]: v
+               for k, v in out.items()
+               if k.startswith(pfx)}
+        assert out == expected
diff --git a/tests/integration/test_dot1.py b/tests/integration/test_dot1.py
new file mode 100644 (file)
index 0000000..79f120f
--- /dev/null
@@ -0,0 +1,30 @@
+import pytest
+
+
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+class TestLldpDot1(object):
+
+    def test_one_vlan(self, lldpd1, lldpd, lldpcli, namespaces, links):
+        with namespaces(2):
+            links.vlan('vlan100', 100, 'eth1')
+            lldpd()
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            assert out['lldp.eth0.vlan'] == 'vlan100'
+            assert out['lldp.eth0.vlan.vlan-id'] == '100'
+
+    def test_several_vlans(self, lldpd1, lldpd, lldpcli, namespaces, links):
+        with namespaces(2):
+            for v in [100, 200, 300, 4000]:
+                links.vlan('vlan{}'.format(v), v, 'eth1')
+            lldpd()
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            # We know that lldpd is walking interfaces in index order
+            assert out['lldp.eth0.vlan'] == \
+                ['vlan100', 'vlan200', 'vlan300', 'vlan4000']
+            assert out['lldp.eth0.vlan.vlan-id'] == \
+                ['100', '200', '300', '4000']
+
+    # TODO: PI and PPVID (but lldpd doesn't know how to generate them)
diff --git a/tests/integration/test_dot3.py b/tests/integration/test_dot3.py
new file mode 100644 (file)
index 0000000..5f266da
--- /dev/null
@@ -0,0 +1,59 @@
+import pytest
+import pyroute2
+import shlex
+import time
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+class TestLldpDot3(object):
+
+    def test_aggregate(self, lldpd1, lldpd, lldpcli, namespaces, links):
+        links(namespaces(3), namespaces(2))  # Another link to setup a bond
+        with namespaces(2):
+            idx = links.bond('bond42', 'eth1', 'eth3')
+            lldpd()
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            assert out['lldp.eth0.port.descr'] == 'eth1'
+            assert out['lldp.eth0.port.aggregation'] == str(idx)
+
+    # TODO: unfortunately, with veth, it's not possible to get an
+    # interface with autoneg.
+
+    @pytest.mark.parametrize("command, expected", [
+        ("pse supported enabled paircontrol powerpairs spare class class-3",
+         {'supported': 'yes',
+          'enabled': 'yes',
+          'paircontrol': 'yes',
+          'device-type': 'PSE',
+          'pairs': 'spare',
+          'class': 'class 3'}),
+        ("pd supported enabled powerpairs spare class class-3 type 1 source "
+         "pse priority low requested 10000 allocated 15000",
+         {'supported': 'yes',
+          'enabled': 'yes',
+          'paircontrol': 'no',
+          'device-type': 'PD',
+          'pairs': 'spare',
+          'class': 'class 3',
+          'power-type': '1',
+          'source': 'Primary power source',
+          'priority': 'low',
+          'requested': '10000',
+          'allocated': '15000'})])
+    def test_power(self, lldpd1, lldpd, lldpcli, namespaces,
+                   command, expected):
+        with namespaces(2):
+            lldpd()
+            result = lldpcli(
+                *shlex.split("configure dot3 power {}".format(command)))
+            assert result.returncode == 0
+            time.sleep(2)
+        with namespaces(1):
+            pfx = "lldp.eth0.port.power."
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            out = {k[len(pfx):]: v
+                   for k, v in out.items()
+                   if k.startswith(pfx)}
+            assert out == expected
diff --git a/tests/integration/test_interfaces.py b/tests/integration/test_interfaces.py
new file mode 100644 (file)
index 0000000..d2252f6
--- /dev/null
@@ -0,0 +1,217 @@
+import pytest
+import pyroute2
+import time
+
+
+def test_simple_bridge(lldpd1, lldpd, lldpcli, namespaces, links):
+    links(namespaces(3), namespaces(2))  # Another link to setup a bridge
+    with namespaces(2):
+        links.bridge('br42', 'eth1', 'eth3')
+        lldpd()
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.chassis.Bridge.enabled'] == 'on'
+
+
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+@pytest.mark.parametrize('when', ['before', 'after'])
+def test_bridge_with_vlan(lldpd1, lldpd, lldpcli, namespaces, links, when):
+    links(namespaces(3), namespaces(2))  # Another link to setup a bridge
+    with namespaces(2):
+        if when == 'after':
+            lldpd()
+        links.bridge('br42', 'eth1', 'eth3')
+        links.vlan('vlan100', 100, 'br42')
+        links.vlan('vlan200', 200, 'br42')
+        links.vlan('vlan300', 300, 'br42')
+        if when == 'before':
+            lldpd()
+        else:
+            # IPv6 DAD may kick in
+            time.sleep(6)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == \
+            ['vlan100', 'vlan200', 'vlan300']
+        assert out['lldp.eth0.vlan.vlan-id'] == \
+            ['100', '200', '300']
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+@pytest.mark.parametrize('when', ['before', 'after'])
+def test_bond(lldpd1, lldpd, lldpcli, namespaces, links, when):
+    links(namespaces(3), namespaces(2))  # Another link to setup a bond
+    with namespaces(2):
+        if when == 'after':
+            lldpd()
+        idx = links.bond('bond42', 'eth3', 'eth1')
+        ipr = pyroute2.IPRoute()
+        # The bond has the MAC of eth3
+        assert ipr.get_links(idx)[0].get_attr('IFLA_ADDRESS') == \
+            "00:00:00:00:00:04"
+        if when == 'before':
+            lldpd()
+        else:
+            # IPv6 DAD may kick in
+            time.sleep(6)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.port.aggregation'] == str(idx)
+        # lldpd should be able to retrieve the right MAC
+        assert out['lldp.eth0.port.mac'] == '00:00:00:00:00:02'
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+@pytest.mark.parametrize('when', ['before', 'after'])
+def test_bond_with_vlan(lldpd1, lldpd, lldpcli, namespaces, links, when):
+    links(namespaces(3), namespaces(2))  # Another link to setup a bond
+    with namespaces(2):
+        if when == 'after':
+            lldpd()
+        links.bond('bond42', 'eth3', 'eth1')
+        links.vlan('vlan300', 300, 'bond42')
+        links.vlan('vlan301', 301, 'bond42')
+        links.vlan('vlan302', 302, 'bond42')
+        links.vlan('vlan303', 303, 'bond42')
+        if when == 'before':
+            lldpd()
+        else:
+            # IPv6 DAD may kick in
+            time.sleep(6)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == \
+            ['vlan300', 'vlan301', 'vlan302', 'vlan303']
+        assert out['lldp.eth0.vlan.vlan-id'] == \
+            ['300', '301', '302', '303']
+
+
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+@pytest.mark.parametrize('when', ['before', 'after'])
+def test_just_vlan(lldpd1, lldpd, lldpcli, namespaces, links, when):
+    with namespaces(2):
+        if when == 'after':
+            lldpd()
+        links.vlan('vlan300', 300, 'eth1')
+        links.vlan('vlan400', 400, 'eth1')
+        if when == 'before':
+            lldpd()
+        else:
+            time.sleep(6)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == ['vlan300', 'vlan400']
+        assert out['lldp.eth0.vlan.vlan-id'] == ['300', '400']
+
+
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+@pytest.mark.parametrize('kind', ['plain', 'bridge', 'bond'])
+def test_remove_vlan(lldpd1, lldpd, lldpcli, namespaces, links, kind):
+    with namespaces(2):
+        if kind == 'bond':
+            iface = 'bond42'
+            links.bond(iface, 'eth1')
+        elif kind == 'bridge':
+            iface = 'bridge42'
+            links.bridge(iface, 'eth1')
+        else:
+            assert kind == 'plain'
+            iface = 'eth1'
+        links.vlan('vlan300', 300, iface)
+        links.vlan('vlan400', 400, iface)
+        links.vlan('vlan500', 500, iface)
+        lldpd()
+        links.remove('vlan300')
+        time.sleep(4)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == ['vlan400', 'vlan500']
+        assert out['lldp.eth0.vlan.vlan-id'] == ['400', '500']
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+def test_unenslave_bond(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        links.bond('bond42', 'eth1')
+        lldpd()
+        links.remove('bond42')
+        links.up('eth1')
+        time.sleep(4)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert 'lldp.eth0.port.aggregation' not in out
+
+
+@pytest.mark.skipif('Dot1' not in pytest.config.lldpd.features,
+                    reason="Dot1 not supported")
+def test_unenslave_bond_with_vlan(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        links.bond('bond42', 'eth1')
+        links.vlan('vlan300', 300, 'bond42')
+        links.vlan('vlan400', 400, 'eth1')
+        lldpd()
+        links.remove('bond42')
+        links.up('eth1')
+        time.sleep(4)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == 'vlan400'
+        assert out['lldp.eth0.vlan.vlan-id'] == '400'
+
+
+def test_down_then_up(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        links.down('eth1')
+        lldpd()
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out == {}
+    with namespaces(2):
+        links.up('eth1')
+        time.sleep(2)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+
+
+def test_down_then_up_with_vlan(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        links.vlan('vlan300', 300, 'eth1')
+        links.vlan('vlan400', 400, 'eth1')
+        links.down('eth1')
+        lldpd()
+        links.up('eth1')
+        time.sleep(2)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.vlan'] == ['vlan300', 'vlan400']
+        assert out['lldp.eth0.vlan.vlan-id'] == ['300', '400']
+
+
+def test_new_interface(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        lldpd()
+        links(namespaces(1), namespaces(2))
+        time.sleep(2)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth2.port.descr'] == 'eth3'
+        assert out['lldp.eth0.rid'] == out['lldp.eth2.rid']  # Same chassis
diff --git a/tests/integration/test_lldpcli.py b/tests/integration/test_lldpcli.py
new file mode 100644 (file)
index 0000000..efd8000
--- /dev/null
@@ -0,0 +1,207 @@
+import pytest
+import time
+import re
+import platform
+import json
+import xml.etree.ElementTree as ET
+
+
+@pytest.fixture(scope='session')
+def uname():
+    return "{} {} {} {}".format(
+        platform.system(),
+        platform.release(),
+        platform.version(),
+        platform.machine())
+
+
+def test_text_output(lldpd1, lldpd, lldpcli, namespaces, uname):
+    with namespaces(2):
+        lldpd()
+    with namespaces(1):
+        result = lldpcli("show", "neighbors", "details")
+        assert result.returncode == 0
+        expected = """-------------------------------------------------------------------------------
+LLDP neighbors:
+-------------------------------------------------------------------------------
+Interface:    eth0, via: LLDP, RID: 1, Time: 0 day, 00:00:{seconds}
+  Chassis:
+    ChassisID:    mac 00:00:00:00:00:02
+    SysName:      ns-2.example.com
+    SysDescr:     Spectacular GNU/Linux 2016 {uname}
+    MgmtIP:       fe80::200:ff:fe00:2
+    Capability:   Bridge, off
+    Capability:   Router, {router}
+    Capability:   Wlan, off
+    Capability:   Station, {station}
+  Port:
+    PortID:       mac 00:00:00:00:00:02
+    PortDescr:    eth1
+-------------------------------------------------------------------------------
+"""
+        out = result.stdout.decode('ascii')
+        seconds = re.search(r'^Interface: .*(\d\d)$',
+                            out,
+                            re.MULTILINE).group(1)
+        router = re.search(r'^    Capability:   Router, (.*)$',
+                           out,
+                           re.MULTILINE).group(1)
+        station = re.search(r'^    Capability:   Station, (.*)$',
+                            out,
+                            re.MULTILINE).group(1)
+        out = re.sub(r' *$', '', out, flags=re.MULTILINE)
+        assert out == expected.format(seconds=seconds,
+                                      router=router,
+                                      station=station,
+                                      uname=uname)
+
+
+@pytest.mark.skipif('JSON' not in pytest.config.lldpcli.outputs,
+                    reason="JSON not supported")
+def test_json_output(lldpd1, lldpd, lldpcli, namespaces, uname):
+    with namespaces(2):
+        lldpd()
+    with namespaces(1):
+        result = lldpcli("-f", "json", "show", "neighbors", "details")
+        assert result.returncode == 0
+        out = result.stdout.decode('ascii')
+        j = json.loads(out)
+
+        eth0 = j['lldp']['interface']['eth0']
+        del eth0['age']
+        del eth0['chassis']['ns-2.example.com']['capability'][3]
+        del eth0['chassis']['ns-2.example.com']['capability'][1]
+        expected = {"lldp": {
+            "interface": {"eth0": {
+                "via": "LLDP",
+                "rid": "1",
+                "chassis": {
+                    "ns-2.example.com": {
+                        "id": {
+                            "type": "mac",
+                            "value": "00:00:00:00:00:02"
+                        },
+                        "descr": "Spectacular GNU/Linux 2016 {}".format(uname),
+                        "mgmt-ip": "fe80::200:ff:fe00:2",
+                        "capability": [
+                            {"type": "Bridge", "enabled": False},
+                            {"type": "Wlan", "enabled": False},
+                        ]
+                    }
+                },
+                "port": {
+                    "id": {
+                        "type": "mac",
+                        "value": "00:00:00:00:00:02"
+                    },
+                    "descr": "eth1"
+                }
+            }}
+        }}
+
+        assert j == expected
+
+
+@pytest.mark.skipif('XML' not in pytest.config.lldpcli.outputs,
+                    reason="XML not supported")
+def test_xml_output(lldpd1, lldpd, lldpcli, namespaces, uname):
+    with namespaces(2):
+        lldpd()
+    with namespaces(1):
+        result = lldpcli("-f", "xml", "show", "neighbors", "details")
+        assert result.returncode == 0
+        out = result.stdout.decode('ascii')
+        xml = ET.fromstring(out)
+
+        age = xml.findall('./interface[1]')[0].attrib['age']
+        router = xml.findall("./interface[1]/chassis/"
+                           "capability[@type='Router']")[0].attrib['enabled']
+        station = xml.findall("./interface[1]/chassis/"
+                            "capability[@type='Station']")[0].attrib['enabled']
+        expected = ET.fromstring("""<?xml version="1.0" encoding="UTF-8"?>
+<lldp label="LLDP neighbors">
+ <interface label="Interface" name="eth0" via="LLDP" rid="1" age="{age}">
+  <chassis label="Chassis">
+   <id label="ChassisID" type="mac">00:00:00:00:00:02</id>
+   <name label="SysName">ns-2.example.com</name>
+   <descr label="SysDescr">Spectacular GNU/Linux 2016 {uname}</descr>
+   <mgmt-ip label="MgmtIP">fe80::200:ff:fe00:2</mgmt-ip>
+   <capability label="Capability" type="Bridge" enabled="off"/>
+   <capability label="Capability" type="Router" enabled="{router}"/>
+   <capability label="Capability" type="Wlan" enabled="off"/>
+   <capability label="Capability" type="Station" enabled="{station}"/>
+  </chassis>
+  <port label="Port">
+   <id label="PortID" type="mac">00:00:00:00:00:02</id>
+   <descr label="PortDescr">eth1</descr>
+  </port>
+ </interface>
+</lldp>
+        """.format(age=age,
+                   router=router,
+                   station=station,
+                   uname=uname))
+        assert ET.tostring(xml) == ET.tostring(expected)
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+def test_configure_one_port(lldpd1, lldpd, lldpcli, namespaces, links):
+    links(namespaces(1), namespaces(2))
+    with namespaces(2):
+        lldpd()
+        result = lldpcli(*("configure ports eth3 dot3 power "
+                           "pse supported enabled paircontrol powerpairs "
+                           "spare class class-3").split())
+        assert result.returncode == 0
+        time.sleep(2)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert 'lldp.eth0.port.power.device-type' not in out
+        assert out['lldp.eth2.port.descr'] == 'eth3'
+        assert out['lldp.eth2.port.power.device-type'] == 'PSE'
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+def test_new_port_take_default(lldpd1, lldpd, lldpcli, namespaces, links):
+    with namespaces(2):
+        lldpd()
+        result = lldpcli(*("configure dot3 power "
+                           "pse supported enabled paircontrol powerpairs "
+                           "spare class class-3").split())
+        assert result.returncode == 0
+        time.sleep(2)
+    with namespaces(1):
+        # Check this worked
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth0.port.descr'] == 'eth1'
+        assert out['lldp.eth0.port.power.device-type'] == 'PSE'
+    links(namespaces(1), namespaces(2))
+    time.sleep(6)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth2.port.descr'] == 'eth3'
+        assert out['lldp.eth2.port.power.device-type'] == 'PSE'
+
+
+@pytest.mark.skipif('Dot3' not in pytest.config.lldpd.features,
+                    reason="Dot3 not supported")
+def test_port_keep_configuration(lldpd1, lldpd, lldpcli, namespaces, links):
+    links(namespaces(1), namespaces(2))
+    with namespaces(2):
+        lldpd()
+        result = lldpcli(*("configure ports eth3 dot3 power "
+                           "pse supported enabled paircontrol powerpairs "
+                           "spare class class-3").split())
+        assert result.returncode == 0
+        time.sleep(2)
+        links.down('eth3')
+        time.sleep(3)
+        links.up('eth3')
+        time.sleep(3)
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out['lldp.eth2.port.descr'] == 'eth3'
+        assert out['lldp.eth2.port.power.device-type'] == 'PSE'
diff --git a/tests/integration/test_med.py b/tests/integration/test_med.py
new file mode 100644 (file)
index 0000000..de40238
--- /dev/null
@@ -0,0 +1,128 @@
+import pytest
+import platform
+import time
+import shlex
+
+
+@pytest.mark.skipif('LLDP-MED' not in pytest.config.lldpd.features,
+                    reason="LLDP-MED not supported")
+class TestLldpMed(object):
+
+    @pytest.mark.parametrize("classe, expected", [
+        (1, "Generic Endpoint (Class I)"),
+        (2, "Media Endpoint (Class II)"),
+        (3, "Communication Device Endpoint (Class III)"),
+        (4, "Network Connectivity Device")])
+    def test_med_devicetype(self, lldpd1, lldpd, lldpcli, namespaces,
+                            classe, expected):
+        with namespaces(2):
+            lldpd("-M", str(classe))
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            assert out['lldp.eth0.lldp-med.device-type'] == expected
+
+    def test_med_capabilities(self, lldpd1, lldpd, lldpcli, namespaces):
+        with namespaces(2):
+            lldpd("-M", "2")
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            out = {k.split(".")[3]: v
+                   for k, v in out.items()
+                   if k.endswith('.available')}
+            assert out == {'Capabilities': 'yes',
+                           'Policy': 'yes',
+                           'Location': 'yes',
+                           'MDI/PSE': 'yes',
+                           'MDI/PD': 'yes',
+                           'Inventory': 'yes'}
+
+    def test_med_inventory(self, lldpd1, lldpd, lldpcli, namespaces,
+                           replace_file):
+        with namespaces(2):
+            # /sys/class/dmi/id/*
+            for what, value in dict(product_version="1.14",
+                                    bios_version="1.10",
+                                    product_serial="45872512",
+                                    sys_vendor="Spectacular",
+                                    product_name="Workstation",
+                                    chassis_asset_tag="487122").items():
+                replace_file("/sys/class/dmi/id/{}".format(what),
+                             value)
+            lldpd("-M", "1")
+        with namespaces(1):
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            assert out['lldp.eth0.chassis.name'] == 'ns-2.example.com'
+            assert out['lldp.eth0.lldp-med.inventory.hardware'] == '1.14'
+            assert out['lldp.eth0.lldp-med.inventory.firmware'] == '1.10'
+            assert out['lldp.eth0.lldp-med.inventory.serial'] == '45872512'
+            assert out['lldp.eth0.lldp-med.inventory.manufacturer'] == \
+                'Spectacular'
+            assert out['lldp.eth0.lldp-med.inventory.model'] == 'Workstation'
+            assert out['lldp.eth0.lldp-med.inventory.asset'] == '487122'
+            assert out['lldp.eth0.lldp-med.inventory.software'] == \
+                platform.release()
+
+    @pytest.mark.parametrize("command, pfx, expected", [
+        # Policies
+        ("policy application voice tagged vlan 500 priority voice dscp 46",
+         "policy",
+         {'apptype': 'Voice',
+          'defined': 'yes',
+          'priority': 'Voice',
+          'pcp': '5',
+          'dscp': '46',
+          'vlan.vid': '500'}),
+        ("policy application video-conferencing unknown dscp 3 priority video",
+         "policy",
+         {'apptype': 'Video Conferencing',
+          'defined': 'no',
+          'priority': 'Video',
+          'pcp': '4',
+          'dscp': '3'}),
+        # Locations
+        ("location coordinate latitude 48.58667N longitude 2.2014E "
+         "altitude 117.47 m datum WGS84",
+         "Coordinates",
+         {'geoid': 'WGS84',
+          'lat': '48.58666N',
+          'lon': '2.2013E',
+          'altitude.unit': 'm',
+          'altitude': '117.46'}),
+        ('location address country US language en_US street '
+         '"Commercial Road" city "Roseville"',
+         "Civic address",
+         {'country': 'US',
+          'language': 'en_US',
+          'city': 'Roseville',
+          'street': 'Commercial Road'}),
+        ('location elin 911',
+         "ELIN",
+         {'ecs': '911'}),
+        # Power
+        ("power pd source pse priority high value 5000",
+         "poe",
+         {'device-type': 'PD',
+          'source': 'PSE',
+          'priority': 'high',
+          'power': '5000'}),
+        ("power pse source backup priority critical value 300",
+         "poe",
+         {'device-type': 'PSE',
+          'source': 'Backup Power Source / Power Conservation Mode',
+          'priority': 'critical',
+          'power': '300'})])
+    def test_med_configuration(self, lldpd1, lldpd, lldpcli, namespaces,
+                               command, pfx, expected):
+        with namespaces(2):
+            lldpd("-M", "1")
+            result = lldpcli(
+                *shlex.split("configure med {}".format(command)))
+            assert result.returncode == 0
+            time.sleep(2)
+        with namespaces(1):
+            pfx = "lldp.eth0.lldp-med.{}.".format(pfx)
+            out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+            out = {k[len(pfx):]: v
+                   for k, v in out.items()
+                   if k.startswith(pfx)}
+            assert out == expected
diff --git a/tests/integration/test_protocols.py b/tests/integration/test_protocols.py
new file mode 100644 (file)
index 0000000..3704573
--- /dev/null
@@ -0,0 +1,83 @@
+import pytest
+import pyroute2
+
+
+@pytest.mark.skipif('CDP' not in pytest.config.lldpd.protocols,
+                    reason="CDP not supported")
+@pytest.mark.parametrize("argument, expected", [
+    ("-cc", "CDPv1"),
+    ("-ccc", "CDPv2")])
+def test_cdp(lldpd, lldpcli, links, namespaces,
+             argument, expected):
+    links(namespaces(1), namespaces(2))
+    with namespaces(1):
+        lldpd("-c", "-ll", "-r")
+    with namespaces(2):
+        lldpd(argument, "-ll")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out["lldp.eth0.via"] == expected
+        assert out["lldp.eth0.chassis.local"] == "ns-2.example.com"
+        assert out["lldp.eth0.chassis.name"] == "ns-2.example.com"
+        assert out["lldp.eth0.chassis.descr"].startswith(
+            "Linux running on Spectacular GNU/Linux 2016")
+        assert out["lldp.eth0.port.ifname"] == "eth1"
+        assert out["lldp.eth0.port.descr"] == "eth1"
+
+
+@pytest.mark.skipif('FDP' not in pytest.config.lldpd.protocols,
+                    reason="FDP not supported")
+def test_fdp(lldpd, lldpcli, links, namespaces):
+    links(namespaces(1), namespaces(2))
+    with namespaces(1):
+        lldpd("-f", "-ll", "-r")
+    with namespaces(2):
+        lldpd("-ff", "-ll")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out["lldp.eth0.via"] == "FDP"
+        assert out["lldp.eth0.chassis.local"] == "ns-2.example.com"
+        assert out["lldp.eth0.chassis.name"] == "ns-2.example.com"
+        assert out["lldp.eth0.chassis.descr"].startswith(
+            "Linux running on Spectacular GNU/Linux 2016")
+        assert out["lldp.eth0.port.ifname"] == "eth1"
+        assert out["lldp.eth0.port.descr"] == "eth1"
+
+
+@pytest.mark.skipif('EDP' not in pytest.config.lldpd.protocols,
+                    reason="EDP not supported")
+def test_edp(lldpd, lldpcli, links, namespaces):
+    links(namespaces(1), namespaces(2))
+    with namespaces(1):
+        lldpd("-e", "-ll", "-r")
+    with namespaces(2):
+        lldpd("-ee", "-ll")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out["lldp.eth0.via"] == "EDP"
+        assert out["lldp.eth0.chassis.mac"] == "00:00:00:00:00:02"
+        assert out["lldp.eth0.chassis.name"] == "ns-2.example.com"
+        assert out["lldp.eth0.chassis.descr"] == \
+            "EDP enabled device, version 7.6.4.99"
+        assert out["lldp.eth0.port.ifname"] == "1/2"
+        assert out["lldp.eth0.port.descr"] == "Slot 1 / Port 2"
+
+
+@pytest.mark.skipif('SONMP' not in pytest.config.lldpd.protocols,
+                    reason="SONMP not supported")
+def test_sonmp(lldpd, lldpcli, links, namespaces):
+    links(namespaces(1), namespaces(2))
+    with namespaces(1):
+        lldpd("-s", "-ll", "-r")
+    with namespaces(2):
+        ipr = pyroute2.IPRoute()
+        idx = ipr.link_lookup(ifname="eth1")[0]
+        ipr.addr('add', index=idx, address="192.168.14.2", mask=24)
+        lldpd("-ss", "-ll")
+    with namespaces(1):
+        out = lldpcli("-f", "keyvalue", "show", "neighbors", "details")
+        assert out["lldp.eth0.via"] == "SONMP"
+        assert out["lldp.eth0.chassis.name"] == "192.168.14.2"
+        assert out["lldp.eth0.chassis.descr"] == "unknown (via SONMP)"
+        assert out["lldp.eth0.port.local"] == "00-00-02"
+        assert out["lldp.eth0.port.descr"] == "port 2"
index 1e5e33536df26cc476fb34c1de40d261c38a15aa..4c04822213288610dc6efede922e8a87e441e842 100644 (file)
@@ -47,7 +47,7 @@ configure med location coordinate latitude 48.58667N longitude 2.2014E altitude
 configure med location address country US language en_US street "Commercial Road" city "Roseville"
 configure med location elin 911
 configure ports eth0 med location elin 911
-configure med policy application voice vlan 500 priority voice dscp 46
+configure med policy application voice tagged vlan 500 priority voice dscp 46
 configure ports eth0 med policy application voice vlan 500 priority voice dscp 46
 configure med power pd source pse priority high value 5000
 configure ports eth0 med power pd source pse priority high value 5000