Add Vagrantfile for local testing. 23/523/11
authorStefan Kobza <skobza@cisco.com>
Sat, 5 Mar 2016 09:19:16 +0000 (10:19 +0100)
committerStefan Kobza <skobza@cisco.com>
Fri, 8 Apr 2016 09:57:10 +0000 (09:57 +0000)
Vagrantfile contains 3 VMs as of now, 2 DUTs 1 TG, with these notes:
 - login is csit/csit
 - by default provision script installs all deb packages from the dir
    where Vagrantfile is
 - developed for, and only tested on vbox (someone can pick up vmware)
 - All nodes have 1 shared mgmt network: 192.168.255.0/24
 - hosts have these IP addresses in host-only network
    TG : 192.168.255.100
    DUT1 : 192.168.255.101
    DUT2 : 192.168.255.102
 - script created to download MAC address information
 - PCI addresses are always the same for vbox (not sure about vmware)

HOWTO (will create a wiki page once one is created for CSIT project):
 - copy Vagrantfile to separate dir on host
 - vagrant up --parallel
    sit-back-and-relax
 - from VM that has access to the same host-only network (192.168.255.0 above)
    - copy your ssh-key to csit@192.168.255.{101,102,250} using
        ssh-copy-id
    - cd ${csit_dir}
    - virtualenv & pip as in README
    - export PYTHONPATH=${csit_dir}
    - resources/tools/topology/update_topology.py -v -f
        -o topologies/available/vagrant_pci.yaml \
        topologies/available/vagrant.yaml
    - pybot -L TRACE \
        -v TOPOLOGY_PATH:topologies/available/vagrant_pci.yaml -s \
        "ipv4" tests
    - see tests results

Change-Id: Ic27626605a9c820bca977b38f4e8ca37d1504ff5
Signed-off-by: Stefan Kobza <skobza@cisco.com>
docs/testing_in_vagrant [new file with mode: 0644]
resources/libraries/python/SetupFramework.py
resources/libraries/python/ssh.py
resources/tools/topology/update_topology.py [new file with mode: 0755]
resources/tools/vagrant/Vagrantfile [new file with mode: 0644]
resources/tools/vagrant/install_debs.sh [new file with mode: 0755]
topologies/available/vagrant.yaml [new file with mode: 0644]

diff --git a/docs/testing_in_vagrant b/docs/testing_in_vagrant
new file mode 100644 (file)
index 0000000..1007621
--- /dev/null
@@ -0,0 +1,16 @@
+HOWTO (will create a wiki page once one is created for CSIT project):
+ - copy Vagrantfile to separate dir on host
+ - vagrant up --parallel
+    sit-back-and-relax
+ - from VM that has access to the same host-only network (192.168.255.0 above)
+    - copy your ssh-key to csit@192.168.255.{101,102,250}
+    - cd ${csit_dir}
+    - virtualenv & pip as in README
+    - PYTHONPATH=`pwd` resources/tools/topology/update_topology.py \
+        topologies/available/vagrant.yaml \
+        -o topologies/available/vagrant_pci.yaml
+    - PYTHONPATH=`pwd` pybot -L TRACE \
+        -v TOPOLOGY_PATH:topologies/available/vagrant_pci.yaml -s \
+        "bridge domain" tests
+    - see tests results
+
index 2a0bd42..b3df489 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""This module exists to provide setup utilities for the framework on topology
+nodes. All tasks required to be run before the actual tests are started is
+supposed to end up here.
+"""
+
 import shlex
 from subprocess import Popen, PIPE, call
 from multiprocessing import Pool
 from tempfile import NamedTemporaryFile
 from os.path import basename
 import shlex
 from subprocess import Popen, PIPE, call
 from multiprocessing import Pool
 from tempfile import NamedTemporaryFile
 from os.path import basename
+
 from robot.api import logger
 from robot.libraries.BuiltIn import BuiltIn
 from robot.api import logger
 from robot.libraries.BuiltIn import BuiltIn
-from ssh import SSH
-from constants import Constants as con
-from topology import NodeType
 
 
-__all__ = ["SetupFramework"]
+from resources.libraries.python.ssh import SSH
+from resources.libraries.python.constants import Constants as con
+from resources.libraries.python.topology import NodeType
 
 
+__all__ = ["SetupFramework"]
 
 def pack_framework_dir():
     """Pack the testing WS into temp file, return its name."""
 
 def pack_framework_dir():
     """Pack the testing WS into temp file, return its name."""
@@ -48,6 +54,14 @@ def pack_framework_dir():
 
 
 def copy_tarball_to_node(tarball, node):
 
 
 def copy_tarball_to_node(tarball, node):
+    """Copy tarball file from local host to remote node.
+
+    :param tarball: path to tarball to upload
+    :param node: dictionary created from topology
+    :type tarball: string
+    :type node: dict
+    :return: nothing
+    """
     logger.console('Copying tarball to {0}'.format(node['host']))
     ssh = SSH()
     ssh.connect(node)
     logger.console('Copying tarball to {0}'.format(node['host']))
     ssh = SSH()
     ssh.connect(node)
@@ -56,6 +70,16 @@ def copy_tarball_to_node(tarball, node):
 
 
 def extract_tarball_at_node(tarball, node):
 
 
 def extract_tarball_at_node(tarball, node):
+    """Extract tarball at given node.
+
+    Extracts tarball using tar on given node to specific CSIT loocation.
+
+    :param tarball: path to tarball to upload
+    :param node: dictionary created from topology
+    :type tarball: string
+    :type node: dict
+    :return: nothing
+    """
     logger.console('Extracting tarball to {0} on {1}'.format(
         con.REMOTE_FW_DIR, node['host']))
     ssh = SSH()
     logger.console('Extracting tarball to {0} on {1}'.format(
         con.REMOTE_FW_DIR, node['host']))
     ssh = SSH()
@@ -63,7 +87,7 @@ def extract_tarball_at_node(tarball, node):
 
     cmd = 'sudo rm -rf {1}; mkdir {1} ; tar -zxf {0} -C {1}; ' \
         'rm -f {0}'.format(tarball, con.REMOTE_FW_DIR)
 
     cmd = 'sudo rm -rf {1}; mkdir {1} ; tar -zxf {0} -C {1}; ' \
         'rm -f {0}'.format(tarball, con.REMOTE_FW_DIR)
-    (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=30)
+    (ret_code, _, stderr) = ssh.exec_command(cmd, timeout=30)
     if 0 != ret_code:
         logger.error('Unpack error: {0}'.format(stderr))
         raise Exception('Failed to unpack {0} at node {1}'.format(
     if 0 != ret_code:
         logger.error('Unpack error: {0}'.format(stderr))
         raise Exception('Failed to unpack {0} at node {1}'.format(
@@ -77,9 +101,9 @@ def create_env_directory_at_node(node):
     ssh = SSH()
     ssh.connect(node)
     (ret_code, stdout, stderr) = ssh.exec_command(
     ssh = SSH()
     ssh.connect(node)
     (ret_code, stdout, stderr) = ssh.exec_command(
-            'cd {0} && rm -rf env && virtualenv env && '
-            '. env/bin/activate && '
-            'pip install -r requirements.txt'.format(con.REMOTE_FW_DIR), timeout=100)
+        'cd {0} && rm -rf env && virtualenv env && . env/bin/activate && '
+        'pip install -r requirements.txt'.format(con.REMOTE_FW_DIR),
+                                                 timeout=100)
     if 0 != ret_code:
         logger.error('Virtualenv creation error: {0}'.format(stdout + stderr))
         raise Exception('Virtualenv setup failed')
     if 0 != ret_code:
         logger.error('Virtualenv creation error: {0}'.format(stdout + stderr))
         raise Exception('Virtualenv setup failed')
@@ -87,18 +111,32 @@ def create_env_directory_at_node(node):
         logger.console('Virtualenv created on {0}'.format(node['host']))
 
 def setup_node(args):
         logger.console('Virtualenv created on {0}'.format(node['host']))
 
 def setup_node(args):
+    """Run all set-up methods for a node.
+
+    This method is used as map_async parameter. It receives tuple with all
+    parameters as passed to map_async function.
+
+    :param args: all parameters needed to setup one node
+    :type args: tuple
+    :return: nothing
+    """
     tarball, remote_tarball, node = args
     copy_tarball_to_node(tarball, node)
     extract_tarball_at_node(remote_tarball, node)
     if node['type'] == NodeType.TG:
         create_env_directory_at_node(node)
     tarball, remote_tarball, node = args
     copy_tarball_to_node(tarball, node)
     extract_tarball_at_node(remote_tarball, node)
     if node['type'] == NodeType.TG:
         create_env_directory_at_node(node)
-
+    logger.console('Setup of node {0} done'.format(node['host']))
 
 def delete_local_tarball(tarball):
 
 def delete_local_tarball(tarball):
-    call(shlex.split('sh -c "rm {0} > /dev/null 2>&1"'.format(tarball)))
+    """Delete local tarball to prevent disk pollution.
 
 
+    :param tarball: path to tarball to upload
+    :type tarball: string
+    :return: nothing
+    """
+    call(shlex.split('sh -c "rm {0} > /dev/null 2>&1"'.format(tarball)))
 
 
-class SetupFramework(object):
+class SetupFramework(object): # pylint: disable=too-few-public-methods
     """Setup suite run on topology nodes.
 
     Many VAT/CLI based tests need the scripts at remote hosts before executing
     """Setup suite run on topology nodes.
 
     Many VAT/CLI based tests need the scripts at remote hosts before executing
@@ -109,7 +147,8 @@ class SetupFramework(object):
     def __init__(self):
         pass
 
     def __init__(self):
         pass
 
-    def setup_framework(self, nodes):
+    @staticmethod
+    def setup_framework(nodes):
         """Pack the whole directory and extract in temp on each node."""
 
         tarball = pack_framework_dir()
         """Pack the whole directory and extract in temp on each node."""
 
         tarball = pack_framework_dir()
@@ -136,3 +175,5 @@ class SetupFramework(object):
         BuiltIn().set_log_level(log_level)
         logger.trace('Test framework copied to all topology nodes')
         delete_local_tarball(tarball)
         BuiltIn().set_log_level(log_level)
         logger.trace('Test framework copied to all topology nodes')
         delete_local_tarball(tarball)
+        logger.console('All nodes are ready')
+
index a94eec4..6914d52 100644 (file)
@@ -10,6 +10,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import socket
 import paramiko
 from paramiko import RSAKey
 import StringIO
 import paramiko
 from paramiko import RSAKey
 import StringIO
@@ -95,11 +96,17 @@ class SSH(object):
             self._ssh.get_transport().getpeername(), end-start))
 
         stdout = ""
             self._ssh.get_transport().getpeername(), end-start))
 
         stdout = ""
-        while True:
-            buf = chan.recv(self.__MAX_RECV_BUF)
-            stdout += buf
-            if not buf:
-                break
+        try:
+            while True:
+                buf = chan.recv(self.__MAX_RECV_BUF)
+                stdout += buf
+                if not buf:
+                    break
+        except socket.timeout:
+            logger.error('Caught timeout exception, current contents '
+                         'of buffer: {0}'.format(stdout))
+            raise
+
 
         stderr = ""
         while True:
 
         stderr = ""
         while True:
diff --git a/resources/tools/topology/update_topology.py b/resources/tools/topology/update_topology.py
new file mode 100755 (executable)
index 0000000..d7a3929
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/env python2.7
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This executable python module gathers MAC address data from topology nodes.
+It requires that all interfaces/port elements in topology have driver field.
+This script binds the port in given node to set linux kernel driver and
+extracts MAC address from it."""
+
+import sys
+import os
+import re
+from argparse import ArgumentParser
+
+import yaml
+
+from resources.libraries.python.ssh import SSH
+
+def load_topology(args):
+    """Load topology file referenced to by parameter passed to this script.
+
+    :param args: arguments parsed from commandline
+    :type args: ArgumentParser().parse_args()
+    :return: Python representation of topology yaml
+    :rtype: dict
+    """
+    data = None
+    with open(args.topology, 'r') as stream:
+        try:
+            data = yaml.load(stream)
+        except yaml.YAMLError as exc:
+            print 'Failed to load topology file: {0}'.format(args.topology)
+            print exc
+            raise
+
+    return data
+
+def ssh_no_error(ssh, cmd):
+    """Execute a command over ssh channel, and log and exit if the command
+    fials.
+
+    :param ssh: SSH() object connected to a node
+    :param cmd: Command line to execute on remote node
+    :type ssh: SSH() object
+    :type cmd: str
+    :return: stdout from the SSH command
+    :rtype: str
+    """
+    ret, stdo, stde = ssh.exec_command(cmd)
+    if 0 != ret:
+        print 'Command execution failed: "{}"'.format(cmd)
+        print 'stdout: {0}'.format(stdo)
+        print 'stderr: {0}'.format(stde)
+        raise RuntimeError('Unexpected ssh command failure')
+
+    return stdo
+
+def update_mac_addresses_for_node(node):
+    """For given node loop over all ports with PCI address and look for its MAC
+    address.
+
+    This function firstly unbinds the PCI device from its current driver
+    and binds it to linux kernel driver. After the device is bound to specific
+    linux kernel driver the MAC address is extracted from /sys/bus/pci location
+    and stored within the node dictionary that was passed to this function.
+    :param node: Node from topology
+    :type node: dict
+    :return: None
+    """
+    for port_name, port in node['interfaces'].items():
+        if not port.has_key('driver'):
+            err_msg = '{0} port {1} has no driver element, exiting'.format(
+                    node['host'], port_name)
+            raise RuntimeError(err_msg)
+
+        ssh = SSH()
+        ssh.connect(node)
+
+        # TODO: make following SSH commands into one-liner to save on SSH opers
+
+        # First unbind from current driver
+        drvr_dir_path = '/sys/bus/pci/devices/{0}/driver'.format(
+                port['pci_address'])
+        cmd = '''\
+            if [ -d {0} ]; then
+                echo {1} | sudo tee {0}/unbind ;
+            else
+                true Do not have to do anything, port already unbound ;
+            fi'''.format(drvr_dir_path, port['pci_address'])
+        ssh_no_error(ssh, cmd)
+
+        # Then bind to the 'driver' from topology for given port
+        cmd = 'echo {0} | sudo tee /sys/bus/pci/drivers/{1}/bind'.\
+                format(port['pci_address'], port['driver'])
+        ssh_no_error(ssh, cmd)
+
+        # Then extract the mac address and store it in the topology
+        cmd = 'cat /sys/bus/pci/devices/{0}/net/*/address'.format(
+                port['pci_address'])
+        mac = ssh_no_error(ssh, cmd).strip()
+        pattern = re.compile("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$")
+        if not pattern.match(mac):
+            raise RuntimeError('MAC address read from host {0} {1} is in '
+                                   'bad format "{2}"'.format(node['host'],
+                                       port['pci_address'], mac))
+        print '{0}: Found MAC address of PCI device {1}: {2}'.format(
+                node['host'], port['pci_address'], mac)
+        port['mac_address'] = mac
+
+def update_nodes_mac_addresses(topology):
+    """Loop over nodes in topology and get mac addresses for all listed ports
+    based on PCI addresses.
+
+    :param topology: Topology information with nodes
+    :type topology: dict
+    :return: None
+    """
+
+    for node in topology['nodes'].values():
+        update_mac_addresses_for_node(node)
+
+def dump_updated_topology(topology, args):
+    """Writes or prints out updated topology file.
+
+    :param topology: Topology information with nodes
+    :param args: arguments parsed from command line
+    :type topology: dict
+    :type args: ArgumentParser().parse_args()
+    :return: 1 if error occured, 0 if successful
+    :rtype: int
+    """
+
+    if args.output_file:
+        if not args.force:
+            if os.path.isfile(args.output_file):
+                print ('File {0} already exists. If you want to overwrite this '
+                       'file, add -f as a parameter to this script'.format(
+                           args.output_file))
+                return 1
+        with open(args.output_file, 'w') as stream:
+            yaml.dump(topology, stream, default_flow_style=False)
+    else:
+        print yaml.dump(topology, default_flow_style=False)
+    return 0
+
+def main():
+    """Main function"""
+    parser = ArgumentParser()
+    parser.add_argument('topology', help="Topology yaml file to read")
+    parser.add_argument('--output-file', '-o', help='Output file')
+    parser.add_argument('-f', '--force', help='Overwrite existing file',
+                        action='store_const', const=True)
+    parser.add_argument('--verbose', '-v', action='store_true')
+    args = parser.parse_args()
+
+    topology = load_topology(args)
+    update_nodes_mac_addresses(topology)
+    ret = dump_updated_topology(topology, args)
+
+    return ret
+
+
+if __name__ == "__main__":
+    sys.exit(main())
+
+
diff --git a/resources/tools/vagrant/Vagrantfile b/resources/tools/vagrant/Vagrantfile
new file mode 100644 (file)
index 0000000..3e18192
--- /dev/null
@@ -0,0 +1,86 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -*- mode: ruby -*-
+# vi: set ts=2 sw=2 sts=2 et ft=ruby :
+
+$user_addition = <<-SHELL
+    sudo deluser csit
+    sudo adduser --disabled-password --gecos "" csit
+    echo csit:csit | sudo chpasswd
+    sudo adduser csit vagrant
+    id csit
+SHELL
+
+$install_prereqs = <<-SHELL
+    sudo apt-get -y update
+    sudo apt-get -y -f install
+    sudo apt-get -y install python-virtualenv python-dev iproute2 debhelper dkms
+    sudo update-alternatives --install /bin/sh sh /bin/bash 100
+SHELL
+
+$install_vpp = <<-SHELL
+    sudo apt-get -y purge vpp\*
+    cd /vagrant
+    if [ -e /vagrant/vpp-*.deb ]; then
+        sudo dpkg -i vpp-*.deb
+    fi
+SHELL
+
+
+def add_dut(config, name, mgmt_ip, net1, net2)
+  config.vm.define name do |node|
+    node.vm.box = "puppetlabs/ubuntu-14.04-64-nocm"
+    node.vm.hostname = name
+    node.vm.provision "shell", inline: $user_addition
+    node.vm.provision "shell", inline: $install_prereqs
+    node.vm.provision "shell", inline: $install_vpp
+
+    node.vm.network "private_network", ip: mgmt_ip
+    node.vm.network "private_network", type: "dhcp", auto_config: false,
+        virtualbox__intnet: net1
+    node.vm.network "private_network", type: "dhcp", auto_config: false,
+        virtualbox__intnet: net2
+    node.vm.provider "virtualbox" do |vb|
+      vb.memory = "2048"
+      vb.customize ["modifyvm", :id, "--nicpromisc3", "allow-all"]
+      vb.customize ["modifyvm", :id, "--nicpromisc4", "allow-all"]
+    end
+  end
+
+end
+
+Vagrant.configure(2) do |config|
+  config.vm.define "tg" do |tg|
+    tg.vm.box = "puppetlabs/ubuntu-14.04-64-nocm"
+    tg.vm.hostname = "tg"
+
+    tg.vm.provision "shell", inline: $user_addition
+    tg.vm.provision "shell", inline: $install_prereqs
+    tg.vm.network "private_network", ip: '192.168.255.100/24'
+    tg.vm.network "private_network", type: "dhcp", auto_config: false,
+        virtualbox__intnet: "tg_dut1"
+    tg.vm.network "private_network", type: "dhcp", auto_config: false,
+        virtualbox__intnet: "tg_dut2"
+    tg.vm.provider "virtualbox" do |vb|
+      vb.memory = "2048"
+      vb.customize ["modifyvm", :id, "--nicpromisc3", "allow-all"]
+      vb.customize ["modifyvm", :id, "--nicpromisc4", "allow-all"]
+    end
+
+  end
+
+  add_dut(config, "dut1", "192.168.255.101/24", "tg_dut1", "dut1_dut2")
+  add_dut(config, "dut2", "192.168.255.102/24", "tg_dut2", "dut1_dut2")
+end
+
diff --git a/resources/tools/vagrant/install_debs.sh b/resources/tools/vagrant/install_debs.sh
new file mode 100755 (executable)
index 0000000..5ace4ba
--- /dev/null
@@ -0,0 +1,37 @@
+#!/bin/bash
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+set -x
+
+USERNAME=csit
+
+function ssh_do_duts {
+    ssh ${USERNAME}@192.168.255.101 ${@} || exit
+    ssh ${USERNAME}@192.168.255.102 ${@} || exit
+}
+
+rsync -avz ${@} ${USERNAME}@192.168.255.101:/tmp/ || exit
+rsync -avz ${@} ${USERNAME}@192.168.255.102:/tmp/ || exit
+
+ssh_do_duts "sudo apt-get -y purge 'vpp.*' ; exit 0"
+ssh_do_duts "sudo dpkg -i /tmp/vpp*.deb"
+ssh_do_duts "echo 128 | sudo tee /proc/sys/vm/nr_hugepages"
+ssh_do_duts "sudo rm -f /etc/vpp/startup.conf.orig ; sudo cp /etc/vpp/startup.conf /etc/vpp/startup.conf.orig"
+ssh_do_duts "sudo rm /etc/vpp/startup.conf"
+ssh_do_duts "sudo sed -e 's/socket-mem [0-9]*/socket-mem 128/' /etc/vpp/startup.conf.orig | sudo tee /etc/vpp/startup.conf"
+ssh_do_duts "echo heapsize 512M | sudo tee -a /etc/vpp/startup.conf"
+ssh_do_duts "sudo sed -e 's/vm.nr_hugepages=.*/vm.nr_hugepages=128/' -i /etc/sysctl.d/80-vpp.conf"
+ssh_do_duts "sudo sed -e 's/vm.max_map_count=.*/vm.max_map_count=256/' -i /etc/sysctl.d/80-vpp.conf"
+
+
+echo Success!
diff --git a/topologies/available/vagrant.yaml b/topologies/available/vagrant.yaml
new file mode 100644 (file)
index 0000000..6c724c4
--- /dev/null
@@ -0,0 +1,60 @@
+---
+metadata:
+  version: 0.1
+  schema:
+    - resources/topology_schemas/3_node_topology.sch.yaml
+    - resources/topology_schemas/topology.sch.yaml
+  tags: [vagrant, 3-node]
+
+nodes:
+  TG:
+    type: TG
+    host: "192.168.255.100"
+    port: 22
+    username: csit
+    password: csit
+    interfaces:
+      port3:
+        mac_address: ""
+        pci_address: "0000:00:09.0"
+        link: link1
+        driver: e1000
+      port5:
+        mac_address: ""
+        pci_address: "0000:00:0a.0"
+        link: link2
+        driver: e1000
+  DUT1:
+    type: DUT
+    host: "192.168.255.101"
+    port: 22
+    username: csit
+    password: csit
+    interfaces:
+      port1:
+        mac_address: ""
+        pci_address: "0000:00:09.0"
+        link: link1
+        driver: e1000
+      port3:
+        mac_address: ""
+        pci_address: "0000:00:0a.0"
+        link: link3
+        driver: e1000
+  DUT2:
+    type: DUT
+    host: "192.168.255.102"
+    port: 22
+    username: csit
+    password: csit
+    interfaces:
+      port1:
+        mac_address: ""
+        pci_address: "0000:00:09.0"
+        link: link2
+        driver: e1000
+      port3:
+        mac_address: ""
+        pci_address: "0000:00:0a.0"
+        link: link3
+        driver: e1000