Adds scenario for DNS-nameserver configuration

Tests that subnet's DNS server configurations are effecting the VMs
1. Configure subnet with DNS nameserver
2. Retrieve the VM's configured DNS and verify it matches the one configured
   for the subnet.
3. Update subnet's DNS
4. Retrieve the VM's configured DNS and verify it matches the new one
   configured for the subnet.

DNS nameservers' addresses are arbitrary as any resolution check would be
testing either:
* L3 forwarding - tested in test_network_basic_ops
* External DNS service resolution - out of scope for Tempest tests

Adds dhcp_client to tempest.conf which should indicate the image DHCP client
used for renewing dhcp lease.
Supported: 'udhcpc', 'dhclient'
Empty value should skip subnet update parts of scenario

TODO: add support for dhcpcd client

Change-Id: Idd8d11929320aa9208dd14d718e1f8f264d9de80
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 61c840b..b1b1f07 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -909,6 +909,11 @@
 # operations testing. (integer value)
 #large_ops_number = 0
 
+# DHCP client used by images to renew DCHP lease. If left empty,
+# update operation will be skipped. Supported clients: "udhcpc",
+# "dhclient" (string value)
+#dhcp_client = udhcpc
+
 
 [service_available]
 
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index d8bfef8..6e61c55 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -97,6 +97,10 @@
         cmd = "/bin/ip addr | awk '/ether/ {print $2}'"
         return self.exec_command(cmd)
 
+    def get_nic_name(self, address):
+        cmd = "/bin/ip -o addr | awk '/%s/ {print $2}'" % address
+        return self.exec_command(cmd)
+
     def get_ip_list(self):
         cmd = "/bin/ip address"
         return self.exec_command(cmd)
@@ -116,3 +120,47 @@
         # Get pid(s) of a process/program
         cmd = "ps -ef | grep %s | grep -v 'grep' | awk {'print $1'}" % pr_name
         return self.exec_command(cmd).split('\n')
+
+    def get_dns_servers(self):
+        cmd = 'cat /etc/resolv.conf'
+        resolve_file = self.exec_command(cmd).strip().split('\n')
+        entries = (l.split() for l in resolve_file)
+        dns_servers = [l[1] for l in entries
+                       if len(l) and l[0] == 'nameserver']
+        return dns_servers
+
+    def send_signal(self, pid, signum):
+        cmd = 'sudo /bin/kill -{sig} {pid}'.format(pid=pid, sig=signum)
+        return self.exec_command(cmd)
+
+    def _renew_lease_udhcpc(self, fixed_ip=None):
+        """Renews DHCP lease via udhcpc client. """
+        file_path = '/var/run/udhcpc.'
+        nic_name = self.get_nic_name(fixed_ip)
+        nic_name = nic_name.strip().lower()
+        pid = self.exec_command('cat {path}{nic}.pid'.
+                                format(path=file_path, nic=nic_name))
+        pid = pid.strip()
+        self.send_signal(pid, 'USR1')
+
+    def _renew_lease_dhclient(self, fixed_ip=None):
+        """Renews DHCP lease via dhclient client. """
+        cmd = "sudo /sbin/dhclient -r && /sbin/dhclient"
+        self.exec_command(cmd)
+
+    def renew_lease(self, fixed_ip=None):
+        """Wrapper method for renewing DHCP lease via given client
+
+        Supporting:
+        * udhcpc
+        * dhclient
+        """
+        # TODO(yfried): add support for dhcpcd
+        suported_clients = ['udhcpc', 'dhclient']
+        dhcp_client = CONF.scenario.dhcp_client
+        if dhcp_client not in suported_clients:
+            raise exceptions.InvalidConfiguration('%s DHCP client unsupported'
+                                                  % dhcp_client)
+        if dhcp_client == 'udhcpc' and not fixed_ip:
+            raise ValueError("need to set 'fixed_ip' for udhcpc client")
+        return getattr(self, '_renew_lease_' + dhcp_client)(fixed_ip=fixed_ip)
\ No newline at end of file
diff --git a/tempest/config.py b/tempest/config.py
index 54a4dd1..4f545a3 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -849,7 +849,14 @@
         'large_ops_number',
         default=0,
         help="specifies how many resources to request at once. Used "
-        "for large operations testing.")
+        "for large operations testing."),
+    # TODO(yfried): add support for dhcpcd
+    cfg.StrOpt('dhcp_client',
+               default='udhcpc',
+               choices=["udhcpc", "dhclient"],
+               help='DHCP client used by images to renew DCHP lease. '
+                    'If left empty, update operation will be skipped. '
+                    'Supported clients: "udhcpc", "dhclient"')
 ]
 
 
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 9cb24b9..a0e6490 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -1012,12 +1012,16 @@
         router.update(admin_state_up=admin_state_up)
         self.assertEqual(admin_state_up, router.admin_state_up)
 
-    def create_networks(self, client=None, tenant_id=None):
+    def create_networks(self, client=None, tenant_id=None,
+                        dns_nameservers=None):
         """Create a network with a subnet connected to a router.
 
         The baremetal driver is a special case since all nodes are
         on the same shared network.
 
+        :param client: network client to create resources with.
+        :param tenant_id: id of tenant to create resources in.
+        :param dns_nameservers: list of dns servers to send to subnet.
         :returns: network, subnet, router
         """
         if CONF.baremetal.driver_enabled:
@@ -1033,7 +1037,12 @@
         else:
             network = self._create_network(client=client, tenant_id=tenant_id)
             router = self._get_router(client=client, tenant_id=tenant_id)
-            subnet = self._create_subnet(network=network, client=client)
+
+            subnet_kwargs = dict(network=network, client=client)
+            # use explicit check because empty list is a valid option
+            if dns_nameservers is not None:
+                subnet_kwargs['dns_nameservers'] = dns_nameservers
+            subnet = self._create_subnet(**subnet_kwargs)
             subnet.add_to_router(router.id)
         return network, subnet, router
 
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 30c3b9d..2cfec14 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -100,10 +100,10 @@
         self.keypairs = {}
         self.servers = []
 
-    def _setup_network_and_servers(self):
+    def _setup_network_and_servers(self, **kwargs):
         self.security_group = \
             self._create_security_group(tenant_id=self.tenant_id)
-        self.network, self.subnet, self.router = self.create_networks()
+        self.network, self.subnet, self.router = self.create_networks(**kwargs)
         self.check_networks()
 
         name = data_utils.rand_name('server-smoke')
@@ -425,3 +425,62 @@
         self.check_public_network_connectivity(
             should_connect=True, msg="after updating "
             "admin_state_up of router to True")
+
+    def _check_dns_server(self, ssh_client, dns_servers):
+        servers = ssh_client.get_dns_servers()
+        self.assertEqual(set(dns_servers), set(servers),
+                         'Looking for servers: {trgt_serv}. '
+                         'Retrieved DNS nameservers: {act_serv} '
+                         'From host: {host}.'
+                         .format(host=ssh_client.ssh_client.host,
+                                 act_serv=servers,
+                                 trgt_serv=dns_servers))
+
+    @testtools.skipUnless(CONF.scenario.dhcp_client,
+                          "DHCP client is not available.")
+    @test.attr(type='smoke')
+    @test.services('compute', 'network')
+    def test_subnet_details(self):
+        """Tests that subnet's extra configuration details are affecting
+        the VMs
+
+         NOTE: Neutron subnets push data to servers via dhcp-agent, so any
+         update in subnet requires server to actively renew its DHCP lease.
+
+         1. Configure subnet with dns nameserver
+         2. retrieve the VM's configured dns and verify it matches the one
+         configured for the subnet.
+         3. update subnet's dns
+         4. retrieve the VM's configured dns and verify it matches the new one
+         configured for the subnet.
+
+         TODO(yfried): add host_routes
+
+         any resolution check would be testing either:
+            * l3 forwarding (tested in test_network_basic_ops)
+            * Name resolution of an external DNS nameserver - out of scope for
+            Tempest
+        """
+        # this test check only updates (no actual resolution) so using
+        # arbitrary ip addresses as nameservers, instead of parsing CONF
+        initial_dns_server = '1.2.3.4'
+        alt_dns_server = '9.8.7.6'
+        self._setup_network_and_servers(dns_nameservers=[initial_dns_server])
+        self.check_public_network_connectivity(should_connect=True)
+
+        floating_ip, server = self.floating_ip_tuple
+        ip_address = floating_ip.floating_ip_address
+        private_key = self._get_server_key(server)
+        ssh_client = self._ssh_to_server(ip_address, private_key)
+
+        self._check_dns_server(ssh_client, [initial_dns_server])
+
+        self.subnet.update(dns_nameservers=[alt_dns_server])
+        # asserts that Neutron DB has updated the nameservers
+        self.assertEqual([alt_dns_server], self.subnet.dns_nameservers,
+                         "Failed to update subnet's nameservers")
+
+        # server needs to renew its dhcp lease in order to get the new dns
+        # definitions from subnet
+        ssh_client.renew_lease(fixed_ip=floating_ip['fixed_ip_address'])
+        self._check_dns_server(ssh_client, [alt_dns_server])
diff --git a/tempest/services/network/resources.py b/tempest/services/network/resources.py
index 513d2cf..4d45515 100644
--- a/tempest/services/network/resources.py
+++ b/tempest/services/network/resources.py
@@ -82,8 +82,10 @@
         self._router_ids = set()
 
     def update(self, *args, **kwargs):
-        result = self.client.update_subnet(subnet=self.id, *args, **kwargs)
-        super(DeletableSubnet, self).update(**result['subnet'])
+        result = self.client.update_subnet(self.id,
+                                           *args,
+                                           **kwargs)
+        return super(DeletableSubnet, self).update(**result['subnet'])
 
     def add_to_router(self, router_id):
         self._router_ids.add(router_id)