Add smoke tests for quantum.

 * Added test_network_basic_ops, a port of the devstack exercise
   script quantum-adv-test.sh.
 * Tenant network connectivity can be tested by setting
   the 'tenant_networks_reachable' key in tempest.conf to 'true'.
 * Public (floating ip) connectivity can be tested by setting the
   'public_network_id' key in tempest.conf.
 * Addresses bug 1043980

Change-Id: I506518c431a8da0b91e3044f2a6aabce48081d93
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index d537396..534f3d9 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -185,6 +185,33 @@
 # Catalog type of the Quantum Service
 catalog_type = network
 
+# This should be the username of a user WITHOUT administrative privileges
+username = demo
+# The above non-administrative user's password
+password = pass
+# The above non-administrative user's tenant name
+tenant_name = demo
+
+# A large private cidr block from which to allocate smaller blocks for
+# tenant networks.
+tenant_network_cidr = 10.100.0.0/16
+
+# The mask bits used to partition the tenant block.
+tenant_network_mask_bits = 29
+
+# If tenant networks are reachable, connectivity checks will be
+# performed directly against addresses on those networks.
+tenant_networks_reachable = false
+
+# Id of the public network that provides external connectivity.
+public_network_id = {$PUBLIC_NETWORK_UUID}
+
+# Id of a shared public router that provides external connectivity.
+# A shared public router would commonly be used where IP namespaces
+# were disabled.  If namespaces are enabled, it would be preferable
+# for each tenant to have their own router.
+public_router_id =
+
 [network-admin]
 # This section contains configuration options for an administrative
 # user of the Network API.
diff --git a/tempest/config.py b/tempest/config.py
index 60baa47..1d1aa49 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -378,6 +378,48 @@
         """Version of Quantum API"""
         return self.get("api_version", "v1.1")
 
+    @property
+    def username(self):
+        """Username to use for Quantum API requests."""
+        return self.get("username", "demo")
+
+    @property
+    def tenant_name(self):
+        """Tenant name to use for Quantum API requests."""
+        return self.get("tenant_name", "demo")
+
+    @property
+    def password(self):
+        """API key to use when authenticating as admin."""
+        return self.get("password", "pass")
+
+    @property
+    def tenant_network_cidr(self):
+        """The cidr block to allocate tenant networks from"""
+        return self.get("tenant_network_cidr", "10.100.0.0/16")
+
+    @property
+    def tenant_network_mask_bits(self):
+        """The mask bits for tenant networks"""
+        return int(self.get("tenant_network_mask_bits", "29"))
+
+    @property
+    def tenant_networks_reachable(self):
+        """Whether tenant network connectivity should be evaluated directly"""
+        return (
+            self.get("tenant_networks_reachable", 'false').lower() != 'false'
+        )
+
+    @property
+    def public_network_id(self):
+        """Id of the public network that provides external connectivity"""
+        return self.get("public_network_id", "")
+
+    @property
+    def public_router_id(self):
+        """Id of the public router that provides external connectivity"""
+        return self.get("public_router_id", "")
+
 
 class NetworkAdminConfig(BaseConfig):
 
@@ -385,7 +427,7 @@
 
     @property
     def username(self):
-        """Administrative Username to use for Quantum  API requests."""
+        """Administrative Username to use for Quantum API requests."""
         return self.get("username", "admin")
 
     @property
diff --git a/tempest/manager.py b/tempest/manager.py
index 8f8c0f8..92caf57 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -145,12 +145,16 @@
                                                     endpoint_type='publicURL')
         return glanceclient.Client('1', endpoint=endpoint, token=token)
 
-    def _get_identity_client(self):
+    def _get_identity_client(self, username=None, password=None,
+                             tenant_name=None):
         # This identity client is not intended to check the security
-        # of the identity service, so use admin credentials.
-        username = self.config.identity_admin.username
-        password = self.config.identity_admin.password
-        tenant_name = self.config.identity_admin.tenant_name
+        # of the identity service, so use admin credentials by default.
+        if not username:
+            username = self.config.identity_admin.username
+        if not password:
+            password = self.config.identity_admin.password
+        if not tenant_name:
+            tenant_name = self.config.identity_admin.tenant_name
 
         if None in (username, password, tenant_name):
             msg = ("Missing required credentials for identity client. "
diff --git a/tempest/tests/network/test_network_basic_ops.py b/tempest/tests/network/test_network_basic_ops.py
new file mode 100644
index 0000000..1d88759
--- /dev/null
+++ b/tempest/tests/network/test_network_basic_ops.py
@@ -0,0 +1,454 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+#    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 logging
+import subprocess
+
+import netaddr
+import nose
+
+from quantumclient.common import exceptions as exc
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import smoke
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class AttributeDict(dict):
+
+    """
+    Provide attribute access (dict.key) to dictionary values.
+    """
+
+    def __getattr__(self, name):
+        """Allow attribute access for all keys in the dict."""
+        if name in self:
+            return self[name]
+        return super(AttributeDict, self).__getattribute__(name)
+
+
+class DeletableResource(AttributeDict):
+
+    """
+    Support deletion of quantum resources (networks, subnets) via a
+    delete() method, as is supported by keystone and nova resources.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.client = kwargs.pop('client', None)
+        super(DeletableResource, self).__init__(*args, **kwargs)
+
+    def __str__(self):
+        return '<%s id="%s" name="%s">' % (self.__class__.__name__,
+                                           self.id, self.name)
+
+    def delete(self):
+        raise NotImplemented()
+
+
+class DeletableNetwork(DeletableResource):
+
+    def delete(self):
+        self.client.delete_network(self.id)
+
+
+class DeletableSubnet(DeletableResource):
+
+    _router_ids = set()
+
+    def add_to_router(self, router_id):
+        self._router_ids.add(router_id)
+        body = dict(subnet_id=self.id)
+        self.client.add_interface_router(router_id, body=body)
+
+    def delete(self):
+        for router_id in self._router_ids.copy():
+            body = dict(subnet_id=self.id)
+            self.client.remove_interface_router(router_id, body=body)
+            self._router_ids.remove(router_id)
+        self.client.delete_subnet(self.id)
+
+
+class DeletableRouter(DeletableResource):
+
+    def add_gateway(self, network_id):
+        body = dict(network_id=network_id)
+        self.client.add_gateway_router(self.id, body=body)
+
+    def delete(self):
+        self.client.remove_gateway_router(self.id)
+        self.client.delete_router(self.id)
+
+
+class DeletableFloatingIp(DeletableResource):
+
+    def delete(self):
+        self.client.delete_floatingip(self.id)
+
+
+class TestNetworkBasicOps(smoke.DefaultClientSmokeTest):
+
+    """
+    This smoke test suite assumes that Nova has been configured to
+    boot VM's with Quantum-managed networking, and attempts to
+    verify network connectivity as follows:
+
+     * For a freshly-booted VM with an IP address ("port") on a given network:
+
+       - the Tempest host can ping the IP address.  This implies that
+         the VM has been assigned the correct IP address and has
+         connectivity to the Tempest host.
+
+       #TODO(mnewby) - Need to implement the following:
+       - the Tempest host can ssh into the VM via the IP address and
+         successfully execute the following:
+
+         - ping an external IP address, implying external connectivity.
+
+         - ping an external hostname, implying that dns is correctly
+           configured.
+
+         - ping an internal IP address, implying connectivity to another
+           VM on the same network.
+
+     There are presumed to be two types of networks: tenant and
+     public.  A tenant network may or may not be reachable from the
+     Tempest host.  A public network is assumed to be reachable from
+     the Tempest host, and it should be possible to associate a public
+     ('floating') IP address with a tenant ('fixed') IP address to
+     faciliate external connectivity to a potentially unroutable
+     tenant IP address.
+
+     This test suite can be configured to test network connectivity to
+     a VM via a tenant network, a public network, or both.  If both
+     networking types are to be evaluated, tests that need to be
+     executed remotely on the VM (via ssh) will only be run against
+     one of the networks (to minimize test execution time).
+
+     Determine which types of networks to test as follows:
+
+     * Configure tenant network checks (via the
+       'tenant_networks_reachable' key) if the Tempest host should
+       have direct connectivity to tenant networks.  This is likely to
+       be the case if Tempest is running on the same host as a
+       single-node devstack installation with IP namespaces disabled.
+
+     * Configure checks for a public network if a public network has
+       been configured prior to the test suite being run and if the
+       Tempest host should have connectivity to that public network.
+       Checking connectivity for a public network requires that a
+       value be provided for 'public_network_id'.  A value can
+       optionally be provided for 'public_router_id' if tenants will
+       use a shared router to access a public network (as is likely to
+       be the case when IP namespaces are not enabled).  If a value is
+       not provided for 'public_router_id', a router will be created
+       for each tenant and use the network identified by
+       'public_network_id' as its gateway.
+
+    """
+
+    @classmethod
+    def check_preconditions(cls):
+        cfg = cls.config.network
+        msg = None
+        if not (cfg.tenant_networks_reachable or cfg.public_network_id):
+            msg = ('Either tenant_networks_reachable must be "true", or '
+                   'public_network_id must be defined.')
+        else:
+            try:
+                cls.network_client.list_networks()
+            except exc.QuantumClientException:
+                msg = 'Unable to connect to Quantum service.'
+
+        cls.enabled = not bool(msg)
+        if msg:
+            raise nose.SkipTest(msg)
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestNetworkBasicOps, cls).setUpClass()
+        cls.check_preconditions()
+        cfg = cls.config.network
+        cls.tenant_id = cls.manager._get_identity_client(
+            cfg.username,
+            cfg.password,
+            cfg.tenant_name).tenant_id
+        # TODO(mnewby) Consider looking up entities as needed instead
+        # of storing them as collections on the class.
+        cls.keypairs = {}
+        cls.security_groups = {}
+        cls.networks = []
+        cls.servers = []
+        cls.floating_ips = {}
+
+    def _create_keypair(self, client):
+        kp_name = rand_name('keypair-smoke-')
+        keypair = client.keypairs.create(kp_name)
+        try:
+            self.assertEqual(keypair.id, kp_name)
+            self.set_resource(kp_name, keypair)
+        except AttributeError:
+            self.fail("Keypair object not successfully created.")
+        return keypair
+
+    def _create_security_group(self, client):
+        # Create security group
+        sg_name = rand_name('secgroup-smoke-')
+        sg_desc = sg_name + " description"
+        secgroup = client.security_groups.create(sg_name, sg_desc)
+        try:
+            self.assertEqual(secgroup.name, sg_name)
+            self.assertEqual(secgroup.description, sg_desc)
+            self.set_resource(sg_name, secgroup)
+        except AttributeError:
+            self.fail("SecurityGroup object not successfully created.")
+
+        # Add rules to the security group
+        rulesets = [
+            {
+                # ssh
+                'ip_protocol': 'tcp',
+                'from_port': 22,
+                'to_port': 22,
+                'cidr': '0.0.0.0/0',
+                'group_id': secgroup.id
+            },
+            {
+                # ping
+                'ip_protocol': 'icmp',
+                'from_port': -1,
+                'to_port': -1,
+                'cidr': '0.0.0.0/0',
+                'group_id': secgroup.id
+            }
+        ]
+        for ruleset in rulesets:
+            try:
+                client.security_group_rules.create(secgroup.id, **ruleset)
+            except Exception:
+                self.fail("Failed to create rule in security group.")
+
+        return secgroup
+
+    def _get_router(self, tenant_id):
+        """Retrieve a router for the given tenant id.
+
+        If a public router has been configured, it will be returned.
+
+        If a public router has not been configured, but a public
+        network has, a tenant router will be created and returned that
+        routes traffic to the public network.
+
+        """
+        router_id = self.config.network.public_router_id
+        network_id = self.config.network.public_network_id
+        if router_id:
+            result = self.network_client.show_router(router_id)
+            return AttributeDict(**result['router'])
+        elif network_id:
+            router = self._create_router(tenant_id)
+            router.add_gateway(network_id)
+            return router
+        else:
+            raise Exception("Neither of 'public_router_id' or "
+                            "'public_network_id' has been defined.")
+
+    def _create_router(self, tenant_id):
+        name = rand_name('router-smoke-')
+        body = dict(
+            router=dict(
+                name=name,
+                admin_state_up=True,
+                tenant_id=tenant_id,
+            ),
+        )
+        result = self.network_client.create_router(body=body)
+        router = DeletableRouter(client=self.network_client,
+                                 **result['router'])
+        self.assertEqual(router.name, name)
+        self.set_resource(name, router)
+        return router
+
+    def _create_network(self, tenant_id):
+        name = rand_name('network-smoke-')
+        body = dict(
+            network=dict(
+                name=name,
+                tenant_id=tenant_id,
+            ),
+        )
+        result = self.network_client.create_network(body=body)
+        network = DeletableNetwork(client=self.network_client,
+                                   **result['network'])
+        self.assertEqual(network.name, name)
+        self.set_resource(name, network)
+        return network
+
+    def _create_subnet(self, network):
+        """
+        Create a subnet for the given network within the cidr block
+        configured for tenant networks.
+        """
+        cfg = self.config.network
+        tenant_cidr = netaddr.IPNetwork(cfg.tenant_network_cidr)
+        result = None
+        # Repeatedly attempt subnet creation with sequential cidr
+        # blocks until an unallocated block is found.
+        for subnet_cidr in tenant_cidr.subnet(cfg.tenant_network_mask_bits):
+            body = dict(
+                subnet=dict(
+                ip_version=4,
+                network_id=network.id,
+                tenant_id=network.tenant_id,
+                cidr=str(subnet_cidr),
+                ),
+            )
+            try:
+                result = self.network_client.create_subnet(body=body)
+                break
+            except exc.QuantumClientException as e:
+                is_overlapping_cidr = 'overlaps with another subnet' in str(e)
+                if not is_overlapping_cidr:
+                    raise
+        self.assertIsNotNone(result, 'Unable to allocate tenant network')
+        subnet = DeletableSubnet(client=self.network_client,
+                                 **result['subnet'])
+        self.assertEqual(subnet.cidr, str(subnet_cidr))
+        self.set_resource(rand_name('subnet-smoke-'), subnet)
+        return subnet
+
+    def _create_server(self, client, network, name, key_name, security_groups):
+        flavor_id = self.config.compute.flavor_ref
+        base_image_id = self.config.compute.image_ref
+        create_kwargs = {
+            'nics': [
+                {'net-id': network.id},
+            ],
+            'key_name': key_name,
+            'security_groups': security_groups,
+        }
+        server = client.servers.create(name, base_image_id, flavor_id,
+                                       **create_kwargs)
+        try:
+            self.assertEqual(server.name, name)
+            self.set_resource(name, server)
+        except AttributeError:
+            self.fail("Server not successfully created.")
+        self.status_timeout(client.servers, server.id, 'ACTIVE')
+        # The instance retrieved on creation is missing network
+        # details, necessitating retrieval after it becomes active to
+        # ensure correct details.
+        server = client.servers.get(server.id)
+        self.set_resource(name, server)
+        return server
+
+    def _create_floating_ip(self, server, external_network_id):
+        result = self.network_client.list_ports(device_id=server.id)
+        ports = result.get('ports', [])
+        self.assertEqual(len(ports), 1,
+                         "Unable to determine which port to target.")
+        port_id = ports[0]['id']
+        body = dict(
+            floatingip=dict(
+                floating_network_id=external_network_id,
+                port_id=port_id,
+                tenant_id=server.tenant_id,
+            )
+        )
+        result = self.network_client.create_floatingip(body=body)
+        floating_ip = DeletableFloatingIp(client=self.network_client,
+                                          **result['floatingip'])
+        self.set_resource(rand_name('floatingip-'), floating_ip)
+        return floating_ip
+
+    def _ping_ip_address(self, ip_address):
+        cmd = ['ping', '-c1', '-w1', ip_address]
+
+        def ping():
+            proc = subprocess.Popen(cmd,
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode == 0:
+                return True
+
+        # TODO(mnewby) Allow configuration of execution and sleep duration.
+        return test.call_until_true(ping, 20, 1)
+
+    def test_001_create_keypairs(self):
+        self.keypairs[self.tenant_id] = self._create_keypair(
+            self.compute_client)
+
+    def test_002_create_security_groups(self):
+        self.security_groups[self.tenant_id] = self._create_security_group(
+            self.compute_client)
+
+    def test_003_create_networks(self):
+        network = self._create_network(self.tenant_id)
+        router = self._get_router(self.tenant_id)
+        subnet = self._create_subnet(network)
+        subnet.add_to_router(router.id)
+        self.networks.append(network)
+
+    def test_004_create_servers(self):
+        if not (self.keypairs or self.security_groups or self.networks):
+            raise nose.SkipTest('Necessary resources have not been defined')
+        for i, network in enumerate(self.networks):
+            tenant_id = network.tenant_id
+            name = rand_name('server-smoke-%d-' % i)
+            keypair_name = self.keypairs[tenant_id].name
+            security_groups = [self.security_groups[tenant_id].name]
+            server = self._create_server(self.compute_client, network,
+                                         name, keypair_name, security_groups)
+            self.servers.append(server)
+
+    def test_005_check_tenant_network_connectivity(self):
+        if not self.config.network.tenant_networks_reachable:
+            msg = 'Tenant networks not configured to be reachable.'
+            raise nose.SkipTest(msg)
+        if not self.servers:
+            raise nose.SkipTest("No VM's have been created")
+        for server in self.servers:
+            for net_name, ip_addresses in server.networks.iteritems():
+                for ip_address in ip_addresses:
+                    self.assertTrue(self._ping_ip_address(ip_address),
+                                    "Timed out waiting for %s's ip to become "
+                                    "reachable" % server.name)
+
+    def test_006_assign_floating_ips(self):
+        public_network_id = self.config.network.public_network_id
+        if not public_network_id:
+            raise nose.SkipTest('Public network not configured')
+        if not self.servers:
+            raise nose.SkipTest("No VM's have been created")
+        for server in self.servers:
+            floating_ip = self._create_floating_ip(server, public_network_id)
+            self.floating_ips.setdefault(server, [])
+            self.floating_ips[server].append(floating_ip)
+
+    def test_007_check_public_network_connectivity(self):
+        if not self.floating_ips:
+            raise nose.SkipTest('No floating ips have been allocated.')
+        for server, floating_ips in self.floating_ips.iteritems():
+            for floating_ip in floating_ips:
+                ip_address = floating_ip.floating_ip_address
+                self.assertTrue(self._ping_ip_address(ip_address),
+                                "Timed out waiting for %s's ip to become "
+                                "reachable" % server.name)
diff --git a/tools/pip-requires b/tools/pip-requires
index 3a2283f..7877906 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -5,3 +5,4 @@
 lxml
 boto>=2.2.1
 paramiko
+netaddr