Merge "Adds scenario for DNS-nameserver configuration"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 88a56be..02609ae 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -920,6 +920,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 d42a805..dd693e5 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -860,7 +860,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 220a7e7..1fdd26a 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)