Merge "Specify region by name."
diff --git a/.gitignore b/.gitignore
index 061c2ff..b4dca86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,6 @@
 *.swo
 *.egg-info
 .tox
+.venv
 dist
 build
diff --git a/run_tests.sh b/run_tests.sh
index 7016a30..af33f7c 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -4,6 +4,10 @@
   echo "Usage: $0 [OPTION]..."
   echo "Run Tempest test suite"
   echo ""
+  echo "  -V, --virtual-env        Always use virtualenv.  Install automatically if not present"
+  echo "  -N, --no-virtual-env     Don't use virtualenv.  Run tests in local environment"
+  echo "  -s, --no-site-packages   Isolate the virtualenv from the global Python environment"
+  echo "  -f, --force              Force a clean re-build of the virtual environment. Useful when dependencies have been added."
   echo "  -s, --smoke              Only run smoke tests"
   echo "  -w, --whitebox           Only run whitebox tests"
   echo "  -p, --pep8               Just run pep8"
@@ -15,6 +19,10 @@
 function process_option {
   case "$1" in
     -h|--help) usage;;
+    -V|--virtual-env) always_venv=1; never_venv=0;;
+    -N|--no-virtual-env) always_venv=0; never_venv=1;;
+    -s|--no-site-packages) no_site_packages=1;;
+    -f|--force) force=1;;
     -d|--debug) set -o xtrace;;
     -p|--pep8) let just_pep8=1;;
     -s|--smoke) noseargs="$noseargs --attr=type=smoke";;
@@ -25,6 +33,14 @@
 
 noseargs=""
 just_pep8=0
+venv=.venv
+with_venv=tools/with_venv.sh
+always_venv=0
+never_venv=0
+no_site_packages=0
+force=0
+wrapper=""
+
 
 export NOSE_WITH_OPENSTACK=1
 export NOSE_OPENSTACK_COLOR=1
@@ -37,6 +53,9 @@
   process_option $arg
 done
 
+if [ $no_site_packages -eq 1 ]; then
+  installvenvopts="--no-site-packages"
+fi
 
 # only add tempest default if we don't specify a test
 if [[ "x$noseargs" =~ "tempest" ]]; then
@@ -47,7 +66,7 @@
 
 
 function run_tests {
-  $NOSETESTS
+  ${wrapper} $NOSETESTS
 }
 
 function run_pep8 {
@@ -58,7 +77,7 @@
 
   ignore='--ignore=N4,E121,E122,E125,E126'
   
-  python tools/hacking.py ${ignore} ${srcfiles}
+  ${wrapper} python tools/hacking.py ${ignore} ${srcfiles}
 }
 
 NOSETESTS="nosetests $noseargs"
@@ -68,6 +87,32 @@
     exit
 fi
 
+if [ $never_venv -eq 0 ]
+then
+  # Remove the virtual environment if --force used
+  if [ $force -eq 1 ]; then
+    echo "Cleaning virtualenv..."
+    rm -rf ${venv}
+  fi
+  if [ -e ${venv} ]; then
+    wrapper="${with_venv}"
+  else
+    if [ $always_venv -eq 1 ]; then
+      # Automatically install the virtualenv
+      python tools/install_venv.py $installvenvopts
+      wrapper="${with_venv}"
+    else
+      echo -e "No virtual environment found...create one? (Y/n) \c"
+      read use_ve
+      if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
+        # Install the virtualenv and run the test suite in it
+        python tools/install_venv.py $installvenvopts
+        wrapper=${with_venv}
+      fi
+    fi
+  fi
+fi
+
 run_tests || exit
 
 if [ -z "$noseargs" ]; then
diff --git a/tempest/openstack.py b/tempest/openstack.py
index e29ea8e..01b4cc1 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -22,33 +22,33 @@
 from tempest.services.boto.clients import APIClientEC2
 from tempest.services.boto.clients import ObjectClientS3
 from tempest.services.compute.json.extensions_client import \
-        ExtensionsClientJSON
+    ExtensionsClientJSON
 from tempest.services.compute.json.flavors_client import FlavorsClientJSON
 from tempest.services.compute.json.floating_ips_client import \
-        FloatingIPsClientJSON
+    FloatingIPsClientJSON
 from tempest.services.compute.json.images_client import ImagesClientJSON
 from tempest.services.compute.json.limits_client import LimitsClientJSON
 from tempest.services.compute.json.servers_client import ServersClientJSON
 from tempest.services.compute.json.security_groups_client import \
-        SecurityGroupsClientJSON
+    SecurityGroupsClientJSON
 from tempest.services.compute.json.keypairs_client import KeyPairsClientJSON
 from tempest.services.compute.json.quotas_client import QuotasClient
 from tempest.services.compute.json.volumes_extensions_client import \
-        VolumesExtensionsClientJSON
+    VolumesExtensionsClientJSON
 from tempest.services.compute.json.console_output_client import \
-        ConsoleOutputsClient
+    ConsoleOutputsClient
 from tempest.services.compute.xml.extensions_client import ExtensionsClientXML
 from tempest.services.compute.xml.flavors_client import FlavorsClientXML
 from tempest.services.compute.xml.floating_ips_client import \
-        FloatingIPsClientXML
+    FloatingIPsClientXML
 from tempest.services.compute.xml.images_client import ImagesClientXML
 from tempest.services.compute.xml.keypairs_client import KeyPairsClientXML
 from tempest.services.compute.xml.limits_client import LimitsClientXML
 from tempest.services.compute.xml.security_groups_client \
-import SecurityGroupsClientXML
+    import SecurityGroupsClientXML
 from tempest.services.compute.xml.servers_client import ServersClientXML
 from tempest.services.compute.xml.volumes_extensions_client import \
-        VolumesExtensionsClientXML
+    VolumesExtensionsClientXML
 from tempest.services.identity.json.admin_client import AdminClientJSON
 from tempest.services.identity.json.admin_client import TokenClientJSON
 from tempest.services.identity.xml.admin_client import AdminClientXML
@@ -60,6 +60,8 @@
 from tempest.services.object_storage.object_client import ObjectClient
 from tempest.services.volume.json.volumes_client import VolumesClientJSON
 from tempest.services.volume.xml.volumes_client import VolumesClientXML
+from tempest.services.object_storage.object_client import \
+    ObjectClientCustomizedHeader
 
 LOG = logging.getLogger(__name__)
 
@@ -191,6 +193,7 @@
         self.object_client = ObjectClient(*client_args)
         self.ec2api_client = APIClientEC2(*client_args)
         self.s3_client = ObjectClientS3(*client_args)
+        self.custom_object_client = ObjectClientCustomizedHeader(*client_args)
 
 
 class AltManager(Manager):
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index c8f63ef..5bf8de0 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -15,6 +15,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import httplib2
+import json
 import re
 from tempest.common.rest_client import RestClient
 
@@ -116,3 +118,44 @@
 
         resp, body = self.copy(url, headers=headers)
         return resp, body
+
+
+class ObjectClientCustomizedHeader(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(ObjectClientCustomizedHeader, self).__init__(config, username,
+                                                           password, auth_url,
+                                                           tenant_name)
+        #Overwrites json-specific header encoding in RestClient
+        self.service = self.config.object_storage.catalog_type
+        self.format = 'json'
+
+    def request(self, method, url, headers=None, body=None, wait=None):
+        """A simple HTTP request interface."""
+        self.http_obj = httplib2.Http()
+        if headers is None:
+            headers = {}
+        if self.base_url is None:
+            self._set_auth()
+
+        req_url = "%s/%s" % (self.base_url, url)
+        resp, resp_body = self.http_obj.request(req_url, method,
+                                                headers=headers, body=body)
+        if resp.status in (401, 403):
+            try:
+                resp_body = json.loads(resp_body)
+                raise exceptions.Unauthorized(resp_body['error']['message'])
+            except ValueError:
+                pass
+        return resp, resp_body
+
+    def get_object(self, container, object_name, metadata=None):
+        """Retrieve object's data."""
+        headers = {}
+        if metadata:
+            for key in metadata:
+                headers[str(key)] = metadata[key]
+
+        url = "{0}/{1}".format(container, object_name)
+        resp, body = self.get(url, headers=headers)
+        return resp, body
diff --git a/tempest/tests/object_storage/base.py b/tempest/tests/object_storage/base.py
index 8edb3d2..f29798f 100644
--- a/tempest/tests/object_storage/base.py
+++ b/tempest/tests/object_storage/base.py
@@ -32,6 +32,7 @@
         cls.container_client = cls.os.container_client
         cls.account_client = cls.os.account_client
         cls.config = cls.os.config
+        cls.custom_object_client = cls.os.custom_object_client
 
         try:
             cls.account_client.list_account_containers()
diff --git a/tempest/tests/object_storage/test_object_services.py b/tempest/tests/object_storage/test_object_services.py
index 3be2bee..807a99e 100644
--- a/tempest/tests/object_storage/test_object_services.py
+++ b/tempest/tests/object_storage/test_object_services.py
@@ -263,3 +263,36 @@
                 src_container_name)
             resp, _ = self.container_client.delete_container(
                 dst_container_name)
+
+    @attr(type='smoke')
+    def test_access_object_without_using_creds(self):
+        """Make container public-readable, and access the object
+           anonymously, e.g. without using credentials"""
+
+        # Update Container Metadata to make public readable
+        headers = {'X-Container-Read': '.r:*,.rlistings'}
+        resp, body = \
+            self.container_client.update_container_metadata(
+                self.container_name, metadata=headers, metadata_prefix='')
+        self.assertIn(resp['status'], '204')
+
+        # Create Object
+        object_name = rand_name(name='Object')
+        data = arbitrary_string(size=len(object_name) * 1,
+                                base_text=object_name)
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, data)
+        self.assertEqual(resp['status'], '201')
+
+        # List container metadata
+        resp, _ = \
+            self.container_client.list_container_metadata(self.container_name)
+        self.assertEqual(resp['status'], '204')
+        self.assertIn('x-container-read', resp)
+        self.assertEqual(resp['x-container-read'], '.r:*,.rlistings')
+
+        # Trying to Get Object with empty Headers as it is public readable
+        resp, body = \
+            self.custom_object_client.get_object(self.container_name,
+                                                 object_name, metadata={})
+        self.assertEqual(body, data)
diff --git a/tools/install_venv.py b/tools/install_venv.py
new file mode 100644
index 0000000..42ed32c
--- /dev/null
+++ b/tools/install_venv.py
@@ -0,0 +1,248 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Copyright 2010 OpenStack, LLC
+#
+#    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.
+
+"""Installation script for Tempest's development virtualenv."""
+
+import optparse
+import os
+import subprocess
+import sys
+
+
+ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+VENV = os.path.join(ROOT, '.venv')
+PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
+TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
+PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
+
+
+def die(message, *args):
+    print >> sys.stderr, message % args
+    sys.exit(1)
+
+
+def check_python_version():
+    if sys.version_info < (2, 6):
+        die("Need Python Version >= 2.6")
+
+
+def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
+    """Runs a command in an out-of-process shell.
+
+    Returns the output of that command.  Working directory is ROOT.
+    """
+    if redirect_output:
+        stdout = subprocess.PIPE
+    else:
+        stdout = None
+
+    proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
+    output = proc.communicate()[0]
+    if check_exit_code and proc.returncode != 0:
+        die('Command "%s" failed.\n%s', ' '.join(cmd), output)
+    return (output, proc.returncode)
+
+
+def run_command(cmd, redirect_output=True, check_exit_code=True):
+    return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
+
+
+class Distro(object):
+
+    def check_cmd(self, cmd):
+        return bool(run_command(['which', cmd], check_exit_code=False).strip())
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if self.check_cmd('easy_install'):
+            print 'Installing virtualenv via easy_install...',
+            if run_command(['easy_install', 'virtualenv']):
+                print 'Succeeded'
+                return
+            else:
+                print 'Failed'
+
+        die('ERROR: virtualenv not found.\n\nTempest development'
+            ' requires virtualenv, please install it using your'
+            ' favorite package management tool')
+
+    def post_process(self):
+        """Any distribution-specific post-processing gets done here.
+
+        In particular, this is useful for applying patches to code inside
+        the venv.
+        """
+        pass
+
+
+class Fedora(Distro):
+    """This covers all Fedora-based distributions.
+
+    Includes: Fedora, RHEL, CentOS, Scientific Linux"""
+
+    def check_pkg(self, pkg):
+        return run_command_with_code(['rpm', '-q', pkg],
+                                     check_exit_code=False)[1] == 0
+
+    def yum_install(self, pkg, **kwargs):
+        print "Attempting to install '%s' via yum" % pkg
+        run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
+
+    def apply_patch(self, originalfile, patchfile):
+        run_command(['patch', originalfile, patchfile])
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if not self.check_pkg('python-virtualenv'):
+            self.yum_install('python-virtualenv', check_exit_code=False)
+
+        super(Fedora, self).install_virtualenv()
+
+    def post_process(self):
+        """Workaround for a bug in eventlet.
+
+        This currently affects RHEL6.1, but the fix can safely be
+        applied to all RHEL and Fedora distributions.
+
+        This can be removed when the fix is applied upstream.
+
+        Nova: https://bugs.launchpad.net/nova/+bug/884915
+        Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
+        """
+
+        # Install "patch" program if it's not there
+        if not self.check_pkg('patch'):
+            self.yum_install('patch')
+
+        # Apply the eventlet patch
+        self.apply_patch(os.path.join(VENV, 'lib', PY_VERSION, 'site-packages',
+                                      'eventlet/green/subprocess.py'),
+                         'contrib/redhat-eventlet.patch')
+
+
+def get_distro():
+    if (os.path.exists('/etc/fedora-release') or
+        os.path.exists('/etc/redhat-release')):
+        return Fedora()
+    else:
+        return Distro()
+
+
+def check_dependencies():
+    get_distro().install_virtualenv()
+
+
+def create_virtualenv(venv=VENV, no_site_packages=True):
+    """Creates the virtual environment and installs PIP.
+
+    Creates the virtual environment and installs PIP only into the
+    virtual environment.
+    """
+    print 'Creating venv...',
+    if no_site_packages:
+        run_command(['virtualenv', '-q', '--no-site-packages', VENV])
+    else:
+        run_command(['virtualenv', '-q', VENV])
+    print 'done.'
+    print 'Installing pip in virtualenv...',
+    if not run_command(['tools/with_venv.sh', 'easy_install',
+                        'pip>1.0']).strip():
+        die("Failed to install pip.")
+    print 'done.'
+
+
+def pip_install(*args):
+    run_command(['tools/with_venv.sh',
+                 'pip', 'install', '--upgrade'] + list(args),
+                redirect_output=False)
+
+
+def install_dependencies(venv=VENV):
+    print 'Installing dependencies with pip (this can take a while)...'
+
+    # First things first, make sure our venv has the latest pip and distribute.
+    # NOTE: we keep pip at version 1.1 since the most recent version causes
+    # the .venv creation to fail. See:
+    # https://bugs.launchpad.net/nova/+bug/1047120
+    pip_install('pip==1.1')
+    pip_install('distribute')
+
+    # Install greenlet by hand - just listing it in the requires file does not
+    # get it in stalled in the right order
+    pip_install('greenlet')
+
+    pip_install('-r', PIP_REQUIRES)
+    pip_install('-r', TEST_REQUIRES)
+
+    # Install tempest into the virtual_env. No more path munging!
+    run_command([os.path.join(venv, 'bin/python'), 'setup.py', 'develop'])
+
+
+def post_process():
+    get_distro().post_process()
+
+
+def print_help():
+    help = """
+    Tempest development environment setup is complete.
+
+    Tempest development uses virtualenv to track and manage Python dependencies
+    while in development and testing.
+
+    To activate the Tempest virtualenv for the extent of your current shell
+    session you can run:
+
+    $ source .venv/bin/activate
+
+    Or, if you prefer, you can run commands in the virtualenv on a case by case
+    basis by running:
+
+    $ tools/with_venv.sh <your command>
+
+    Also, make test will automatically use the virtualenv.
+    """
+    print help
+
+
+def parse_args():
+    """Parses command-line arguments."""
+    parser = optparse.OptionParser()
+    parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
+                      default=False, action="store_true",
+                      help="Do not inherit packages from global Python"
+                           " install")
+    return parser.parse_args()
+
+
+def main(argv):
+    (options, args) = parse_args()
+    check_python_version()
+    check_dependencies()
+    create_virtualenv(no_site_packages=options.no_site_packages)
+    install_dependencies()
+    post_process()
+    print_help()
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/tools/pip-requires b/tools/pip-requires
index 7877906..6542a3c 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -6,3 +6,6 @@
 boto>=2.2.1
 paramiko
 netaddr
+python-glanceclient>=0.5.0,<2
+python-keystoneclient>=0.2.0
+python-novaclient>=2.10.0,<3
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
index 2e2b855..550c477 100755
--- a/tools/with_venv.sh
+++ b/tools/with_venv.sh
@@ -1,4 +1,4 @@
 #!/bin/bash
 TOOLS=`dirname $0`
-VENV=$TOOLS/../.kong-venv
-source $VENV/bin/activate && $@
+VENV=$TOOLS/../.venv
+source $VENV/bin/activate && "$@"