Add start of the EC2/S3 API testing to tempest

Continues the effort of the https://review.openstack.org/#/c/3064/

* add EC2 keypair and volume tests
* add EC2 snapshot from volume test
* add EC2 floating ip disasscioation
* add EC2 operation on security group
* add EC2/S3 image registration
* add EC2 instance run test
* add Integration test with ssh, console, volume
* add S3 object and bucket tests

Change-Id: I0dff9b05f215b56456272f22aa1c014cd53b4f4b
diff --git a/.gitignore b/.gitignore
index 5b28711..55096ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@
 include/swift_objects/swift_large
 *.log
 *.swp
+*.swo
 *.egg-info
 .tox
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 2987c56..053c36e 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -221,3 +221,48 @@
 # custom Keystone service catalog implementation, you probably want to leave
 # this value as "object-store"
 catalog_type = object-store
+
+[boto]
+# This section contains configuration options used when executing tests
+# with boto.
+
+# EC2 URL
+ec2_url = http://localhost:8773/services/Cloud
+# S3 URL
+s3_url = http://localhost:3333
+
+# Use keystone ec2-* command to get those values for your test user and tenant
+aws_access =
+aws_secret =
+
+#Region
+aws_region = RegionOne
+
+#Image materials for S3 upload
+# ALL content of the specified directory will be uploaded to S3
+s3_materials_path = /opt/stack/devstack/files/images/s3-materials/cirros-0.3.0
+
+# The manifest.xml files, must be in the s3_materials_path directory
+# Subdirectories not allowed!
+# The filenames will be used as a Keys in the S3 Buckets
+
+#ARI Ramdisk manifest. Must be in the above s3_materials_path
+ari_manifest = cirros-0.3.0-x86_64-initrd.manifest.xml
+
+#AMI Machine Image manifest. Must be in the above s3_materials_path
+ami_manifest = cirros-0.3.0-x86_64-blank.img.manifest.xml
+
+#AKI Kernel Image manifest, Must be in the above s3_materials_path
+aki_manifest = cirros-0.3.0-x86_64-vmlinuz.manifest.xml
+
+#Instance type
+instance_type = m1.tiny
+
+#TCP/IP connection timeout
+http_socket_timeout = 5
+
+# Status change wait timout
+build_timeout = 120
+
+# Status change wait interval
+build_interval = 1
diff --git a/etc/tempest.conf.tpl b/etc/tempest.conf.tpl
index 5e2ee7f..880a3c1 100644
--- a/etc/tempest.conf.tpl
+++ b/etc/tempest.conf.tpl
@@ -191,3 +191,48 @@
 # custom Keystone service catalog implementation, you probably want to leave
 # this value as "object-store"
 catalog_type = %OBJECT_CATALOG_TYPE%
+
+[boto]
+# This section contains configuration options used when executing tests
+# with boto.
+
+# EC2 URL
+ec2_url = %BOTO_EC2_URL%
+# S3 URL
+s3_url = %BOTO_S3_URL%
+
+# Use keystone ec2-* command to get those values for your test user and tenant
+aws_access = %BOTO_AWS_ACCESS%
+aws_secret = %BOTO_AWS_SECRET%
+
+#Region
+aws_region = %BOTO_AWS_REGION%
+
+#Image materials for S3 upload
+# ALL content of the specified directory will be uploaded to S3
+s3_materials_path = %BOTO_S3_MATERIALS_PATH%
+
+# The manifest.xml files, must be in the s3_materials_path directory
+# Subdirectories not allowed!
+# The filenames will be used as a Keys in the S3 Buckets
+
+#ARI Ramdisk manifest. Must be in the above s3_materials_path directory
+ari_manifest = %BOTO_ARI_MANIFEST%
+
+#AMI Machine Image manifest. Must be in the above s3_materials_path directory
+ami_manifest = %BOTO_AMI_MANIFEST%
+
+#AKI Kernel Image manifest, Must be in the above s3_materials_path directory
+aki_manifest = %BOTO_AKI_MANIFEST%
+
+#Instance type
+instance_type = %BOTO_FLAVOR_NAME%
+
+#TCP/IP connection timeout
+http_socket_timeout = %BOTO_SOCKET_TIMEOUT%
+
+# Status change wait timout
+build_timeout = %BOTO_BUILD_TIMEOUT%
+
+# Status change wait interval
+build_interval = %BOTO_BUILD_INTERVAL%
diff --git a/tempest/common/ssh.py b/tempest/common/ssh.py
index faf182a..c03a90c 100644
--- a/tempest/common/ssh.py
+++ b/tempest/common/ssh.py
@@ -20,21 +20,26 @@
 import warnings
 import select
 
+from cStringIO import StringIO
 from tempest import exceptions
 
 
 with warnings.catch_warnings():
     warnings.simplefilter("ignore")
     import paramiko
+    from paramiko import RSAKey
 
 
 class Client(object):
 
-    def __init__(self, host, username, password=None, timeout=300,
+    def __init__(self, host, username, password=None, timeout=300, pkey=None,
                  channel_timeout=10, look_for_keys=False, key_filename=None):
         self.host = host
         self.username = username
         self.password = password
+        if isinstance(pkey, basestring):
+            pkey = RSAKey.from_private_key(StringIO(str(pkey)))
+        self.pkey = pkey
         self.look_for_keys = look_for_keys
         self.key_filename = key_filename
         self.timeout = int(timeout)
@@ -55,7 +60,7 @@
                             password=self.password,
                             look_for_keys=self.look_for_keys,
                             key_filename=self.key_filename,
-                            timeout=self.timeout)
+                            timeout=self.timeout, pkey=self.pkey)
                 _timeout = False
                 break
             except socket.error:
diff --git a/tempest/common/utils/file_utils.py b/tempest/common/utils/file_utils.py
new file mode 100644
index 0000000..99047ab
--- /dev/null
+++ b/tempest/common/utils/file_utils.py
@@ -0,0 +1,25 @@
+# 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.
+
+
+def have_effective_read_access(path):
+    try:
+        fh = open(path, "rb")
+    except IOError:
+        return False
+    fh.close()
+    return True
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index f7b467b..ca1557f 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -4,25 +4,29 @@
 from tempest.exceptions import SSHTimeout, ServerUnreachable
 
 import time
+import re
 
 
 class RemoteClient():
 
-    def __init__(self, server, username, password):
+    #Note(afazekas): It should always get an address instead of server
+    def __init__(self, server, username, password=None, pkey=None):
         ssh_timeout = TempestConfig().compute.ssh_timeout
         network = TempestConfig().compute.network_for_ssh
         ip_version = TempestConfig().compute.ip_version_for_ssh
-        addresses = server['addresses'][network]
+        if isinstance(server, basestring):
+            ip_address = server
+        else:
+            addresses = server['addresses'][network]
+            for address in addresses:
+                if address['version'] == ip_version:
+                    ip_address = address['addr']
+                    break
+            else:
+                raise ServerUnreachable()
 
-        for address in addresses:
-            if address['version'] == ip_version:
-                ip_address = address['addr']
-                break
-
-        if ip_address is None:
-            raise ServerUnreachable()
-
-        self.ssh_client = Client(ip_address, username, password, ssh_timeout)
+        self.ssh_client = Client(ip_address, username, password, ssh_timeout,
+                                 pkey=pkey)
         if not self.ssh_client.test_connection_auth():
             raise SSHTimeout()
 
@@ -62,3 +66,9 @@
         boot_time_string = self.ssh_client.exec_command(cmd)
         boot_time_string = boot_time_string.replace('\n', '')
         return time.strptime(boot_time_string, utils.LAST_REBOOT_TIME_FORMAT)
+
+    def write_to_console(self, message):
+        message = re.sub("([$\\`])", "\\\\\\\\\\1", message)
+        # usually to /dev/ttyS0
+        cmd = 'sudo sh -c "echo \\"%s\\" >/dev/console"' % message
+        return self.ssh_client.exec_command(cmd)
diff --git a/tempest/config.py b/tempest/config.py
index c46a007..0ccd4b6 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -37,6 +37,12 @@
         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
             return default_value
 
+    def getboolean(self, item_name, default_value=None):
+        try:
+            return self.conf.getboolean(self.SECTION_NAME, item_name)
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+            return default_value
+
 
 class IdentityConfig(BaseConfig):
 
@@ -414,6 +420,80 @@
         return self.get("catalog_type", 'object-store')
 
 
+class BotoConfig(BaseConfig):
+    """Provides configuration information for connecting to EC2/S3."""
+    SECTION_NAME = "boto"
+
+    @property
+    def ec2_url(self):
+        """EC2 URL"""
+        return self.get("ec2_url", "http://localhost:8773/services/Cloud")
+
+    @property
+    def s3_url(self):
+        """S3 URL"""
+        return self.get("s3_url", "http://localhost:8080")
+
+    @property
+    def aws_secret(self):
+        """AWS Secret Key"""
+        return self.get("aws_secret")
+
+    @property
+    def aws_access(self):
+        """AWS Access Key"""
+        return self.get("aws_access")
+
+    @property
+    def aws_region(self):
+        """AWS Region"""
+        return self.get("aws_region", "RegionOne")
+
+    @property
+    def s3_materials_path(self):
+        return self.get("s3_materials_path",
+                        "/opt/stack/devstack/files/images/"
+                        "s3-materials/cirros-0.3.0")
+
+    @property
+    def ari_manifest(self):
+        """ARI Ramdisk Image manifest"""
+        return self.get("ari_manifest",
+                        "cirros-0.3.0-x86_64-initrd.manifest.xml")
+
+    @property
+    def ami_manifest(self):
+        """AMI Machine Image manifest"""
+        return self.get("ami_manifest",
+                        "cirros-0.3.0-x86_64-blank.img.manifest.xml")
+
+    @property
+    def aki_manifest(self):
+        """AKI Kernel Image manifest"""
+        return self.get("aki_manifest",
+                        "cirros-0.3.0-x86_64-vmlinuz.manifest.xml")
+
+    @property
+    def instance_type(self):
+        """Instance type"""
+        return self.get("Instance type", "m1.tiny")
+
+    @property
+    def http_socket_timeout(self):
+        """boto Http socket timeout"""
+        return self.get("http_socket_timeout", "3")
+
+    @property
+    def build_timeout(self):
+        """status change timeout"""
+        return float(self.get("build_timeout", "60"))
+
+    @property
+    def build_interval(self):
+        """status change test interval"""
+        return float(self.get("build_interval", 1))
+
+
 # TODO(jaypipes): Move this to a common utils (not data_utils...)
 def singleton(cls):
     """Simple wrapper for classes that should only have a single instance"""
@@ -463,6 +543,7 @@
         self.network = NetworkConfig(self._conf)
         self.volume = VolumeConfig(self._conf)
         self.object_storage = ObjectStorageConfig(self._conf)
+        self.boto = BotoConfig(self._conf)
 
     def load_config(self, path):
         """Read configuration from given path and return a config object."""
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 7154b80..016de69 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -1,3 +1,21 @@
+# 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.
+
+
 class TempestException(Exception):
     """
     Base Tempest Exception
@@ -51,6 +69,11 @@
     message = "Image %(image_id) failed to become ACTIVE in the allotted time"
 
 
+class EC2RegisterImageException(TempestException):
+    message = ("Image %(image_id) failed to become 'available' "
+               "in the allotted time")
+
+
 class VolumeBuildErrorException(TempestException):
     message = "Volume %(volume_id)s failed to build and is in ERROR status"
 
@@ -106,3 +129,7 @@
 
 class SQLException(TempestException):
     message = "SQL error: %(message)s"
+
+
+class TearDownException(TempestException):
+    message = "%(num)d cleanUp operation failed"
diff --git a/tempest/openstack.py b/tempest/openstack.py
index 35562b1..dc73bd7 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -57,6 +57,8 @@
 from tempest.services.object_storage.account_client import AccountClient
 from tempest.services.object_storage.container_client import ContainerClient
 from tempest.services.object_storage.object_client import ObjectClient
+from tempest.services.boto.clients import APIClientEC2
+from tempest.services.boto.clients import ObjectClientS3
 
 
 LOG = logging.getLogger(__name__)
@@ -186,6 +188,8 @@
         self.account_client = AccountClient(*client_args)
         self.container_client = ContainerClient(*client_args)
         self.object_client = ObjectClient(*client_args)
+        self.ec2api_client = APIClientEC2(*client_args)
+        self.s3_client = ObjectClientS3(*client_args)
 
 
 class AltManager(Manager):
diff --git a/tempest/services/boto/__init__.py b/tempest/services/boto/__init__.py
new file mode 100644
index 0000000..9b9fceb
--- /dev/null
+++ b/tempest/services/boto/__init__.py
@@ -0,0 +1,102 @@
+# 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 boto
+
+from ConfigParser import DuplicateSectionError
+
+from tempest.exceptions import InvalidConfiguration
+from tempest.exceptions import NotFound
+
+import re
+from types import MethodType
+from contextlib import closing
+
+
+class BotoClientBase(object):
+
+    ALLOWED_METHODS = set()
+
+    def __init__(self, config,
+                 username=None, password=None,
+                 auth_url=None, tenant_name=None,
+                 *args, **kwargs):
+
+        self.connection_timeout = config.boto.http_socket_timeout
+        self.build_timeout = config.boto.build_timeout
+        # We do not need the "path":  "/token" part
+        if auth_url:
+            auth_url = re.sub("(.*)" + re.escape(config.identity.path) + "$",
+                              "\\1", auth_url)
+        self.ks_cred = {"username": username,
+                        "password": password,
+                        "auth_url": auth_url,
+                        "tenant_name": tenant_name}
+
+    def _keystone_aws_get(self):
+        import keystoneclient.v2_0.client
+
+        keystone = keystoneclient.v2_0.client.Client(**self.ks_cred)
+        ec2_cred_list = keystone.ec2.list(keystone.auth_user_id)
+        ec2_cred = None
+        for cred in ec2_cred_list:
+            if cred.tenant_id == keystone.auth_tenant_id:
+                ec2_cred = cred
+                break
+        else:
+            ec2_cred = keystone.ec2.create(keystone.auth_user_id,
+                                           keystone.auth_tenant_id)
+        if not all((ec2_cred, ec2_cred.access, ec2_cred.secret)):
+            raise NotFound("Unable to get access and secret keys")
+        return ec2_cred
+
+    def _config_boto_timeout(self, timeout):
+        try:
+            boto.config.add_section("Boto")
+        except DuplicateSectionError:
+            pass
+        boto.config.set("Boto", "http_socket_timeout", timeout)
+
+    def __getattr__(self, name):
+        """Automatically creates methods for the allowed methods set"""
+        if name in self.ALLOWED_METHODS:
+            def func(self, *args, **kwargs):
+                with closing(self.get_connection()) as conn:
+                    return getattr(conn, name)(*args, **kwargs)
+
+            func.__name__ = name
+            setattr(self, name, MethodType(func, self, self.__class__))
+            setattr(self.__class__, name,
+                    MethodType(func, None, self.__class__))
+            return getattr(self, name)
+        else:
+            raise AttributeError(name)
+
+    def get_connection(self):
+        self._config_boto_timeout(self.connection_timeout)
+        if not all((self.connection_data["aws_access_key_id"],
+                   self.connection_data["aws_secret_access_key"])):
+            if all(self.ks_cred.itervalues()):
+                ec2_cred = self._keystone_aws_get()
+                self.connection_data["aws_access_key_id"] = \
+                    ec2_cred.access
+                self.connection_data["aws_secret_access_key"] = \
+                    ec2_cred.secret
+            else:
+                raise InvalidConfiguration(
+                                    "Unable to get access and secret keys")
+        return  self.connect_method(**self.connection_data)
diff --git a/tempest/services/boto/clients.py b/tempest/services/boto/clients.py
new file mode 100644
index 0000000..5fabcae
--- /dev/null
+++ b/tempest/services/boto/clients.py
@@ -0,0 +1,139 @@
+# 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 boto
+from boto.s3.connection import OrdinaryCallingFormat
+from boto.ec2.regioninfo import RegionInfo
+from tempest.services.boto import BotoClientBase
+import urlparse
+
+
+class APIClientEC2(BotoClientBase):
+
+    def connect_method(self, *args, **kwargs):
+        return boto.connect_ec2(*args, **kwargs)
+
+    def __init__(self, config, *args, **kwargs):
+        super(APIClientEC2, self).__init__(config, *args, **kwargs)
+        aws_access = config.boto.aws_access
+        aws_secret = config.boto.aws_secret
+        purl = urlparse.urlparse(config.boto.ec2_url)
+
+        region = RegionInfo(name=config.boto.aws_region,
+                            endpoint=purl.hostname)
+        port = purl.port
+        if port is None:
+            if purl.scheme is not "https":
+                port = 80
+            else:
+                port = 443
+        else:
+            port = int(port)
+        self.connection_data = {"aws_access_key_id": aws_access,
+                                "aws_secret_access_key": aws_secret,
+                                "is_secure": purl.scheme == "https",
+                                "region": region,
+                                "host": purl.hostname,
+                                "port": port,
+                                "path": purl.path}
+
+    ALLOWED_METHODS = set(('create_key_pair', 'get_key_pair',
+                           'delete_key_pair', 'import_key_pair',
+                           'get_all_key_pairs',
+                           'create_image', 'get_image',
+                           'register_image', 'deregister_image',
+                           'get_all_images', 'get_image_attribute',
+                           'modify_image_attribute', 'reset_image_attribute',
+                           'get_all_kernels',
+                           'create_volume', 'delete_volume',
+                           'get_all_volume_status', 'get_all_volumes',
+                           'get_volume_attribute', 'modify_volume_attribute'
+                           'bundle_instance', 'cancel_spot_instance_requests',
+                           'confirm_product_instanc',
+                           'get_all_instance_status', 'get_all_instances',
+                           'get_all_reserved_instances',
+                           'get_all_spot_instance_requests',
+                           'get_instance_attribute', 'monitor_instance',
+                           'monitor_instances', 'unmonitor_instance',
+                           'unmonitor_instances',
+                           'purchase_reserved_instance_offering',
+                           'reboot_instances', 'request_spot_instances',
+                           'reset_instance_attribute', 'run_instances',
+                           'start_instances', 'stop_instances',
+                           'terminate_instances',
+                           'attach_network_interface', 'attach_volume',
+                           'detach_network_interface', 'detach_volume',
+                           'get_console_output',
+                           'delete_network_interface', 'create_subnet',
+                           'create_network_interface', 'delete_subnet',
+                           'get_all_network_interfaces',
+                           'allocate_address', 'associate_address',
+                           'disassociate_address', 'get_all_addresses',
+                           'release_address',
+                           'create_snapshot', 'delete_snapshot',
+                           'get_all_snapshots', 'get_snapshot_attribute',
+                           'modify_snapshot_attribute',
+                           'reset_snapshot_attribute', 'trim_snapshots',
+                           'get_all_regions', 'get_all_zones',
+                           'get_all_security_groups', 'create_security_group',
+                           'delete_security_group', 'authorize_security_group',
+                           'authorize_security_group_egress',
+                           'revoke_security_group',
+                           'revoke_security_group_egress'))
+
+    def get_good_zone(self):
+        """
+        :rtype: BaseString
+        :return: Returns with the first available zone name
+        """
+        for zone in self.get_all_zones():
+            #NOTE(afazekas): zone.region_name was None
+            if (zone.state == "available" and
+                zone.region.name == self.connection_data["region"].name):
+                return zone.name
+        else:
+            raise IndexError("Don't have a good zone")
+
+
+class ObjectClientS3(BotoClientBase):
+
+    def connect_method(self, *args, **kwargs):
+        return boto.connect_s3(*args, **kwargs)
+
+    def __init__(self, config, *args, **kwargs):
+        super(ObjectClientS3, self).__init__(config, *args, **kwargs)
+        aws_access = config.boto.aws_access
+        aws_secret = config.boto.aws_secret
+        purl = urlparse.urlparse(config.boto.s3_url)
+        port = purl.port
+        if port is None:
+            if purl.scheme is not "https":
+                port = 80
+            else:
+                port = 443
+        else:
+            port = int(port)
+        self.connection_data = {"aws_access_key_id": aws_access,
+                                "aws_secret_access_key": aws_secret,
+                                "is_secure": purl.scheme == "https",
+                                "host": purl.hostname,
+                                "port": port,
+                                "calling_format": OrdinaryCallingFormat()}
+
+    ALLOWED_METHODS = set(('create_bucket', 'delete_bucket', 'generate_url',
+                           'get_all_buckets', 'get_bucket', 'delete_key',
+                           'lookup'))
diff --git a/tempest/testboto.py b/tempest/testboto.py
new file mode 100644
index 0000000..6c51346
--- /dev/null
+++ b/tempest/testboto.py
@@ -0,0 +1,535 @@
+# 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 unittest2 as unittest
+import nose
+import tempest.tests.boto
+from tempest.exceptions import TearDownException
+from tempest.tests.boto.utils.wait import state_wait, wait_no_exception
+from tempest.tests.boto.utils.wait import re_search_wait, wait_exception
+import boto
+from boto.s3.key import Key
+from boto.s3.bucket import Bucket
+from boto.exception import BotoServerError
+from contextlib import closing
+import re
+import logging
+import time
+
+LOG = logging.getLogger(__name__)
+
+
+class BotoExceptionMatcher(object):
+    STATUS_RE = r'[45]\d\d'
+    CODE_RE = '.*'  # regexp makes sense in group match
+
+    def match(self, exc):
+        if not isinstance(exc, BotoServerError):
+            return "%r not an BotoServerError instance" % exc
+        LOG.info("Status: %s , error_code: %s", exc.status, exc.error_code)
+        if re.match(self.STATUS_RE, str(exc.status)) is None:
+            return ("Status code (%s) does not match"
+                    "the expected re pattern \"%s\""
+                    % (exc.status, self.STATUS_RE))
+        if re.match(self.CODE_RE, str(exc.error_code)) is None:
+            return ("Error code (%s) does not match" +
+                    "the expected re pattern \"%s\"") %\
+                   (exc.error_code, self.CODE_RE)
+
+
+class ClientError(BotoExceptionMatcher):
+    STATUS_RE = r'4\d\d'
+
+
+class ServerError(BotoExceptionMatcher):
+    STATUS_RE = r'5\d\d'
+
+
+def _add_matcher_class(error_cls, error_data, base=BotoExceptionMatcher):
+    """
+        Usable for adding an ExceptionMatcher(s) into the exception tree.
+        The not leaf elements does wildcard match
+    """
+    # in error_code just literal and '.' characters expected
+    if not isinstance(error_data, basestring):
+        (error_code, status_code) = map(str, error_data)
+    else:
+        status_code = None
+        error_code = error_data
+    parts = error_code.split('.')
+    basematch = ""
+    num_parts = len(parts)
+    max_index = num_parts - 1
+    add_cls = error_cls
+    for i_part in xrange(num_parts):
+        part = parts[i_part]
+        leaf = i_part == max_index
+        if not leaf:
+            match = basematch + part + "[.].*"
+        else:
+            match = basematch + part
+
+        basematch += part + "[.]"
+        if not hasattr(add_cls, part):
+            cls_dict = {"CODE_RE": match}
+            if leaf and status_code is not None:
+                cls_dict["STATUS_RE"] = status_code
+            cls = type(part, (base, ), cls_dict)
+            setattr(add_cls, part, cls())
+            add_cls = cls
+        elif leaf:
+            raise LookupError("Tries to redefine an error code \"%s\"" % part)
+        else:
+            add_cls = getattr(add_cls, part)
+
+
+#TODO(afazekas): classmethod handling
+def friendly_function_name_simple(call_able):
+    name = ""
+    if hasattr(call_able, "im_class"):
+        name += call_able.im_class.__name__ + "."
+    name += call_able.__name__
+    return name
+
+
+def friendly_function_call_str(call_able, *args, **kwargs):
+    string = friendly_function_name_simple(call_able)
+    string += "(" + ", ".join(map(str, args))
+    if len(kwargs):
+        if len(args):
+            string += ", "
+    string += ", ".join("=".join(map(str, (key, value)))
+              for (key, value) in kwargs.items())
+    return string + ")"
+
+
+class BotoTestCase(unittest.TestCase):
+    """Recommended to use as base class for boto related test"""
+    @classmethod
+    def setUpClass(cls):
+        # The trash contains cleanup functions and paramaters in tuples
+        # (function, *args, **kwargs)
+        cls._resource_trash_bin = {}
+        cls._sequence = -1
+        if (hasattr(cls, "EC2") and
+            tempest.tests.boto.EC2_CAN_CONNECT_ERROR is not None):
+                raise nose.SkipTest("EC2 " + cls.__name__ + ": " +
+                                    tempest.tests.boto.EC2_CAN_CONNECT_ERROR)
+        if (hasattr(cls, "S3") and
+            tempest.tests.boto.S3_CAN_CONNECT_ERROR is not None):
+                raise nose.SkipTest("S3 " + cls.__name__ + ": " +
+                                    tempest.tests.boto.S3_CAN_CONNECT_ERROR)
+
+    @classmethod
+    def addResourceCleanUp(cls, function, *args, **kwargs):
+        """Adds CleanUp callable, used by tearDownClass.
+        Recommended to a use (deep)copy on the mutable args"""
+        cls._sequence = cls._sequence + 1
+        cls._resource_trash_bin[cls._sequence] = (function, args, kwargs)
+        return cls._sequence
+
+    @classmethod
+    def cancelResourceCleanUp(cls, key):
+        """Cancel Clean up request"""
+        del cls._resource_trash_bin[key]
+
+    #TODO(afazekas): Add "with" context handling
+    def assertBotoError(self, excMatcher, callableObj,
+                        *args, **kwargs):
+        """Example usage:
+            self.assertBotoError(self.ec2_error_code.client.
+                                 InvalidKeyPair.Duplicate,
+                                 self.client.create_keypair,
+                                 key_name)"""
+        try:
+            callableObj(*args, **kwargs)
+        except BotoServerError as exc:
+            error_msg = excMatcher.match(exc)
+            if error_msg is not None:
+                raise self.failureException, error_msg
+        else:
+            raise self.failureException, "BotoServerError not raised"
+
+    @classmethod
+    def tearDownClass(cls):
+        """ Calls the callables added by addResourceCleanUp,
+        when you overwire this function dont't forget to call this too"""
+        fail_count = 0
+        trash_keys = sorted(cls._resource_trash_bin, reverse=True)
+        for key in trash_keys:
+            (function, pos_args, kw_args) = cls._resource_trash_bin[key]
+            try:
+                LOG.debug("Cleaning up: %s" %
+                          friendly_function_call_str(function, *pos_args,
+                                                     **kw_args))
+                function(*pos_args, **kw_args)
+            except BaseException as exc:
+                fail_count += 1
+                LOG.exception(exc)
+            finally:
+                del cls._resource_trash_bin[key]
+        if fail_count:
+            raise TearDownException(num=fail_count)
+
+    ec2_error_code = BotoExceptionMatcher()
+    # InsufficientInstanceCapacity can be both server and client error
+    ec2_error_code.server = ServerError()
+    ec2_error_code.client = ClientError()
+    s3_error_code = BotoExceptionMatcher()
+    s3_error_code.server = ServerError()
+    s3_error_code.client = ClientError()
+    valid_image_state = set(('available', 'pending', 'failed'))
+    valid_instance_state = set(('pending', 'running', 'shutting-down',
+                                'terminated', 'stopping', 'stopped'))
+    valid_volume_status = set(('creating', 'available', 'in-use',
+                               'deleting', 'deleted', 'error'))
+    valid_snapshot_status = set(('pending', 'completed', 'error'))
+
+    #TODO(afazekas): object base version for resurces supports update
+    def waitImageState(self, lfunction, wait_for):
+        state = state_wait(lfunction, wait_for, self.valid_image_state)
+        self.assertIn(state, self.valid_image_state)
+        return state
+
+    def waitInstanceState(self, lfunction, wait_for):
+        state = state_wait(lfunction, wait_for, self.valid_instance_state)
+        self.assertIn(state, self.valid_instance_state)
+        return state
+
+    def waitVolumeStatus(self, lfunction, wait_for):
+        state = state_wait(lfunction, wait_for, self.valid_volume_status)
+        self.assertIn(state, self.valid_volume_status)
+        return state
+
+    def waitSnapshotStatus(self, lfunction, wait_for):
+        state = state_wait(lfunction, wait_for, self.valid_snapshot_status)
+        self.assertIn(state, self.valid_snapshot_status)
+        return state
+
+    def assertImageStateWait(self, lfunction, wait_for):
+        state = self.waitImageState(lfunction, wait_for)
+        self.assertIn(state, wait_for)
+
+    def assertInstanceStateWait(self, lfunction, wait_for):
+        state = self.waitInstanceState(lfunction, wait_for)
+        self.assertIn(state, wait_for)
+
+    def assertVolumeStatusWait(self, lfunction, wait_for):
+        state = self.waitVolumeStatus(lfunction, wait_for)
+        self.assertIn(state, wait_for)
+
+    def assertSnapshotStatusWait(self, lfunction, wait_for):
+        state = self.waitSnapshotStatus(lfunction, wait_for)
+        self.assertIn(state, wait_for)
+
+    def assertAddressDissasociatedWait(self, address):
+
+        def _disassociate():
+            cli = self.ec2_client
+            addresses = cli.get_all_addresses(addresses=(address.public_ip,))
+            if len(addresses) != 1:
+                return "INVALID"
+            if addresses[0].instance_id:
+                LOG.info("%s associated to %s",
+                         address.public_ip,
+                         addresses[0].instance_id)
+                return "ASSOCIATED"
+            return "DISASSOCIATED"
+
+        state = state_wait(_disassociate, "DISASSOCIATED",
+                           set(("ASSOCIATED", "DISASSOCIATED")))
+        self.assertEqual(state, "DISASSOCIATED")
+
+    def assertAddressReleasedWait(self, address):
+
+        def _address_delete():
+            #NOTE(afazekas): the filter gives back IP
+            # even if it is not associated to my tenant
+            if (address.public_ip not in map(lambda a: a.public_ip,
+                self.ec2_client.get_all_addresses())):
+                    return "DELETED"
+            return "NOTDELETED"
+
+        state = state_wait(_address_delete, "DELETED")
+        self.assertEqual(state, "DELETED")
+
+    def assertReSearch(self, regexp, string):
+        if re.search(regexp, string) is None:
+            raise self.failureException("regexp: '%s' not found in '%s'" %
+                                        (regexp, string))
+
+    def assertNotReSearch(self, regexp, string):
+        if re.search(regexp, string) is not None:
+            raise self.failureException("regexp: '%s' found in '%s'" %
+                                        (regexp, string))
+
+    def assertReMatch(self, regexp, string):
+        if re.match(regexp, string) is None:
+            raise self.failureException("regexp: '%s' not matches on '%s'" %
+                                        (regexp, string))
+
+    def assertNotReMatch(self, regexp, string):
+        if re.match(regexp, string) is not None:
+            raise self.failureException("regexp: '%s' matches on '%s'" %
+                                        (regexp, string))
+
+    @classmethod
+    def destroy_bucket(cls, connection_data, bucket):
+        """Destroys the bucket and its content, just for teardown"""
+        exc_num = 0
+        try:
+            with closing(boto.connect_s3(**connection_data)) as conn:
+                if isinstance(bucket, basestring):
+                    bucket = conn.lookup(bucket)
+                    assert isinstance(bucket, Bucket)
+                for obj in bucket.list():
+                    try:
+                        bucket.delete_key(obj.key)
+                        obj.close()
+                    except BaseException as exc:
+                        LOG.exception(exc)
+                        exc_num += 1
+            conn.delete_bucket(bucket)
+        except BaseException as exc:
+            LOG.exception(exc)
+            exc_num += 1
+        if exc_num:
+            raise TearDownException(num=exc_num)
+
+    @classmethod
+    def destroy_reservation(cls, reservation):
+        """Terminate instances in a reservation, just for teardown"""
+        exc_num = 0
+
+        def _instance_state():
+            try:
+                instance.update(validate=True)
+            except ValueError:
+                return "terminated"
+            return instance.state
+
+        for instance in reservation.instances:
+            try:
+                instance.terminate()
+                re_search_wait(_instance_state, "terminated")
+            except BaseException as exc:
+                LOG.exception(exc)
+                exc_num += 1
+        if exc_num:
+            raise TearDownException(num=exc_num)
+
+    #NOTE(afazekas): The incorrect ErrorCodes makes very, very difficult
+    # to write better teardown
+
+    @classmethod
+    def destroy_security_group_wait(cls, group):
+        """Delete group.
+           Use just for teardown!
+        """
+        #NOTE(afazekas): should wait/try until all related instance terminates
+        #2.   looks like it is locked even if the instance not listed
+        time.sleep(1)
+        group.delete()
+
+    @classmethod
+    def destroy_volume_wait(cls, volume):
+        """Delete volume, tryies to detach first.
+           Use just for teardown!
+        """
+        exc_num = 0
+        snaps = volume.snapshots()
+        if len(snaps):
+            LOG.critical("%s Volume has %s snapshot(s)", volume.id,
+                         map(snps.id, snaps))
+
+        #Note(afazekas): detaching/attching not valid EC2 status
+        def _volume_state():
+            volume.update(validate=True)
+            try:
+                if volume.status != "available":
+                    volume.detach(force=True)
+            except BaseException as exc:
+                LOG.exception(exc)
+                #exc_num += 1 "nonlocal" not in python2
+            return volume.status
+
+        try:
+            re_search_wait(_volume_state, "available")  # not validates status
+            LOG.info(_volume_state())
+            volume.delete()
+        except BaseException as exc:
+            LOG.exception(exc)
+            exc_num += 1
+        if exc_num:
+            raise TearDownException(num=exc_num)
+
+    @classmethod
+    def destroy_snapshot_wait(cls, snapshot):
+        """delete snaphot, wait until not exists"""
+        snapshot.delete()
+
+        def _update():
+            snapshot.update(validate=True)
+
+        wait_exception(_update)
+
+
+# you can specify tuples if you want to specify the status pattern
+for code in ('AddressLimitExceeded', 'AttachmentLimitExceeded', 'AuthFailure',
+             'Blocked', 'CustomerGatewayLimitExceeded', 'DependencyViolation',
+             'DiskImageSizeTooLarge', 'FilterLimitExceeded',
+             'Gateway.NotAttached', 'IdempotentParameterMismatch',
+             'IncorrectInstanceState', 'IncorrectState',
+             'InstanceLimitExceeded', 'InsufficientInstanceCapacity',
+             'InsufficientReservedInstancesCapacity',
+             'InternetGatewayLimitExceeded', 'InvalidAMIAttributeItemValue',
+             'InvalidAMIID.Malformed', 'InvalidAMIID.NotFound',
+             'InvalidAMIID.Unavailable', 'InvalidAssociationID.NotFound',
+             'InvalidAttachment.NotFound', 'InvalidConversionTaskId',
+             'InvalidCustomerGateway.DuplicateIpAddress',
+             'InvalidCustomerGatewayID.NotFound', 'InvalidDevice.InUse',
+             'InvalidDhcpOptionsID.NotFound', 'InvalidFormat',
+             'InvalidFilter', 'InvalidGatewayID.NotFound',
+             'InvalidGroup.Duplicate', 'InvalidGroupId.Malformed',
+             'InvalidGroup.InUse', 'InvalidGroup.NotFound',
+             'InvalidGroup.Reserved', 'InvalidInstanceID.Malformed',
+             'InvalidInstanceID.NotFound',
+             'InvalidInternetGatewayID.NotFound', 'InvalidIPAddress.InUse',
+             'InvalidKeyPair.Duplicate', 'InvalidKeyPair.Format',
+             'InvalidKeyPair.NotFound', 'InvalidManifest',
+             'InvalidNetworkAclEntry.NotFound',
+             'InvalidNetworkAclID.NotFound', 'InvalidParameterCombination',
+             'InvalidParameterValue', 'InvalidPermission.Duplicate',
+             'InvalidPermission.Malformed', 'InvalidReservationID.Malformed',
+             'InvalidReservationID.NotFound', 'InvalidRoute.NotFound',
+             'InvalidRouteTableID.NotFound',
+             'InvalidSecurity.RequestHasExpired',
+             'InvalidSnapshotID.Malformed', 'InvalidSnapshot.NotFound',
+             'InvalidUserID.Malformed', 'InvalidReservedInstancesId',
+             'InvalidReservedInstancesOfferingId',
+             'InvalidSubnetID.NotFound', 'InvalidVolumeID.Duplicate',
+             'InvalidVolumeID.Malformed', 'InvalidVolumeID.ZoneMismatch',
+             'InvalidVolume.NotFound', 'InvalidVpcID.NotFound',
+             'InvalidVpnConnectionID.NotFound',
+             'InvalidVpnGatewayID.NotFound',
+             'InvalidZone.NotFound', 'LegacySecurityGroup',
+             'MissingParameter', 'NetworkAclEntryAlreadyExists',
+             'NetworkAclEntryLimitExceeded', 'NetworkAclLimitExceeded',
+             'NonEBSInstance', 'PendingSnapshotLimitExceeded',
+             'PendingVerification', 'OptInRequired', 'RequestLimitExceeded',
+             'ReservedInstancesLimitExceeded', 'Resource.AlreadyAssociated',
+             'ResourceLimitExceeded', 'RouteAlreadyExists',
+             'RouteLimitExceeded', 'RouteTableLimitExceeded',
+             'RulesPerSecurityGroupLimitExceeded',
+             'SecurityGroupLimitExceeded',
+             'SecurityGroupsPerInstanceLimitExceeded',
+             'SnapshotLimitExceeded', 'SubnetLimitExceeded',
+             'UnknownParameter', 'UnsupportedOperation',
+             'VolumeLimitExceeded', 'VpcLimitExceeded',
+             'VpnConnectionLimitExceeded',
+             'VpnGatewayAttachmentLimitExceeded', 'VpnGatewayLimitExceeded'):
+    _add_matcher_class(BotoTestCase.ec2_error_code.client,
+                       code, base=ClientError)
+
+for code in ('InsufficientAddressCapacity', 'InsufficientInstanceCapacity',
+             'InsufficientReservedInstanceCapacity', 'InternalError',
+             'Unavailable'):
+    _add_matcher_class(BotoTestCase.ec2_error_code.server,
+                       code, base=ServerError)
+
+
+for code in (('AccessDenied', 403),
+            ('AccountProblem', 403),
+            ('AmbiguousGrantByEmailAddress', 400),
+            ('BadDigest', 400),
+            ('BucketAlreadyExists', 409),
+            ('BucketAlreadyOwnedByYou', 409),
+            ('BucketNotEmpty', 409),
+            ('CredentialsNotSupported', 400),
+            ('CrossLocationLoggingProhibited', 403),
+            ('EntityTooSmall', 400),
+            ('EntityTooLarge', 400),
+            ('ExpiredToken', 400),
+            ('IllegalVersioningConfigurationException', 400),
+            ('IncompleteBody', 400),
+            ('IncorrectNumberOfFilesInPostRequest', 400),
+            ('InlineDataTooLarge', 400),
+            ('InvalidAccessKeyId', 403),
+             'InvalidAddressingHeader',
+            ('InvalidArgument', 400),
+            ('InvalidBucketName', 400),
+            ('InvalidBucketState', 409),
+            ('InvalidDigest', 400),
+            ('InvalidLocationConstraint', 400),
+            ('InvalidPart', 400),
+            ('InvalidPartOrder', 400),
+            ('InvalidPayer', 403),
+            ('InvalidPolicyDocument', 400),
+            ('InvalidRange', 416),
+            ('InvalidRequest', 400),
+            ('InvalidSecurity', 403),
+            ('InvalidSOAPRequest', 400),
+            ('InvalidStorageClass', 400),
+            ('InvalidTargetBucketForLogging', 400),
+            ('InvalidToken', 400),
+            ('InvalidURI', 400),
+            ('KeyTooLong', 400),
+            ('MalformedACLError', 400),
+            ('MalformedPOSTRequest', 400),
+            ('MalformedXML', 400),
+            ('MaxMessageLengthExceeded', 400),
+            ('MaxPostPreDataLengthExceededError', 400),
+            ('MetadataTooLarge', 400),
+            ('MethodNotAllowed', 405),
+            ('MissingAttachment'),
+            ('MissingContentLength', 411),
+            ('MissingRequestBodyError', 400),
+            ('MissingSecurityElement', 400),
+            ('MissingSecurityHeader', 400),
+            ('NoLoggingStatusForKey', 400),
+            ('NoSuchBucket', 404),
+            ('NoSuchKey', 404),
+            ('NoSuchLifecycleConfiguration', 404),
+            ('NoSuchUpload', 404),
+            ('NoSuchVersion', 404),
+            ('NotSignedUp', 403),
+            ('NotSuchBucketPolicy', 404),
+            ('OperationAborted', 409),
+            ('PermanentRedirect', 301),
+            ('PreconditionFailed', 412),
+            ('Redirect', 307),
+            ('RequestIsNotMultiPartContent', 400),
+            ('RequestTimeout', 400),
+            ('RequestTimeTooSkewed', 403),
+            ('RequestTorrentOfBucketError', 400),
+            ('SignatureDoesNotMatch', 403),
+            ('TemporaryRedirect', 307),
+            ('TokenRefreshRequired', 400),
+            ('TooManyBuckets', 400),
+            ('UnexpectedContent', 400),
+            ('UnresolvableGrantByEmailAddress', 400),
+            ('UserKeyMustBeSpecified', 400)):
+    _add_matcher_class(BotoTestCase.s3_error_code.client,
+                       code, base=ClientError)
+
+
+for code in (('InternalError', 500),
+            ('NotImplemented', 501),
+            ('ServiceUnavailable', 503),
+            ('SlowDown', 503)):
+    _add_matcher_class(BotoTestCase.s3_error_code.server,
+                       code, base=ServerError)
diff --git a/tempest/tests/boto/__init__.py b/tempest/tests/boto/__init__.py
new file mode 100644
index 0000000..3d5ea6c
--- /dev/null
+++ b/tempest/tests/boto/__init__.py
@@ -0,0 +1,98 @@
+# 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 tempest.config
+from tempest.common.utils.file_utils import have_effective_read_access
+import os
+import tempest.openstack
+import re
+import keystoneclient.exceptions
+import boto.exception
+import logging
+import urlparse
+
+A_I_IMAGES_READY = False  # ari,ami,aki
+S3_CAN_CONNECT_ERROR = "Unknown Error"
+EC2_CAN_CONNECT_ERROR = "Unknown Error"
+
+
+def setup_package():
+    global A_I_IMAGES_READY
+    global S3_CAN_CONNECT_ERROR
+    global EC2_CAN_CONNECT_ERROR
+    secret_matcher = re.compile("[A-Za-z0-9+/]{32,}")  # 40 in other system
+    id_matcher = re.compile("[A-Za-z0-9]{20,}")
+
+    def all_read(*args):
+        return all(map(have_effective_read_access, args))
+
+    config = tempest.config.TempestConfig()
+    materials_path = config.boto.s3_materials_path
+    ami_path = materials_path + os.sep + config.boto.ami_manifest
+    aki_path = materials_path + os.sep + config.boto.aki_manifest
+    ari_path = materials_path + os.sep + config.boto.ari_manifest
+
+    A_I_IMAGES_READY = all_read(ami_path, aki_path, ari_path)
+    boto_logger = logging.getLogger('boto')
+    level = boto_logger.level
+    boto_logger.setLevel(logging.CRITICAL)  # suppress logging for these
+
+    def _cred_sub_check(connection_data):
+        if not id_matcher.match(connection_data["aws_access_key_id"]):
+            raise Exception("Invalid AWS access Key")
+        if not secret_matcher.match(connection_data["aws_secret_access_key"]):
+            raise Exception("Invalid AWS secret Key")
+        raise Exception("Unknown (Authentication?) Error")
+    openstack = tempest.openstack.Manager()
+    try:
+        if urlparse.urlparse(config.boto.ec2_url).hostname is None:
+            raise Exception("Failed to get hostname from the ec2_url")
+        ec2client = openstack.ec2api_client
+        try:
+            ec2client.get_all_regions()
+        except boto.exception.BotoServerError as exc:
+                if exc.error_code is None:
+                    raise Exception("EC2 target does not looks EC2 service")
+                _cred_sub_check(ec2client.connection_data)
+
+    except keystoneclient.exceptions.Unauthorized:
+        EC2_CAN_CONNECT_ERROR = "AWS credentials not set," +\
+                                " faild to get them even by keystoneclient"
+    except Exception as exc:
+        logging.exception(exc)
+        EC2_CAN_CONNECT_ERROR = str(exc)
+    else:
+        EC2_CAN_CONNECT_ERROR = None
+
+    try:
+        if urlparse.urlparse(config.boto.s3_url).hostname is None:
+            raise Exception("Failed to get hostname from the s3_url")
+        s3client = openstack.s3_client
+        try:
+            s3client.get_bucket("^INVALID*#()@INVALID.")
+        except  boto.exception.BotoServerError as exc:
+            if exc.status == 403:
+                _cred_sub_check(s3client.connection_data)
+    except Exception as exc:
+        logging.exception(exc)
+        S3_CAN_CONNECT_ERROR = str(exc)
+    except keystoneclient.exceptions.Unauthorized:
+        S3_CAN_CONNECT_ERROR = "AWS credentials not set," +\
+                               " faild to get them even by keystoneclient"
+    else:
+        S3_CAN_CONNECT_ERROR = None
+    boto_logger.setLevel(level)
diff --git a/tempest/tests/boto/test_ec2_instance_run.py b/tempest/tests/boto/test_ec2_instance_run.py
new file mode 100644
index 0000000..e5c61fb
--- /dev/null
+++ b/tempest/tests/boto/test_ec2_instance_run.py
@@ -0,0 +1,249 @@
+# 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 nose
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest.tests.boto.utils.s3 import s3_upload_dir
+import tempest.tests.boto
+from tempest.common.utils.data_utils import rand_name
+from tempest.exceptions import EC2RegisterImageException
+from tempest.tests.boto.utils.wait import state_wait, re_search_wait
+from tempest import openstack
+from tempest.common.utils.linux.remote_client import RemoteClient
+from boto.s3.key import Key
+from contextlib import closing
+import logging
+
+
+LOG = logging.getLogger(__name__)
+
+
+@attr("S3", "EC2")
+class InstanceRunTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(InstanceRunTest, cls).setUpClass()
+        if not tempest.tests.boto.A_I_IMAGES_READY:
+            raise nose.SkipTest("".join(("EC2 ", cls.__name__,
+                                ": requires ami/aki/ari manifest")))
+        cls.os = openstack.Manager()
+        cls.s3_client = cls.os.s3_client
+        cls.ec2_client = cls.os.ec2api_client
+        config = cls.os.config
+        cls.zone = cls.ec2_client.get_good_zone()
+        cls.materials_path = config.boto.s3_materials_path
+        ami_manifest = config.boto.ami_manifest
+        aki_manifest = config.boto.aki_manifest
+        ari_manifest = config.boto.ari_manifest
+        cls.instance_type = config.boto.instance_type
+        cls.bucket_name = rand_name("s3bucket-")
+        cls.keypair_name = rand_name("keypair-")
+        cls.keypair = cls.ec2_client.create_key_pair(cls.keypair_name)
+        cls.addResourceCleanUp(cls.ec2_client.delete_key_pair,
+                               cls.keypair_name)
+        bucket = cls.s3_client.create_bucket(cls.bucket_name)
+        cls.addResourceCleanUp(cls.destroy_bucket,
+                               cls.s3_client.connection_data,
+                               cls.bucket_name)
+        s3_upload_dir(bucket, cls.materials_path)
+        cls.images = {"ami":
+                      {"name": rand_name("ami-name-"),
+                       "location": cls.bucket_name + "/" + ami_manifest},
+                      "aki":
+                      {"name": rand_name("aki-name-"),
+                       "location": cls.bucket_name + "/" + aki_manifest},
+                      "ari":
+                      {"name": rand_name("ari-name-"),
+                       "location": cls.bucket_name + "/" + ari_manifest}}
+        for image in cls.images.itervalues():
+            image["image_id"] = cls.ec2_client.register_image(
+                                name=image["name"],
+                                image_location=image["location"])
+            cls.addResourceCleanUp(cls.ec2_client.deregister_image,
+                                   image["image_id"])
+
+        for image in cls.images.itervalues():
+            def _state():
+                retr = cls.ec2_client.get_image(image["image_id"])
+                return retr.state
+            state = state_wait(_state, "available")
+            if state != "available":
+                for _image in cls.images.itervalues():
+                    ec2_client.deregister_image(_image["image_id"])
+                raise RegisterImageException(image_id=image["image_id"])
+
+    @attr(type='smoke')
+    def test_run_stop_terminate_instance(self):
+        """EC2 run, stop and terminate instance"""
+        image_ami = self.ec2_client.get_image(self.images["ami"]
+                                              ["image_id"])
+        reservation = image_ami.run(kernel_id=self.images["aki"]["image_id"],
+                                    ramdisk_id=self.images["ari"]["image_id"],
+                                    instance_type=self.instance_type)
+        rcuk = self.addResourceCleanUp(self.destroy_reservation, reservation)
+
+        def _state():
+            instance.update(validate=True)
+            return instance.state
+
+        for instance in reservation.instances:
+            LOG.info("state: %s", instance.state)
+            if instance.state != "running":
+                self.assertInstanceStateWait(_state, "running")
+
+        for instance in reservation.instances:
+            instance.stop()
+            LOG.info("state: %s", instance.state)
+            if instance.state != "stopped":
+                self.assertInstanceStateWait(_state, "stopped")
+
+        for instance in reservation.instances:
+            instance.terminate()
+        self.cancelResourceCleanUp(rcuk)
+
+    @attr(type='smoke')
+    def test_run_terminate_instance(self):
+        """EC2 run, terminate immediately"""
+        image_ami = self.ec2_client.get_image(self.images["ami"]
+                                              ["image_id"])
+        reservation = image_ami.run(kernel_id=self.images["aki"]["image_id"],
+                                    ramdisk_id=self.images["ari"]["image_id"],
+                                    instance_type=self.instance_type)
+
+        for instance in reservation.instances:
+            instance.terminate()
+
+        instance.update(validate=True)
+        self.assertNotEqual(instance.state, "running")
+
+    #NOTE(afazekas): doctored test case,
+    # with normal validation it would fail
+    @attr("slow", type='smoke')
+    def test_integration_1(self):
+        """EC2 1. integration test (not strict)"""
+        image_ami = self.ec2_client.get_image(self.images["ami"]["image_id"])
+        sec_group_name = rand_name("securitygroup-")
+        group_desc = sec_group_name + " security group description "
+        security_group = self.ec2_client.create_security_group(sec_group_name,
+                                                               group_desc)
+        self.addResourceCleanUp(self.destroy_security_group_wait,
+                                security_group)
+        self.ec2_client.authorize_security_group(sec_group_name,
+                                                 ip_protocol="icmp",
+                                                 cidr_ip="0.0.0.0/0",
+                                                 from_port=-1,
+                                                 to_port=-1)
+        self.ec2_client.authorize_security_group(sec_group_name,
+                                                 ip_protocol="tcp",
+                                                 cidr_ip="0.0.0.0/0",
+                                                 from_port=22,
+                                                 to_port=22)
+        reservation = image_ami.run(kernel_id=self.images["aki"]["image_id"],
+                                    ramdisk_id=self.images["ari"]["image_id"],
+                                    instance_type=self.instance_type,
+                                    key_name=self.keypair_name,
+                                    security_groups=(sec_group_name,))
+        self.addResourceCleanUp(self.destroy_reservation,
+                                reservation)
+        volume = self.ec2_client.create_volume(1, self.zone)
+        self.addResourceCleanUp(self.destroy_volume_wait, volume)
+        instance = reservation.instances[0]
+
+        def _instance_state():
+            instance.update(validate=True)
+            return instance.state
+
+        def _volume_state():
+            volume.update(validate=True)
+            return volume.status
+
+        LOG.info("state: %s", instance.state)
+        if instance.state != "running":
+            self.assertInstanceStateWait(_instance_state, "running")
+
+        address = self.ec2_client.allocate_address()
+        rcuk_a = self.addResourceCleanUp(address.delete)
+        address.associate(instance.id)
+
+        rcuk_da = self.addResourceCleanUp(address.disassociate)
+        #TODO(afazekas): ping test. dependecy/permission ?
+
+        self.assertVolumeStatusWait(_volume_state, "available")
+        #NOTE(afazekas): it may be reports availble before it is available
+
+        ssh = RemoteClient(address.public_ip,
+                           self.os.config.compute.ssh_user,
+                           pkey=self.keypair.material)
+        text = rand_name("Pattern text for console output -")
+        resp = ssh.write_to_console(text)
+        self.assertFalse(resp)
+
+        def _output():
+            output = instance.get_console_output()
+            return output.output
+
+        re_search_wait(_output, text)
+        part_lines = ssh.get_partitions().split('\n')
+        # "attaching" invalid EC2 state ! #1074901
+        volume.attach(instance.id, "/dev/vdh")
+
+        #self.assertVolumeStatusWait(_volume_state, "in-use")  # #1074901
+        re_search_wait(_volume_state, "in-use")
+
+        #NOTE(afazekas):  Different Hypervisor backends names
+        # differently the devices,
+        # now we just test is the partition number increased/decrised
+
+        def _part_state():
+            current = ssh.get_partitions().split('\n')
+            if current > part_lines:
+                return 'INCREASE'
+            if current < part_lines:
+                return 'DECREASE'
+            return 'EQUAL'
+
+        state_wait(_part_state, 'INCREASE')
+        part_lines = ssh.get_partitions().split('\n')
+
+        #TODO(afazekas): Resource compare to the flavor settings
+
+        volume.detach()  # "detaching" invalid EC2 status #1074901
+
+        #self.assertVolumeStatusWait(_volume_state, "available")
+        re_search_wait(_volume_state, "available")
+        LOG.info("Volume %s state: %s", volume.id, volume.status)
+
+        state_wait(_part_state, 'DECREASE')
+
+        instance.stop()
+        address.disassociate()
+        self.assertAddressDissasociatedWait(address)
+        self.cancelResourceCleanUp(rcuk_da)
+        address.release()
+        self.assertAddressReleasedWait(address)
+        self.cancelResourceCleanUp(rcuk_a)
+
+        LOG.info("state: %s", instance.state)
+        if instance.state != "stopped":
+            self.assertInstanceStateWait(_instance_state, "stopped")
+        #TODO(afazekas): move steps from teardown to the test case
+
+
+#TODO(afazekas): Snapshot/volume read/write test case
diff --git a/tempest/tests/boto/test_ec2_keys.py b/tempest/tests/boto/test_ec2_keys.py
new file mode 100644
index 0000000..79d0b2b
--- /dev/null
+++ b/tempest/tests/boto/test_ec2_keys.py
@@ -0,0 +1,79 @@
+# 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.
+
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest.common.utils.data_utils import rand_name
+from tempest import openstack
+
+
+def compare_key_pairs(a, b):
+    return (a.name == b.name and
+            a.fingerprint == b.fingerprint)
+
+
+@attr("EC2")
+class EC2KeysTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(EC2KeysTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.ec2api_client
+
+    @attr(type='smoke')
+    def test_create_ec2_keypair(self):
+        """EC2 create KeyPair"""
+        key_name = rand_name("keypair-")
+        self.addResourceCleanUp(self.client.delete_key_pair, key_name)
+        keypair = self.client.create_key_pair(key_name)
+        self.assertTrue(compare_key_pairs(keypair,
+                        self.client.get_key_pair(key_name)))
+
+    @attr(type='smoke')
+    @unittest.skip("Skipped until the Bug #1072318 is resolved")
+    def test_delete_ec2_keypair(self):
+        """EC2 delete KeyPair"""
+        key_name = rand_name("keypair-")
+        self.client.create_key_pair(key_name)
+        self.client.delete_key_pair(key_name)
+        self.assertEqual(None, self.client.get_key_pair(key_name))
+
+    @attr(type='smoke')
+    def test_get_ec2_keypair(self):
+        """EC2 get KeyPair"""
+        key_name = rand_name("keypair-")
+        self.addResourceCleanUp(self.client.delete_key_pair, key_name)
+        keypair = self.client.create_key_pair(key_name)
+        self.assertTrue(compare_key_pairs(keypair,
+                        self.client.get_key_pair(key_name)))
+
+    @attr(type='smoke')
+    @unittest.skip("Skipped until the Bug #1072762 is resolved")
+    def test_duplicate_ec2_keypair(self):
+        """EC2 duplicate KeyPair"""
+        key_name = rand_name("keypair-")
+        self.addResourceCleanUp(self.client.delete_key_pair, key_name)
+        keypair = self.client.create_key_pair(key_name)
+        self.assertEC2ResponseError(self.error_code.client.
+                                    InvalidKeyPair.Duplicate,
+                                    self.client.create_key_pair,
+                                    key_name)
+        self.assertTrue(compare_key_pairs(keypair,
+                        self.client.get_key_pair(key_name)))
diff --git a/tempest/tests/boto/test_ec2_network.py b/tempest/tests/boto/test_ec2_network.py
new file mode 100644
index 0000000..accf677
--- /dev/null
+++ b/tempest/tests/boto/test_ec2_network.py
@@ -0,0 +1,49 @@
+# 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.
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest import openstack
+
+
+@attr("EC2")
+class EC2NetworkTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(EC2NetworkTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.ec2api_client
+
+#Note(afazekas): these tests for things duable without an instance
+    @unittest.skip("Skipped until the Bug #1080406 is resolved")
+    @attr(type='smoke')
+    def test_disassociate_not_associated_floating_ip(self):
+        """EC2 disassociate not associated floating ip"""
+        ec2_codes = self.ec2_error_code
+        address = self.client.allocate_address()
+        public_ip = address.public_ip
+        rcuk = self.addResourceCleanUp(self.client.release_address, public_ip)
+        addresses_get = self.client.get_all_addresses(addresses=(public_ip,))
+        self.assertEqual(len(addresses_get), 1)
+        self.assertEqual(addresses_get[0].public_ip, public_ip)
+        self.assertBotoError(ec2_codes.client.InvalidAssociationID.NotFound,
+                             address.disassociate)
+        self.client.release_address(public_ip)
+        self.cancelResourceCleanUp(rcuk)
+        assertAddressReleasedWait(address)
diff --git a/tempest/tests/boto/test_ec2_security_groups.py b/tempest/tests/boto/test_ec2_security_groups.py
new file mode 100644
index 0000000..4e978e1
--- /dev/null
+++ b/tempest/tests/boto/test_ec2_security_groups.py
@@ -0,0 +1,78 @@
+# 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.
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest.common.utils.data_utils import rand_name
+from tempest import openstack
+
+
+@attr("EC2")
+class EC2SecurityGroupTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(EC2SecurityGroupTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.ec2api_client
+
+    @attr(type='smoke')
+    def test_create_authorize_security_group(self):
+        """EC2 Create, authorize/revoke security group"""
+        group_name = rand_name("securty_group-")
+        group_description = group_name + " security group description "
+        group = self.client.create_security_group(group_name,
+                                                  group_description)
+        self.addResourceCleanUp(self.client.delete_security_group, group_name)
+        groups_get = self.client.get_all_security_groups(groupnames=
+                                                         (group_name,))
+        self.assertEqual(len(groups_get), 1)
+        group_get = groups_get[0]
+        self.assertEqual(group.name, group_get.name)
+        self.assertEqual(group.name, group_get.name)
+        #ping (icmp_echo) and other icmp allowed from everywhere
+        # from_port and to_port act as icmp type
+        success = self.client.authorize_security_group(group_name,
+                                                       ip_protocol="icmp",
+                                                       cidr_ip="0.0.0.0/0",
+                                                       from_port=-1,
+                                                       to_port=-1)
+        self.assertTrue(success)
+        #allow standard ssh port from anywhere
+        success = self.client.authorize_security_group(group_name,
+                                                       ip_protocol="tcp",
+                                                       cidr_ip="0.0.0.0/0",
+                                                       from_port=22,
+                                                       to_port=22)
+        self.assertTrue(success)
+        #TODO(afazekas): Duplicate tests
+        group_get = self.client.get_all_security_groups(groupnames=
+                                                        (group_name,))[0]
+        #remove listed rules
+        for ip_permission in group_get.rules:
+            for cidr in ip_permission.grants:
+                self.assertTrue(self.client.revoke_security_group(group_name,
+                                ip_protocol=ip_permission.ip_protocol,
+                                cidr_ip=cidr,
+                                from_port=ip_permission.from_port,
+                                to_port=ip_permission.to_port))
+
+        group_get = self.client.get_all_security_groups(groupnames=
+                                                        (group_name,))[0]
+        #all rules shuld be removed now
+        self.assertEqual(0, len(group_get.rules))
diff --git a/tempest/tests/boto/test_ec2_volumes.py b/tempest/tests/boto/test_ec2_volumes.py
new file mode 100644
index 0000000..8b7e6be
--- /dev/null
+++ b/tempest/tests/boto/test_ec2_volumes.py
@@ -0,0 +1,93 @@
+# 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.
+
+
+from nose.plugins.attrib import attr
+from tempest.testboto import BotoTestCase
+from tempest import openstack
+import unittest2 as unittest
+import logging
+import time
+
+LOG = logging.getLogger(__name__)
+
+
+def compare_volumes(a, b):
+    return (a.id == b.id and
+            a.size == b.size)
+
+
+@attr("EC2")
+class EC2VolumesTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(EC2VolumesTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.ec2api_client
+        cls.zone = cls.client.get_good_zone()
+
+#NOTE(afazekas): as admin it can trigger the Bug #1074901
+    @attr(type='smoke')
+    def test_create_get_delete(self):
+        """EC2 Create, get, delete Volume"""
+        volume = self.client.create_volume(1, self.zone)
+        cuk = self.addResourceCleanUp(self.client.delete_volume, volume.id)
+        self.assertIn(volume.status, self.valid_volume_status)
+        retrieved = self.client.get_all_volumes((volume.id,))
+        self.assertEqual(1, len(retrieved))
+        self.assertTrue(compare_volumes(volume, retrieved[0]))
+
+        def _status():
+            volume.update(validate=True)
+            return volume.status
+
+        self.assertVolumeStatusWait(_status, "available")
+        self.client.delete_volume(volume.id)
+        self.cancelResourceCleanUp(cuk)
+
+    @unittest.skip("Skipped until the Bug #1080284 is resolved")
+    def test_create_volme_from_snapshot(self):
+        """EC2 Create volume from snapshot"""
+        volume = self.client.create_volume(1, self.zone)
+        self.addResourceCleanUp(self.client.delete_volume, volume.id)
+
+        def _status():
+            volume.update(validate=True)
+            return volume.status
+
+        self.assertVolumeStatusWait(_status, "available")
+        snap = self.client.create_snapshot(volume.id)
+        self.addResourceCleanUp(self.destroy_snapshot_wait, snap)
+
+        def _snap_status():
+            snap.update(validate=True)
+            return snap.status
+
+        #self.assertVolumeStatusWait(_snap_status, "available")  # not a volume
+        self.assertSnapshotStatusWait(_snap_status, "completed")
+
+        svol = self.client.create_volume(1, self.zone, snapshot=snap)
+        cuk = self.addResourceCleanUp(svol.delete)
+
+        def _snap_vol_status():
+            svol.update(validate=True)
+            return svol.status
+
+        self.assertVolumeStatusWait(_snap_vol_status, "available")
+        svol.delete()
+        self.cancelResourceCleanUp(cuk)
diff --git a/tempest/tests/boto/test_s3_buckets.py b/tempest/tests/boto/test_s3_buckets.py
new file mode 100644
index 0000000..56cf52c
--- /dev/null
+++ b/tempest/tests/boto/test_s3_buckets.py
@@ -0,0 +1,49 @@
+# 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.
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest.common.utils.data_utils import rand_name
+from tempest import openstack
+
+
+@attr("S3")
+class S3BucketsTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(S3BucketsTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.s3_client
+        cls.config = cls.os.config
+
+    @unittest.skip("Skipped until the Bug #1076965 is resolved")
+    @attr(type='smoke')
+    def test_create_and_get_delete_bucket(self):
+        """S3 Create, get and delete bucket"""
+        bucket_name = rand_name("s3bucket-")
+        cleanup_key = self.addResourceCleanUp(self.client.delete_bucket,
+                                              bucket_name)
+        bucket = self.client.create_bucket(bucket_name)
+        self.assertTrue(bucket.name == bucket_name)
+        bucket = self.client.get_bucket(bucket_name)
+        self.assertTrue(bucket.name == bucket_name)
+        self.client.delete_bucket(bucket_name)
+        self.assertBotoError(self.s3_error_code.client.NoSuchBucket,
+                             self.client.get_bucket, bucket_name)
+        self.cancelResourceCleanUp(cleanup_key)
diff --git a/tempest/tests/boto/test_s3_ec2_images.py b/tempest/tests/boto/test_s3_ec2_images.py
new file mode 100644
index 0000000..eeb7039
--- /dev/null
+++ b/tempest/tests/boto/test_s3_ec2_images.py
@@ -0,0 +1,144 @@
+# 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.
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest import openstack
+from tempest.testboto import BotoTestCase
+import tempest.tests.boto
+from tempest.tests.boto.utils.wait import state_wait
+from tempest.tests.boto.utils.s3 import s3_upload_dir
+from tempest.common.utils.data_utils import rand_name
+from contextlib import closing
+from boto.s3.key import Key
+import logging
+import nose
+import os
+
+
+@attr("S3", "EC2")
+class S3ImagesTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(S3ImagesTest, cls).setUpClass()
+        if not tempest.tests.boto.A_I_IMAGES_READY:
+            raise nose.SkipTest("".join(("EC2 ", cls.__name__,
+                                ": requires ami/aki/ari manifest")))
+        cls.os = openstack.Manager()
+        cls.s3_client = cls.os.s3_client
+        cls.images_client = cls.os.ec2api_client
+        config = cls.os.config
+        cls.materials_path = config.boto.s3_materials_path
+        cls.ami_manifest = config.boto.ami_manifest
+        cls.aki_manifest = config.boto.aki_manifest
+        cls.ari_manifest = config.boto.ari_manifest
+        cls.ami_path = cls.materials_path + os.sep + cls.ami_manifest
+        cls.aki_path = cls.materials_path + os.sep + cls.aki_manifest
+        cls.ari_path = cls.materials_path + os.sep + cls.ari_manifest
+        cls.bucket_name = rand_name("bucket-")
+        bucket = cls.s3_client.create_bucket(cls.bucket_name)
+        cls.addResourceCleanUp(cls.destroy_bucket,
+                               cls.s3_client.connection_data,
+                               cls.bucket_name)
+        s3_upload_dir(bucket, cls.materials_path)
+
+    #Note(afazekas): Without the normal status change test!
+    # otherwise I would skip it too
+    @attr(type='smoke')
+    def test_register_get_deregister_ami_image(self):
+        """Register and deregister ami image"""
+        image = {"name": rand_name("ami-name-"),
+                 "location": self.bucket_name + "/" + self.ami_manifest,
+                 "type": "ami"}
+        image["image_id"] = self.images_client.register_image(
+                            name=image["name"],
+                            image_location=image["location"])
+        #Note(afazekas): delete_snapshot=True might trigger boto lib? bug
+        image["cleanUp"] = self.addResourceCleanUp(
+                                self.images_client.deregister_image,
+                                image["image_id"])
+        self.assertEqual(image["image_id"][0:3], image["type"])
+        retrieved_image = self.images_client.get_image(image["image_id"])
+        self.assertTrue(retrieved_image.name == image["name"])
+        self.assertTrue(retrieved_image.id == image["image_id"])
+        state = retrieved_image.state
+        if state != "available":
+            def _state():
+                retr = self.images_client.get_image(image["image_id"])
+                return retr.state
+            state = state_wait(_state, "available")
+        self.assertEqual("available", state)
+        self.images_client.deregister_image(image["image_id"])
+        #TODO(afazekas): double deregister ?
+        self.cancelResourceCleanUp(image["cleanUp"])
+
+    @unittest.skip("Skipped until the Bug #1074904 is resolved")
+    def test_register_get_deregister_aki_image(self):
+        """Register and deregister aki image"""
+        image = {"name": rand_name("aki-name-"),
+                 "location": self.bucket_name + "/" + self.ari_manifest,
+                 "type": "aki"}
+        image["image_id"] = self.images_client.register_image(
+                            name=image["name"],
+                            image_location=image["location"])
+        image["cleanUp"] = self.addResourceCleanUp(
+                                self.images_client.deregister_image,
+                                image["image_id"])
+        self.assertEqual(image["image_id"][0:3], image["type"])
+        retrieved_image = self.images_client.get_image(image["image_id"])
+        self.assertTrue(retrieved_image.name == image["name"])
+        self.assertTrue(retrieved_image.id == image["image_id"])
+        self.assertIn(retrieved_image.state, self.valid_image_state)
+        if retrieved_image.state != "available":
+            def _state():
+                retr = self.images_client.get_image(image["image_id"])
+                return retr.state
+            self.assertImageStateWait(_state, "available")
+        self.images_client.deregister_image(image["image_id"])
+        #TODO(afazekas): verify deregister in  a better way
+        retrieved_image = self.images_client.get_image(image["image_id"])
+        self.assertIn(retrieved_image.state, self.valid_image_state)
+        self.cancelResourceCleanUp(image["cleanUp"])
+
+    @unittest.skip("Skipped until the Bug #1074908 and #1074904 is resolved")
+    def test_register_get_deregister_ari_image(self):
+        """Register and deregister ari image"""
+        image = {"name": rand_name("ari-name-"),
+                 "location": "/" + self.bucket_name + "/" + self.ari_manifest,
+                 "type": "ari"}
+        image["image_id"] = self.images_client.register_image(
+                            name=image["name"],
+                            image_location=image["location"])
+        image["cleanUp"] = self.addResourceCleanUp(
+                                self.images_client.deregister_image,
+                                image["image_id"])
+        self.assertEqual(image["image_id"][0:3], image["type"])
+        retrieved_image = self.images_client.get_image(image["image_id"])
+        self.assertIn(retrieved_image.state, self.valid_image_state)
+        if retrieved_image.state != "available":
+            def _state():
+                retr = self.images_client.get_image(image["image_id"])
+                return retr.state
+            self.assertImageStateWait(_state, "available")
+        self.assertIn(retrieved_image.state, self.valid_image_state)
+        self.assertTrue(retrieved_image.name == image["name"])
+        self.assertTrue(retrieved_image.id == image["image_id"])
+        self.images_client.deregister_image(image["image_id"])
+        self.cancelResourceCleanUp(image["cleanUp"])
+
+#TODO(afazekas): less copy-paste style
diff --git a/tempest/tests/boto/test_s3_objects.py b/tempest/tests/boto/test_s3_objects.py
new file mode 100644
index 0000000..c31ad6e
--- /dev/null
+++ b/tempest/tests/boto/test_s3_objects.py
@@ -0,0 +1,58 @@
+# 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.
+
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+from tempest.testboto import BotoTestCase
+from tempest.common.utils.data_utils import rand_name
+from tempest import openstack
+from tempest.tests import boto
+from boto.s3.key import Key
+from contextlib import closing
+
+
+@attr("S3")
+class S3BucketsTest(BotoTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(S3BucketsTest, cls).setUpClass()
+        cls.os = openstack.Manager()
+        cls.client = cls.os.s3_client
+        cls.config = cls.os.config
+
+    @unittest.skip("Skipped until the Bug #1076534 is resolved")
+    @attr(type='smoke')
+    def test_create_get_delete_object(self):
+        """S3 Create, get and delete object"""
+        bucket_name = rand_name("s3bucket-")
+        object_name = rand_name("s3object-")
+        content = 'x' * 42
+        bucket = self.client.create_bucket(bucket_name)
+        self.addResourceCleanUp(self.destroy_bucket,
+                                self.client.connection_data,
+                                bucket_name)
+
+        self.assertTrue(bucket.name == bucket_name)
+        with closing(Key(bucket)) as key:
+            key.key = object_name
+            key.set_contents_from_string(content)
+            readback = key.get_contents_as_string()
+            self.assertTrue(readback == content)
+            bucket.delete_key(key)
+            self.assertBotoError(self.s3_error_code.client.NoSuchKey,
+                                 key.get_contents_as_string)
diff --git a/tempest/tests/boto/utils/__init__.py b/tempest/tests/boto/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/boto/utils/__init__.py
diff --git a/tempest/tests/boto/utils/s3.py b/tempest/tests/boto/utils/s3.py
new file mode 100644
index 0000000..70d9263
--- /dev/null
+++ b/tempest/tests/boto/utils/s3.py
@@ -0,0 +1,42 @@
+# 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 boto
+from boto.s3.key import Key
+from contextlib import closing
+import os
+import re
+import logging
+
+
+LOG = logging.getLogger(__name__)
+
+
+def s3_upload_dir(bucket, path, prefix="", connection_data=None):
+    if isinstance(bucket, basestring):
+        with closing(boto.connect_s3(**connection_data)) as conn:
+            bucket = conn.lookup(bucket)
+    for root, dirs, files in os.walk(path):
+        for fil in files:
+            with closing(Key(bucket)) as key:
+                source = root + os.sep + fil
+                target = re.sub("^" + re.escape(path) + "?/", prefix, source)
+                if os.sep != '/':
+                    target = re.sub(re.escape(os.sep), '/', target)
+                key.key = target
+                LOG.info("Uploading %s to %s/%s", source, bucket.name, target)
+                key.set_contents_from_filename(source)
diff --git a/tempest/tests/boto/utils/wait.py b/tempest/tests/boto/utils/wait.py
new file mode 100644
index 0000000..38b6ba1
--- /dev/null
+++ b/tempest/tests/boto/utils/wait.py
@@ -0,0 +1,130 @@
+# 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 tempest.config
+import time
+from unittest2 import TestCase
+import logging
+import re
+from boto.exception import BotoServerError
+
+LOG = logging.getLogger(__name__)
+
+_boto_config = tempest.config.TempestConfig().boto
+
+default_timeout = _boto_config.build_timeout
+
+default_check_interval = _boto_config.build_interval
+
+
+def state_wait(lfunction, final_set=set(), valid_set=None):
+    #TODO(afazekas): evaluate using ABC here
+    if not isinstance(final_set, set):
+        final_set = set((final_set,))
+    if not isinstance(valid_set, set) and valid_set is not None:
+        valid_set = set((valid_set,))
+    start_time = time.time()
+    old_status = status = lfunction()
+    while True:
+        if status != old_status:
+            LOG.info('State transition "%s" ==> "%s" %d second', old_status,
+                     status, time.time() - start_time)
+        if status in final_set:
+            return status
+        if valid_set is not None and status not in valid_set:
+            return status
+        dtime = time.time() - start_time
+        if dtime > default_timeout:
+            raise TestCase.failureException("State change timeout exceeded!"
+                                            '(%ds) While waiting'
+                                            'for %s at "%s"' %
+                                            (dtime,
+                                            final_set, status))
+        time.sleep(default_check_interval)
+        old_status = status
+        status = lfunction()
+
+
+def re_search_wait(lfunction, regexp):
+    """Stops waiting on success"""
+    start_time = time.time()
+    while True:
+        text = lfunction()
+        result = re.search(regexp, text)
+        if result is not None:
+            LOG.info('Pattern "%s" found in %d second in "%s"',
+                     regexp,
+                     time.time() - start_time,
+                     text)
+            return result
+        dtime = time.time() - start_time
+        if dtime > default_timeout:
+            raise TestCase.failureException('Pattern find timeout exceeded!'
+                                            '(%ds) While waiting for'
+                                            '"%s" pattern in "%s"' %
+                                            (dtime,
+                                            regexp, text))
+        time.sleep(default_check_interval)
+
+
+def wait_no_exception(lfunction, exc_class=None, exc_matcher=None):
+    """Stops waiting on success"""
+    start_time = time.time()
+    if exc_matcher is not None:
+        exc_class = BotoServerError
+
+    if exc_class is None:
+        exc_class = BaseException
+    while True:
+        result = None
+        try:
+            result = lfunction()
+            LOG.info('No Exception in %d second',
+                     time.time() - start_time)
+            return result
+        except exc_class as exc:
+            if exc_matcher is not None:
+                res = exc_matcher.match(exc)
+                if res is not None:
+                    LOG.info(res)
+                    raise exc
+        # Let the other exceptions propagate
+        dtime = time.time() - start_time
+        if dtime > default_timeout:
+            raise TestCase.failureException("Wait timeout exceeded! (%ds)" %
+                                            dtime)
+        time.sleep(default_check_interval)
+
+
+#NOTE(afazekas): EC2/boto normally raise exception instead of empty list
+def wait_exception(lfunction):
+    """Returns with the exception or raises one"""
+    start_time = time.time()
+    while True:
+        try:
+            lfunction()
+        except BaseException as exc:
+            LOG.info('Exception in %d second',
+                     time.time() - start_time)
+            return exc
+        dtime = time.time() - start_time
+        if dtime > default_timeout:
+            raise TestCase.failureException("Wait timeout exceeded! (%ds)" %
+                                            dtime)
+        time.sleep(default_check_interval)
+
+#TODO(afazekas): consider strategy design pattern..
diff --git a/tools/pip-requires b/tools/pip-requires
index d3f9db7..9c861d9 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -4,3 +4,4 @@
 pika
 unittest2
 lxml
+boto>=2.2.1