Merge "Fix to use an API to get the default quota set"
diff --git a/tempest/clients.py b/tempest/clients.py
index 7d9a263..678b595 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -77,6 +77,8 @@
     InterfacesClientJSON
 from tempest.services.compute.xml.interfaces_client import \
     InterfacesClientXML
+from tempest.services.compute.json.fixed_ips_client import FixedIPsClientJSON
+from tempest.services.compute.xml.fixed_ips_client import FixedIPsClientXML
 
 LOG = logging.getLogger(__name__)
 
@@ -165,6 +167,11 @@
     "xml": EndPointClientXML,
 }
 
+FIXED_IPS_CLIENT = {
+    "json": FixedIPsClientJSON,
+    "xml": FixedIPsClientXML
+}
+
 
 class Manager(object):
 
@@ -228,6 +235,7 @@
                 SECURITY_GROUPS_CLIENT[interface](*client_args)
             self.interfaces_client = INTERFACES_CLIENT[interface](*client_args)
             self.endpoints_client = ENDPOINT_CLIENT[interface](*client_args)
+            self.fixed_ips_client = FIXED_IPS_CLIENT[interface](*client_args)
         except KeyError:
             msg = "Unsupported interface type `%s'" % interface
             raise exceptions.InvalidConfiguration(msg)
diff --git a/tempest/services/compute/json/fixed_ips_client.py b/tempest/services/compute/json/fixed_ips_client.py
new file mode 100644
index 0000000..4ef7c4c
--- /dev/null
+++ b/tempest/services/compute/json/fixed_ips_client.py
@@ -0,0 +1,40 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp
+# 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 json
+
+from tempest.common.rest_client import RestClient
+
+
+class FixedIPsClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(FixedIPsClientJSON, self).__init__(config, username, password,
+                                                 auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def get_fixed_ip_details(self, fixed_ip):
+        url = "os-fixed-ips/%s" % (fixed_ip)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['fixed_ip']
+
+    def reserve_fixed_ip(self, ip, body):
+        """This reserves and unreserves fixed ips."""
+        url = "os-fixed-ips/%s/action" % (ip)
+        resp, body = self.post(url, json.dumps(body), self.headers)
+        return resp, body
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index bc9d9bd..9e71f3d 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -52,6 +52,7 @@
         min_count: Count of minimum number of instances to launch.
         max_count: Count of maximum number of instances to launch.
         disk_config: Determines if user or admin controls disk configuration.
+        return_reservation_id: Enable/Disable the return of reservation id
         """
         post_body = {
             'name': name,
@@ -63,7 +64,8 @@
                        'security_groups', 'networks', 'user_data',
                        'availability_zone', 'accessIPv4', 'accessIPv6',
                        'min_count', 'max_count', ('metadata', 'meta'),
-                       ('OS-DCF:diskConfig', 'disk_config')]:
+                       ('OS-DCF:diskConfig', 'disk_config'),
+                       'return_reservation_id']:
             if isinstance(option, tuple):
                 post_param = option[0]
                 key = option[1]
@@ -77,6 +79,10 @@
         resp, body = self.post('servers', post_body, self.headers)
 
         body = json.loads(body)
+        # NOTE(maurosr): this deals with the case of multiple server create
+        # with return reservation id set True
+        if 'reservation_id' in body:
+            return resp, body
         return resp, body['server']
 
     def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
diff --git a/tempest/services/compute/xml/fixed_ips_client.py b/tempest/services/compute/xml/fixed_ips_client.py
new file mode 100644
index 0000000..ef023f0
--- /dev/null
+++ b/tempest/services/compute/xml/fixed_ips_client.py
@@ -0,0 +1,54 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp
+# 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.
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class FixedIPsClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(FixedIPsClientXML, self).__init__(config, username, password,
+                                                auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def _parse_fixed_ip_details(self, body):
+        body = xml_to_json(etree.fromstring(body))
+        return body
+
+    def get_fixed_ip_details(self, fixed_ip):
+        url = "os-fixed-ips/%s" % (fixed_ip)
+        resp, body = self.get(url, self.headers)
+        body = self._parse_resp(body)
+        return resp, body
+
+    def reserve_fixed_ip(self, ip, body):
+        """This reserves and unreserves fixed ips."""
+        url = "os-fixed-ips/%s/action" % (ip)
+        # NOTE(maurosr): First converts the dict body to a json string then
+        # accept any action key value here to permit tests to cover cases with
+        # invalid actions raising badrequest.
+        key, value = body.popitem()
+        xml_body = Element(key)
+        xml_body.append(Text(value))
+        resp, body = self.post(url, str(Document(xml_body)), self.headers)
+        return resp, body
diff --git a/tempest/services/compute/xml/servers_client.py b/tempest/services/compute/xml/servers_client.py
index e6c2a6c..331d560 100644
--- a/tempest/services/compute/xml/servers_client.py
+++ b/tempest/services/compute/xml/servers_client.py
@@ -42,6 +42,13 @@
     version = ip.get('version')
     if version:
         ip['version'] = int(version)
+    # NOTE(maurosr): just a fast way to avoid the xml version with the
+    # expanded xml namespace.
+    type_ns_prefix = ('{http://docs.openstack.org/compute/ext/extended_ips/'
+                      'api/v1.1}type')
+    if type_ns_prefix in ip:
+        ip['OS-EXT-IPS:type'] = ip[type_ns_prefix]
+        ip.pop(type_ns_prefix)
     return ip
 
 
@@ -235,7 +242,8 @@
                          name=name)
 
         for attr in ["adminPass", "accessIPv4", "accessIPv6", "key_name",
-                     "user_data", "availability_zone"]:
+                     "user_data", "availability_zone", "min_count",
+                     "max_count", "return_reservation_id"]:
             if attr in kwargs:
                 server.add_attr(attr, kwargs[attr])
 
diff --git a/tempest/tests/compute/admin/test_fixed_ips.py b/tempest/tests/compute/admin/test_fixed_ips.py
new file mode 100644
index 0000000..d8b1359
--- /dev/null
+++ b/tempest/tests/compute/admin/test_fixed_ips.py
@@ -0,0 +1,108 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp
+# 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.
+
+from tempest import exceptions
+from tempest.test import attr
+from tempest.tests.compute import base
+
+
+class FixedIPsBase(base.BaseComputeAdminTest):
+    _interface = 'json'
+    ip = None
+
+    @classmethod
+    def setUpClass(cls):
+        super(FixedIPsBase, cls).setUpClass()
+        # NOTE(maurosr): The idea here is: the server creation is just an
+        # auxiliary element to the ip details or reservation, there was no way
+        # (at least none in my mind) to get an valid and existing ip except
+        # by creating a server and using its ip. So the intention is to create
+        # fewer server possible (one) and use it to both: json and xml tests.
+        # This decreased time to run both tests, in my test machine, from 53
+        # secs to 29 (agains 23 secs when running only json tests)
+        if cls.ip is None:
+            cls.client = cls.os_adm.fixed_ips_client
+            cls.non_admin_client = cls.fixed_ips_client
+            resp, server = cls.create_server(wait_until='ACTIVE')
+            resp, server = cls.servers_client.get_server(server['id'])
+            for ip_set in server['addresses']:
+                for ip in server['addresses'][ip_set]:
+                    if ip['OS-EXT-IPS:type'] == 'fixed':
+                        cls.ip = ip['addr']
+                        break
+                if cls.ip:
+                    break
+
+
+class FixedIPsTestJson(FixedIPsBase):
+    _interface = 'json'
+
+    @attr(type='positive')
+    def test_list_fixed_ip_details(self):
+        resp, fixed_ip = self.client.get_fixed_ip_details(self.ip)
+        self.assertEqual(fixed_ip['address'], self.ip)
+
+    @attr(type='negative')
+    def test_list_fixed_ip_details_with_non_admin_user(self):
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.get_fixed_ip_details, self.ip)
+
+    @attr(type='positive')
+    def test_set_reserve(self):
+        body = {"reserve": "None"}
+        resp, body = self.client.reserve_fixed_ip(self.ip, body)
+        self.assertEqual(resp.status, 202)
+
+    @attr(type='positive')
+    def test_set_unreserve(self):
+        body = {"unreserve": "None"}
+        resp, body = self.client.reserve_fixed_ip(self.ip, body)
+        self.assertEqual(resp.status, 202)
+
+    @attr(type='negative')
+    def test_set_reserve_with_non_admin_user(self):
+        body = {"reserve": "None"}
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.reserve_fixed_ip,
+                          self.ip, body)
+
+    @attr(type='negative')
+    def test_set_unreserve_with_non_admin_user(self):
+        body = {"unreserve": "None"}
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.reserve_fixed_ip,
+                          self.ip, body)
+
+    @attr(type='negative')
+    def test_set_reserve_with_invalid_ip(self):
+        # NOTE(maurosr): since this exercises the same code snippet, we do it
+        # only for reserve action
+        body = {"reserve": "None"}
+        self.assertRaises(exceptions.NotFound,
+                          self.client.reserve_fixed_ip,
+                          "my.invalid.ip", body)
+
+    @attr(type='negative')
+    def test_fixed_ip_with_invalid_action(self):
+        body = {"invalid_action": "None"}
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.reserve_fixed_ip,
+                          self.ip, body)
+
+
+class FixedIPsTestXml(FixedIPsTestJson):
+    _interface = 'xml'
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index 87aa889..7716922 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -60,6 +60,7 @@
         cls.volumes_extensions_client = os.volumes_extensions_client
         cls.volumes_client = os.volumes_client
         cls.interfaces_client = os.interfaces_client
+        cls.fixed_ips_client = os.fixed_ips_client
         cls.build_interval = cls.config.compute.build_interval
         cls.build_timeout = cls.config.compute.build_timeout
         cls.ssh_user = cls.config.compute.ssh_user
diff --git a/tempest/tests/compute/servers/test_multiple_create.py b/tempest/tests/compute/servers/test_multiple_create.py
new file mode 100644
index 0000000..ad5d604
--- /dev/null
+++ b/tempest/tests/compute/servers/test_multiple_create.py
@@ -0,0 +1,117 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp
+# 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.
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.test import attr
+from tempest.tests.compute import base
+
+
+class MultipleCreateTestJSON(base.BaseComputeTest):
+    _interface = 'json'
+    _name = 'multiple-create-test'
+
+    def _get_created_servers(self, name):
+        """Get servers created which name match with name param."""
+        resp, body = self.servers_client.list_servers()
+        servers = body['servers']
+        servers_created = []
+        for server in servers:
+            if server['name'].startswith(name):
+                servers_created.append(server)
+        return servers_created
+
+    def _generate_name(self):
+        return rand_name(self._name)
+
+    def _create_multiple_servers(self, name=None, wait_until=None, **kwargs):
+        """
+        This is the right way to create_multiple servers and manage to get the
+        created servers into the servers list to be cleaned up after all.
+        """
+        kwargs['name'] = kwargs.get('name', self._generate_name())
+        resp, body = self.create_server(**kwargs)
+        created_servers = self._get_created_servers(kwargs['name'])
+        # NOTE(maurosr): append it to cls.servers list from base.BaseCompute
+        # class.
+        self.servers.append(created_servers)
+        # NOTE(maurosr): get a server list, check status of the ones with names
+        # that match and wait for them become active. At a first look, since
+        # they are building in parallel, wait inside the for doesn't seem be
+        # harmful to the performance
+        if wait_until is not None:
+            for server in created_servers:
+                self.servers_client.wait_for_server_status(server['id'],
+                                                           wait_until)
+
+        return resp, body
+
+    @attr(type='positive')
+    def test_multiple_create(self):
+        resp, body = self._create_multiple_servers(wait_until='ACTIVE',
+                                                   min_count=1,
+                                                   max_count=2)
+        # NOTE(maurosr): do status response check and also make sure that
+        # reservation_id is not in the response body when the request send
+        # contains return_reservation_id=False
+        self.assertEqual('202', resp['status'])
+        self.assertFalse('reservation_id' in body)
+
+    @attr(type='negative')
+    def test_min_count_less_than_one(self):
+        invalid_min_count = 0
+        self.assertRaises(exceptions.BadRequest, self._create_multiple_servers,
+                          min_count=invalid_min_count)
+
+    @attr(type='negative')
+    def test_min_count_non_integer(self):
+        invalid_min_count = 2.5
+        self.assertRaises(exceptions.BadRequest, self._create_multiple_servers,
+                          min_count=invalid_min_count)
+
+    @attr(type='negative')
+    def test_max_count_less_than_one(self):
+        invalid_max_count = 0
+        self.assertRaises(exceptions.BadRequest, self._create_multiple_servers,
+                          max_count=invalid_max_count)
+
+    @attr(type='negative')
+    def test_max_count_non_integer(self):
+        invalid_max_count = 2.5
+        self.assertRaises(exceptions.BadRequest, self._create_multiple_servers,
+                          max_count=invalid_max_count)
+
+    @attr(type='negative')
+    def test_max_count_less_than_min_count(self):
+        min_count = 3
+        max_count = 2
+        self.assertRaises(exceptions.BadRequest, self._create_multiple_servers,
+                          min_count=min_count,
+                          max_count=max_count)
+
+    @attr(type='positive')
+    def test_multiple_create_with_reservation_return(self):
+        resp, body = self._create_multiple_servers(wait_until='ACTIVE',
+                                                   min_count=1,
+                                                   max_count=2,
+                                                   return_reservation_id=True)
+        self.assertTrue(resp['status'], 202)
+        self.assertIn('reservation_id', body)
+
+
+class MultipleCreateTestXML(MultipleCreateTestJSON):
+    _interface = 'xml'
diff --git a/tempest/tests/network/test_network_basic_ops.py b/tempest/tests/network/test_network_basic_ops.py
index 3afe8e3..92ca65f 100644
--- a/tempest/tests/network/test_network_basic_ops.py
+++ b/tempest/tests/network/test_network_basic_ops.py
@@ -17,6 +17,7 @@
 #    under the License.
 
 from tempest.common.utils.data_utils import rand_name
+from tempest.test import attr
 import tempest.tests.network.common as net_common
 
 
@@ -148,14 +149,17 @@
         self.set_resource(name, router)
         return router
 
+    @attr(type='smoke')
     def test_001_create_keypairs(self):
         self.keypairs[self.tenant_id] = self._create_keypair(
             self.compute_client)
 
+    @attr(type='smoke')
     def test_002_create_security_groups(self):
         self.security_groups[self.tenant_id] = self._create_security_group(
             self.compute_client)
 
+    @attr(type='smoke')
     def test_003_create_networks(self):
         network = self._create_network(self.tenant_id)
         router = self._get_router(self.tenant_id)
@@ -165,6 +169,7 @@
         self.subnets.append(subnet)
         self.routers.append(router)
 
+    @attr(type='smoke')
     def test_004_check_networks(self):
         #Checks that we see the newly created network/subnet/router via
         #checking the result of list_[networks,routers,subnets]
@@ -188,6 +193,7 @@
             self.assertIn(myrouter.name, seen_router_names)
             self.assertIn(myrouter.id, seen_router_ids)
 
+    @attr(type='smoke')
     def test_005_create_servers(self):
         if not (self.keypairs or self.security_groups or self.networks):
             raise self.skipTest('Necessary resources have not been defined')
@@ -200,6 +206,7 @@
                                          name, keypair_name, security_groups)
             self.servers.append(server)
 
+    @attr(type='smoke')
     def test_006_check_tenant_network_connectivity(self):
         if not self.config.network.tenant_networks_reachable:
             msg = 'Tenant networks not configured to be reachable.'
@@ -213,6 +220,7 @@
                                     "Timed out waiting for %s's ip to become "
                                     "reachable" % server.name)
 
+    @attr(type='smoke')
     def test_007_assign_floating_ips(self):
         public_network_id = self.config.network.public_network_id
         if not public_network_id:
@@ -224,6 +232,7 @@
             self.floating_ips.setdefault(server, [])
             self.floating_ips[server].append(floating_ip)
 
+    @attr(type='smoke')
     def test_008_check_public_network_connectivity(self):
         if not self.floating_ips:
             raise self.skipTest('No floating ips have been allocated.')
diff --git a/tools/find_stack_traces.py b/tools/find_stack_traces.py
index e6c1990..3129484 100755
--- a/tools/find_stack_traces.py
+++ b/tools/find_stack_traces.py
@@ -22,6 +22,46 @@
 import sys
 import urllib2
 
+import pprint
+pp = pprint.PrettyPrinter()
+
+NOVA_TIMESTAMP = r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d"
+
+NOVA_REGEX = r"(?P<timestamp>%s) (?P<pid>\d+ )?(?P<level>(ERROR|TRACE)) " \
+    "(?P<module>[\w\.]+) (?P<msg>.*)" % (NOVA_TIMESTAMP)
+
+
+class StackTrace(object):
+    timestamp = None
+    pid = None
+    level = ""
+    module = ""
+    msg = ""
+
+    def __init__(self, timestamp=None, pid=None, level="", module="",
+                 msg=""):
+        self.timestamp = timestamp
+        self.pid = pid
+        self.level = level
+        self.module = module
+        self.msg = msg
+
+    def append(self, msg):
+        self.msg = self.msg + msg
+
+    def is_same(self, data):
+        return (data['timestamp'] == self.timestamp and
+                data['level'] == self.level)
+
+    def not_none(self):
+        return self.timestamp is not None
+
+    def __str__(self):
+        buff = "<%s %s %s>\n" % (self.timestamp, self.level, self.module)
+        for line in self.msg.splitlines():
+            buff = buff + line + "\n"
+        return buff
+
 
 def hunt_for_stacktrace(url):
     """Return TRACE or ERROR lines out of logs."""
@@ -29,11 +69,33 @@
     buf = StringIO.StringIO(page.read())
     f = gzip.GzipFile(fileobj=buf)
     content = f.read()
-    traces = re.findall('^(.*? (TRACE|ERROR) .*?)$', content, re.MULTILINE)
-    tracelist = map(lambda x: x[0], traces)
-    # filter out log definitions as false possitives
-    return filter(lambda x: not re.search('logging_exception_prefix', x),
-                  tracelist)
+
+    traces = []
+    trace = StackTrace()
+    for line in content.splitlines():
+        m = re.match(NOVA_REGEX, line)
+        if m:
+            data = m.groupdict()
+            if trace.not_none() and trace.is_same(data):
+                trace.append(data['msg'] + "\n")
+            else:
+                trace = StackTrace(
+                    timestamp=data.get('timestamp'),
+                    pid=data.get('pid'),
+                    level=data.get('level'),
+                    module=data.get('module'),
+                    msg=data.get('msg'))
+
+        else:
+            if trace.not_none():
+                traces.append(trace)
+                trace = StackTrace()
+
+    # once more at the end to pick up any stragglers
+    if trace.not_none():
+        traces.append(trace)
+
+    return traces
 
 
 def log_url(url, log):
@@ -60,6 +122,18 @@
     sys.exit(0)
 
 
+def print_stats(items, fname, verbose=False):
+    errors = len(filter(lambda x: x.level == "ERROR", items))
+    traces = len(filter(lambda x: x.level == "TRACE", items))
+    print "%d ERRORS found in %s" % (errors, fname)
+    print "%d TRACES found in %s" % (traces, fname)
+
+    if verbose:
+        for item in items:
+            print item
+        print "\n\n"
+
+
 def main():
     if len(sys.argv) == 2:
         url = sys.argv[1]
@@ -72,10 +146,10 @@
         for log in loglist:
             logurl = log_url(url, log)
             traces = hunt_for_stacktrace(logurl)
+
             if traces:
-                print "\n\nTRACES found in %s\n" % log
-                for line in traces:
-                    print line
+                print_stats(traces, log, verbose=True)
+
     else:
         usage()