Test: Adding dhcprelay tests.
Added isc-dhcp-server for cord-test container builds.
If cord-test container already exists, refresh using:
 cord-test.py build test

Change-Id: I1aaff75612b7551fc9fd145fe2483ab72b5c3128
diff --git a/Dockerfile.tester b/Dockerfile.tester
index 6cc827b..26ddf33 100644
--- a/Dockerfile.tester
+++ b/Dockerfile.tester
@@ -6,7 +6,7 @@
         unzip libpcre3-dev flex bison libboost-dev \
         python python-pip python-setuptools python-scapy tcpdump doxygen doxypy wget \
         openvswitch-common openvswitch-switch \
-        python-twisted python-sqlite sqlite3 python-pexpect telnet arping
+        python-twisted python-sqlite sqlite3 python-pexpect telnet arping isc-dhcp-server
 RUN easy_install nose
 RUN mkdir -p /root/ovs
 WORKDIR /root
diff --git a/src/test/dhcprelay/__init__.py b/src/test/dhcprelay/__init__.py
new file mode 100644
index 0000000..0a5ce19
--- /dev/null
+++ b/src/test/dhcprelay/__init__.py
@@ -0,0 +1,24 @@
+#
+# Copyright 2016-present Ciena Corporation
+#
+# 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.
+#
+import os,sys
+##add the python path to lookup the utils
+working_dir = os.path.dirname(os.path.realpath(sys.argv[-1]))
+utils_dir = os.path.join(working_dir, '../utils')
+fsm_dir = os.path.join(working_dir, '../fsm')
+subscriber_dir = os.path.join(working_dir, '../subscriber')
+__path__.append(utils_dir)
+__path__.append(fsm_dir)
+__path__.append(subscriber_dir)
diff --git a/src/test/dhcprelay/dhcprelayTest.py b/src/test/dhcprelay/dhcprelayTest.py
new file mode 100644
index 0000000..b5b8546
--- /dev/null
+++ b/src/test/dhcprelay/dhcprelayTest.py
@@ -0,0 +1,189 @@
+#
+# Copyright 2016-present Ciena Corporation
+#
+# 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.
+#
+import unittest
+from nose.tools import *
+from nose.twistedtools import reactor, deferred
+from twisted.internet import defer
+from scapy.all import *
+import time
+import os
+from DHCP import DHCPTest
+from OnosCtrl import OnosCtrl
+from OnosFlowCtrl import get_mac
+from portmaps import g_subscriber_port_map
+log.setLevel('INFO')
+
+class dhcprelay_exchange(unittest.TestCase):
+
+    app = 'org.onosproject.dhcprelay'
+    app_dhcp = 'org.onosproject.dhcp'
+    relay_device_id = 'of:' + get_mac('ovsbr0')
+    relay_interface_port = 100
+    relay_interfaces = (g_subscriber_port_map[relay_interface_port],)
+    interface_to_mac_map = {}
+    dhcp_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'setup')
+    default_config = { 'default-lease-time' : 600, 'max-lease-time' : 7200, }
+    default_options = [ ('subnet-mask', '255.255.255.0'),
+                     ('broadcast-address', '192.168.1.255'),
+                     ('domain-name-servers', '192.168.1.1'),
+                     ('domain-name', '"mydomain.cord-tester"'),
+                   ]
+    ##specify the IP for the dhcp interface matching the subnet and subnet config
+    ##this is done for each interface dhcpd server would be listening on
+    default_subnet_config = [ ('192.168.1.2',
+'''
+subnet 192.168.1.0 netmask 255.255.255.0 {
+    range 192.168.1.10 192.168.1.100;
+}
+'''), ]
+
+    @classmethod
+    def setUpClass(cls):
+        ''' Activate the dhcprelay app'''
+        OnosCtrl(cls.app_dhcp).deactivate()
+        time.sleep(3)
+        cls.onos_ctrl = OnosCtrl(cls.app)
+        status, _ = cls.onos_ctrl.activate()
+        assert_equal(status, True)
+        time.sleep(3)
+        ##start dhcpd initially with default config
+        cls.dhcpd_start()
+        cls.onos_dhcp_relay_load()
+
+    @classmethod
+    def tearDownClass(cls):
+        '''Deactivate the dhcp relay app'''
+        try:
+            os.unlink('{}/dhcpd.conf'.format(cls.dhcp_data_dir))
+            os.unlink('{}/dhcpd.leases'.format(cls.dhcp_data_dir))
+        except: pass
+        cls.onos_ctrl.deactivate()
+        cls.dhcpd_stop()
+
+    @classmethod
+    def onos_load_config(cls, config):
+        status, code = OnosCtrl.config(config)
+        if status is False:
+            log.info('JSON request returned status %d' %code)
+            assert_equal(status, True)
+        time.sleep(3)
+
+    @classmethod
+    def onos_dhcp_relay_load(cls):
+        relay_device_map = '{}/{}'.format(cls.relay_device_id, cls.relay_interface_port)
+        dhcp_dict = {'apps':{'org.onosproject.dhcp-relay':{'dhcprelay':
+                                                          {'dhcpserverConnectPoint':relay_device_map}}}}
+        cls.onos_load_config(dhcp_dict)
+
+    @classmethod
+    def host_load(cls, iface):
+        '''Have ONOS discover the hosts for dhcp-relay responses'''
+        port = g_subscriber_port_map[iface]
+        host = '173.17.1.{}'.format(port)
+        cmds = ( 'ifconfig {} 0'.format(iface),
+                 'ifconfig {0} {1}'.format(iface, host),
+                 'arping -I {0} {1} -c 2'.format(iface, host),
+                 'ifconfig {} 0'.format(iface), )
+        for c in cmds:
+            os.system(c)
+
+    @classmethod
+    def dhcpd_conf_generate(cls, config = default_config, options = default_options,
+                            subnet = default_subnet_config):
+        conf = ''
+        for k, v in config.items():
+            conf += '{} {};\n'.format(k, v)
+
+        opts = ''
+        for k, v in options:
+            opts += 'option {} {};\n'.format(k, v)
+
+        subnet_config = ''
+        for _, v in subnet:
+            subnet_config += '{}\n'.format(v)
+
+        return '{}{}{}'.format(conf, opts, subnet_config)
+
+    @classmethod
+    def dhcpd_start(cls, intf_list = relay_interfaces,
+                    config = default_config, options = default_options,
+                    subnet = default_subnet_config):
+        '''Start the dhcpd server by generating the conf file'''
+        ##stop dhcpd if already running
+        cls.dhcpd_stop()
+        dhcp_conf = cls.dhcpd_conf_generate(config = config, options = options,
+                                            subnet = subnet)
+        ##first touch dhcpd.leases if it doesn't exist
+        lease_file = '{}/dhcpd.leases'.format(cls.dhcp_data_dir)
+        if os.access(lease_file, os.F_OK) is False:
+            with open(lease_file, 'w') as fd: pass
+
+        conf_file = '{}/dhcpd.conf'.format(cls.dhcp_data_dir)
+        with open(conf_file, 'w') as fd:
+            fd.write(dhcp_conf)
+
+        #now configure the dhcpd interfaces for various subnets
+        index = 0
+        for ip,_ in subnet:
+            intf = intf_list[index]
+            index += 1
+            os.system('ifconfig {} {}'.format(intf, ip))
+
+        intf_str = ','.join(intf_list)
+        dhcpd_cmd = '/usr/sbin/dhcpd -4 --no-pid -cf {0} -lf {1} {2}'.format(conf_file, lease_file, intf_str)
+        log.info('Starting DHCPD server with command: %s' %dhcpd_cmd)
+        ret = os.system(dhcpd_cmd)
+        assert_equal(ret, 0)
+        time.sleep(3)
+        cls.relay_interfaces = intf_list
+
+    @classmethod
+    def dhcpd_stop(cls):
+        os.system('pkill -9 dhcpd')
+        for intf in cls.relay_interfaces:
+            os.system('ifconfig {} 0'.format(intf))
+
+    def get_mac(self, iface):
+        if self.interface_to_mac_map.has_key(iface):
+            return self.interface_to_mac_map[iface]
+        mac = get_mac(iface, pad = 0)
+        self.interface_to_mac_map[iface] = mac
+        return mac
+
+    def send_recv(self, mac, update_seed = False, validate = True):
+        cip, sip = self.dhcp.discover(mac = mac, update_seed = update_seed)
+        if validate:
+            assert_not_equal(cip, None)
+            assert_not_equal(sip, None)
+            log.info('Got dhcp client IP %s from server %s for mac %s' %
+                     (cip, sip, self.dhcp.get_mac(cip)[0]))
+        return cip,sip
+
+    def test_dhcp_1request(self, iface = 'veth0'):
+        mac = self.get_mac(iface)
+        self.host_load(iface)
+        ##we use the defaults for this test that serves as an example for others
+        ##You don't need to restart dhcpd server if retaining default config
+        config = self.default_config
+        options = self.default_options
+        subnet = self.default_subnet_config
+        dhcpd_interface_list = self.relay_interfaces
+        self.dhcpd_start(intf_list = dhcpd_interface_list,
+                         config = config,
+                         options = options,
+                         subnet = subnet)
+        self.dhcp = DHCPTest(seed_ip = '10.10.10.1', iface = iface)
+        self.send_recv(mac)
diff --git a/src/test/setup/cord-test.py b/src/test/setup/cord-test.py
index 01d1ba2..d81e830 100755
--- a/src/test/setup/cord-test.py
+++ b/src/test/setup/cord-test.py
@@ -155,7 +155,7 @@
         unzip libpcre3-dev flex bison libboost-dev \
         python python-pip python-setuptools python-scapy tcpdump doxygen doxypy wget \
         openvswitch-common openvswitch-switch \
-        python-twisted python-sqlite sqlite3 python-pexpect telnet arping
+        python-twisted python-sqlite sqlite3 python-pexpect telnet arping isc-dhcp-server
 RUN easy_install nose
 RUN mkdir -p /root/ovs
 WORKDIR /root
diff --git a/src/test/utils/OnosFlowCtrl.py b/src/test/utils/OnosFlowCtrl.py
index 2810817..f1b72cf 100644
--- a/src/test/utils/OnosFlowCtrl.py
+++ b/src/test/utils/OnosFlowCtrl.py
@@ -27,7 +27,11 @@
         info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15]))
     except:
         info = ['0'] * 24
-    return '0'*pad + ''.join(['%02x' %ord(char) for char in info[18:24]])
+    s.close()
+    sep = ''
+    if pad == 0:
+        sep = ':'
+    return '0'*pad + sep.join(['%02x' %ord(char) for char in info[18:24]])
 
 class OnosFlowCtrl: