You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2016/03/12 22:52:35 UTC

[01/10] libcloud git commit: [LIBCLOUD-802] Add drivers for Aliyun cloud

Repository: libcloud
Updated Branches:
  refs/heads/trunk 6f3279c74 -> 3da15748f


http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/test_slb.py
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/test_slb.py b/libcloud/test/loadbalancer/test_slb.py
new file mode 100644
index 0000000..926aed0
--- /dev/null
+++ b/libcloud/test/loadbalancer/test_slb.py
@@ -0,0 +1,566 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 sys
+import unittest
+
+from libcloud.compute.base import Node
+from libcloud.compute.types import NodeState
+from libcloud.loadbalancer.base import Member, Algorithm
+from libcloud.loadbalancer.drivers.slb import SLBDriver, \
+    SLBLoadBalancerHttpListener, SLBLoadBalancerHttpsListener, \
+    SLBLoadBalancerTcpListener, SLBLoadBalancerUdpListener
+from libcloud.loadbalancer.types import State
+from libcloud.test.file_fixtures import LoadBalancerFileFixtures
+from libcloud.test import MockHttpTestCase
+from libcloud.test.secrets import LB_SLB_PARAMS
+from libcloud.utils.py3 import httplib
+
+
+class SLBDriverTestCases(unittest.TestCase):
+    region = LB_SLB_PARAMS[2]
+
+    def setUp(self):
+        SLBMockHttp.test = self
+        SLBDriver.connectionCls.conn_classes = (None,
+                                                SLBMockHttp)
+        SLBMockHttp.type = None
+        SLBMockHttp.use_param = 'Action'
+
+        self.driver = SLBDriver(*LB_SLB_PARAMS)
+
+    def test_list_protocols(self):
+        protocols = self.driver.list_protocols()
+        self.assertEqual(4, len(protocols))
+        expected = ['tcp', 'udp', 'http', 'https']
+        diff = set(expected) - set(protocols)
+        self.assertEqual(0, len(diff))
+
+    def test_list_balancers(self):
+        balancers = self.driver.list_balancers()
+
+        self.assertEqual(len(balancers), 1)
+        balancer = balancers[0]
+        self.assertEqual('15229f88562-cn-hangzhou-dg-a01', balancer.id)
+        self.assertEqual('abc', balancer.name)
+        self.assertEqual(State.RUNNING, balancer.state)
+        self.assertEqual('120.27.186.149', balancer.ip)
+        self.assertTrue(balancer.port is None)
+        self.assertEqual(self.driver, balancer.driver)
+        expected_extra = {
+            'create_timestamp': 1452403099000,
+            'address_type': 'internet',
+            'region_id': 'cn-hangzhou-dg-a01',
+            'region_id_alias': 'cn-hangzhou',
+            'create_time': '2016-01-10T13:18Z',
+            'master_zone_id': 'cn-hangzhou-d',
+            'slave_zone_id': 'cn-hangzhou-b',
+            'network_type': 'classic'
+        }
+        self._validate_extras(expected_extra, balancer.extra)
+
+    def _validate_extras(self, expected, actual):
+        self.assertTrue(actual is not None)
+        for key, value in iter(expected.items()):
+            self.assertTrue(key in actual)
+            self.assertEqual(value, actual[key], ('extra %(key)s not equal, '
+                                                  'expected: "%(expected)s", '
+                                                  'actual: "%(actual)s"' %
+                                                  {'key': key,
+                                                   'expected': value,
+                                                   'actual': actual[key]}))
+
+    def test_list_balancers_with_ids(self):
+        SLBMockHttp.type = 'list_balancers_ids'
+        self.balancer_ids = ['id1', 'id2']
+        balancers = self.driver.list_balancers(
+            ex_balancer_ids=self.balancer_ids)
+        self.assertTrue(balancers is not None)
+
+    def test_list_balancers_with_ex_filters(self):
+        SLBMockHttp.type = 'list_balancers_filters'
+        self.ex_filters = {'AddressType': 'internet'}
+        balancers = self.driver.list_balancers(ex_filters=self.ex_filters)
+        self.assertTrue(balancers is not None)
+
+    def test_get_balancer(self):
+        SLBMockHttp.type = 'get_balancer'
+        balancer = self.driver.get_balancer(balancer_id='tests')
+
+        self.assertEqual(balancer.id, '15229f88562-cn-hangzhou-dg-a01')
+        self.assertEqual(balancer.name, 'abc')
+        self.assertEqual(balancer.state, State.RUNNING)
+
+    def test_destroy_balancer(self):
+        balancer = self.driver.get_balancer(balancer_id='tests')
+
+        self.assertTrue(self.driver.destroy_balancer(balancer))
+
+    def test_create_balancer(self):
+        self.name = 'balancer1'
+        self.port = 80
+        self.protocol = 'http'
+        self.algorithm = Algorithm.WEIGHTED_ROUND_ROBIN
+        self.extra = {
+            'AddressType': 'internet',
+            'InternetChargeType': 'paybytraffic',
+            'Bandwidth': 1,
+            'MasterZoneId': 'cn-hangzhou-d',
+            'SlaveZoneId': 'cn-hangzhou-b',
+            'StickySession': 'on',
+            'HealthCheck': 'on'}
+        self.members = [Member('node1', None, None)]
+
+        balancer = self.driver.create_balancer(name=self.name, port=self.port,
+                                               protocol=self.protocol,
+                                               algorithm=self.algorithm,
+                                               members=self.members,
+                                               **self.extra)
+
+        self.assertEqual(balancer.name, self.name)
+        self.assertEqual(balancer.port, self.port)
+        self.assertEqual(balancer.state, State.UNKNOWN)
+
+    def test_create_balancer_no_port_exception(self):
+        self.assertRaises(AttributeError, self.driver.create_balancer,
+                          None, None, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
+                          None)
+
+    def test_create_balancer_unsupport_protocol_exception(self):
+        self.assertRaises(AttributeError, self.driver.create_balancer,
+                          None, 443, 'ssl', Algorithm.WEIGHTED_ROUND_ROBIN,
+                          None)
+
+    def test_create_balancer_multiple_member_ports_exception(self):
+        members = [Member('m1', '1.2.3.4', 80),
+                   Member('m2', '1.2.3.5', 81)]
+        self.assertRaises(AttributeError, self.driver.create_balancer,
+                          None, 80, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
+                          members)
+
+    def test_create_balancer_bandwidth_value_error(self):
+        self.assertRaises(AttributeError, self.driver.create_balancer,
+                          None, 80, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
+                          None, Bandwidth='NAN')
+
+    def test_create_balancer_paybybandwidth_without_bandwidth_exception(self):
+        self.assertRaises(AttributeError, self.driver.create_balancer,
+                          None, 80, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
+                          None, InternetChargeType='paybybandwidth')
+
+    def test_balancer_list_members(self):
+        balancer = self.driver.get_balancer(balancer_id='tests')
+        members = balancer.list_members()
+
+        self.assertEqual(len(members), 1)
+        self.assertEqual(members[0].balancer, balancer)
+        self.assertEqual('i-23tshnsdq', members[0].id)
+
+    def test_balancer_list_listeners(self):
+        balancer = self.driver.get_balancer(balancer_id='tests')
+        listeners = self.driver.ex_list_listeners(balancer)
+
+        self.assertEqual(1, len(listeners))
+        listener = listeners[0]
+        self.assertEqual('80', listener.port)
+
+    def test_balancer_detach_member(self):
+        self.balancer = self.driver.get_balancer(balancer_id='tests')
+        self.member = Member('i-23tshnsdq', None, None)
+
+        self.assertTrue(self.balancer.detach_member(self.member))
+
+    def test_balancer_attach_compute_node(self):
+        SLBMockHttp.type = 'attach_compute_node'
+        self.balancer = self.driver.get_balancer(balancer_id='tests')
+        self.node = Node(id='node1', name='node-name',
+                         state=NodeState.RUNNING,
+                         public_ips=['1.2.3.4'], private_ips=['4.3.2.1'],
+                         driver=self.driver)
+        member = self.driver.balancer_attach_compute_node(
+            self.balancer, self.node)
+        self.assertEqual(self.node.id, member.id)
+        self.assertEqual(self.node.public_ips[0], member.ip)
+        self.assertEqual(self.balancer.port, member.port)
+
+    def test_ex_create_listener(self):
+        SLBMockHttp.type = 'create_listener'
+        self.balancer = self.driver.get_balancer(balancer_id='tests')
+        self.backend_port = 80
+        self.protocol = 'http'
+        self.algorithm = Algorithm.WEIGHTED_ROUND_ROBIN
+        self.bandwidth = 1
+        self.extra = {'StickySession': 'off', 'HealthCheck': 'off'}
+        self.assertTrue(self.driver.ex_create_listener(self.balancer,
+                                                       self.backend_port,
+                                                       self.protocol,
+                                                       self.algorithm,
+                                                       self.bandwidth,
+                                                       **self.extra))
+
+    def test_ex_create_listener_override_port(self):
+        SLBMockHttp.type = 'create_listener_override_port'
+        self.balancer = self.driver.get_balancer(balancer_id='tests')
+        self.backend_port = 80
+        self.protocol = 'http'
+        self.algorithm = Algorithm.WEIGHTED_ROUND_ROBIN
+        self.bandwidth = 1
+        self.extra = {'StickySession': 'off',
+                      'HealthCheck': 'off',
+                      'ListenerPort': 8080}
+        self.assertTrue(self.driver.ex_create_listener(self.balancer,
+                                                       self.backend_port,
+                                                       self.protocol,
+                                                       self.algorithm,
+                                                       self.bandwidth,
+                                                       **self.extra))
+
+    def test_ex_start_listener(self):
+        SLBMockHttp.type = 'start_listener'
+        balancer = self.driver.get_balancer(balancer_id='tests')
+        self.port = 80
+        self.assertTrue(self.driver.ex_start_listener(balancer, self.port))
+
+    def test_ex_stop_listener(self):
+        SLBMockHttp.type = 'stop_listener'
+        balancer = self.driver.get_balancer(balancer_id='tests')
+        self.port = 80
+        self.assertTrue(self.driver.ex_stop_listener(balancer, self.port))
+
+    def test_ex_upload_certificate(self):
+        self.name = 'cert1'
+        self.cert = 'cert-data'
+        self.key = 'key-data'
+        certificate = self.driver.ex_upload_certificate(self.name, self.cert,
+                                                        self.key)
+        self.assertEqual(self.name, certificate.name)
+        self.assertEqual('01:DF:AB:CD', certificate.fingerprint)
+
+    def test_ex_list_certificates(self):
+        certs = self.driver.ex_list_certificates()
+        self.assertEqual(2, len(certs))
+        cert = certs[0]
+        self.assertEqual('139a00604ad-cn-east-hangzhou-01', cert.id)
+        self.assertEqual('abe', cert.name)
+        self.assertEqual('A:B:E', cert.fingerprint)
+
+    def test_ex_list_certificates_ids(self):
+        SLBMockHttp.type = 'list_certificates_ids'
+        self.cert_ids = ['cert1', 'cert2']
+        certs = self.driver.ex_list_certificates(certificate_ids=self.cert_ids)
+        self.assertTrue(certs is not None)
+
+    def test_ex_delete_certificate(self):
+        self.cert_id = 'cert1'
+        self.assertTrue(self.driver.ex_delete_certificate(self.cert_id))
+
+    def test_ex_set_certificate_name(self):
+        self.cert_id = 'cert1'
+        self.cert_name = 'cert-name'
+        self.assertTrue(self.driver.ex_set_certificate_name(self.cert_id,
+                                                            self.cert_name))
+
+
+class SLBMockHttp(MockHttpTestCase):
+    fixtures = LoadBalancerFileFixtures('slb')
+
+    def _DescribeLoadBalancers(self, method, url, body, headers):
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _list_balancers_ids_DescribeLoadBalancers(self, method, url, body,
+                                                  headers):
+        params = {'LoadBalancerId': ','.join(self.test.balancer_ids)}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _list_balancers_filters_DescribeLoadBalancers(self, method, url, body,
+                                                      headers):
+        params = {'AddressType': 'internet'}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _get_balancer_DescribeLoadBalancers(self, method, url, body, headers):
+        params = {'LoadBalancerId': 'tests'}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeLoadBalancers(method, url, body, headers)
+
+    def _DeleteLoadBalancer(self, method, url, body, headers):
+        params = {'LoadBalancerId': '15229f88562-cn-hangzhou-dg-a01'}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('delete_load_balancer.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeLoadBalancerAttribute(self, method, url, body, headers):
+        body = self.fixtures.load('describe_load_balancer_attribute.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _CreateLoadBalancer(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'LoadBalancerName': self.test.name}
+        balancer_keys = [
+            'AddressType',
+            'InternetChargeType',
+            'Bandwidth',
+            'MasterZoneId',
+            'SlaveZoneId'
+        ]
+        for key in balancer_keys:
+            params[key] = str(self.test.extra[key])
+
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('create_load_balancer.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _AddBackendServers(self, method, url, body, headers):
+        _id = self.test.members[0].id
+        self.assertTrue("ServerId" in url and _id in url)
+        self.assertTrue("Weight" in url and "100" in url)
+        body = self.fixtures.load('add_backend_servers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _CreateLoadBalancerHTTPListener(self, method, url, body, headers):
+        body = self.fixtures.load('create_load_balancer_http_listener.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _StartLoadBalancerListener(self, method, url, body, headers):
+        body = self.fixtures.load('start_load_balancer_listener.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _RemoveBackendServers(self, method, url, body, headers):
+        _id = self.test.member.id
+        servers_json = '["%s"]' % _id
+        params = {'LoadBalancerId': self.test.balancer.id,
+                  'BackendServers': servers_json}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('add_backend_servers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _attach_compute_node_DescribeLoadBalancers(self, method, url, body,
+                                                   headers):
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _attach_compute_node_AddBackendServers(self, method, url, body,
+                                               headers):
+        _id = self.test.node.id
+        self.assertTrue("ServerId" in url and _id in url)
+        self.assertTrue("Weight" in url and "100" in url)
+        body = self.fixtures.load('add_backend_servers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _create_listener_CreateLoadBalancerHTTPListener(self, method, url,
+                                                        body, headers):
+        params = {'LoadBalancerId': self.test.balancer.id,
+                  'RegionId': self.test.region,
+                  'ListenerPort': str(self.test.balancer.port),
+                  'BackendServerPort': str(self.test.backend_port),
+                  'Scheduler': 'wrr',
+                  'Bandwidth': '1',
+                  'StickySession': 'off',
+                  'HealthCheck': 'off'}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('create_load_balancer_http_listener.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _create_listener_DescribeLoadBalancers(self, method, url, body,
+                                               headers):
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _create_listener_override_port_CreateLoadBalancerHTTPListener(
+            self, method, url, body, headers):
+        params = {'LoadBalancerId': self.test.balancer.id,
+                  'RegionId': self.test.region,
+                  'ListenerPort': str(self.test.extra['ListenerPort']),
+                  'BackendServerPort': str(self.test.backend_port),
+                  'Scheduler': 'wrr',
+                  'Bandwidth': '1',
+                  'StickySession': 'off',
+                  'HealthCheck': 'off'}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('create_load_balancer_http_listener.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _create_listener_override_port_DescribeLoadBalancers(
+            self, method, url, body, headers):
+        body = self.fixtures.load('describe_load_balancers.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _start_listener_DescribeLoadBalancers(self, method, url, body,
+                                              headers):
+        return self._DescribeLoadBalancers(method, url, body, headers)
+
+    def _start_listener_StartLoadBalancerListener(self, method, url, body,
+                                                  headers):
+        params = {'ListenerPort': str(self.test.port)}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._StartLoadBalancerListener(method, url, body, headers)
+
+    def _stop_listener_DescribeLoadBalancers(self, method, url, body,
+                                             headers):
+        return self._DescribeLoadBalancers(method, url, body, headers)
+
+    def _stop_listener_StopLoadBalancerListener(self, method, url, body,
+                                                headers):
+        params = {'ListenerPort': str(self.test.port)}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._StartLoadBalancerListener(method, url, body, headers)
+
+    def _UploadServerCertificate(self, method, url, body, headers):
+        body = self.fixtures.load('upload_server_certificate.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeServerCertificates(self, method, url, body, headers):
+        body = self.fixtures.load('describe_server_certificates.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _list_certificates_ids_DescribeServerCertificates(self, method, url,
+                                                          body, headers):
+        params = {'RegionId': self.test.region,
+                  'ServerCertificateId': ','.join(self.test.cert_ids)}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('describe_server_certificates.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _DeleteServerCertificate(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ServerCertificateId': self.test.cert_id}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('delete_server_certificate.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _SetServerCertificateName(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ServerCertificateId': self.test.cert_id,
+                  'ServerCertificateName': self.test.cert_name}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('set_server_certificate_name.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+class AssertDictMixin(object):
+    def assert_dict_equals(self, expected, actual):
+        expected_keys = set(expected.keys())
+        actual_keys = set(actual.keys())
+        self.assertEqual(len(expected_keys), len(actual_keys))
+        self.assertEqual(0, len(expected_keys - actual_keys))
+        for key in expected:
+            self.assertEqual(expected[key], actual[key])
+
+
+class SLBLoadBalancerHttpListenerTestCase(unittest.TestCase, AssertDictMixin):
+    def setUp(self):
+        self.listener = SLBLoadBalancerHttpListener.create(
+            80, 8080, Algorithm.WEIGHTED_ROUND_ROBIN, 1,
+            extra={'StickySession': 'on',
+                   'StickySessionType': 'insert',
+                   'HealthCheck': 'on'}
+        )
+
+    def test_get_required_params(self):
+        expected = {'Action': 'CreateLoadBalancerHTTPListener',
+                    'ListenerPort': 80,
+                    'BackendServerPort': 8080,
+                    'Scheduler': 'wrr',
+                    'Bandwidth': 1,
+                    'StickySession': 'on',
+                    'HealthCheck': 'on'}
+        self.assert_dict_equals(expected,
+                                self.listener.get_required_params())
+
+    def test_get_optional_params(self):
+        expected = {'StickySessionType': 'insert'}
+        self.assert_dict_equals(expected, self.listener.get_optional_params())
+
+    def test_repr(self):
+        self.assertTrue('SLBLoadBalancerHttpListener' in str(self.listener))
+
+
+class SLBLoadBalancerHttpsListenerTestCase(unittest.TestCase, AssertDictMixin):
+    def setUp(self):
+        self.listener = SLBLoadBalancerHttpsListener.create(
+            80, 8080, Algorithm.WEIGHTED_ROUND_ROBIN, 1,
+            extra={'StickySession': 'on',
+                   'StickySessionType': 'insert',
+                   'HealthCheck': 'on',
+                   'ServerCertificateId': 'fake-cert1'}
+        )
+
+    def test_get_required_params(self):
+        expected = {'Action': 'CreateLoadBalancerHTTPSListener',
+                    'ListenerPort': 80,
+                    'BackendServerPort': 8080,
+                    'Scheduler': 'wrr',
+                    'Bandwidth': 1,
+                    'StickySession': 'on',
+                    'HealthCheck': 'on',
+                    'ServerCertificateId': 'fake-cert1'}
+        self.assert_dict_equals(expected,
+                                self.listener.get_required_params())
+
+    def test_get_optional_params(self):
+        expected = {'StickySessionType': 'insert'}
+        self.assert_dict_equals(expected, self.listener.get_optional_params())
+
+
+class SLBLoadBalancerTcpListenerTestCase(unittest.TestCase, AssertDictMixin):
+    def setUp(self):
+        self.listener = SLBLoadBalancerTcpListener.create(
+            80, 8080, Algorithm.WEIGHTED_ROUND_ROBIN, 1,
+            extra={'PersistenceTimeout': 0,
+                   'HealthCheckDomain': ''}
+        )
+
+    def test_get_required_params(self):
+        expected = {'Action': 'CreateLoadBalancerTCPListener',
+                    'ListenerPort': 80,
+                    'BackendServerPort': 8080,
+                    'Scheduler': 'wrr',
+                    'Bandwidth': 1}
+        self.assert_dict_equals(expected,
+                                self.listener.get_required_params())
+
+    def test_get_optional_params(self):
+        expected = {'PersistenceTimeout': 0,
+                    'HealthCheckDomain': ''}
+        self.assert_dict_equals(expected, self.listener.get_optional_params())
+
+
+class SLBLoadBalancerUdpListenerTestCase(unittest.TestCase, AssertDictMixin):
+    def setUp(self):
+        self.listener = SLBLoadBalancerUdpListener.create(
+            80, 8080, Algorithm.WEIGHTED_ROUND_ROBIN, 1,
+            extra={'PersistenceTimeout': 0}
+        )
+
+    def test_get_required_params(self):
+        expected = {'Action': 'CreateLoadBalancerUDPListener',
+                    'ListenerPort': 80,
+                    'BackendServerPort': 8080,
+                    'Scheduler': 'wrr',
+                    'Bandwidth': 1}
+        self.assert_dict_equals(expected,
+                                self.listener.get_required_params())
+
+    def test_get_optional_params(self):
+        expected = {'PersistenceTimeout': 0}
+        self.assert_dict_equals(expected, self.listener.get_optional_params())
+
+
+if __name__ == "__main__":
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index 216a7d5..879e8d5 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -50,9 +50,11 @@ CLOUDFRAMES_PARAMS = ('key', 'secret', False, 'host', 8888)
 PROFIT_BRICKS_PARAMS = ('user', 'key')
 VULTR_PARAMS = ('key')
 PACKET_PARAMS = ('api_key')
+ECS_PARAMS = ('access_key', 'access_secret')
 
 # Storage
 STORAGE_S3_PARAMS = ('key', 'secret')
+STORAGE_OSS_PARAMS = ('key', 'secret')
 # Google key = 20 char alphanumeric string starting with GOOG
 STORAGE_GOOGLE_STORAGE_PARAMS = ('GOOG0123456789ABCXYZ', 'secret')
 
@@ -62,6 +64,7 @@ STORAGE_AZURE_BLOBS_PARAMS = ('account', 'cGFzc3dvcmQ=')
 # Loadbalancer
 LB_BRIGHTBOX_PARAMS = ('user', 'key')
 LB_ELB_PARAMS = ('access_id', 'secret', 'region')
+LB_SLB_PARAMS = ('access_id', 'secret', 'region')
 
 # DNS
 DNS_PARAMS_LINODE = ('user', 'key')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/complete_multipart_upload.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/complete_multipart_upload.xml b/libcloud/test/storage/fixtures/oss/complete_multipart_upload.xml
new file mode 100644
index 0000000..ff8da76
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/complete_multipart_upload.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CompleteMultipartUploadResult>
+    <Location>http://oss-example.oss-cn-hangzhou.aliyuncs.com/multipart.data</Location>
+    <Bucket>oss-example</Bucket>
+    <Key>multipart.data</Key>
+    <ETag>B864DB6A936D376F9F8D3ED3BBE540DD-3</ETag>
+</CompleteMultipartUploadResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p1.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p1.xml b/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p1.xml
new file mode 100644
index 0000000..912d947
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p1.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListMultipartUploadsResult>
+    <Bucket>oss-example</Bucket>
+    <KeyMarker></KeyMarker>
+    <UploadIdMarker></UploadIdMarker>
+    <NextKeyMarker>oss.avi</NextKeyMarker>
+    <NextUploadIdMarker>0004B99B8E707874FC2D692FA5D77D3F</NextUploadIdMarker>
+    <Delimiter></Delimiter>
+    <Prefix></Prefix>
+    <MaxUploads>2</MaxUploads>
+    <IsTruncated>true</IsTruncated>
+    <Upload>
+        <Key>multipart.data</Key>
+        <UploadId>0004B999EF518A1FE585B0C9360DC4C8</UploadId>
+        <Initiated>2012-02-23T04:18:23.000Z</Initiated>
+    </Upload>
+    <Upload>
+        <Key>oss.avi</Key>
+        <UploadId>0004B99B8E707874FC2D692FA5D77D3F</UploadId>
+        <Initiated>2012-02-23T06:14:27.000Z</Initiated>
+    </Upload>
+</ListMultipartUploadsResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p2.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p2.xml b/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p2.xml
new file mode 100644
index 0000000..9e256ef
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/ex_iterate_multipart_uploads_p2.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListMultipartUploadsResult>
+    <Bucket>oss-example</Bucket>
+    <KeyMarker></KeyMarker>
+    <UploadIdMarker></UploadIdMarker>
+    <NextKeyMarker></NextKeyMarker>
+    <NextUploadIdMarker></NextUploadIdMarker>
+    <Delimiter></Delimiter>
+    <Prefix></Prefix>
+    <MaxUploads>2</MaxUploads>
+    <IsTruncated>false</IsTruncated>
+    <Upload>
+        <Key>multipart.data</Key>
+        <UploadId>0004B999EF5A239BB9138C6227D69F95</UploadId>
+        <Initiated>2012-02-23T04:18:23.000Z</Initiated>
+    </Upload>
+</ListMultipartUploadsResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/initiate_multipart_upload.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/initiate_multipart_upload.xml b/libcloud/test/storage/fixtures/oss/initiate_multipart_upload.xml
new file mode 100644
index 0000000..182c248
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/initiate_multipart_upload.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<InitiateMultipartUploadResult>
+    <Bucket>multipart_upload</Bucket>
+    <Key>multipart.data</Key>
+    <UploadId>0004B9894A22E5B1888A1E29F8236E2D</UploadId>
+</InitiateMultipartUploadResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_container_objects.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_container_objects.xml b/libcloud/test/storage/fixtures/oss/list_container_objects.xml
new file mode 100644
index 0000000..c301627
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_container_objects.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListBucketResult>
+  <Name>20150624</Name>
+  <Prefix></Prefix>
+  <Marker></Marker>
+  <MaxKeys>100</MaxKeys>
+  <Delimiter></Delimiter>
+  <IsTruncated>false</IsTruncated>
+  <Contents>
+    <Key>en/</Key>
+    <LastModified>2016-01-15T14:43:15.000Z</LastModified>
+    <ETag>"D41D8CD98F00B204E9800998ECF8427E"</ETag>
+    <Type>Normal</Type>
+    <Size>0</Size>
+    <StorageClass>Standard</StorageClass>
+    <Owner>
+      <ID>1751306716098727</ID>
+      <DisplayName>1751306716098727</DisplayName>
+    </Owner>
+  </Contents>
+  <Contents>
+    <Key>test.txt</Key>
+    <LastModified>2016-01-15T14:42:46.000Z</LastModified>
+    <ETag>"26AB0DB90D72E28AD0BA1E22EE510510"</ETag>
+    <Type>Normal</Type>
+    <Size>2</Size>
+    <StorageClass>Standard</StorageClass>
+    <Owner>
+      <ID>1751306716098727</ID>
+      <DisplayName>1751306716098727</DisplayName>
+    </Owner>
+  </Contents>
+</ListBucketResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_container_objects_chinese.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_container_objects_chinese.xml b/libcloud/test/storage/fixtures/oss/list_container_objects_chinese.xml
new file mode 100644
index 0000000..2c4f9fc
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_container_objects_chinese.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListBucketResult>
+    <Name>test_container</Name>
+    <Prefix></Prefix>
+    <Marker></Marker>
+    <MaxKeys>100</MaxKeys>
+    <Delimiter></Delimiter>
+    <IsTruncated>false</IsTruncated>
+    <Contents>
+        <Key>WEB\xe6\x8e\xa7\xe5\x88\xb6\xe5\x8f\xb0.odp</Key>
+        <LastModified>2016-01-15T14:43:06.000Z</LastModified>
+        <ETag>"281371EA1618CF0E645D6BB90A158276"</ETag>
+	<Type>Normal</Type>
+        <Size>1234567</Size>
+	<StorageClass>Standard</StorageClass>
+        <Owner>
+            <ID>1751306716098727</ID>
+        </Owner>
+    </Contents>
+    <Contents>
+      <Key>\xe4\xb8\xad\xe6\x96\x87/</Key>
+      <LastModified>2016-01-15T14:43:34.000Z</LastModified>
+      <ETag>"D41D8CD98F00B204E9800998ECF8427E"</ETag>
+      <Type>Normal</Type>
+      <Size>0</Size>
+      <StorageClass>Standard</StorageClass>
+      <Owner>
+        <ID>1751306716098727</ID>
+        <DisplayName>1751306716098727</DisplayName>
+      </Owner>
+    </Contents>
+</ListBucketResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_container_objects_empty.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_container_objects_empty.xml b/libcloud/test/storage/fixtures/oss/list_container_objects_empty.xml
new file mode 100644
index 0000000..463bb71
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_container_objects_empty.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListBucketResult>
+  <Name>20160116</Name>
+  <Prefix></Prefix>
+  <Marker></Marker>
+  <MaxKeys>100</MaxKeys>
+  <Delimiter></Delimiter>
+  <IsTruncated>false</IsTruncated>
+</ListBucketResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_container_objects_prefix.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_container_objects_prefix.xml b/libcloud/test/storage/fixtures/oss/list_container_objects_prefix.xml
new file mode 100644
index 0000000..38d850a
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_container_objects_prefix.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListBucketResult>
+  <Name>20150624</Name>
+  <Prefix>en</Prefix>
+  <Marker></Marker>
+  <MaxKeys>100</MaxKeys>
+  <Delimiter></Delimiter>
+  <IsTruncated>false</IsTruncated>
+  <Contents>
+    <Key>en/</Key>
+    <LastModified>2016-01-15T14:43:15.000Z</LastModified>
+    <ETag>"D41D8CD98F00B204E9800998ECF8427E"</ETag>
+    <Type>Normal</Type>
+    <Size>0</Size>
+    <StorageClass>Standard</StorageClass>
+    <Owner>
+      <ID>1751306716098727</ID>
+      <DisplayName>1751306716098727</DisplayName>
+    </Owner>
+  </Contents>
+  <Contents>
+    <Key>en/test.txt</Key>
+    <LastModified>2016-01-15T14:42:46.000Z</LastModified>
+    <ETag>"26AB0DB90D72E28AD0BA1E22EE510510"</ETag>
+    <Type>Normal</Type>
+    <Size>2</Size>
+    <StorageClass>Standard</StorageClass>
+    <Owner>
+      <ID>1751306716098727</ID>
+      <DisplayName>1751306716098727</DisplayName>
+    </Owner>
+  </Contents>
+</ListBucketResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_containers.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_containers.xml b/libcloud/test/storage/fixtures/oss/list_containers.xml
new file mode 100644
index 0000000..d904ef2
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_containers.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListAllMyBucketsResult>
+  <Owner>
+    <ID>ut_test_put_bucket</ID>
+    <DisplayName>ut_test_put_bucket</DisplayName>
+  </Owner>
+  <Buckets>
+    <Bucket>
+      <Location>oss-cn-hangzhou-a</Location>
+      <Name>xz02tphky6fjfiuc0</Name>
+      <CreationDate>2014-05-15T11:18:32.000Z</CreationDate>
+    </Bucket>
+    <Bucket>
+      <Location>oss-cn-hangzhou-a</Location>
+      <Name>xz02tphky6fjfiuc1</Name>
+      <CreationDate>2014-05-15T11:18:32.000Z</CreationDate>
+    </Bucket>
+  </Buckets>
+</ListAllMyBucketsResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/fixtures/oss/list_containers_empty.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/fixtures/oss/list_containers_empty.xml b/libcloud/test/storage/fixtures/oss/list_containers_empty.xml
new file mode 100644
index 0000000..f846f72
--- /dev/null
+++ b/libcloud/test/storage/fixtures/oss/list_containers_empty.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ListAllMyBucketsResult>
+    <Owner>
+        <ID>af4rf45db0927637c66fb848dfc972b8b5126e1237bde6fe02862b11481fdfd9</ID>
+        <DisplayName>foobar</DisplayName>
+    </Owner>
+    <Buckets>
+    </Buckets>
+</ListAllMyBucketsResult>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/storage/test_oss.py
----------------------------------------------------------------------
diff --git a/libcloud/test/storage/test_oss.py b/libcloud/test/storage/test_oss.py
new file mode 100644
index 0000000..e5eb24f
--- /dev/null
+++ b/libcloud/test/storage/test_oss.py
@@ -0,0 +1,881 @@
+# -*- coding=utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 __future__ import unicode_literals
+
+import os
+import sys
+import unittest
+
+try:
+    import mock
+except ImportError:
+    from unittest import mock
+
+try:
+    from lxml import etree as ET
+except ImportError:
+    from xml.etree import ElementTree as ET
+
+from libcloud.utils.py3 import b
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import urlparse
+from libcloud.utils.py3 import parse_qs
+from libcloud.utils.py3 import PY3
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.types import MalformedResponseError
+from libcloud.storage.base import Container, Object
+from libcloud.storage.types import ContainerDoesNotExistError
+from libcloud.storage.types import ContainerError
+from libcloud.storage.types import ContainerIsNotEmptyError
+from libcloud.storage.types import InvalidContainerNameError
+from libcloud.storage.types import ObjectDoesNotExistError
+from libcloud.storage.types import ObjectHashMismatchError
+from libcloud.storage.drivers.oss import OSSConnection
+from libcloud.storage.drivers.oss import OSSStorageDriver
+from libcloud.storage.drivers.oss import CHUNK_SIZE
+from libcloud.storage.drivers.dummy import DummyIterator
+from libcloud.test import StorageMockHttp, MockRawResponse  # pylint: disable-msg=E0611
+from libcloud.test import MockHttpTestCase  # pylint: disable-msg=E0611
+from libcloud.test.file_fixtures import StorageFileFixtures  # pylint: disable-msg=E0611
+from libcloud.test.secrets import STORAGE_OSS_PARAMS
+
+
+class OSSConnectionTestCase(unittest.TestCase):
+    def setUp(self):
+        self.conn = OSSConnection('44CF9590006BF252F707',
+                                  'OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV')
+
+    def test_signature(self):
+        expected = b('26NBxoKdsyly4EDv6inkoDft/yA=')
+        headers = {
+            'Content-MD5': 'ODBGOERFMDMzQTczRUY3NUE3NzA5QzdFNUYzMDQxNEM=',
+            'Content-Type': 'text/html',
+            'Expires': 'Thu, 17 Nov 2005 18:49:58 GMT',
+            'X-OSS-Meta-Author': 'foo@bar.com',
+            'X-OSS-Magic': 'abracadabra',
+            'Host': 'oss-example.oss-cn-hangzhou.aliyuncs.com'
+        }
+        action = '/oss-example/nelson'
+        actual = OSSConnection._get_auth_signature('PUT', headers, {},
+                                                   headers['Expires'],
+                                                   self.conn.key,
+                                                   action,
+                                                   'x-oss-')
+        self.assertEqual(expected, actual)
+
+
+class ObjectTestCase(unittest.TestCase):
+    def test_object_with_chinese_name(self):
+        driver = OSSStorageDriver(*STORAGE_OSS_PARAMS)
+        obj = Object(name='中文', size=0, hash=None, extra=None,
+                     meta_data=None, container=None, driver=driver)
+        self.assertTrue(obj.__repr__() is not None)
+
+
+class OSSMockHttp(StorageMockHttp, MockHttpTestCase):
+
+    fixtures = StorageFileFixtures('oss')
+    base_headers = {}
+
+    def _unauthorized(self, method, url, body, headers):
+        return (httplib.UNAUTHORIZED,
+                '',
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_containers_empty(self, method, url, body, headers):
+        body = self.fixtures.load('list_containers_empty.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_containers(self, method, url, body, headers):
+        body = self.fixtures.load('list_containers.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_container_objects_empty(self, method, url, body, headers):
+        body = self.fixtures.load('list_container_objects_empty.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_container_objects(self, method, url, body, headers):
+        body = self.fixtures.load('list_container_objects.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_container_objects_chinese(self, method, url, body, headers):
+        body = self.fixtures.load('list_container_objects_chinese.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _list_container_objects_prefix(self, method, url, body, headers):
+        params = {'prefix': self.test.prefix}
+        self.assertUrlContainsQueryParams(url, params)
+        body = self.fixtures.load('list_container_objects_prefix.xml')
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _get_container(self, method, url, body, headers):
+        return self._list_containers(method, url, body, headers)
+
+    def _get_object(self, method, url, body, headers):
+        return self._list_containers(method, url, body, headers)
+
+    def _notexisted_get_object(self, method, url, body, headers):
+        return (httplib.NOT_FOUND,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.NOT_FOUND])
+
+    def _test_get_object(self, method, url, body, headers):
+        self.base_headers.update(
+            {'accept-ranges': 'bytes',
+             'connection': 'keep-alive',
+             'content-length': '0',
+             'content-type': 'application/octet-stream',
+             'date': 'Sat, 16 Jan 2016 15:38:14 GMT',
+             'etag': '"D41D8CD98F00B204E9800998ECF8427E"',
+             'last-modified': 'Fri, 15 Jan 2016 14:43:15 GMT',
+             'server': 'AliyunOSS',
+             'x-oss-object-type': 'Normal',
+             'x-oss-request-id': '569A63E6257784731E3D877F',
+             'x-oss-meta-rabbits': 'monkeys'})
+
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _invalid_name(self, method, url, body, headers):
+        # test_create_container_bad_request
+        return (httplib.BAD_REQUEST,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _already_exists(self, method, url, body, headers):
+        # test_create_container_already_existed
+        return (httplib.CONFLICT,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _create_container(self, method, url, body, headers):
+        # test_create_container_success
+        self.assertEqual('PUT', method)
+        self.assertEqual('', body)
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _create_container_location(self, method, url, body, headers):
+        # test_create_container_success
+        self.assertEqual('PUT', method)
+        location_constraint = ('<CreateBucketConfiguration>'
+                               '<LocationConstraint>%s</LocationConstraint>'
+                               '</CreateBucketConfiguration>' %
+                               self.test.ex_location)
+        self.assertEqual(location_constraint, body)
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _delete_container_doesnt_exist(self, method, url, body, headers):
+        # test_delete_container_doesnt_exist
+        return (httplib.NOT_FOUND,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _delete_container_not_empty(self, method, url, body, headers):
+        # test_delete_container_not_empty
+        return (httplib.CONFLICT,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _delete_container(self, method, url, body, headers):
+        return (httplib.NO_CONTENT,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _foo_bar_object_not_found(self, method, url, body, headers):
+        # test_delete_object_not_found
+        return (httplib.NOT_FOUND,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_bar_object(self, method, url, body, headers):
+        # test_delete_object
+        return (httplib.NO_CONTENT,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_test_stream_data_multipart(self, method, url, body, headers):
+        headers = {'etag': '"0cc175b9c0f1b6a831c399e269772661"'}
+        TEST_UPLOAD_ID = '0004B9894A22E5B1888A1E29F8236E2D'
+
+        query_string = urlparse.urlsplit(url).query
+        query = parse_qs(query_string)
+
+        if not query.get('uploadId', False):
+            self.fail('Request doesnt contain uploadId query parameter')
+
+        upload_id = query['uploadId'][0]
+        if upload_id != TEST_UPLOAD_ID:
+            self.fail('first uploadId doesnt match')
+
+        if method == 'PUT':
+            # PUT is used for uploading the part. part number is mandatory
+            if not query.get('partNumber', False):
+                self.fail('Request is missing partNumber query parameter')
+
+            body = ''
+            return (httplib.OK,
+                    body,
+                    headers,
+                    httplib.responses[httplib.OK])
+
+        elif method == 'DELETE':
+            # DELETE is done for aborting the upload
+            body = ''
+            return (httplib.NO_CONTENT,
+                    body,
+                    headers,
+                    httplib.responses[httplib.NO_CONTENT])
+
+        else:
+            commit = ET.fromstring(body)
+            count = 0
+
+            for part in commit.findall('Part'):
+                count += 1
+                part_no = part.find('PartNumber').text
+                etag = part.find('ETag').text
+
+                self.assertEqual(part_no, str(count))
+                self.assertEqual(etag, headers['etag'])
+
+            # Make sure that manifest contains at least one part
+            self.assertTrue(count >= 1)
+
+            body = self.fixtures.load('complete_multipart_upload.xml')
+            return (httplib.OK,
+                    body,
+                    headers,
+                    httplib.responses[httplib.OK])
+
+    def _list_multipart(self, method, url, body, headers):
+        query_string = urlparse.urlsplit(url).query
+        query = parse_qs(query_string)
+
+        if 'key-marker' not in query:
+            body = self.fixtures.load('ex_iterate_multipart_uploads_p1.xml')
+        else:
+            body = self.fixtures.load('ex_iterate_multipart_uploads_p2.xml')
+
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+
+class OSSMockRawResponse(MockRawResponse, MockHttpTestCase):
+
+    fixtures = StorageFileFixtures('oss')
+
+    def parse_body(self):
+        if len(self.body) == 0 and not self.parse_zero_length_body:
+            return self.body
+
+        try:
+            if PY3:
+                parser = ET.XMLParser(encoding='utf-8')
+                body = ET.XML(self.body.encode('utf-8'), parser=parser)
+            else:
+                body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse XML",
+                                         body=self.body,
+                                         driver=self.connection.driver)
+        return body
+
+    def _foo_bar_object(self, method, url, body, headers):
+        # test_download_object_success
+        body = self._generate_random_data(1000)
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_bar_object_invalid_size(self, method, url, body, headers):
+        # test_upload_object_invalid_file_size
+        body = ''
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_bar_object_not_found(self, method, url, body, headers):
+        # test_upload_object_not_found
+        return (httplib.NOT_FOUND,
+                body,
+                headers,
+                httplib.responses[httplib.NOT_FOUND])
+
+    def _foo_test_upload_invalid_hash1(self, method, url, body, headers):
+        body = ''
+        headers = {}
+        headers['etag'] = '"foobar"'
+        # test_upload_object_invalid_hash1
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_test_upload(self, method, url, body, headers):
+        # test_upload_object_success
+        body = ''
+        headers = {'etag': '"0CC175B9C0F1B6A831C399E269772661"'}
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_test_upload_acl(self, method, url, body, headers):
+        # test_upload_object_with_acl
+        body = ''
+        headers = {'etag': '"0CC175B9C0F1B6A831C399E269772661"'}
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_test_stream_data(self, method, url, body, headers):
+        # test_upload_object_via_stream
+        body = ''
+        headers = {'etag': '"0cc175b9c0f1b6a831c399e269772661"'}
+        return (httplib.OK,
+                body,
+                headers,
+                httplib.responses[httplib.OK])
+
+    def _foo_test_stream_data_multipart(self, method, url, body, headers):
+        headers = {}
+        # POST is done for initiating multipart upload
+        if method == 'POST':
+            body = self.fixtures.load('initiate_multipart_upload.xml')
+            return (httplib.OK,
+                    body,
+                    headers,
+                    httplib.responses[httplib.OK])
+        else:
+            body = ''
+            return (httplib.BAD_REQUEST,
+                    body,
+                    headers,
+                    httplib.responses[httplib.BAD_REQUEST])
+
+
+class OSSStorageDriverTestCase(unittest.TestCase):
+    driver_type = OSSStorageDriver
+    driver_args = STORAGE_OSS_PARAMS
+    mock_response_klass = OSSMockHttp
+    mock_raw_response_klass = OSSMockRawResponse
+
+    @classmethod
+    def create_driver(self):
+        return self.driver_type(*self.driver_args)
+
+    def setUp(self):
+        self.driver_type.connectionCls.conn_classes = (
+            None, self.mock_response_klass)
+        self.driver_type.connectionCls.rawResponseCls = \
+            self.mock_raw_response_klass
+        self.mock_response_klass.type = None
+        self.mock_response_klass.test = self
+        self.mock_raw_response_klass.type = None
+        self.mock_raw_response_klass.test = self
+        self.driver = self.create_driver()
+
+    def tearDown(self):
+        self._remove_test_file()
+
+    def _remove_test_file(self):
+        file_path = os.path.abspath(__file__) + '.temp'
+
+        try:
+            os.unlink(file_path)
+        except OSError:
+            pass
+
+    def test_invalid_credentials(self):
+        self.mock_response_klass.type = 'unauthorized'
+        self.assertRaises(InvalidCredsError, self.driver.list_containers)
+
+    def test_list_containers_empty(self):
+        self.mock_response_klass.type = 'list_containers_empty'
+        containers = self.driver.list_containers()
+        self.assertEqual(len(containers), 0)
+
+    def test_list_containers_success(self):
+        self.mock_response_klass.type = 'list_containers'
+        containers = self.driver.list_containers()
+        self.assertEqual(len(containers), 2)
+
+        container = containers[0]
+        self.assertEqual('xz02tphky6fjfiuc0', container.name)
+        self.assertTrue('creation_date' in container.extra)
+        self.assertEqual('2014-05-15T11:18:32.000Z',
+                         container.extra['creation_date'])
+        self.assertTrue('location' in container.extra)
+        self.assertEqual('oss-cn-hangzhou-a', container.extra['location'])
+        self.assertEqual(self.driver, container.driver)
+
+    def test_list_container_objects_empty(self):
+        self.mock_response_klass.type = 'list_container_objects_empty'
+        container = Container(name='test_container', extra={},
+                              driver=self.driver)
+        objects = self.driver.list_container_objects(container=container)
+        self.assertEqual(len(objects), 0)
+
+    def test_list_container_objects_success(self):
+        self.mock_response_klass.type = 'list_container_objects'
+        container = Container(name='test_container', extra={},
+                              driver=self.driver)
+        objects = self.driver.list_container_objects(container=container)
+        self.assertEqual(len(objects), 2)
+
+        obj = objects[0]
+        self.assertEqual(obj.name, 'en/')
+        self.assertEqual(obj.hash, 'D41D8CD98F00B204E9800998ECF8427E')
+        self.assertEqual(obj.size, 0)
+        self.assertEqual(obj.container.name, 'test_container')
+        self.assertEqual(
+            obj.extra['last_modified'], '2016-01-15T14:43:15.000Z')
+        self.assertTrue('owner' in obj.meta_data)
+
+    def test_list_container_objects_with_chinese(self):
+        self.mock_response_klass.type = 'list_container_objects_chinese'
+        container = Container(name='test_container', extra={},
+                              driver=self.driver)
+        objects = self.driver.list_container_objects(container=container)
+        self.assertEqual(len(objects), 2)
+
+        obj = [o for o in objects
+               if o.name == 'WEB控制台.odp'][0]
+        self.assertEqual(obj.hash, '281371EA1618CF0E645D6BB90A158276')
+        self.assertEqual(obj.size, 1234567)
+        self.assertEqual(obj.container.name, 'test_container')
+        self.assertEqual(
+            obj.extra['last_modified'], '2016-01-15T14:43:06.000Z')
+        self.assertTrue('owner' in obj.meta_data)
+
+    def test_list_container_objects_with_prefix(self):
+        self.mock_response_klass.type = 'list_container_objects_prefix'
+        container = Container(name='test_container', extra={},
+                              driver=self.driver)
+        self.prefix = 'test_prefix'
+        objects = self.driver.list_container_objects(container=container,
+                                                     ex_prefix=self.prefix)
+        self.assertEqual(len(objects), 2)
+
+    def test_get_container_doesnt_exist(self):
+        self.mock_response_klass.type = 'get_container'
+        self.assertRaises(ContainerDoesNotExistError,
+                          self.driver.get_container,
+                          container_name='not-existed')
+
+    def test_get_container_success(self):
+        self.mock_response_klass.type = 'get_container'
+        container = self.driver.get_container(
+            container_name='xz02tphky6fjfiuc0')
+        self.assertTrue(container.name, 'xz02tphky6fjfiuc0')
+
+    def test_get_object_container_doesnt_exist(self):
+        self.mock_response_klass.type = 'get_object'
+        self.assertRaises(ObjectDoesNotExistError,
+                          self.driver.get_object,
+                          container_name='xz02tphky6fjfiuc0',
+                          object_name='notexisted')
+
+    def test_get_object_success(self):
+        self.mock_response_klass.type = 'get_object'
+        obj = self.driver.get_object(container_name='xz02tphky6fjfiuc0',
+                                     object_name='test')
+
+        self.assertEqual(obj.name, 'test')
+        self.assertEqual(obj.container.name, 'xz02tphky6fjfiuc0')
+        self.assertEqual(obj.size, 0)
+        self.assertEqual(obj.hash, 'D41D8CD98F00B204E9800998ECF8427E')
+        self.assertEqual(obj.extra['last_modified'],
+                         'Fri, 15 Jan 2016 14:43:15 GMT')
+        self.assertEqual(obj.extra['content_type'], 'application/octet-stream')
+        self.assertEqual(obj.meta_data['rabbits'], 'monkeys')
+
+    def test_create_container_bad_request(self):
+        # invalid container name, returns a 400 bad request
+        self.mock_response_klass.type = 'invalid_name'
+        self.assertRaises(ContainerError,
+                          self.driver.create_container,
+                          container_name='invalid_name')
+
+    def test_create_container_already_exists(self):
+        # container with this name already exists
+        self.mock_response_klass.type = 'already_exists'
+        self.assertRaises(InvalidContainerNameError,
+                          self.driver.create_container,
+                          container_name='new-container')
+
+    def test_create_container_success(self):
+        # success
+        self.mock_response_klass.type = 'create_container'
+        name = 'new_container'
+        container = self.driver.create_container(container_name=name)
+        self.assertEqual(container.name, name)
+
+    def test_create_container_with_ex_location(self):
+        self.mock_response_klass.type = 'create_container_location'
+        name = 'new_container'
+        self.ex_location = 'oss-cn-beijing'
+        container = self.driver.create_container(container_name=name,
+                                                 ex_location=self.ex_location)
+        self.assertEqual(container.name, name)
+        self.assertTrue(container.extra['location'], self.ex_location)
+
+    def test_delete_container_doesnt_exist(self):
+        container = Container(name='new_container', extra=None,
+                              driver=self.driver)
+        self.mock_response_klass.type = 'delete_container_doesnt_exist'
+        self.assertRaises(ContainerDoesNotExistError,
+                          self.driver.delete_container,
+                          container=container)
+
+    def test_delete_container_not_empty(self):
+        container = Container(name='new_container', extra=None,
+                              driver=self.driver)
+        self.mock_response_klass.type = 'delete_container_not_empty'
+        self.assertRaises(ContainerIsNotEmptyError,
+                          self.driver.delete_container,
+                          container=container)
+
+    def test_delete_container_success(self):
+        self.mock_response_klass.type = 'delete_container'
+        container = Container(name='new_container', extra=None,
+                              driver=self.driver)
+        self.assertTrue(self.driver.delete_container(container=container))
+
+    def test_download_object_success(self):
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        obj = Object(name='foo_bar_object', size=1000, hash=None, extra={},
+                     container=container, meta_data=None,
+                     driver=self.driver_type)
+        destination_path = os.path.abspath(__file__) + '.temp'
+        result = self.driver.download_object(obj=obj,
+                                             destination_path=destination_path,
+                                             overwrite_existing=False,
+                                             delete_on_failure=True)
+        self.assertTrue(result)
+
+    def test_download_object_invalid_file_size(self):
+        self.mock_raw_response_klass.type = 'invalid_size'
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        obj = Object(name='foo_bar_object', size=1000, hash=None, extra={},
+                     container=container, meta_data=None,
+                     driver=self.driver_type)
+        destination_path = os.path.abspath(__file__) + '.temp'
+        result = self.driver.download_object(obj=obj,
+                                             destination_path=destination_path,
+                                             overwrite_existing=False,
+                                             delete_on_failure=True)
+        self.assertFalse(result)
+
+    def test_download_object_not_found(self):
+        self.mock_raw_response_klass.type = 'not_found'
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        obj = Object(name='foo_bar_object', size=1000, hash=None, extra={},
+                     container=container, meta_data=None,
+                     driver=self.driver_type)
+        destination_path = os.path.abspath(__file__) + '.temp'
+        self.assertRaises(ObjectDoesNotExistError,
+                          self.driver.download_object,
+                          obj=obj,
+                          destination_path=destination_path,
+                          overwrite_existing=False,
+                          delete_on_failure=True)
+
+    def test_download_object_as_stream_success(self):
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+
+        obj = Object(name='foo_bar_object', size=1000, hash=None, extra={},
+                     container=container, meta_data=None,
+                     driver=self.driver_type)
+
+        stream = self.driver.download_object_as_stream(obj=obj,
+                                                       chunk_size=None)
+        self.assertTrue(hasattr(stream, '__iter__'))
+
+    def test_upload_object_invalid_hash1(self):
+        def upload_file(self, response, file_path, chunked=False,
+                        calculate_hash=True):
+            return True, 'hash343hhash89h932439jsaa89', 1000
+
+        self.mock_raw_response_klass.type = 'invalid_hash1'
+
+        old_func = self.driver_type._upload_file
+        try:
+            self.driver_type._upload_file = upload_file
+            file_path = os.path.abspath(__file__)
+            container = Container(name='foo_bar_container', extra={},
+                                  driver=self.driver)
+            object_name = 'foo_test_upload'
+            self.assertRaises(ObjectHashMismatchError,
+                              self.driver.upload_object,
+                              file_path=file_path,
+                              container=container,
+                              object_name=object_name,
+                              verify_hash=True)
+        finally:
+            self.driver_type._upload_file = old_func
+
+    def test_upload_object_success(self):
+        def upload_file(self, response, file_path, chunked=False,
+                        calculate_hash=True):
+            return True, '0cc175b9c0f1b6a831c399e269772661', 1000
+
+        old_func = self.driver_type._upload_file
+        try:
+            self.driver_type._upload_file = upload_file
+            file_path = os.path.abspath(__file__)
+            container = Container(name='foo_bar_container', extra={},
+                                  driver=self.driver)
+            object_name = 'foo_test_upload'
+            extra = {'meta_data': {'some-value': 'foobar'}}
+            obj = self.driver.upload_object(file_path=file_path,
+                                            container=container,
+                                            object_name=object_name,
+                                            extra=extra,
+                                            verify_hash=True)
+            self.assertEqual(obj.name, 'foo_test_upload')
+            self.assertEqual(obj.size, 1000)
+            self.assertTrue('some-value' in obj.meta_data)
+        finally:
+            self.driver_type._upload_file = old_func
+
+    def test_upload_object_with_acl(self):
+        def upload_file(self, response, file_path, chunked=False,
+                        calculate_hash=True):
+            return True, '0cc175b9c0f1b6a831c399e269772661', 1000
+
+        old_func = self.driver_type._upload_file
+        try:
+            self.driver_type._upload_file = upload_file
+            self.mock_raw_response_klass.type = 'acl'
+            file_path = os.path.abspath(__file__)
+            container = Container(name='foo_bar_container', extra={},
+                                  driver=self.driver)
+            object_name = 'foo_test_upload'
+            extra = {'acl': 'public-read'}
+            obj = self.driver.upload_object(file_path=file_path,
+                                            container=container,
+                                            object_name=object_name,
+                                            extra=extra,
+                                            verify_hash=True)
+            self.assertEqual(obj.name, 'foo_test_upload')
+            self.assertEqual(obj.size, 1000)
+            self.assertEqual(obj.extra['acl'], 'public-read')
+        finally:
+            self.driver_type._upload_file = old_func
+
+    def test_upload_object_with_invalid_acl(self):
+        file_path = os.path.abspath(__file__)
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        object_name = 'foo_test_upload'
+        extra = {'acl': 'invalid-acl'}
+        self.assertRaises(AttributeError,
+                          self.driver.upload_object,
+                          file_path=file_path,
+                          container=container,
+                          object_name=object_name,
+                          extra=extra,
+                          verify_hash=True)
+
+    def test_upload_empty_object_via_stream(self):
+        if self.driver.supports_multipart_upload:
+            self.mock_raw_response_klass.type = 'multipart'
+            self.mock_response_klass.type = 'multipart'
+        else:
+            self.mock_raw_response_klass.type = None
+            self.mock_response_klass.type = None
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        object_name = 'foo_test_stream_data'
+        iterator = DummyIterator(data=[''])
+        extra = {'content_type': 'text/plain'}
+        obj = self.driver.upload_object_via_stream(container=container,
+                                                   object_name=object_name,
+                                                   iterator=iterator,
+                                                   extra=extra)
+
+        self.assertEqual(obj.name, object_name)
+        self.assertEqual(obj.size, 0)
+
+    def test_upload_small_object_via_stream(self):
+        if self.driver.supports_multipart_upload:
+            self.mock_raw_response_klass.type = 'multipart'
+            self.mock_response_klass.type = 'multipart'
+        else:
+            self.mock_raw_response_klass.type = None
+            self.mock_response_klass.type = None
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        object_name = 'foo_test_stream_data'
+        iterator = DummyIterator(data=['2', '3', '5'])
+        extra = {'content_type': 'text/plain'}
+        obj = self.driver.upload_object_via_stream(container=container,
+                                                   object_name=object_name,
+                                                   iterator=iterator,
+                                                   extra=extra)
+
+        self.assertEqual(obj.name, object_name)
+        self.assertEqual(obj.size, 3)
+
+    def test_upload_big_object_via_stream(self):
+        if self.driver.supports_multipart_upload:
+            self.mock_raw_response_klass.type = 'multipart'
+            self.mock_response_klass.type = 'multipart'
+        else:
+            self.mock_raw_response_klass.type = None
+            self.mock_response_klass.type = None
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        object_name = 'foo_test_stream_data'
+        iterator = DummyIterator(
+            data=['2' * CHUNK_SIZE, '3' * CHUNK_SIZE, '5'])
+        extra = {'content_type': 'text/plain'}
+        obj = self.driver.upload_object_via_stream(container=container,
+                                                   object_name=object_name,
+                                                   iterator=iterator,
+                                                   extra=extra)
+
+        self.assertEqual(obj.name, object_name)
+        self.assertEqual(obj.size, CHUNK_SIZE * 2 + 1)
+
+    def test_upload_object_via_stream_abort(self):
+        if not self.driver.supports_multipart_upload:
+            return
+
+        self.mock_raw_response_klass.type = 'MULTIPART'
+        self.mock_response_klass.type = 'MULTIPART'
+
+        def _faulty_iterator():
+            for i in range(0, 5):
+                yield str(i)
+            raise RuntimeError('Error in fetching data')
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        object_name = 'foo_test_stream_data'
+        iterator = _faulty_iterator()
+        extra = {'content_type': 'text/plain'}
+
+        try:
+            self.driver.upload_object_via_stream(container=container,
+                                                 object_name=object_name,
+                                                 iterator=iterator,
+                                                 extra=extra)
+        except Exception:
+            pass
+
+        return
+
+    def test_ex_iterate_multipart_uploads(self):
+        if not self.driver.supports_multipart_upload:
+            return
+
+        self.mock_response_klass.type = 'list_multipart'
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+
+        for upload in self.driver.ex_iterate_multipart_uploads(container,
+                                                               max_uploads=2):
+            self.assertTrue(upload.key is not None)
+            self.assertTrue(upload.id is not None)
+            self.assertTrue(upload.initiated is not None)
+
+    def test_ex_abort_all_multipart_uploads(self):
+        if not self.driver.supports_multipart_upload:
+            return
+
+        self.mock_response_klass.type = 'list_multipart'
+
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+
+        with mock.patch('libcloud.storage.drivers.oss.OSSStorageDriver'
+                        '._abort_multipart', autospec=True) as mock_abort:
+            self.driver.ex_abort_all_multipart_uploads(container)
+
+            self.assertEqual(3, mock_abort.call_count)
+
+    def test_delete_object_not_found(self):
+        self.mock_response_klass.type = 'not_found'
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        obj = Object(name='foo_bar_object', size=1234, hash=None, extra=None,
+                     meta_data=None, container=container, driver=self.driver)
+        self.assertRaises(ObjectDoesNotExistError,
+                          self.driver.delete_object,
+                          obj=obj)
+
+    def test_delete_object_success(self):
+        container = Container(name='foo_bar_container', extra={},
+                              driver=self.driver)
+        obj = Object(name='foo_bar_object', size=1234, hash=None, extra=None,
+                     meta_data=None, container=container, driver=self.driver)
+
+        result = self.driver.delete_object(obj=obj)
+        self.assertTrue(result)
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())


[09/10] libcloud git commit: Update changelog.

Posted by to...@apache.org.
Update changelog.


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/2e5adcc1
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/2e5adcc1
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/2e5adcc1

Branch: refs/heads/trunk
Commit: 2e5adcc1e458609e69771fd494d1f2ff80375a0b
Parents: a8479e1
Author: Tomaz Muraus <to...@tomaz.me>
Authored: Sat Mar 12 13:27:50 2016 -0800
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sat Mar 12 13:29:11 2016 -0800

----------------------------------------------------------------------
 CHANGES.rst | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/2e5adcc1/CHANGES.rst
----------------------------------------------------------------------
diff --git a/CHANGES.rst b/CHANGES.rst
index 9ba7997..1cc9377 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -86,6 +86,10 @@ Compute
   (GITHUB-698)
   [Allard Hoeve] [Rick van de Loo]
 
+- New driver for Aliyun Elastic Compute Service.
+  (LIBCLOUD-802, GITHUB-712)
+  [Sam Song]
+
 Storage
 ~~~~~~~
 
@@ -101,6 +105,17 @@ Storage
   (GITHUB-696)
   [Jay jshridha]
 
+- New driver for Aliyun OSS Storage Service.
+  (LIBCLOUD-802, GITHUB-712)
+  [Sam Song]
+
+Loadbalancer
+~~~~~~~~~~~~
+
+- New driver for Aliyun SLB Loadbalancer Service.
+  (LIBCLOUD-802, GITHUB-712)
+  [Sam Song]
+
 DNS
 ~~~~
 


[08/10] libcloud git commit: [LIBCLOUD-802] Add doc page and examples for Aliyun ecs

Posted by to...@apache.org.
[LIBCLOUD-802] Add doc page and examples for Aliyun ecs

Signed-off-by: Tomaz Muraus <to...@tomaz.me>

Closes #712


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/a8479e18
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/a8479e18
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/a8479e18

Branch: refs/heads/trunk
Commit: a8479e18b288460f64d8b82327d9e11db1abf23d
Parents: ae4c482
Author: xg.song <sa...@gmail.com>
Authored: Sat Mar 12 00:25:43 2016 +0800
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sat Mar 12 13:29:01 2016 -0800

----------------------------------------------------------------------
 docs/compute/drivers/aliyun_ecs.rst             | 101 +++++++++++++++++++
 .../compute/ecs/ex_list_security_groups.py      |  15 +++
 docs/examples/compute/ecs/list_images.py        |  15 +++
 docs/examples/compute/ecs/list_locations.py     |  14 +++
 docs/examples/compute/ecs/list_sizes.py         |  15 +++
 docs/examples/compute/ecs/manage_nodes.py       |  56 ++++++++++
 .../compute/ecs/manage_volumes_and_snapshots.py |  26 +++++
 libcloud/compute/drivers/ecs.py                 |  75 ++++++++++----
 8 files changed, 299 insertions(+), 18 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/compute/drivers/aliyun_ecs.rst
----------------------------------------------------------------------
diff --git a/docs/compute/drivers/aliyun_ecs.rst b/docs/compute/drivers/aliyun_ecs.rst
new file mode 100644
index 0000000..4103c10
--- /dev/null
+++ b/docs/compute/drivers/aliyun_ecs.rst
@@ -0,0 +1,101 @@
+Aliyun(AliCloud) ECS Driver Documentation
+=========================================
+
+`Aliyun(AliCloud) Elastic Compute Service (ECS)`_ is a simple and efficient computing service, whose processing capacity is scalable. It can help you quickly build a more stable and secure application. It helps you improve the efficiency of operation and maintenance, and reduce the cost of IT. ECS enables you to focus on core business innovation.
+
+Regions
+-------
+
+The Aliyun supports mutiple regions, which indicates the distinct physical location all over the world. The current available regions in China are Hangzhou, Qingdao, Beijing, and Shenzhen. Other regions available outside of Chinese Mainland are Hong Kong, Singapore, and United States.
+
+You can select the AliCloud region according to the customer base, cost effectiveness, disaster recovery site, or any compliance requirements. ECS instances in the same region can communicate with each other over intranet, whereas cross-region ECS communication requires the internet connection.
+
+A region equals to `NodeLocation` in libcloud. Users can list all available regions, for example:
+
+.. literalinclude:: /examples/compute/ecs/list_locations.py
+   :language: python
+
+ECS Instance Types
+------------------
+
+An instance type defines some computing capabilities, including CPU, memory, associated with a set of ECS instances.
+
+Aliyun provides two generations, three instance type families, and more than ten instance types to support different usecases.
+
+For more information, please refer to the `Instance Generations`_ section, `Instance Type Families`_ section and `Instance Type`_ section of the official documentation.
+
+An instance type equals to the `NodeSize` in libcloud. Users can list all available instance types, for example:
+
+.. literalinclude:: /examples/compute/ecs/list_sizes.py
+   :language: python
+
+Security Groups
+---------------
+
+A security group is a logical grouping which groups instances in the same region with the same security requirements and mutual trust. Security groups, like firewalls, are used to configure network access controls for one or more ECS instances.
+
+Each instance belongs to at least one security group and this must be specified at the time of creation.
+
+Users can list all defined security groups, for example:
+
+.. literalinclude:: /examples/compute/ecs/ex_list_security_groups.py
+   :language: python
+
+For more information, please refer to the `Security Groups`_ section of the official documentation.
+
+Images
+------
+
+An image is an ECS instance operating environment template. It generally includes the operating system and preloaded software. It equals to `NodeImage` in libcloud.
+
+Users can list all available images, for example:
+
+.. literalinclude:: /examples/compute/ecs/list_images.py
+   :language: python
+
+Storage
+-------
+
+Aliyun ECS provides multiple types of storage disks for instances to meet the requirements of different application scenarios. An instance can use all these types of volumes independently.
+
+There are three types of disks: general cloud disk, SSD cloud disk and ephemeral SSD disk.
+
+Aliyun provides the snapshot mechanism. This creates a snapshot that retains a copy of the data on a disk at a certain time point manually or automatically.
+
+Aliyun storage disks equal to `StorageVolume` in libcloud. Users can manage volumes and snapshots, for example:
+
+.. literalinclude:: /examples/compute/ecs/manage_volumes_and_snapshots.py
+   :language: python
+
+For more information, please refer to the `Storage`_ section of the official documentation.
+
+IP Address
+----------
+
+IP addresses are an important means for users to access ECS instances and for ECS instances to provide external services. Each instance will be allocated a private network card and bound to a specific private IP and a public network card by default.
+
+Private IPs can be used for SLB load balancing, intranet mutual access between ECS instances or between an ECS instance and another cloud service within the same region. Data traffic through private IPs between instances in the same region is free.
+
+Public IPs are used to access the instance from the internet. Public network traffic is not free.
+
+Users can select different internet charge type and bandwidth limitations.
+
+Instance lifecycle management
+-----------------------------
+
+.. literalinclude:: /examples/compute/ecs/manage_nodes.py
+   :language: python
+
+API Reference
+-------------
+
+.. autoclass:: libcloud.compute.drivers.ecs.ECSDriver
+    :members:
+    :inherited-members:
+
+.. _`Aliyun(AliCloud) Elastic Compute Service (ECS)`: https://www.aliyun.com/product/ecs/?lang=en
+.. _`Instance Generations`: https://docs.aliyun.com/en#/pub/ecs_en_us/product-introduction/instance&instancegeneration
+.. _`Instance Type Families`: https://docs.aliyun.com/en#/pub/ecs_en_us/product-introduction/instance&instancetypefamily
+.. _`Instance Type`: https://docs.aliyun.com/en#/pub/ecs_en_us/product-introduction/instance&type
+.. _`Security Groups`: https://docs.aliyun.com/en#/pub/ecs_en_us/product-introduction/network&securitygroup
+.. _`Storage`: https://docs.aliyun.com/en#/pub/ecs_en_us/product-introduction/storage&summary

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/ex_list_security_groups.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/ex_list_security_groups.py b/docs/examples/compute/ecs/ex_list_security_groups.py
new file mode 100644
index 0000000..554f627
--- /dev/null
+++ b/docs/examples/compute/ecs/ex_list_security_groups.py
@@ -0,0 +1,15 @@
+import pprint
+
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+sec_groups = driver.ex_list_security_groups()
+pprint.pprint(sec_groups)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/list_images.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/list_images.py b/docs/examples/compute/ecs/list_images.py
new file mode 100644
index 0000000..e70a99d
--- /dev/null
+++ b/docs/examples/compute/ecs/list_images.py
@@ -0,0 +1,15 @@
+import pprint
+
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+images = driver.list_images()
+pprint.pprint(images)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/list_locations.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/list_locations.py b/docs/examples/compute/ecs/list_locations.py
new file mode 100644
index 0000000..c824c76
--- /dev/null
+++ b/docs/examples/compute/ecs/list_locations.py
@@ -0,0 +1,14 @@
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+locations = driver.list_locations()
+for each in locations:
+    print('id: {0:>16s}'.format(each.id))

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/list_sizes.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/list_sizes.py b/docs/examples/compute/ecs/list_sizes.py
new file mode 100644
index 0000000..7df6630
--- /dev/null
+++ b/docs/examples/compute/ecs/list_sizes.py
@@ -0,0 +1,15 @@
+import pprint
+
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+sizes = driver.list_sizes()
+pprint.pprint(sizes)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/manage_nodes.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/manage_nodes.py b/docs/examples/compute/ecs/manage_nodes.py
new file mode 100644
index 0000000..7507048
--- /dev/null
+++ b/docs/examples/compute/ecs/manage_nodes.py
@@ -0,0 +1,56 @@
+from libcloud.compute.base import NodeAuthPassword
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+# Query the size ecs.t1.small
+sizes = driver.list_sizes()
+t1_small = sizes[1]
+# Query the first ubuntu OS image
+images = driver.list_images()
+for each in images:
+    if 'ubuntu' in each.id.lower():
+        ubuntu = each
+        break
+else:
+    ubuntu = images[0]
+# Query the default security group
+sg = driver.ex_list_security_groups()[0]
+
+# Create a cloud type data disk which is 5GB and deleted with the node
+data_disk = {
+    'size': 5,
+    'category': driver.disk_categories.CLOUD,
+    'disk_name': 'data_disk1',
+    'delete_with_instance': True}
+
+# Set a password to access the guest OS
+auth = NodeAuthPassword('P@$$w0rd')
+
+# Create the node
+node = driver.create_node(
+    image=ubuntu, size=t1_small, name='test_node',
+    ex_security_group_id=sg.id,
+    ex_internet_charge_type=driver.internet_charge_types.BY_TRAFFIC,
+    ex_internet_max_bandwidth_out=1,
+    ex_data_disks=data_disk,
+    auth=auth)
+
+# Reboot the node
+node.reboot()
+
+# Stop the node
+driver.ex_stop_node(node)
+
+# Start the node
+driver.ex_start_node(node)
+
+# Destroy the node
+node.destroy()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/docs/examples/compute/ecs/manage_volumes_and_snapshots.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/ecs/manage_volumes_and_snapshots.py b/docs/examples/compute/ecs/manage_volumes_and_snapshots.py
new file mode 100644
index 0000000..de5aeb6
--- /dev/null
+++ b/docs/examples/compute/ecs/manage_volumes_and_snapshots.py
@@ -0,0 +1,26 @@
+import time
+
+from libcloud.compute.providers import get_driver
+from libcloud.compute.types import Provider
+
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
+
+region = 'cn-hangzhou'
+access_key_id = 'CHANGE IT'
+access_key_secret = 'CHANGE IT'
+
+driver = ECSDriver(access_key_id, access_key_secret, region=region)
+
+node = driver.list_nodes()[0]
+zone = driver.ex_list_zones()[0]
+
+new_volume = driver.create_volume(
+    size=5, name='data_volume1',
+    ex_zone_id=zone.id,
+    ex_disk_category=driver.disk_categories.CLOUD)
+driver.attach_volume(node, new_volume)
+# Wait 10s for attaching finished
+time.sleep(10)
+
+snapshot = driver.create_volume_snapshot(new_volume,
+                                         name='data_volume1_snapshot1')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a8479e18/libcloud/compute/drivers/ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ecs.py b/libcloud/compute/drivers/ecs.py
index 7929720..c9d53f8 100644
--- a/libcloud/compute/drivers/ecs.py
+++ b/libcloud/compute/drivers/ecs.py
@@ -457,6 +457,12 @@ class ECSDriver(NodeDriver):
     Aliyun ECS node driver.
 
     Used for Aliyun ECS service.
+
+    TODO:
+    Create public IP address
+    Get guest OS root password
+    Adjust internet bandwidth settings
+    Manage security groups and rules
     """
 
     name = 'Aliyun ECS'
@@ -693,7 +699,10 @@ class ECSDriver(NodeDriver):
 
     def reboot_node(self, node, ex_force_stop=False):
         """
+        Reboot the given node
+
         @inherits :class:`NodeDriver.reboot_node`
+
         :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
                                 otherwise, stop node normally,
                                 default to ``False``
@@ -725,7 +734,9 @@ class ECSDriver(NodeDriver):
         """
         Start node to running state.
 
-        :param node: ``Node`` object
+        :param node: the ``Node`` object to start
+        :type node: ``Node``
+
         :return: starting operation result.
         :rtype: ``bool``
         """
@@ -741,10 +752,12 @@ class ECSDriver(NodeDriver):
 
         :param node: The node to stop
         :type node: :class:`Node`
+
         :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
                                 otherwise, stop node normally,
                                 default to ``False``
         :type ex_force_stop: ``bool``
+
         :return: stopping operation result.
         :rtype: ``bool``
         """
@@ -761,6 +774,7 @@ class ECSDriver(NodeDriver):
 
         :keyword ex_filters: security group attributes to filter results.
         :type ex_filters: ``dict``
+
         :return: a list of defined security groups
         :rtype: ``list`` of ``ECSSecurityGroup``
         """
@@ -782,8 +796,10 @@ class ECSDriver(NodeDriver):
     def ex_list_zones(self, region_id=None):
         """
         List availability zones in the given region or the current region.
+
         :keyword region_id: the id of the region to query zones from
         :type region_id: ``str``
+
         :return: list of zones
         :rtype: ``list`` of ``ECSZone``
         """
@@ -808,16 +824,17 @@ class ECSDriver(NodeDriver):
 
         @inherits: :class:`NodeDriver.list_volumes`
 
-        :keyword  ex_volume_ids: a list of volume's ids used to filter volumes.
-                                 Only the volumes which's id in this list
-                                 will be returned.
-        :type   ex_volume_ids: ``list`` of ``str``
-        :keyword  ex_filters: volume attribute and value pairs to filter
-                              volumes. Only the volumes which matchs all will
-                              be returned.
-                              If the filter attribute need a json array value,
-                              use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
+        :keyword ex_volume_ids: a list of volume's ids used to filter volumes.
+                                Only the volumes which's id in this list
+                                will be returned.
+        :type ex_volume_ids: ``list`` of ``str``
+
+        :keyword ex_filters: volume attribute and value pairs to filter
+                             volumes. Only the volumes which matchs all will
+                             be returned.
+                             If the filter attribute need a json array value,
+                             use ``list`` object, the driver will convert it.
+        :type ex_filters: ``dict``
         """
         params = {'Action': 'DescribeDisks',
                   'RegionId': self.region}
@@ -851,15 +868,17 @@ class ECSDriver(NodeDriver):
         List snapshots for a storage volume.
 
         @inherites :class:`NodeDriver.list_volume_snapshots`
+
         :keyword ex_snapshot_ids: a list of snapshot ids to filter the
                                   snapshots returned.
         :type ex_snapshot_ids: ``list`` of ``str``
+
         :keyword ex_filters: snapshot attribute and value pairs to filter
                              snapshots. Only the snapshot which matchs all
                              the pairs will be returned.
                              If the filter attribute need a json array value,
                              use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
+        :type ex_filters: ``dict``
         """
         params = {'Action': 'DescribeSnapshots',
                   'RegionId': self.region}
@@ -888,14 +907,18 @@ class ECSDriver(NodeDriver):
         Create a new volume.
 
         @inherites :class:`NodeDriver.create_volume`
+
         :keyword ex_zone_id: the availability zone id (required)
         :type ex_zone_id: ``str``
+
         :keyword ex_description: volume description
         :type ex_description: ``unicode``
+
         :keyword ex_disk_category: disk category for data disk
         :type ex_disk_category: ``str``
+
         :keyword ex_client_token: a token generated by client to identify
-                                each request.
+                                  each request.
         :type ex_client_token: ``str``
         """
         params = {'Action': 'CreateDisk',
@@ -927,11 +950,14 @@ class ECSDriver(NodeDriver):
                                ex_client_token=None):
         """
         Creates a snapshot of the storage volume.
+
         @inherits :class:`NodeDriver.create_volume_snapshot`
+
         :keyword ex_description: description of the snapshot.
         :type ex_description: ``unicode``
+
         :keyword ex_client_token: a token generated by client to identify
-                                each request.
+                                  each request.
         :type ex_client_token: ``str``
         """
         params = {'Action': 'CreateSnapshot',
@@ -960,8 +986,11 @@ class ECSDriver(NodeDriver):
 
         @inherits :class:`NodeDriver.attach_volume`
 
+        :keyword device: device path allocated for this attached volume
+        :type device: ``str`` between /dev/xvdb to xvdz,
+                      if empty, allocated by the system
         :keyword ex_delete_with_instance: if to delete this volume when the
-                                        instance is deleted.
+                                          instance is deleted.
         :type ex_delete_with_instance: ``bool``
         """
         params = {'Action': 'AttachDisk',
@@ -1035,16 +1064,19 @@ class ECSDriver(NodeDriver):
     def list_images(self, location=None, ex_image_ids=None, ex_filters=None):
         """
         List images on a provider.
+
         @inherits :class:`NodeDriver.list_images`
+
         :keyword ex_image_ids: a list of image ids to filter the images to
                                be returned.
         :type ex_image_ids: ``list`` of ``str``
+
         :keyword ex_filters: image attribute and value pairs to filter
                              images. Only the image which matchs all
                              the pairs will be returned.
                              If the filter attribute need a json array value,
                              use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
+        :type ex_filters: ``dict``
         """
 
         if location and isinstance(location, NodeLocation):
@@ -1075,14 +1107,18 @@ class ECSDriver(NodeDriver):
                      ex_image_version=None, ex_client_token=None):
         """
         Creates an image from a system disk snapshot.
+
         @inherits :class:`NodeDriver.create_image`
+
         :keyword ex_snapshot_id: the id of the snapshot to create the image.
                                  (required)
         :type ex_snapshot_id: ``str``
+
         :keyword ex_image_version: the version number of the image
         :type ex_image_version: ``str``
+
         :keyword ex_client_token: a token generated by client to identify
-                                each request.
+                                  each request.
         :type ex_client_token: ``str``
         """
         params = {'Action': 'CreateImage',
@@ -1130,11 +1166,14 @@ class ECSDriver(NodeDriver):
         """
         Copies an image from a source region to the destination region.
         If not provide a destination region, default to the current region.
+
         @inherits :class:`NodeDriver.copy_image`
+
         :keyword ex_destination_region_id: id of the destination region
         :type ex_destination_region_id: ``str``
+
         :keyword ex_client_token: a token generated by client to identify
-                                each request.
+                                  each request.
         :type ex_client_token: ``str``
         """
         params = {'Action': 'CopyImage',


[02/10] libcloud git commit: [LIBCLOUD-802] Add drivers for Aliyun cloud

Posted by to...@apache.org.
http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_disks.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_disks.xml b/libcloud/test/compute/fixtures/ecs/describe_disks.xml
new file mode 100644
index 0000000..87e1846
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_disks.xml
@@ -0,0 +1,62 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeDisksResponse>
+    <Disks>
+        <Disk>
+	    <DiskChargeType>PostPaid</DiskChargeType>
+            <DeleteAutoSnapshot>true</DeleteAutoSnapshot>
+            <DeleteWithInstance>false</DeleteWithInstance>
+            <EnableAutoSnapshot>false</EnableAutoSnapshot>
+            <Category>cloud</Category>
+            <Description></Description>
+            <DiskName></DiskName>
+            <Size>5</Size>
+            <Type>data</Type>
+            <InstanceId></InstanceId>
+            <CreationTime>2014-07-23T02:44:07Z</CreationTime>
+            <ImageId></ImageId>
+            <ZoneId>cn-qingdao-b</ZoneId>
+            <AttachedTime>2014-07-23T07:47:35Z</AttachedTime>
+            <DetachedTime>2014-07-23T08:28:48Z</DetachedTime>
+            <Device></Device>
+            <OperationLocks></OperationLocks>
+            <Portable>true</Portable>
+            <ProductCode></ProductCode>
+            <RegionId>cn-qingdao</RegionId>
+            <DiskId>d-28m5zbua0</DiskId>
+            <SourceSnapshotId></SourceSnapshotId>
+            <Status>Available</Status>
+	    <Tags />
+        </Disk>
+        <Disk>
+	    <DiskChargeType>PostPaid</DiskChargeType>
+            <DeleteAutoSnapshot>true</DeleteAutoSnapshot>
+            <DeleteWithInstance>true</DeleteWithInstance>
+            <EnableAutoSnapshot>true</EnableAutoSnapshot>
+            <Category>cloud</Category>
+            <Description>Description</Description>
+            <DiskName>ubuntu1404sys</DiskName>
+            <Size>5</Size>
+            <Type>system</Type>
+            <InstanceId>i-28whl2nj2</InstanceId>
+            <CreationTime>2014-07-23T02:44:06Z</CreationTime>
+            <ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+            <ZoneId>cn-qingdao-b</ZoneId>
+            <AttachedTime>2016-01-04T15:02:17Z</AttachedTime>
+            <DetachedTime></DetachedTime>
+	    <Device>/dev/xvda</Device>
+            <OperationLocks></OperationLocks>
+            <Portable>false</Portable>
+            <ProductCode></ProductCode>
+            <RegionId>cn-qingdao</RegionId>
+            <DiskId>d-28zfrmo13</DiskId>
+            <SourceSnapshotId></SourceSnapshotId>
+            <Status>In_use</Status>
+	    <OperationLocks />
+	    <ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+        </Disk>
+    </Disks>
+    <PageNumber>1</PageNumber>
+    <PageSize>10</PageSize>
+    <TotalCount>2</TotalCount>
+    <RequestId>ED5CF6DD-71CA-462C-9C94-A61A78A01479</RequestId>
+</DescribeDisksResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_images.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_images.xml b/libcloud/test/compute/fixtures/ecs/describe_images.xml
new file mode 100644
index 0000000..80b1a31
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_images.xml
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeImagesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RegionId>cn-qingdao</RegionId>
+	<RequestId>FAD4D9B9-D75F-4A9E-BC13-991C0F06F50F</RequestId>
+	<Images>
+		<Image>
+			<ImageId>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageId>
+			<Description>freebsd1001_64_20G_aliaegis_20150527.vhd</Description>
+			<ProductCode></ProductCode>
+			<OSType>linux</OSType>
+			<Architecture>x86_64</Architecture>
+			<OSName>FreeBSD  10.1 64位</OSName>
+			<DiskDeviceMappings>
+				<DiskDeviceMapping>
+					<ImportOSSObject></ImportOSSObject>
+					<Format></Format>
+					<Device>/dev/xvda</Device>
+					<SnapshotId></SnapshotId>
+					<ImportOSSBucket></ImportOSSBucket>
+					<Size>20</Size>
+				</DiskDeviceMapping>
+			</DiskDeviceMappings>
+			<ImageOwnerAlias>system</ImageOwnerAlias>
+			<Progress>100%</Progress>
+			<Usage>instance</Usage>
+			<CreationTime>2015-06-19T07:25:42Z</CreationTime>
+			<Tags />
+			<ImageVersion>1.0.0</ImageVersion>
+			<Status>Available</Status>
+			<ImageName>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageName>
+			<IsSelfShared></IsSelfShared>
+			<IsCopied>false</IsCopied>
+			<IsSubscribed>false</IsSubscribed>
+			<Platform>Freebsd</Platform>
+			<Size>20</Size>
+		</Image>
+	</Images>
+</DescribeImagesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_instance_types.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_instance_types.xml b/libcloud/test/compute/fixtures/ecs/describe_instance_types.xml
new file mode 100644
index 0000000..6009564
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_instance_types.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstanceTypesResponse>
+    <RequestId>1651FBB6-4FBF-49FF-A9F5-DF5D696C7EC6</RequestId>
+    <InstanceTypes>
+        <InstanceType>
+            <InstanceTypeId>ecs.t1.xsmall</InstanceTypeId>
+            <CpuCoreCount>1</CpuCoreCount>
+            <MemorySize>0.5</MemorySize>
+            <InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+        </InstanceType>
+        <InstanceType>
+            <InstanceTypeId>ecs.s2.small</InstanceTypeId>
+            <CpuCoreCount>2</CpuCoreCount>
+            <MemorySize>1.0</MemorySize>
+            <InstanceTypeFamily>ecs.s2</InstanceTypeFamily>
+        </InstanceType>
+    </InstanceTypes>
+</DescribeInstanceTypesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_instances.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_instances.xml b/libcloud/test/compute/fixtures/ecs/describe_instances.xml
new file mode 100644
index 0000000..f1c7713
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_instances.xml
@@ -0,0 +1,56 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstancesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>CA75EE06-D5F7-433C-870B-5042EED6C1DC</RequestId>
+	<Instances>
+		<Instance>
+			<ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+			<InnerIpAddress>
+				<IpAddress>10.163.197.74</IpAddress>
+			</InnerIpAddress>
+			<InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+			<VlanId></VlanId>
+			<InstanceId>i-28n7dkvov</InstanceId>
+			<EipAddress>
+				<IpAddress></IpAddress>
+				<AllocationId></AllocationId>
+				<InternetChargeType></InternetChargeType>
+			</EipAddress>
+			<InternetMaxBandwidthIn>-1</InternetMaxBandwidthIn>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<InternetChargeType>PayByTraffic</InternetChargeType>
+			<SerialNumber>ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3</SerialNumber>
+			<IoOptimized>false</IoOptimized>
+			<Memory>1024</Memory>
+			<Cpu>1</Cpu>
+			<VpcAttributes>
+				<NatIpAddress></NatIpAddress>
+				<PrivateIpAddress />
+				<VSwitchId></VSwitchId>
+				<VpcId></VpcId>
+			</VpcAttributes>
+			<InternetMaxBandwidthOut>1</InternetMaxBandwidthOut>
+			<DeviceAvailable>true</DeviceAvailable>
+			<SecurityGroupIds>
+				<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			</SecurityGroupIds>
+			<InstanceName>iZ28n7dkvovZ</InstanceName>
+			<Description></Description>
+			<InstanceNetworkType>classic</InstanceNetworkType>
+			<PublicIpAddress>
+				<IpAddress>114.215.124.73</IpAddress>
+			</PublicIpAddress>
+			<HostName>iZ28n7dkvovZ</HostName>
+			<InstanceType>ecs.t1.small</InstanceType>
+			<CreationTime>2015-12-27T07:35Z</CreationTime>
+			<Status>Starting</Status>
+			<ClusterId></ClusterId>
+			<RegionId>cn-qingdao</RegionId>
+			<OperationLocks />
+			<InstanceChargeType>PostPaid</InstanceChargeType>
+			<ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+		</Instance>
+	</Instances>
+</DescribeInstancesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_regions.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_regions.xml b/libcloud/test/compute/fixtures/ecs/describe_regions.xml
new file mode 100644
index 0000000..91fef57
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_regions.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeRegionsResponse>
+	<RequestId>FD28A957-20B2-447E-8A5A-952F50C4EDF0</RequestId>
+	<Regions>
+		<Region>
+			<RegionId>ap-southeast-1</RegionId>
+			<LocalName>亚太(新加坡)</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-shenzhen</RegionId>
+			<LocalName>深圳</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-qingdao</RegionId>
+			<LocalName>青岛</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-beijing</RegionId>
+			<LocalName>北京</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-shanghai</RegionId>
+			<LocalName>上海</LocalName>
+		</Region>
+		<Region>
+			<RegionId>us-east-1</RegionId>
+			<LocalName>美东弗吉尼亚</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-hongkong</RegionId>
+			<LocalName>香港</LocalName>
+		</Region>
+		<Region>
+			<RegionId>cn-hangzhou</RegionId>
+			<LocalName>杭州</LocalName>
+		</Region>
+		<Region>
+			<RegionId>us-west-1</RegionId>
+			<LocalName>美国硅谷</LocalName>
+		</Region>
+	</Regions>
+</DescribeRegionsResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_security_groups.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_security_groups.xml b/libcloud/test/compute/fixtures/ecs/describe_security_groups.xml
new file mode 100644
index 0000000..24be5c3
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_security_groups.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeSecurityGroupsResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RegionId>cn-qingdao</RegionId>
+	<RequestId>7F154B0C-2594-416D-B546-75021185A6DA</RequestId>
+	<SecurityGroups>
+		<SecurityGroup>
+			<CreationTime>2015-06-26T08:35:30Z</CreationTime>
+			<Tags />
+			<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			<SecurityGroupName>sg-28ou0f3xa</SecurityGroupName>
+			<Description>System created security group.</Description>
+			<VpcId></VpcId>
+		</SecurityGroup>
+	</SecurityGroups>
+</DescribeSecurityGroupsResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_snapshots.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_snapshots.xml b/libcloud/test/compute/fixtures/ecs/describe_snapshots.xml
new file mode 100644
index 0000000..d30afb4
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_snapshots.xml
@@ -0,0 +1,25 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeSnapshotsResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>09CAE6FF-B864-4104-BBF4-FE1EF08066A6</RequestId>
+	<Snapshots>
+		<Snapshot>
+			<CreationTime>2016-01-05T11:12:42Z</CreationTime>
+			<Tags />
+			<Status>progressing</Status>
+			<Description>
+			</Description>
+			<ProductCode>
+			</ProductCode>
+			<SnapshotName>sys-snapshot-20160108</SnapshotName>
+			<SourceDiskType>system</SourceDiskType>
+			<SourceDiskId>d-28x069z28</SourceDiskId>
+			<SnapshotId>s-28n9lltbf</SnapshotId>
+			<SourceDiskSize>20</SourceDiskSize>
+			<Progress>0%</Progress>
+			<Usage>none</Usage>
+		</Snapshot>
+	</Snapshots>
+</DescribeSnapshotsResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/describe_zones.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/describe_zones.xml b/libcloud/test/compute/fixtures/ecs/describe_zones.xml
new file mode 100644
index 0000000..a434836
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/describe_zones.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeZonesResponse>
+	<RequestId>CE75D5FB-C343-47DB-882D-E0709D95D61E</RequestId>
+	<Zones>
+		<Zone>
+			<AvailableResourceCreation>
+				<ResourceTypes>IoOptimized</ResourceTypes>
+				<ResourceTypes>Instance</ResourceTypes>
+				<ResourceTypes>Disk</ResourceTypes>
+			</AvailableResourceCreation>
+			<AvailableInstanceTypes>
+				<InstanceTypes>ecs.m2.medium</InstanceTypes>
+				<InstanceTypes>ecs.m1.medium</InstanceTypes>
+				<InstanceTypes>ecs.s2.xlarge</InstanceTypes>
+				<InstanceTypes>ecs.t1.xsmall</InstanceTypes>
+				<InstanceTypes>ecs.s2.large</InstanceTypes>
+				<InstanceTypes>ecs.s2.2xlarge</InstanceTypes>
+				<InstanceTypes>ecs.s3.medium</InstanceTypes>
+				<InstanceTypes>ecs.m1.xlarge</InstanceTypes>
+				<InstanceTypes>ecs.s1.small</InstanceTypes>
+				<InstanceTypes>ecs.s1.large</InstanceTypes>
+				<InstanceTypes>ecs.c2.xlarge</InstanceTypes>
+				<InstanceTypes>ecs.c2.large</InstanceTypes>
+				<InstanceTypes>ecs.s3.large</InstanceTypes>
+				<InstanceTypes>ecs.c1.small</InstanceTypes>
+				<InstanceTypes>ecs.m2.xlarge</InstanceTypes>
+				<InstanceTypes>ecs.c2.medium</InstanceTypes>
+				<InstanceTypes>ecs.t1.small</InstanceTypes>
+				<InstanceTypes>ecs.c1.large</InstanceTypes>
+				<InstanceTypes>ecs.s2.small</InstanceTypes>
+				<InstanceTypes>ecs.s1.medium</InstanceTypes>
+			</AvailableInstanceTypes>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<LocalName>青岛可用区B</LocalName>
+			<AvailableDiskCategories>
+				<DiskCategories>cloud_ssd</DiskCategories>
+				<DiskCategories>ephemeral</DiskCategories>
+				<DiskCategories>cloud</DiskCategories>
+			</AvailableDiskCategories>
+		</Zone>
+	</Zones>
+</DescribeZonesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/destroy_node_describe_instances.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/destroy_node_describe_instances.xml b/libcloud/test/compute/fixtures/ecs/destroy_node_describe_instances.xml
new file mode 100644
index 0000000..329bb79
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/destroy_node_describe_instances.xml
@@ -0,0 +1,56 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstancesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>CA75EE06-D5F7-433C-870B-5042EED6C1DC</RequestId>
+	<Instances>
+		<Instance>
+			<ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+			<InnerIpAddress>
+				<IpAddress>10.163.197.74</IpAddress>
+			</InnerIpAddress>
+			<InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+			<VlanId></VlanId>
+			<InstanceId>i-28n7dkvov</InstanceId>
+			<EipAddress>
+				<IpAddress></IpAddress>
+				<AllocationId></AllocationId>
+				<InternetChargeType></InternetChargeType>
+			</EipAddress>
+			<InternetMaxBandwidthIn>-1</InternetMaxBandwidthIn>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<InternetChargeType>PayByTraffic</InternetChargeType>
+			<SerialNumber>ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3</SerialNumber>
+			<IoOptimized>false</IoOptimized>
+			<Memory>1024</Memory>
+			<Cpu>1</Cpu>
+			<VpcAttributes>
+				<NatIpAddress></NatIpAddress>
+				<PrivateIpAddress />
+				<VSwitchId></VSwitchId>
+				<VpcId></VpcId>
+			</VpcAttributes>
+			<InternetMaxBandwidthOut>1</InternetMaxBandwidthOut>
+			<DeviceAvailable>true</DeviceAvailable>
+			<SecurityGroupIds>
+				<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			</SecurityGroupIds>
+			<InstanceName>iZ28n7dkvovZ</InstanceName>
+			<Description></Description>
+			<InstanceNetworkType>classic</InstanceNetworkType>
+			<PublicIpAddress>
+				<IpAddress>114.215.124.73</IpAddress>
+			</PublicIpAddress>
+			<HostName>iZ28n7dkvovZ</HostName>
+			<InstanceType>ecs.t1.small</InstanceType>
+			<CreationTime>2015-12-27T07:35Z</CreationTime>
+			<Status>Stopped</Status>
+			<ClusterId></ClusterId>
+			<RegionId>cn-qingdao</RegionId>
+			<OperationLocks />
+			<InstanceChargeType>PostPaid</InstanceChargeType>
+			<ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+		</Instance>
+	</Instances>
+</DescribeInstancesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/destroy_volume_describe_disks.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/destroy_volume_describe_disks.xml b/libcloud/test/compute/fixtures/ecs/destroy_volume_describe_disks.xml
new file mode 100644
index 0000000..5b045e6
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/destroy_volume_describe_disks.xml
@@ -0,0 +1,36 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeDisksResponse>
+    <Disks>
+        <Disk>
+	    <DiskChargeType>PostPaid</DiskChargeType>
+            <DeleteAutoSnapshot>true</DeleteAutoSnapshot>
+            <DeleteWithInstance>true</DeleteWithInstance>
+            <EnableAutoSnapshot>true</EnableAutoSnapshot>
+            <Category>cloud</Category>
+            <Description>Description</Description>
+            <DiskName>ubuntu1404sys</DiskName>
+            <Size>5</Size>
+            <Type>system</Type>
+            <InstanceId>i-28whl2nj2</InstanceId>
+            <CreationTime>2014-07-23T02:44:06Z</CreationTime>
+            <ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+            <ZoneId>cn-qingdao-b</ZoneId>
+            <AttachedTime>2016-01-04T15:02:17Z</AttachedTime>
+            <DetachedTime></DetachedTime>
+	    <Device>/dev/xvda</Device>
+            <OperationLocks></OperationLocks>
+            <Portable>false</Portable>
+            <ProductCode></ProductCode>
+            <RegionId>cn-qingdao</RegionId>
+            <DiskId>d-28zfrmo13</DiskId>
+            <SourceSnapshotId></SourceSnapshotId>
+            <Status>Available</Status>
+	    <OperationLocks />
+	    <ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+        </Disk>
+    </Disks>
+    <PageNumber>1</PageNumber>
+    <PageSize>10</PageSize>
+    <TotalCount>1</TotalCount>
+    <RequestId>ED5CF6DD-71CA-462C-9C94-A61A78A01479</RequestId>
+</DescribeDisksResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/detach_disk.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/detach_disk.xml b/libcloud/test/compute/fixtures/ecs/detach_disk.xml
new file mode 100644
index 0000000..cc2d357
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/detach_disk.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DetachDiskResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</DetachDiskResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/detach_volume_describe_disks.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/detach_volume_describe_disks.xml b/libcloud/test/compute/fixtures/ecs/detach_volume_describe_disks.xml
new file mode 100644
index 0000000..d25c9a1
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/detach_volume_describe_disks.xml
@@ -0,0 +1,36 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeDisksResponse>
+    <Disks>
+        <Disk>
+	    <DiskChargeType>PostPaid</DiskChargeType>
+            <DeleteAutoSnapshot>true</DeleteAutoSnapshot>
+            <DeleteWithInstance>true</DeleteWithInstance>
+            <EnableAutoSnapshot>true</EnableAutoSnapshot>
+            <Category>cloud</Category>
+            <Description>Description</Description>
+            <DiskName>ubuntu1404sys</DiskName>
+            <Size>5</Size>
+            <Type>system</Type>
+            <InstanceId>i-28whl2nj2</InstanceId>
+            <CreationTime>2014-07-23T02:44:06Z</CreationTime>
+            <ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+            <ZoneId>cn-qingdao-b</ZoneId>
+            <AttachedTime>2016-01-04T15:02:17Z</AttachedTime>
+            <DetachedTime></DetachedTime>
+	    <Device>/dev/xvda</Device>
+            <OperationLocks></OperationLocks>
+            <Portable>false</Portable>
+            <ProductCode></ProductCode>
+            <RegionId>cn-qingdao</RegionId>
+            <DiskId>d-28zfrmo13</DiskId>
+            <SourceSnapshotId></SourceSnapshotId>
+            <Status>In_use</Status>
+	    <OperationLocks />
+	    <ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+        </Disk>
+    </Disks>
+    <PageNumber>1</PageNumber>
+    <PageSize>10</PageSize>
+    <TotalCount>1</TotalCount>
+    <RequestId>ED5CF6DD-71CA-462C-9C94-A61A78A01479</RequestId>
+</DescribeDisksResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/get_image_describe_images.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/get_image_describe_images.xml b/libcloud/test/compute/fixtures/ecs/get_image_describe_images.xml
new file mode 100644
index 0000000..e1b0d65
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/get_image_describe_images.xml
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeImagesResponse>
+	<PageNumber>0</PageNumber>
+	<TotalCount>0</TotalCount>
+	<PageSize>10</PageSize>
+	<RegionId>cn-qingdao</RegionId>
+	<RequestId>FAD4D9B9-D75F-4A9E-BC13-991C0F06F50F</RequestId>
+	<Images>
+	</Images>
+</DescribeImagesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/pages_describe_images.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/pages_describe_images.xml b/libcloud/test/compute/fixtures/ecs/pages_describe_images.xml
new file mode 100644
index 0000000..76e8877
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/pages_describe_images.xml
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeImagesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>2</TotalCount>
+	<PageSize>1</PageSize>
+	<RegionId>cn-qingdao</RegionId>
+	<RequestId>FAD4D9B9-D75F-4A9E-BC13-991C0F06F50F</RequestId>
+	<Images>
+		<Image>
+			<ImageId>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageId>
+			<Description>freebsd1001_64_20G_aliaegis_20150527.vhd</Description>
+			<ProductCode></ProductCode>
+			<OSType>linux</OSType>
+			<Architecture>x86_64</Architecture>
+			<OSName>FreeBSD  10.1 64位</OSName>
+			<DiskDeviceMappings>
+				<DiskDeviceMapping>
+					<ImportOSSObject></ImportOSSObject>
+					<Format></Format>
+					<Device>/dev/xvda</Device>
+					<SnapshotId></SnapshotId>
+					<ImportOSSBucket></ImportOSSBucket>
+					<Size>20</Size>
+				</DiskDeviceMapping>
+			</DiskDeviceMappings>
+			<ImageOwnerAlias>system</ImageOwnerAlias>
+			<Progress>100%</Progress>
+			<Usage>instance</Usage>
+			<CreationTime>2015-06-19T07:25:42Z</CreationTime>
+			<Tags />
+			<ImageVersion>1.0.0</ImageVersion>
+			<Status>Available</Status>
+			<ImageName>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageName>
+			<IsSelfShared></IsSelfShared>
+			<IsCopied>false</IsCopied>
+			<IsSubscribed>false</IsSubscribed>
+			<Platform>Freebsd</Platform>
+			<Size>20</Size>
+		</Image>
+	</Images>
+</DescribeImagesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/pages_describe_images_page2.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/pages_describe_images_page2.xml b/libcloud/test/compute/fixtures/ecs/pages_describe_images_page2.xml
new file mode 100644
index 0000000..615e152
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/pages_describe_images_page2.xml
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeImagesResponse>
+	<PageNumber>2</PageNumber>
+	<TotalCount>2</TotalCount>
+	<PageSize>1</PageSize>
+	<RegionId>cn-qingdao</RegionId>
+	<RequestId>FAD4D9B9-D75F-4A9E-BC13-991C0F06F50F</RequestId>
+	<Images>
+		<Image>
+			<ImageId>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageId>
+			<Description>freebsd1001_64_20G_aliaegis_20150527.vhd</Description>
+			<ProductCode></ProductCode>
+			<OSType>linux</OSType>
+			<Architecture>x86_64</Architecture>
+			<OSName>FreeBSD  10.1 64位</OSName>
+			<DiskDeviceMappings>
+				<DiskDeviceMapping>
+					<ImportOSSObject></ImportOSSObject>
+					<Format></Format>
+					<Device>/dev/xvda</Device>
+					<SnapshotId></SnapshotId>
+					<ImportOSSBucket></ImportOSSBucket>
+					<Size>20</Size>
+				</DiskDeviceMapping>
+			</DiskDeviceMappings>
+			<ImageOwnerAlias>system</ImageOwnerAlias>
+			<Progress>100%</Progress>
+			<Usage>instance</Usage>
+			<CreationTime>2015-06-19T07:25:42Z</CreationTime>
+			<Tags />
+			<ImageVersion>1.0.0</ImageVersion>
+			<Status>Available</Status>
+			<ImageName>freebsd1001_64_20G_aliaegis_20150527.vhd</ImageName>
+			<IsSelfShared></IsSelfShared>
+			<IsCopied>false</IsCopied>
+			<IsSubscribed>false</IsSubscribed>
+			<Platform>Freebsd</Platform>
+			<Size>20</Size>
+		</Image>
+	</Images>
+</DescribeImagesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/reboot_instance.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/reboot_instance.xml b/libcloud/test/compute/fixtures/ecs/reboot_instance.xml
new file mode 100644
index 0000000..5fc39aa
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/reboot_instance.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<RebootInstanceResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</RebootInstanceResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/reboot_node_describe_instances.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/reboot_node_describe_instances.xml b/libcloud/test/compute/fixtures/ecs/reboot_node_describe_instances.xml
new file mode 100644
index 0000000..24eae7e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/reboot_node_describe_instances.xml
@@ -0,0 +1,56 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstancesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>CA75EE06-D5F7-433C-870B-5042EED6C1DC</RequestId>
+	<Instances>
+		<Instance>
+			<ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+			<InnerIpAddress>
+				<IpAddress>10.163.197.74</IpAddress>
+			</InnerIpAddress>
+			<InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+			<VlanId></VlanId>
+			<InstanceId>i-28n7dkvov</InstanceId>
+			<EipAddress>
+				<IpAddress></IpAddress>
+				<AllocationId></AllocationId>
+				<InternetChargeType></InternetChargeType>
+			</EipAddress>
+			<InternetMaxBandwidthIn>-1</InternetMaxBandwidthIn>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<InternetChargeType>PayByTraffic</InternetChargeType>
+			<SerialNumber>ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3</SerialNumber>
+			<IoOptimized>false</IoOptimized>
+			<Memory>1024</Memory>
+			<Cpu>1</Cpu>
+			<VpcAttributes>
+				<NatIpAddress></NatIpAddress>
+				<PrivateIpAddress />
+				<VSwitchId></VSwitchId>
+				<VpcId></VpcId>
+			</VpcAttributes>
+			<InternetMaxBandwidthOut>1</InternetMaxBandwidthOut>
+			<DeviceAvailable>true</DeviceAvailable>
+			<SecurityGroupIds>
+				<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			</SecurityGroupIds>
+			<InstanceName>iZ28n7dkvovZ</InstanceName>
+			<Description></Description>
+			<InstanceNetworkType>classic</InstanceNetworkType>
+			<PublicIpAddress>
+				<IpAddress>114.215.124.73</IpAddress>
+			</PublicIpAddress>
+			<HostName>iZ28n7dkvovZ</HostName>
+			<InstanceType>ecs.t1.small</InstanceType>
+			<CreationTime>2015-12-27T07:35Z</CreationTime>
+			<Status>Running</Status>
+			<ClusterId></ClusterId>
+			<RegionId>cn-qingdao</RegionId>
+			<OperationLocks />
+			<InstanceChargeType>PostPaid</InstanceChargeType>
+			<ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+		</Instance>
+	</Instances>
+</DescribeInstancesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/start_instance.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/start_instance.xml b/libcloud/test/compute/fixtures/ecs/start_instance.xml
new file mode 100644
index 0000000..f7e87e0
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/start_instance.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<StartInstanceResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</StartInstanceResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/stop_instance.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/stop_instance.xml b/libcloud/test/compute/fixtures/ecs/stop_instance.xml
new file mode 100644
index 0000000..156b113
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/stop_instance.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<StopInstanceResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</StopInstanceResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/stop_node_describe_instances.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/stop_node_describe_instances.xml b/libcloud/test/compute/fixtures/ecs/stop_node_describe_instances.xml
new file mode 100644
index 0000000..329bb79
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/stop_node_describe_instances.xml
@@ -0,0 +1,56 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstancesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>CA75EE06-D5F7-433C-870B-5042EED6C1DC</RequestId>
+	<Instances>
+		<Instance>
+			<ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+			<InnerIpAddress>
+				<IpAddress>10.163.197.74</IpAddress>
+			</InnerIpAddress>
+			<InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+			<VlanId></VlanId>
+			<InstanceId>i-28n7dkvov</InstanceId>
+			<EipAddress>
+				<IpAddress></IpAddress>
+				<AllocationId></AllocationId>
+				<InternetChargeType></InternetChargeType>
+			</EipAddress>
+			<InternetMaxBandwidthIn>-1</InternetMaxBandwidthIn>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<InternetChargeType>PayByTraffic</InternetChargeType>
+			<SerialNumber>ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3</SerialNumber>
+			<IoOptimized>false</IoOptimized>
+			<Memory>1024</Memory>
+			<Cpu>1</Cpu>
+			<VpcAttributes>
+				<NatIpAddress></NatIpAddress>
+				<PrivateIpAddress />
+				<VSwitchId></VSwitchId>
+				<VpcId></VpcId>
+			</VpcAttributes>
+			<InternetMaxBandwidthOut>1</InternetMaxBandwidthOut>
+			<DeviceAvailable>true</DeviceAvailable>
+			<SecurityGroupIds>
+				<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			</SecurityGroupIds>
+			<InstanceName>iZ28n7dkvovZ</InstanceName>
+			<Description></Description>
+			<InstanceNetworkType>classic</InstanceNetworkType>
+			<PublicIpAddress>
+				<IpAddress>114.215.124.73</IpAddress>
+			</PublicIpAddress>
+			<HostName>iZ28n7dkvovZ</HostName>
+			<InstanceType>ecs.t1.small</InstanceType>
+			<CreationTime>2015-12-27T07:35Z</CreationTime>
+			<Status>Stopped</Status>
+			<ClusterId></ClusterId>
+			<RegionId>cn-qingdao</RegionId>
+			<OperationLocks />
+			<InstanceChargeType>PostPaid</InstanceChargeType>
+			<ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+		</Instance>
+	</Instances>
+</DescribeInstancesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/test_ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_ecs.py b/libcloud/test/compute/test_ecs.py
new file mode 100644
index 0000000..96e482e
--- /dev/null
+++ b/libcloud/test/compute/test_ecs.py
@@ -0,0 +1,912 @@
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 __future__ import unicode_literals
+
+import sys
+import unittest
+
+from libcloud.common.types import LibcloudError
+from libcloud.compute.base import Node, NodeAuthPassword, NodeImage, \
+    NodeLocation, NodeSize, StorageVolume, VolumeSnapshot
+from libcloud.compute.drivers.ecs import ECSDriver
+from libcloud.compute.types import NodeState, StorageVolumeState
+from libcloud.test import MockHttpTestCase, LibcloudTestCase
+from libcloud.test.file_fixtures import ComputeFileFixtures
+from libcloud.test.secrets import ECS_PARAMS
+from libcloud.utils.py3 import httplib
+
+
+class ECSDriverTestCase(LibcloudTestCase):
+    region = 'cn-qingdao'
+    zone = 'cn-qingdao-b'
+    image_id = 'ubuntu1404_64_20G_aliaegis_20150325.vhd'
+
+    def setUp(self):
+        ECSMockHttp.test = self
+        ECSDriver.connectionCls.conn_classes = (ECSMockHttp, ECSMockHttp)
+        ECSMockHttp.use_param = 'Action'
+        ECSMockHttp.type = None
+
+        self.driver = ECSDriver(*ECS_PARAMS, region=self.region)
+        self.fake_size = NodeSize('ecs.t1.small', 'ecs t1 small',
+                                  None, None, None, None,
+                                  self.driver)
+        self.fake_image = NodeImage(self.image_id, name='ubuntu 14.04 64bit',
+                                    driver=self.driver)
+        self.fake_node = Node(id='fake-node1', name='fake-node',
+                              state=NodeState.RUNNING,
+                              public_ips=None,
+                              private_ips=None,
+                              driver=self.driver)
+        self.fake_volume = StorageVolume(id='fake-volume1', name='fake-volume',
+                                         size=self.fake_size,
+                                         driver=self.driver)
+        self.fake_snapshot = VolumeSnapshot(id='fake-snapshot1',
+                                            driver=self.driver)
+        self.fake_location = NodeLocation(id=self.region, name=self.region,
+                                          country=None, driver=self.driver)
+
+    def test_list_nodes(self):
+        nodes = self.driver.list_nodes()
+        self.assertIsNotNone(nodes)
+        self.assertEqual(1, len(nodes))
+        node = nodes[0]
+        self.assertEqual('iZ28n7dkvovZ', node.name)
+        self.assertEqual('i-28n7dkvov', node.id)
+        self.assertEqual(NodeState.PENDING, node.state)
+        self.assertEqual(1, len(node.public_ips))
+        self.assertEqual('114.215.124.73', node.public_ips[0])
+        self.assertEqual(1, len(node.private_ips))
+        self.assertEqual('10.163.197.74', node.private_ips[0])
+        expected_extra = {
+            'image_id': 'ubuntu1404_64_20G_aliaegis_20150325.vhd',
+            'description': '',
+            'instance_type_family': 'ecs.t1',
+            'zone_id': 'cn-qingdao-b',
+            'internet_charge_type': 'PayByTraffic',
+            'serial_number': 'ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3',
+            'io_optimized': 'false',
+            'device_available': 'true',
+            'instance_network_type': 'classic',
+            'hostname': 'iZ28n7dkvovZ',
+            'instance_type': 'ecs.t1.small',
+            'creation_time': '2015-12-27T07:35Z',
+            'instance_charge_type': 'PostPaid',
+            'expired_time': '2999-09-08T16:00Z'
+        }
+        self._validate_extras(expected_extra, node.extra)
+        vpc = {
+            'vpc_id': '',
+            'vswitch_id': '',
+            'private_ip_address': None,
+            'nat_ip_address': ''
+        }
+        self._validate_extras(vpc, node.extra['vpc_attributes'])
+        eip_address = {
+            'allocation_id': '',
+            'ip_address': '',
+            'internet_charge_type': '',
+            'bandwidth': None
+        }
+        self._validate_extras(eip_address, node.extra['eip_address'])
+        self.assertIsNone(node.extra['operation_locks']['lock_reason'])
+
+    def test_list_nodes_with_ex_node_ids(self):
+        ECSMockHttp.type = 'list_nodes_ex_node_ids'
+        nodes = self.driver.list_nodes(ex_node_ids=['i-28n7dkvov',
+                                                    'not-existed-id'])
+        self.assertIsNotNone(nodes)
+
+    def test_list_nodes_with_ex_filters(self):
+        ECSMockHttp.type = 'list_nodes_ex_filters'
+        nodes = self.driver.list_nodes(ex_filters={'ZoneId': self.zone})
+        self.assertIsNotNone(nodes)
+
+    def _validate_extras(self, expected, actual):
+        self.assertIsNotNone(actual)
+        for key, value in iter(expected.items()):
+            self.assertTrue(key in actual)
+            self.assertEqual(value, actual[key], ('extra %(key)s not equal, '
+                                                  'expected: "%(expected)s", '
+                                                  'actual: "%(actual)s"' %
+                                                  {'key': key,
+                                                   'expected': value,
+                                                   'actual': actual[key]}))
+
+    def test_create_node(self):
+        ECSMockHttp.type = 'create_node'
+        name = 'test_create_node'
+        node = self.driver.create_node(name=name, image=self.fake_image,
+                                       size=self.fake_size,
+                                       ex_security_group_id='sg-28ou0f3xa',
+                                       ex_description='description',
+                                       ex_internet_charge_type='PayByTraffic',
+                                       ex_internet_max_bandwidth_out=1,
+                                       ex_internet_max_bandwidth_in=200,
+                                       ex_hostname='hostname',
+                                       auth=NodeAuthPassword('password'),
+                                       ex_io_optimized=True,
+                                       ex_system_disk={'category': 'cloud',
+                                                       'disk_name': 'root',
+                                                       'description': 'sys'},
+                                       ex_vswitch_id='vswitch-id1',
+                                       ex_private_ip_address='1.1.1.2',
+                                       ex_client_token='client_token')
+        self.assertIsNotNone(node)
+
+    def test_create_node_with_data_disk(self):
+        ECSMockHttp.type = 'create_node_with_data'
+        self.name = 'test_create_node'
+        self.data_disk = {
+            'size': 5,
+            'category': self.driver.disk_categories.CLOUD,
+            'disk_name': 'data1',
+            'description': 'description',
+            'device': '/dev/xvdb',
+            'delete_with_instance': True}
+        node = self.driver.create_node(name=self.name, image=self.fake_image,
+                                       size=self.fake_size,
+                                       ex_security_group_id='sg-28ou0f3xa',
+                                       ex_data_disk=self.data_disk)
+        self.assertIsNotNone(node)
+
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(2, len(sizes))
+        size = sizes[0]
+        self.assertEqual('ecs.t1.xsmall', size.id)
+        self.assertEqual('ecs.t1.xsmall', size.name)
+        self.assertEqual(0.5, size.ram)
+        self.assertEqual(1, size.extra['cpu_core_count'])
+        self.assertEqual('ecs.t1', size.extra['instance_type_family'])
+        size = sizes[1]
+        self.assertEqual('ecs.s2.small', size.id)
+        self.assertEqual('ecs.s2.small', size.name)
+        self.assertEqual(1.0, size.ram)
+        self.assertEqual(2, size.extra['cpu_core_count'])
+        self.assertEqual('ecs.s2', size.extra['instance_type_family'])
+
+    def test_list_locations(self):
+        locations = self.driver.list_locations()
+        self.assertEqual(9, len(locations))
+        location = locations[0]
+        self.assertEqual('ap-southeast-1', location.id)
+        self.assertEqual('亚太(新加坡)', location.name)
+        self.assertIsNone(location.country)
+
+    def test_create_node_without_sg_id_exception(self):
+        name = 'test_create_node_without_sg_id_exception'
+        self.assertRaises(AttributeError, self.driver.create_node,
+                          name=name, image=self.fake_image,
+                          size=self.fake_size)
+
+    def test_creat_node_paybytraffic_exception(self):
+        name = 'test_create_node_paybytraffic_exception'
+        self.assertRaises(AttributeError, self.driver.create_node,
+                          name=name, image=self.fake_image,
+                          size=self.fake_size,
+                          ex_security_group_id='sg-id1',
+                          ex_internet_charge_type='PayByTraffic')
+
+    def test_create_node_ex_system_disk_exception(self):
+        name = 'test_creat_node_ex_system_disk_exception'
+        self.assertRaises(AttributeError, self.driver.create_node,
+                          name=name, image=self.fake_image,
+                          size=self.fake_size,
+                          ex_security_group_id='sg-id1',
+                          ex_system_disk=None)
+
+    def test_create_node_ex_private_ip_address_exception(self):
+        name = 'test_create_node_ex_private_ip_address_exception'
+        self.assertRaises(AttributeError, self.driver.create_node,
+                          name=name, image=self.fake_image,
+                          size=self.fake_size,
+                          ex_security_group_id='sg-id1',
+                          ex_private_ip_address='1.1.1.2')
+
+    def test_reboot_node(self):
+        ECSMockHttp.type = 'reboot_node'
+        result = self.driver.reboot_node(self.fake_node)
+        self.assertTrue(result)
+
+    def test_reboot_node_with_ex_force_stop(self):
+        ECSMockHttp.type = 'reboot_node_force_stop'
+        result = self.driver.reboot_node(self.fake_node, ex_force_stop=True)
+        self.assertTrue(result)
+
+    def test_destroy_node(self):
+        ECSMockHttp.type = 'destroy_node'
+        result = self.driver.destroy_node(self.fake_node)
+        self.assertTrue(result)
+
+    def test_ex_start_node(self):
+        ECSMockHttp.type = 'start_node'
+        result = self.driver.ex_start_node(self.fake_node)
+        self.assertTrue(result)
+
+    def test_ex_stop_node(self):
+        ECSMockHttp.type = 'stop_node'
+        result = self.driver.ex_stop_node(self.fake_node)
+        self.assertTrue(result)
+
+    def test_stop_node_with_ex_force_stop(self):
+        ECSMockHttp.type = 'stop_node_force_stop'
+        result = self.driver.ex_stop_node(self.fake_node, ex_force_stop=True)
+        self.assertTrue(result)
+
+    def test_list_volumes(self):
+        volumes = self.driver.list_volumes()
+        self.assertEqual(2, len(volumes))
+        volume = volumes[0]
+        self.assertEqual('d-28m5zbua0', volume.id)
+        self.assertEqual('', volume.name)
+        self.assertEqual(5, volume.size)
+        self.assertEqual(StorageVolumeState.AVAILABLE, volume.state)
+        expected_extras = {
+            'region_id': 'cn-qingdao',
+            'zone_id': 'cn-qingdao-b',
+            'description': '',
+            'type': 'data',
+            'category': 'cloud',
+            'image_id': '',
+            'source_snapshot_id': '',
+            'product_code': '',
+            'portable': True,
+            'instance_id': '',
+            'device': '',
+            'delete_with_instance': False,
+            'enable_auto_snapshot': False,
+            'creation_time': '2014-07-23T02:44:07Z',
+            'attached_time': '2014-07-23T07:47:35Z',
+            'detached_time': '2014-07-23T08:28:48Z',
+            'disk_charge_type': 'PostPaid',
+            'operation_locks': {'lock_reason': None}
+        }
+        self._validate_extras(expected_extras, volume.extra)
+        volume = volumes[1]
+        self.assertEqual('d-28zfrmo13', volume.id)
+        self.assertEqual('ubuntu1404sys', volume.name)
+        self.assertEqual(5, volume.size)
+        self.assertEqual(StorageVolumeState.INUSE, volume.state)
+        expected_extras = {
+            'region_id': 'cn-qingdao',
+            'zone_id': 'cn-qingdao-b',
+            'description': 'Description',
+            'type': 'system',
+            'category': 'cloud',
+            'image_id': 'ubuntu1404_64_20G_aliaegis_20150325.vhd',
+            'source_snapshot_id': '',
+            'product_code': '',
+            'portable': False,
+            'instance_id': 'i-28whl2nj2',
+            'device': '/dev/xvda',
+            'delete_with_instance': True,
+            'enable_auto_snapshot': True,
+            'creation_time': '2014-07-23T02:44:06Z',
+            'attached_time': '2016-01-04T15:02:17Z',
+            'detached_time': '',
+            'disk_charge_type': 'PostPaid',
+            'operation_locks': {'lock_reason': None}
+        }
+        self._validate_extras(expected_extras, volume.extra)
+
+    def test_list_volumes_with_ex_volume_ids(self):
+        ECSMockHttp.type = 'list_volumes_ex_volume_ids'
+        volumes = self.driver.list_volumes(ex_volume_ids=['i-28n7dkvov',
+                                                          'not-existed-id'])
+        self.assertIsNotNone(volumes)
+
+    def test_list_volumes_with_ex_filters(self):
+        ECSMockHttp.type = 'list_volumes_ex_filters'
+        ex_filters = {'InstanceId': self.fake_node.id}
+        volumes = self.driver.list_volumes(ex_filters=ex_filters)
+        self.assertIsNotNone(volumes)
+
+    def test_list_volume_snapshots(self):
+        snapshots = self.driver.list_volume_snapshots(self.fake_volume)
+        self.assertEqual(1, len(snapshots))
+
+    def test_list_volume_snapshots_with_ex_snapshot_ids(self):
+        ECSMockHttp.type = 'list_volume_snapshots_ex_snapshot_ids'
+        ex_snapshot_ids = ['fake-snapshot1']
+        self.driver.list_volume_snapshots(self.fake_volume,
+                                          ex_snapshot_ids=ex_snapshot_ids)
+
+    def test_list_volume_snapshots_with_ex_filters(self):
+        ECSMockHttp.type = 'list_volume_snapshots_ex_filters'
+        ex_filters = {'InstanceId': self.fake_node.id}
+        self.driver.list_volume_snapshots(self.fake_volume,
+                                          ex_filters=ex_filters)
+
+    def test_create_volume(self):
+        ECSMockHttp.type = 'create_volume'
+        self.volume_size = 1
+        self.volume_name = 'fake-volume-name'
+        self.description = 'fake-description'
+        self.disk_category = 'system'
+        self.client_token = 'client_token'
+        volume = self.driver.create_volume(self.volume_size, self.volume_name,
+                                           snapshot=self.fake_snapshot,
+                                           ex_zone_id=self.zone,
+                                           ex_description=self.description,
+                                           ex_disk_category=self.disk_category,
+                                           ex_client_token=self.client_token)
+        self.assertIsNotNone(volume)
+
+    def test_create_volume_without_ex_zone_id_exception(self):
+        self.assertRaises(AttributeError,
+                          self.driver.create_volume,
+                          1, 'fake-volume-name')
+
+    def test_create_volume_snapshot(self):
+        ECSMockHttp.type = 'create_volume_snapshot'
+        self.snapshot_name = 'fake-snapshot1'
+        self.description = 'fake-description'
+        self.client_token = 'client-token'
+        snapshot = self.driver.create_volume_snapshot(
+            self.fake_volume, name=self.snapshot_name,
+            ex_description=self.description,
+            ex_client_token=self.client_token)
+        self.assertIsNotNone(snapshot)
+
+    def test_attach_volume(self):
+        self.device = '/dev/sdb'
+        self.delete_with_instance = True
+        attached = self.driver.attach_volume(
+            self.fake_node, self.fake_volume, device=self.device,
+            ex_delete_with_instance=self.delete_with_instance)
+        self.assertTrue(attached)
+
+    def test_detach_volume(self):
+        self.instance_id = 'fake-node1'
+        result = self.driver.detach_volume(self.fake_volume,
+                                           ex_instance_id=self.instance_id)
+        self.assertTrue(result)
+
+    def test_detach_volume_query_instance_id(self):
+        ECSMockHttp.type = 'detach_volume'
+        result = self.driver.detach_volume(self.fake_volume)
+        self.assertTrue(result)
+
+    def test_detach_volume_query_instance_id_exception(self):
+        self.assertRaises(AttributeError, self.driver.detach_volume,
+                          self.fake_volume)
+
+    def test_destroy_volume(self):
+        ECSMockHttp.type = 'destroy_volume'
+        result = self.driver.destroy_volume(self.fake_volume)
+        self.assertTrue(result)
+
+    def test_destroy_volume_query_volumes_exception(self):
+        self.assertRaises(LibcloudError, self.driver.destroy_volume,
+                          self.fake_volume)
+
+    def test_destroy_volume_state_exception(self):
+        ECSMockHttp.type = 'destroy_volume_state'
+        self.assertRaises(LibcloudError, self.driver.destroy_volume,
+                          self.fake_volume)
+
+    def test_destroy_volume_snapshot(self):
+        result = self.driver.destroy_volume_snapshot(self.fake_snapshot)
+        self.assertTrue(result)
+
+    def test_destroy_volume_snapshot_exception(self):
+        self.assertRaises(AttributeError, self.driver.destroy_volume_snapshot,
+                          self.fake_volume)
+
+    def test_list_images(self):
+        images = self.driver.list_images(self.fake_location)
+        self.assertEqual(1, len(images))
+        image = images[0]
+        self.assertEqual('freebsd1001_64_20G_aliaegis_20150527.vhd', image.id)
+        self.assertEqual('freebsd1001_64_20G_aliaegis_20150527.vhd',
+                         image.name)
+        expected_extra = {
+            'image_version': '1.0.0',
+            'os_type': 'linux',
+            'platform': 'Freebsd',
+            'architecture': 'x86_64',
+            'description': 'freebsd1001_64_20G_aliaegis_20150527.vhd',
+            'size': 20,
+            'image_owner_alias': 'system',
+            'os_name': 'FreeBSD  10.1 64位',
+            'product_code': '',
+            'is_subscribed': False,
+            'progress': '100%',
+            'creation_time': '2015-06-19T07:25:42Z',
+            'usage': 'instance',
+            'is_copied': False
+        }
+        self._validate_extras(expected_extra, image.extra)
+        expected_dev_mappings = {
+            'snapshot_id': '',
+            'size': 20,
+            'device': '/dev/xvda',
+            'format': '',
+            'import_oss_bucket': '',
+            'import_oss_object': ''
+        }
+        self._validate_extras(expected_dev_mappings,
+                              image.extra['disk_device_mappings'])
+
+    def test_list_images_with_ex_image_ids(self):
+        ECSMockHttp.type = 'list_images_ex_image_ids'
+        self.driver.list_images(location=self.fake_location,
+                                ex_image_ids=[self.fake_image.id,
+                                              'not-existed'])
+
+    def test_list_images_with_ex_image_ids_type_exception(self):
+        self.assertRaises(AttributeError, self.driver.list_images,
+                          location=self.fake_location,
+                          ex_image_ids={'image_ids': 'id1,id2'})
+
+    def test_list_images_with_ex_filters(self):
+        ECSMockHttp.type = 'list_images_ex_filters'
+        ex_filters = {'Status': 'Available'}
+        self.driver.list_images(location=self.fake_location,
+                                ex_filters=ex_filters)
+
+    def test_list_images_multiple_pages(self):
+        ECSMockHttp.type = 'list_images_pages'
+        images = self.driver.list_images()
+        self.assertEqual(2, len(images))
+
+    def test_create_image(self):
+        self.image_name = 'fake-image1'
+        self.description = 'description'
+        self.image_version = '1.0.0'
+        self.client_token = 'client_token'
+        image = self.driver.create_image(None, self.image_name,
+                                         self.description,
+                                         ex_snapshot_id=self.fake_snapshot.id,
+                                         ex_image_version=self.image_version,
+                                         ex_client_token=self.client_token)
+        self.assertIsNotNone(image)
+
+    def test_creaet_image_exception(self):
+        self.assertRaises(AttributeError, self.driver.create_image,
+                          None, None)
+
+    def test_delete_image(self):
+        result = self.driver.delete_image(self.fake_image)
+        self.assertTrue(result)
+
+    def test_get_image(self):
+        ECSMockHttp.type = 'get_image'
+        image = self.driver.get_image(self.fake_image.id)
+        self.assertIsNotNone(image)
+
+    def test_get_image_not_found_exception(self):
+        ECSMockHttp.type = 'get_image_not_found'
+        self.assertRaises(LibcloudError, self.driver.get_image,
+                          self.fake_image.id)
+
+    def test_copy_image(self):
+        self.image_name = 'copied-image1'
+        self.description = 'description'
+        self.dest_region = 'cn-hangzhou'
+        self.client_token = 'client-token'
+        image = self.driver.copy_image(
+            self.region, self.fake_image,
+            self.image_name,
+            description=self.description,
+            ex_destination_region_id=self.dest_region,
+            ex_client_token=self.client_token)
+        self.assertIsNotNone(image)
+
+    def test_copy_image_in_the_same_region(self):
+        ECSMockHttp.type = 'copy_image_same_region'
+        image = self.driver.copy_image(self.region, self.fake_image, None)
+        self.assertIsNotNone(image)
+
+    def test_ex_list_security_groups(self):
+        sgs = self.driver.ex_list_security_groups()
+        self.assertEqual(1, len(sgs))
+        sg = sgs[0]
+        self.assertEqual('sg-28ou0f3xa', sg.id)
+        self.assertEqual('sg-28ou0f3xa', sg.name)
+        self.assertEqual('System created security group.', sg.description)
+        self.assertEqual('', sg.vpc_id)
+        self.assertEqual('2015-06-26T08:35:30Z', sg.creation_time)
+
+    def test_ex_list_security_groups_with_ex_filters(self):
+        ECSMockHttp.type = 'list_sgs_filters'
+        self.vpc_id = 'vpc1'
+        ex_filters = {'VpcId': self.vpc_id}
+        sgs = self.driver.ex_list_security_groups(ex_filters=ex_filters)
+        self.assertEqual(1, len(sgs))
+
+    def test_ex_list_zones(self):
+        zones = self.driver.ex_list_zones()
+        self.assertEqual(1, len(zones))
+        zone = zones[0]
+        self.assertEqual('cn-qingdao-b', zone.id)
+        self.assertEqual('青岛可用区B', zone.name)
+        self.assertEqual(self.driver, zone.driver)
+        self.assertIsNotNone(zone.available_resource_types)
+        self.assertEqual('IoOptimized', zone.available_resource_types[0])
+        self.assertIsNotNone(zone.available_instance_types)
+        self.assertEqual('ecs.m2.medium', zone.available_instance_types[0])
+        self.assertIsNotNone(zone.available_disk_categories)
+        self.assertEqual('cloud_ssd', zone.available_disk_categories[0])
+
+
+class ECSMockHttp(MockHttpTestCase):
+    fixtures = ComputeFileFixtures('ecs')
+
+    def _DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_nodes_ex_node_ids_DescribeInstances(self, method, url, body,
+                                                  headers):
+        params = {'InstanceIds': '["i-28n7dkvov", "not-existed-id"]'}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeInstances(method, url, body, headers)
+
+    def _list_nodes_ex_filters_DescribeInstances(self, method, url, body,
+                                                 headers):
+        params = {'ZoneId': self.test.zone}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeInstances(method, url, body, headers)
+
+    def _DescribeInstanceTypes(self, method, url, body, headers):
+        resp_body = self.fixtures.load('describe_instance_types.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeRegions(self, method, url, body, headers):
+        resp_body = self.fixtures.load('describe_regions.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_CreateInstance(self, method, url, body, headers):
+        params = {'SecurityGroupId': 'sg-28ou0f3xa',
+                  'Description': 'description',
+                  'InternetChargeType': 'PayByTraffic',
+                  'InternetMaxBandwidthOut': '1',
+                  'InternetMaxBandwidthIn': '200',
+                  'HostName': 'hostname',
+                  'Password': 'password',
+                  'IoOptimized': 'true',
+                  'SystemDisk.Category': 'cloud',
+                  'SystemDisk.DiskName': 'root',
+                  'SystemDisk.Description': 'sys',
+                  'VSwitchId': 'vswitch-id1',
+                  'PrivateIpAddress': '1.1.1.2',
+                  'ClientToken': 'client_token'}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('create_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('create_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_StartInstance(self, method, url, body, headers):
+        resp_body = self.fixtures.load('start_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_with_data_CreateInstance(self, method, url, body,
+                                              headers):
+        params = {'SecurityGroupId': 'sg-28ou0f3xa',
+                  'DataDisk.1.Size': '5',
+                  'DataDisk.1.Category': 'cloud',
+                  'DataDisk.1.DiskName': 'data1',
+                  'DataDisk.1.Description': 'description',
+                  'DataDisk.1.Device': '/dev/xvdb',
+                  'DataDisk.1.DeleteWithInstance': 'true'}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('create_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_with_data_DescribeInstances(self, method, url, body,
+                                                 headers):
+        resp_body = self.fixtures.load('create_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_node_with_data_StartInstance(self, method, url, body,
+                                             headers):
+        resp_body = self.fixtures.load('start_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _reboot_node_RebootInstance(self, method, url, body, headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id,
+                                                'ForceStop': 'false'})
+        resp_body = self.fixtures.load('reboot_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _reboot_node_DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('reboot_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _reboot_node_force_stop_RebootInstance(self, method, url, body,
+                                               headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id,
+                                                'ForceStop': 'true'})
+        resp_body = self.fixtures.load('reboot_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _reboot_node_force_stop_DescribeInstances(self, method, url, body,
+                                                  headers):
+        resp_body = self.fixtures.load('reboot_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _destroy_node_DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('destroy_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _destroy_node_DeleteInstance(self, method, url, body, headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id})
+        resp_body = self.fixtures.load('delete_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _start_node_StartInstance(self, method, url, body, headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id})
+        resp_body = self.fixtures.load('start_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _start_node_DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('reboot_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _stop_node_StopInstance(self, method, url, body, headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id,
+                                                'ForceStop': 'false'})
+        resp_body = self.fixtures.load('stop_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _stop_node_DescribeInstances(self, method, url, body, headers):
+        resp_body = self.fixtures.load('stop_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _stop_node_force_stop_StopInstance(self, method, url, body, headers):
+        node_id = self.test.fake_node.id
+        self.assertUrlContainsQueryParams(url, {'InstanceId': node_id,
+                                                'ForceStop': 'true'})
+        resp_body = self.fixtures.load('stop_instance.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _stop_node_force_stop_DescribeInstances(self, method, url, body,
+                                                headers):
+        resp_body = self.fixtures.load('stop_node_describe_instances.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeDisks(self, method, url, body, headers):
+        resp_body = self.fixtures.load('describe_disks.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_volumes_ex_volume_ids_DescribeDisks(self, method, url, body,
+                                                  headers):
+        region = self.test.region
+        params = {'DiskIds': '["i-28n7dkvov", "not-existed-id"]',
+                  'RegionId': region}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeInstances(method, url, body, headers)
+
+    def _list_volumes_ex_filters_DescribeDisks(self, method, url, body,
+                                               headers):
+        params = {'InstanceId': self.test.fake_node.id}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeDisks(method, url, body, headers)
+
+    def _DescribeSnapshots(self, method, url, body, headers):
+        region = self.test.region
+        volume_id = self.test.fake_volume.id
+        params = {'RegionId': region,
+                  'DiskId': volume_id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('describe_snapshots.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_volume_snapshots_ex_snapshot_ids_DescribeSnapshots(
+            self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'SnapshotIds': '["fake-snapshot1"]'}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeSnapshots(method, url, body, headers)
+
+    def _list_volume_snapshots_ex_filters_DescribeSnapshots(self, method, url, body,
+                                                            headers):
+        params = {'InstanceId': self.test.fake_node.id}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeSnapshots(method, url, body, headers)
+
+    def _create_volume_CreateDisk(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'DiskName': self.test.volume_name,
+                  'Size': str(self.test.volume_size),
+                  'ZoneId': self.test.zone,
+                  'SnapshotId': self.test.fake_snapshot.id,
+                  'Description': self.test.description,
+                  'DiskCategory': self.test.disk_category,
+                  'ClientToken': self.test.client_token}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('create_disk.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_volume_DescribeDisks(self, method, url, body, headers):
+        resp_body = self.fixtures.load('create_volume_describe_disks.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_volume_snapshot_CreateSnapshot(self, method, url, body,
+                                               headers):
+        params = {'DiskId': self.test.fake_volume.id,
+                  'SnapshotName': self.test.snapshot_name,
+                  'Description': self.test.description,
+                  'ClientToken': self.test.client_token}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('create_snapshot.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _create_volume_snapshot_DescribeSnapshots(self, method, url, body,
+                                                  headers):
+        resp_body = self.fixtures.load('describe_snapshots.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _AttachDisk(self, method, url, body, headers):
+        delete_with_instance = str(self.test.delete_with_instance).lower()
+        params = {'InstanceId': self.test.fake_node.id,
+                  'DiskId': self.test.fake_volume.id,
+                  'Device': self.test.device,
+                  'DeleteWithInstance': delete_with_instance}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('attach_disk.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _DetachDisk(self, method, url, body, headers):
+        params = {'DiskId': self.test.fake_volume.id,
+                  'InstanceId': self.test.instance_id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('detach_disk.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _detach_volume_DescribeDisks(self, method, url, body, headers):
+        params = {'DiskIds': '["' + self.test.fake_volume.id + '"]'}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('detach_volume_describe_disks.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _detach_volume_DetachDisk(self, method, url, body, headers):
+        params = {'DiskId': self.test.fake_volume.id,
+                  'InstanceId': 'i-28whl2nj2'}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('detach_disk.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _destroy_volume_DescribeDisks(self, method, url, body, headers):
+        params = {'DiskIds': '["' + self.test.fake_volume.id + '"]'}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('destroy_volume_describe_disks.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _destroy_volume_DeleteDisk(self, method, url, body, headers):
+        params = {'DiskId': self.test.fake_volume.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('delete_disk.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _destroy_volume_state_DescribeDisks(self, method, url, body, headers):
+        return self._detach_volume_DescribeDisks(method, url, body, headers)
+
+    def _DeleteSnapshot(self, method, url, body, header):
+        params = {'SnapshotId': self.test.fake_snapshot.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('delete_snapshot.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeImages(self, method, url, body, headers):
+        params = {'RegionId': self.test.fake_location.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('describe_images.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_images_pages_DescribeImages(self, method, url, body, headers):
+        if 'PageNumber=2' in url:
+            resp_body = self.fixtures.load('pages_describe_images_page2.xml')
+        else:
+            resp_body = self.fixtures.load('pages_describe_images.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_images_ex_image_ids_DescribeImages(self, method, url, body,
+                                                 headers):
+        params = {'ImageId': self.test.fake_image.id + ',not-existed'}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeImages(method, url, body, headers)
+
+    def _list_images_ex_filters_DescribeImages(self, method, url, body,
+                                               headers):
+        params = {'Status': 'Available'}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeImages(method, url, body, headers)
+
+    def _CreateImage(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageName': self.test.image_name,
+                  'Description': self.test.description,
+                  'SnapshotId': self.test.fake_snapshot.id,
+                  'ImageVersion': self.test.image_version,
+                  'ClientToken': self.test.client_token}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('create_image.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _DeleteImage(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageId': self.test.fake_image.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('delete_image.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _get_image_DescribeImages(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageId': self.test.fake_image.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('describe_images.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _get_image_not_found_DescribeImages(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageId': self.test.fake_image.id}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('get_image_describe_images.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _CopyImage(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageId': self.test.fake_image.id,
+                  'DestinationRegionId': self.test.dest_region,
+                  'DestinationImageName': self.test.image_name,
+                  'DestinationDescription': self.test.description,
+                  'ClientToken': self.test.client_token}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('copy_image.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _copy_image_same_region_CopyImage(self, method, url, body, headers):
+        params = {'RegionId': self.test.region,
+                  'ImageId': self.test.fake_image.id,
+                  'DestinationRegionId': self.test.region}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('copy_image.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _copy_image_same_region_DescribeImages(self, method, url, body,
+                                               headers):
+        return self._DescribeImages(method, url, body, headers)
+
+    def _DescribeSecurityGroups(self, method, url, body, headers):
+        params = {'RegionId': self.test.region}
+        self.assertUrlContainsQueryParams(url, params)
+        resp_body = self.fixtures.load('describe_security_groups.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+    def _list_sgs_filters_DescribeSecurityGroups(self, method, url, body,
+                                                 headers):
+        params = {'VpcId': self.test.vpc_id}
+        self.assertUrlContainsQueryParams(url, params)
+        return self._DescribeSecurityGroups(method, url, body, headers)
+
+    def _DescribeZones(self, method, url, body, headers):
+        resp_body = self.fixtures.load('describe_zones.xml')
+        return (httplib.OK, resp_body, {}, httplib.responses[httplib.OK])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/add_backend_servers.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/add_backend_servers.xml b/libcloud/test/loadbalancer/fixtures/slb/add_backend_servers.xml
new file mode 100644
index 0000000..73d7db5
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/add_backend_servers.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AddBackendServersResponse>
+    <RequestId>365F4154-92F6-4AE4-92F8-7FF34B540710</RequestId>
+    <LoadBalancerId>139a00604ad-cn-east-hangzhou-01</LoadBalancerId>
+    <BackendServers>
+        <BackendServer>
+            <ServerId>node1</ServerId>
+            <Weight>100</Weight>
+        </BackendServer>
+    </BackendServers>
+</AddBackendServersResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer.xml b/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer.xml
new file mode 100644
index 0000000..b328027
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CreateLoadBalancerResponse>
+    <RequestId>365F4154-92F6-4AE4-92F8-7FF34B540710</RequestId>
+    <LoadBalancerId>139a00604ad-cn-east-hangzhou-01</LoadBalancerId>
+    <Address>42.250.6.36</Address>
+    <NetworkType>classic</NetworkType>
+    <MasterZoneId>cn-hangzhou-d</MasterZoneId>
+    <SlaveZoneId>cn-hangzhou-b</SlaveZoneId>
+    <LoadBalancerName>balancer1</LoadBalancerName>
+</CreateLoadBalancerResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer_http_listener.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer_http_listener.xml b/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer_http_listener.xml
new file mode 100644
index 0000000..fc33ff1
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/create_load_balancer_http_listener.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CreateLoadBalancerHTTPListenerResponse>
+    <RequestId>CEF72CEB-54B6-4AE8-B225-F876FF7BA984</RequestId>
+</CreateLoadBalancerHTTPListenerResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/delete_load_balancer.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/delete_load_balancer.xml b/libcloud/test/loadbalancer/fixtures/slb/delete_load_balancer.xml
new file mode 100644
index 0000000..ff01059
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/delete_load_balancer.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<DeleteLoadBalancerResponse>
+	<RequestId>CEF72CEB-54B6-4AE8-B225-F876FF7BA984</RequestId>
+</DeleteLoadBalancerResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/delete_server_certificate.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/delete_server_certificate.xml b/libcloud/test/loadbalancer/fixtures/slb/delete_server_certificate.xml
new file mode 100644
index 0000000..e0351dd
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/delete_server_certificate.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<DeleteServerCertificateResponse>
+      <RequestId>CEF72CEB-54B6-4AE8-B225-F876FF7BA984</RequestId>
+</DeleteServerCertificateResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancer_attribute.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancer_attribute.xml b/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancer_attribute.xml
new file mode 100644
index 0000000..ef88068
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancer_attribute.xml
@@ -0,0 +1,38 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeLoadBalancerAttributeResponse>
+	<CreateTimeStamp>1452403099000</CreateTimeStamp>
+	<RegionIdAlias>cn-hangzhou</RegionIdAlias>
+	<BackendServers>
+		<BackendServer>
+			<ServerId>i-23tshnsdq</ServerId>
+			<Weight>100</Weight>
+		</BackendServer>
+	</BackendServers>
+	<LoadBalancerId>15229f88562-cn-hangzhou-dg-a01</LoadBalancerId>
+	<ListenerPorts>
+		<ListenerPort>80</ListenerPort>
+	</ListenerPorts>
+	<InternetChargeType>paybytraffic</InternetChargeType>
+	<SlaveZoneId>cn-hangzhou-b</SlaveZoneId>
+	<NetworkType>classic</NetworkType>
+	<MasterZoneId>cn-hangzhou-d</MasterZoneId>
+	<ListenerPortsAndProtocal>
+		<ListenerPortAndProtocal>
+			<ListenerProtocal>http</ListenerProtocal>
+			<ListenerPort>80</ListenerPort>
+		</ListenerPortAndProtocal>
+	</ListenerPortsAndProtocal>
+	<CreateTime>2016-01-10 13:18:19</CreateTime>
+	<Address>120.27.186.149</Address>
+	<RegionId>cn-hangzhou-dg-a01</RegionId>
+	<RequestId>D67B80CD-C359-4352-AE3C-4F286CC3782D</RequestId>
+	<AddressType>internet</AddressType>
+	<ListenerPortsAndProtocol>
+		<ListenerPortAndProtocol>
+			<ListenerProtocol>http</ListenerProtocol>
+			<ListenerPort>80</ListenerPort>
+		</ListenerPortAndProtocol>
+	</ListenerPortsAndProtocol>
+	<LoadBalancerStatus>active</LoadBalancerStatus>
+	<Bandwidth>1</Bandwidth>
+</DescribeLoadBalancerAttributeResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancers.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancers.xml b/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancers.xml
new file mode 100644
index 0000000..5275086
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/describe_load_balancers.xml
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeLoadBalancersResponse>
+	<LoadBalancers>
+		<LoadBalancer>
+			<CreateTimeStamp>1452403099000</CreateTimeStamp>
+			<SlaveZoneId>cn-hangzhou-b</SlaveZoneId>
+			<NetworkType>classic</NetworkType>
+			<MasterZoneId>cn-hangzhou-d</MasterZoneId>
+			<RegionIdAlias>cn-hangzhou</RegionIdAlias>
+			<CreateTime>2016-01-10T13:18Z</CreateTime>
+			<RegionId>cn-hangzhou-dg-a01</RegionId>
+			<Address>120.27.186.149</Address>
+			<AddressType>internet</AddressType>
+			<LoadBalancerId>15229f88562-cn-hangzhou-dg-a01</LoadBalancerId>
+			<LoadBalancerName>abc</LoadBalancerName>
+			<LoadBalancerStatus>active</LoadBalancerStatus>
+		</LoadBalancer>
+	</LoadBalancers>
+	<RequestId>A0DAF856-B181-4098-B507-6CE9E40420E8</RequestId>
+</DescribeLoadBalancersResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/describe_server_certificates.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/describe_server_certificates.xml b/libcloud/test/loadbalancer/fixtures/slb/describe_server_certificates.xml
new file mode 100644
index 0000000..b927a6d
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/describe_server_certificates.xml
@@ -0,0 +1,15 @@
+<DescribeServerCertificateResponse>
+  <RequestId>365F4154-92F6-4AE4-92F8-7FF34B540710</RequestId>
+    <ServerCertificates>
+          <ServerCertificate>
+              <ServerCertificateId>139a00604ad-cn-east-hangzhou-01</ServerCertificateId>
+              <ServerCertificateName>abe</ServerCertificateName>
+              <Fingerprint>A:B:E</Fingerprint>
+          </ServerCertificate>
+          <ServerCertificate>
+              <ServerCertificateId>139a00604ad-cn-east-hangzhou-02</ServerCertificateId>
+              <ServerCertificateName>abf</ServerCertificateName>
+              <Fingerprint>A:B:F</Fingerprint>
+          </ServerCertificate>
+    </ServerCertificates>
+</DescribeServerCertificateResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/remove_backend_servers.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/remove_backend_servers.xml b/libcloud/test/loadbalancer/fixtures/slb/remove_backend_servers.xml
new file mode 100644
index 0000000..4201472
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/remove_backend_servers.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RemoveBackendServersResponse>
+    <RequestId>365F4154-92F6-4AE4-92F8-7FF34B540710</RequestId>
+    <LoadBalancerId>139a00604ad-cn-east-hangzhou-01</LoadBalancerId>
+    <BackendServers>
+        <BackendServer>
+            <ServerId>node1</ServerId>
+            <Weight>100</Weight>
+        </BackendServer>
+    </BackendServers>
+</RemoveBackendServersResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/set_server_certificate_name.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/set_server_certificate_name.xml b/libcloud/test/loadbalancer/fixtures/slb/set_server_certificate_name.xml
new file mode 100644
index 0000000..d03763d
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/set_server_certificate_name.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SetServerCertificateNameResponse>
+      <RequestId>CEF72CEB-54B6-4AE8-B225-F876FF7BA984</RequestId>
+</SetServerCertificateNameResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/start_load_balancer_listener.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/start_load_balancer_listener.xml b/libcloud/test/loadbalancer/fixtures/slb/start_load_balancer_listener.xml
new file mode 100644
index 0000000..b7fd414
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/start_load_balancer_listener.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SetLoadBanancerListenerStatusResponse>
+    <RequestId>CEF72CEB-54B6-4AE8-B225-F876FF7BA984</RequestId>
+</SetLoadBanancerListenerStatusResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/loadbalancer/fixtures/slb/upload_server_certificate.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/fixtures/slb/upload_server_certificate.xml b/libcloud/test/loadbalancer/fixtures/slb/upload_server_certificate.xml
new file mode 100644
index 0000000..711cd56
--- /dev/null
+++ b/libcloud/test/loadbalancer/fixtures/slb/upload_server_certificate.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<UploadServerCertificateResponse>
+  <RequestId>365F4154-92F6-4AE4-92F8-7FF34B540710</RequestId>
+  <ServerCertificateId>idkp-123-cn-test-01</ServerCertificateId>
+  <ServerCertificateName>cert1</ServerCertificateName>
+  <Fingerprint>01:DF:AB:CD</Fingerprint>
+</UploadServerCertificateResponse>


[07/10] libcloud git commit: [LIBCLOUD-802] Refactor codes according to the project convention

Posted by to...@apache.org.
[LIBCLOUD-802] Refactor codes according to the project convention

Use immutable value for argument default
Prefer explicit arguments to kwargs
Add ALIYUN_ prefix to providers constant

Closes #712

Signed-off-by: Tomaz Muraus <to...@tomaz.me>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/ae4c482b
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/ae4c482b
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/ae4c482b

Branch: refs/heads/trunk
Commit: ae4c482b7d9c01f6593e842775bf74e7e81eac6d
Parents: e6002e0
Author: xg.song <sa...@gmail.com>
Authored: Sun Mar 6 01:47:51 2016 +0800
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sat Mar 12 13:28:46 2016 -0800

----------------------------------------------------------------------
 example_aliyun_ecs.py                  |    2 +-
 example_aliyun_oss.py                  |    2 +-
 example_aliyun_slb.py                  |    4 +-
 libcloud/compute/drivers/ecs.py        | 2208 ++++++++++++++-------------
 libcloud/compute/providers.py          |    2 +-
 libcloud/compute/types.py              |    4 +-
 libcloud/loadbalancer/drivers/slb.py   |  131 +-
 libcloud/loadbalancer/providers.py     |    2 +-
 libcloud/loadbalancer/types.py         |    4 +-
 libcloud/storage/providers.py          |    2 +-
 libcloud/storage/types.py              |    4 +-
 libcloud/test/compute/test_ecs.py      |    2 +-
 libcloud/test/loadbalancer/test_slb.py |   30 +-
 13 files changed, 1289 insertions(+), 1108 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/example_aliyun_ecs.py
----------------------------------------------------------------------
diff --git a/example_aliyun_ecs.py b/example_aliyun_ecs.py
index 87ee13c..bbaaf00 100644
--- a/example_aliyun_ecs.py
+++ b/example_aliyun_ecs.py
@@ -17,7 +17,7 @@ from libcloud.compute.types import Provider
 from libcloud.compute.providers import get_driver
 from libcloud.compute.base import NodeAuthPassword
 
-ECSDriver = get_driver(Provider.ECS)
+ECSDriver = get_driver(Provider.ALIYUN_ECS)
 
 region = 'cn-hangzhou'
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/example_aliyun_oss.py
----------------------------------------------------------------------
diff --git a/example_aliyun_oss.py b/example_aliyun_oss.py
index 0d335ea..6bda227 100644
--- a/example_aliyun_oss.py
+++ b/example_aliyun_oss.py
@@ -16,7 +16,7 @@
 from libcloud.storage.types import Provider
 from libcloud.storage.providers import get_driver
 
-OSSDriver = get_driver(Provider.OSS)
+OSSDriver = get_driver(Provider.ALIYUN_OSS)
 
 your_access_key_id = ''
 your_access_key_secret = ''

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/example_aliyun_slb.py
----------------------------------------------------------------------
diff --git a/example_aliyun_slb.py b/example_aliyun_slb.py
index 991be8c..0b41839 100644
--- a/example_aliyun_slb.py
+++ b/example_aliyun_slb.py
@@ -19,8 +19,8 @@ from libcloud.loadbalancer.providers import get_driver
 from libcloud.loadbalancer.base import Algorithm, Member
 from libcloud.loadbalancer.types import Provider
 
-SLBDriver = get_driver(Provider.SLB)
-ECSDriver = get_node_driver(NodeProvider.ECS)
+SLBDriver = get_driver(Provider.ALIYUN_SLB)
+ECSDriver = get_node_driver(NodeProvider.ALIYUN_ECS)
 
 region = 'cn-hangzhou'
 


[10/10] libcloud git commit: To avoid potential conflicts in the future, prefix all the Aliyun type constant values with "aliyun".

Posted by to...@apache.org.
To avoid potential conflicts in the future, prefix all the Aliyun type constant
values with "aliyun".


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/3da15748
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/3da15748
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/3da15748

Branch: refs/heads/trunk
Commit: 3da15748f114cea6b42bc0aa26c034598a8d8948
Parents: 2e5adcc
Author: Tomaz Muraus <to...@tomaz.me>
Authored: Sat Mar 12 13:51:06 2016 -0800
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sat Mar 12 13:51:06 2016 -0800

----------------------------------------------------------------------
 libcloud/compute/types.py      | 2 +-
 libcloud/loadbalancer/types.py | 2 +-
 libcloud/storage/types.py      | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/3da15748/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 226966f..eb19bf8 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -160,7 +160,7 @@ class Provider(Type):
     NTTA = 'ntta'
     MEDONE = 'medone'
     CISCOCCS = 'ciscoccs'
-    ALIYUN_ECS = 'ecs'
+    ALIYUN_ECS = 'aliyun_ecs'
 
     # OpenStack based providers
     HPCLOUD = 'hpcloud'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3da15748/libcloud/loadbalancer/types.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/types.py b/libcloud/loadbalancer/types.py
index 45d6c6a..cfb010b 100644
--- a/libcloud/loadbalancer/types.py
+++ b/libcloud/loadbalancer/types.py
@@ -44,7 +44,7 @@ class Provider(object):
     GCE = 'gce'
     SOFTLAYER = 'softlayer'
     DIMENSIONDATA = 'dimensiondata'
-    ALIYUN_SLB = 'slb'
+    ALIYUN_SLB = 'aliyun_slb'
 
     # Deprecated
     RACKSPACE_US = 'rackspace_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3da15748/libcloud/storage/types.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/types.py b/libcloud/storage/types.py
index 247e59f..2c17ef0 100644
--- a/libcloud/storage/types.py
+++ b/libcloud/storage/types.py
@@ -65,7 +65,7 @@ class Provider(object):
     KTUCLOUD = 'ktucloud'
     AURORAOBJECTS = 'auroraobjects'
     BACKBLAZE_B2 = 'backblaze_b2'
-    ALIYUN_OSS = 'oss'
+    ALIYUN_OSS = 'aliyun_oss'
 
     # Deperecated
     CLOUDFILES_US = 'cloudfiles_us'


[06/10] libcloud git commit: [LIBCLOUD-802] Refactor codes according to the project convention

Posted by to...@apache.org.
http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/compute/drivers/ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ecs.py b/libcloud/compute/drivers/ecs.py
index 2e5e59e..7929720 100644
--- a/libcloud/compute/drivers/ecs.py
+++ b/libcloud/compute/drivers/ecs.py
@@ -33,8 +33,12 @@ from libcloud.utils.py3 import _real_unicode as u
 from libcloud.utils.xml import findall, findattr, findtext
 
 __all__ = [
+    'DiskCategory',
+    'InternetChargeType',
     'ECS_API_VERSION',
-    'ECSDriver'
+    'ECSDriver',
+    'ECSSecurityGroup',
+    'ECSZone'
 ]
 
 ECS_API_VERSION = '2014-05-26'
@@ -42,1057 +46,12 @@ ECS_API_ENDPOINT = 'ecs.aliyuncs.com'
 DEFAULT_SIGNATURE_VERSION = '1.0'
 
 
-class ECSConnection(SignedAliyunConnection):
-    """
-    Represents a single connection to the Aliyun ECS Endpoint.
-    """
-
-    version = ECS_API_VERSION
-    host = ECS_API_ENDPOINT
-    responseCls = AliyunXmlResponse
-    service_name = 'ecs'
-
-
-class ECSSecurityGroup(object):
-    """
-    Security group used to control nodes internet and intranet accessibility.
-    """
-    def __init__(self, id, name, description=None, driver=None, vpc_id=None,
-                 creation_time=None):
-        self.id = id
-        self.name = name
-        self.description = description
-        self.driver = driver
-        self.vpc_id = vpc_id
-        self.creation_time = creation_time
-
-    def __repr__(self):
-        return ('<ECSSecurityGroup: id=%s, name=%s, driver=%s ...>' %
-                (self.id, self.name, self.driver.name))
-
-
-class ECSZone(object):
-    """
-    ECSZone used to represent an availability zone in a region.
-    """
-    def __init__(self, id, name, driver=None,
-                 available_resource_types=None,
-                 available_instance_types=None,
-                 available_disk_categories=None):
-        self.id = id
-        self.name = name
-        self.driver = driver
-        self.available_resource_types = available_resource_types
-        self.available_instance_types = available_instance_types
-        self.available_disk_categories = available_disk_categories
-
-    def __repr__(self):
-        return ('<ECSZone: id=%s, name=%s, driver=%s>' %
-                (self.id, self.name, self.driver))
-
-
-class InternetChargeType(object):
-    """
-    Internet connection billing types for Aliyun Nodes.
-    """
-    BY_BANDWIDTH = 'PayByBandwidth'
-    BY_TRAFFIC = 'PayByTraffic'
-
-
-class DiskCategory(object):
-    """
-    Enum defined disk types supported by Aliyun system and data disks.
-    """
-    CLOUD = 'cloud'
-    CLOUD_EFFICIENCY = 'cloud_efficiency'
-    CLOUD_SSD = 'cloud_ssd'
-    EPHEMERAL_SSD = 'ephemeral_ssd'
-
-
-class Pagination(object):
-    """
-    Pagination used to describe the multiple pages results.
-    """
-    def __init__(self, total, size, current):
-        """
-        Create a pagination.
-
-        :param total: the total count of the results
-        :param size: the page size of each page
-        :param current: the current page number, 1-based
-        """
-        self.total = total
-        self.size = size
-        self.current = current
-
-    def next(self):
-        """
-        Switch to the next page.
-        :return: the new pagination or None when no more page
-        :rtype: ``Pagination``
-        """
-        if self.total is None or (self.size * self.current >= self.total):
-            return None
-        self.current += 1
-        return self
-
-    def to_dict(self):
-        return {'PageNumber': self.current,
-                'PageSize': self.size}
-
-    def __repr__(self):
-        return ('<Pagination total=%d, size=%d, current page=%d>' %
-                (self.total, self.size, self.current))
-
-
-class ECSDriver(NodeDriver):
-    """
-    Aliyun ECS node driver.
-
-    Used for Aliyun ECS service.
-    """
-
-    name = 'Aliyun ECS'
-    website = 'https://www.aliyun.com/product/ecs'
-    connectionCls = ECSConnection
-    features = {'create_node': ['password']}
-    namespace = None
-    path = '/'
-
-    internet_charge_types = InternetChargeType
-    disk_categories = DiskCategory
-
-    NODE_STATE_MAPPING = {
-        'Starting': NodeState.PENDING,
-        'Running': NodeState.RUNNING,
-        'Stopping': NodeState.PENDING,
-        'Stopped': NodeState.STOPPED
-    }
-
-    VOLUME_STATE_MAPPING = {
-        'In_use': StorageVolumeState.INUSE,
-        'Available': StorageVolumeState.AVAILABLE,
-        'Attaching': StorageVolumeState.ATTACHING,
-        'Detaching': StorageVolumeState.INUSE,
-        'Creating': StorageVolumeState.CREATING,
-        'ReIniting': StorageVolumeState.CREATING}
-
-    SNAPSHOT_STATE_MAPPING = {
-        'progressing': VolumeSnapshotState.CREATING,
-        'accomplished': VolumeSnapshotState.AVAILABLE,
-        'failed': VolumeSnapshotState.ERROR}
-
-    def list_nodes(self, ex_node_ids=[], ex_filters=None):
-        """
-        List all nodes.
-
-        @inherits: :class:`NodeDriver.create_node`
-
-        :keyword  ex_node_ids: a list of node's ids used to filter nodes.
-                               Only the nodes which's id in this list
-                               will be returned.
-        :type   ex_node_ids: ``list`` of ``str``
-        :keyword  ex_filters: node attribute and value pairs to filter nodes.
-                              Only the nodes which matchs all the pairs will
-                              be returned.
-                              If the filter attribute need a json array value,
-                              use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
-        """
-
-        params = {'Action': 'DescribeInstances',
-                  'RegionId': self.region}
-
-        if ex_node_ids:
-            if isinstance(ex_node_ids, list):
-                params['InstanceIds'] = json.dumps(ex_node_ids)
-            else:
-                raise AttributeError('ex_node_ids should be a list of '
-                                     'node ids.')
-
-        if ex_filters:
-            if isinstance(ex_filters, dict):
-                params.update(ex_filters)
-            else:
-                raise AttributeError('ex_filters should be a dict of '
-                                     'node attributes.')
-
-        nodes = self._request_multiple_pages(self.path, params,
-                                             self._to_nodes)
-        return nodes
-
-    def list_sizes(self, location=None):
-        params = {'Action': 'DescribeInstanceTypes'}
-
-        resp_body = self.connection.request(self.path, params).object
-        size_elements = findall(resp_body, 'InstanceTypes/InstanceType',
-                                namespace=self.namespace)
-        sizes = [self._to_size(each) for each in size_elements]
-        return sizes
-
-    def list_locations(self):
-        params = {'Action': 'DescribeRegions'}
-
-        resp_body = self.connection.request(self.path, params).object
-        location_elements = findall(resp_body, 'Regions/Region',
-                                    namespace=self.namespace)
-        locations = [self._to_location(each) for each in location_elements]
-        return locations
-
-    def create_node(self, **kwargs):
-        name = kwargs['name']
-        size = kwargs['size']
-        image = kwargs['image']
-
-        params = {'Action': 'CreateInstance',
-                  'RegionId': self.region,
-                  'ImageId': image.id,
-                  'InstanceType': size.id,
-                  'InstanceName': name}
-
-        if 'ex_security_group_id' not in kwargs:
-            raise AttributeError('ex_security_group_id is mandatory')
-        params['SecurityGroupId'] = kwargs['ex_security_group_id']
-
-        if 'ex_description' in kwargs:
-            params['Description'] = kwargs['ex_description']
-
-        if 'ex_internet_charge_type' in kwargs:
-            params['InternetChargeType'] = kwargs['ex_internet_charge_type']
-            if kwargs['ex_internet_charge_type'].lower() == 'paybytraffic':
-                if 'ex_internet_max_bandwidth_out' not in kwargs:
-                    raise AttributeError('ex_internet_max_bandwidth_out is '
-                                         'mandatory for PayByTraffic internet'
-                                         ' charge type.')
-                else:
-                    params['InternetMaxBandwidthOut'] = \
-                        kwargs['ex_internet_max_bandwidth_out']
-
-        if 'ex_internet_max_bandwidth_in' in kwargs:
-            params['InternetMaxBandwidthIn'] = \
-                kwargs['ex_internet_max_bandwidth_in']
-
-        if 'ex_hostname' in kwargs:
-            params['HostName'] = kwargs['ex_hostname']
-
-        if 'auth' in kwargs:
-            auth = self._get_and_check_auth(kwargs['auth'])
-            params['Password'] = auth.password
-
-        if 'ex_io_optimized' in kwargs:
-            optimized = kwargs['ex_io_optimized']
-            if not isinstance(optimized, bool):
-                optimized = str(optimized).lower() == 'true'
-            params['IoOptimized'] = 'true' if optimized else 'false'
-
-        if 'ex_system_disk' in kwargs:
-            if not isinstance(kwargs['ex_system_disk'], dict):
-                raise AttributeError('ex_system_disk is not a dict')
-            sys_disk_dict = kwargs['ex_system_disk']
-            key_base = 'SystemDisk.'
-            mappings = {'category': 'Category',
-                        'disk_name': 'DiskName',
-                        'description': 'Description'}
-            for attr in mappings.keys():
-                if attr in sys_disk_dict:
-                    params[key_base + mappings[attr]] = sys_disk_dict[attr]
-
-        if 'ex_data_disk' in kwargs:
-            if isinstance(kwargs['ex_data_disk'], dict):
-                data_disks = [kwargs['ex_data_disk']]
-            elif isinstance(kwargs['ex_data_disk'], list):
-                data_disks = kwargs['ex_data_disk']
-            disk_key_base = 'DataDisk.'
-            mappings = {'size': 'Size',
-                        'category': 'Category',
-                        'snapshot_id': 'SnapshotId',
-                        'disk_name': 'DiskName',
-                        'description': 'Description',
-                        'device': 'Device',
-                        'delete_with_instance': 'DeleteWithInstance'}
-            for idx, disk in enumerate(data_disks):
-                key_base = disk_key_base + str(idx + 1) + '.'
-                for attr in mappings.keys():
-                    if attr in disk:
-                        if attr == 'delete_with_instance':
-                            # Convert bool value to str
-                            value = str(disk[attr]).lower()
-                        else:
-                            value = disk[attr]
-                        params[key_base + mappings[attr]] = value
-
-        if 'ex_vswitch_id' in kwargs:
-            params['VSwitchId'] = kwargs['ex_vswitch_id']
-
-        if 'ex_private_ip_address' in kwargs:
-            if 'ex_vswitch_id' not in kwargs:
-                raise AttributeError('must provide ex_private_ip_address  '
-                                     'and ex_vswitch_id at the same time')
-            else:
-                params['PrivateIpAddress'] = kwargs['ex_private_ip_address']
-
-        if 'ex_client_token' in kwargs:
-            params['ClientToken'] = kwargs['ex_client_token']
-
-        resp = self.connection.request(self.path, params=params)
-        node_id = findtext(resp.object, xpath='InstanceId',
-                           namespace=self.namespace)
-        nodes = self.list_nodes(ex_node_ids=[node_id])
-        if len(nodes) != 1:
-            raise LibcloudError('could not find the new created node '
-                                'with id %s. ' % node_id,
-                                driver=self)
-        node = nodes[0]
-        self.ex_start_node(node)
-        self._wait_until_state(nodes, NodeState.RUNNING)
-        return node
-
-    def reboot_node(self, node, ex_force_stop=False):
-        """
-        @inherits :class:`NodeDriver.reboot_node`
-        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
-                                otherwise, stop node normally,
-                                default to ``False``
-        :type ex_force_stop: ``bool``
-        """
-        params = {'Action': 'RebootInstance',
-                  'InstanceId': node.id,
-                  'ForceStop': u(ex_force_stop).lower()}
-        resp = self.connection.request(self.path, params=params)
-        return resp.success() and \
-            self._wait_until_state([node], NodeState.RUNNING)
-
-    def destroy_node(self, node):
-        nodes = self.list_nodes(ex_node_ids=[node.id])
-        if len(nodes) != 1 and node.id != nodes[0].id:
-            raise LibcloudError('could not find the node with id %s.'
-                                % node.id)
-        current = nodes[0]
-        if current.state == NodeState.RUNNING:
-            # stop node first
-            self.ex_stop_node(node)
-            self._wait_until_state(nodes, NodeState.STOPPED)
-        params = {'Action': 'DeleteInstance',
-                  'InstanceId': node.id}
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    def ex_start_node(self, node):
-        """
-        Start node to running state.
-
-        :param node: ``Node`` object
-        :return: starting operation result.
-        :rtype: ``bool``
-        """
-        params = {'Action': 'StartInstance',
-                  'InstanceId': node.id}
-        resp = self.connection.request(self.path, params)
-        return resp.success() and \
-            self._wait_until_state([node], NodeState.RUNNING)
-
-    def ex_stop_node(self, node, ex_force_stop=False):
-        """
-        Stop a running node.
-
-        :param node: The node to stop
-        :type node: :class:`Node`
-        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
-                                otherwise, stop node normally,
-                                default to ``False``
-        :type ex_force_stop: ``bool``
-        :return: stopping operation result.
-        :rtype: ``bool``
-        """
-        params = {'Action': 'StopInstance',
-                  'InstanceId': node.id,
-                  'ForceStop': u(ex_force_stop).lower()}
-        resp = self.connection.request(self.path, params)
-        return resp.success() and \
-            self._wait_until_state([node], NodeState.STOPPED)
-
-    def ex_list_security_groups(self, ex_filters=None):
-        """
-        List security groups in the current region.
-
-        :keyword ex_filters: security group attributes to filter results.
-        :type ex_filters: ``dict``
-        :return: a list of defined security groups
-        :rtype: ``list`` of ``ECSSecurityGroup``
-        """
-        params = {'Action': 'DescribeSecurityGroups',
-                  'RegionId': self.region}
-
-        if ex_filters and isinstance(ex_filters, dict):
-            ex_filters.update(params)
-            params = ex_filters
-
-        def _parse_response(resp_object):
-            sg_elements = findall(resp_object, 'SecurityGroups/SecurityGroup',
-                                  namespace=self.namespace)
-            sgs = [self._to_security_group(el) for el in sg_elements]
-            return sgs
-        return self._request_multiple_pages(self.path, params,
-                                            _parse_response)
-
-    def ex_list_zones(self, region_id=None):
-        """
-        List availability zones in the given region or the current region.
-        :keyword region_id: the id of the region to query zones from
-        :type region_id: ``str``
-        :return: list of zones
-        :rtype: ``list`` of ``ECSZone``
-        """
-        params = {'Action': 'DescribeZones'}
-        if region_id:
-            params['RegionId'] = region_id
-        else:
-            params['RegionId'] = self.region
-        resp_body = self.connection.request(self.path, params).object
-        zone_elements = findall(resp_body, 'Zones/Zone',
-                                namespace=self.namespace)
-        zones = [self._to_zone(el) for el in zone_elements]
-        return zones
-
-    ##
-    # Volume and snapshot management methods
-    ##
-
-    def list_volumes(self, ex_volume_ids=[], ex_filters=None):
-        """
-        List all volumes.
-
-        @inherits: :class:`NodeDriver.list_volumes`
-
-        :keyword  ex_volume_ids: a list of volume's ids used to filter volumes.
-                                 Only the volumes which's id in this list
-                                 will be returned.
-        :type   ex_volume_ids: ``list`` of ``str``
-        :keyword  ex_filters: volume attribute and value pairs to filter
-                              volumes. Only the volumes which matchs all will
-                              be returned.
-                              If the filter attribute need a json array value,
-                              use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
-        """
-        params = {'Action': 'DescribeDisks',
-                  'RegionId': self.region}
-
-        if ex_volume_ids:
-            if isinstance(ex_volume_ids, list):
-                params['DiskIds'] = json.dumps(ex_volume_ids)
-            else:
-                raise AttributeError('ex_volume_ids should be a list of '
-                                     'volume ids.')
-
-        if ex_filters:
-            if not isinstance(ex_filters, dict):
-                raise AttributeError('ex_filters should be a dict of '
-                                     'volume attributes.')
-            else:
-                for key in ex_filters.keys():
-                    params[key] = ex_filters[key]
-
-        def _parse_response(resp_object):
-            disk_elements = findall(resp_object, 'Disks/Disk',
-                                    namespace=self.namespace)
-            volumes = [self._to_volume(each) for each in disk_elements]
-            return volumes
-        return self._request_multiple_pages(self.path, params,
-                                            _parse_response)
-
-    def list_volume_snapshots(self, volume, ex_snapshot_ids=[],
-                              ex_filters=None):
-        """
-        List snapshots for a storage volume.
-
-        @inherites :class:`NodeDriver.list_volume_snapshots`
-        :keyword ex_snapshot_ids: a list of snapshot ids to filter the
-                                  snapshots returned.
-        :type ex_snapshot_ids: ``list`` of ``str``
-        :keyword ex_filters: snapshot attribute and value pairs to filter
-                             snapshots. Only the snapshot which matchs all
-                             the pairs will be returned.
-                             If the filter attribute need a json array value,
-                             use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
-        """
-        params = {'Action': 'DescribeSnapshots',
-                  'RegionId': self.region}
-
-        if volume:
-            params['DiskId'] = volume.id
-        if ex_snapshot_ids and isinstance(ex_snapshot_ids, list):
-            params['SnapshotIds'] = self._list_to_json_array(ex_snapshot_ids)
-        if ex_filters and isinstance(ex_filters, dict):
-            for key in ex_filters.keys():
-                params[key] = ex_filters[key]
-
-        def _parse_response(resp_body):
-            snapshot_elements = findall(resp_body, 'Snapshots/Snapshot',
-                                        namespace=self.namespace)
-            snapshots = [self._to_snapshot(each) for each in snapshot_elements]
-            return snapshots
-
-        return self._request_multiple_pages(self.path, params,
-                                            _parse_response)
-
-    def create_volume(self, size, name, location=None, snapshot=None,
-                      ex_zone_id=None, ex_description=None,
-                      ex_disk_category=None, ex_client_token=None):
-        """
-        Create a new volume.
-
-        @inherites :class:`NodeDriver.create_volume`
-        :keyword ex_zone_id: the availability zone id (required)
-        :type ex_zone_id: ``str``
-        :keyword ex_description: volume description
-        :type ex_description: ``unicode``
-        :keyword ex_disk_category: disk category for data disk
-        :type ex_disk_category: ``str``
-        :keyword ex_client_token: a token generated by client to identify
-                                each request.
-        :type ex_client_token: ``str``
-        """
-        params = {'Action': 'CreateDisk',
-                  'RegionId': self.region,
-                  'DiskName': name,
-                  'Size': size}
-        if ex_zone_id is None:
-            raise AttributeError('ex_zone_id is required')
-        params['ZoneId'] = ex_zone_id
-
-        if snapshot is not None and isinstance(snapshot, VolumeSnapshot):
-            params['SnapshotId'] = snapshot.id
-        if ex_description:
-            params['Description'] = ex_description
-        if ex_disk_category:
-            params['DiskCategory'] = ex_disk_category
-        if ex_client_token:
-            params['ClientToken'] = ex_client_token
-        resp = self.connection.request(self.path, params).object
-        volume_id = findtext(resp, 'DiskId', namespace=self.namespace)
-        volumes = self.list_volumes(ex_volume_ids=[volume_id])
-        if len(volumes) != 1:
-            raise LibcloudError('could not find the new create volume '
-                                'with id %s.' % volume_id,
-                                driver=self)
-        return volumes[0]
-
-    def create_volume_snapshot(self, volume, name=None, ex_description=None,
-                               ex_client_token=None):
-        """
-        Creates a snapshot of the storage volume.
-        @inherits :class:`NodeDriver.create_volume_snapshot`
-        :keyword ex_description: description of the snapshot.
-        :type ex_description: ``unicode``
-        :keyword ex_client_token: a token generated by client to identify
-                                each request.
-        :type ex_client_token: ``str``
-        """
-        params = {'Action': 'CreateSnapshot',
-                  'DiskId': volume.id}
-        if name:
-            params['SnapshotName'] = name
-        if ex_description:
-            params['Description'] = ex_description
-        if ex_client_token:
-            params['ClientToken'] = ex_client_token
-
-        snapshot_elements = self.connection.request(self.path, params).object
-        snapshot_id = findtext(snapshot_elements, 'SnapshotId',
-                               namespace=self.namespace)
-        snapshots = self.list_volume_snapshots(volume=None,
-                                               ex_snapshot_ids=[snapshot_id])
-        if len(snapshots) != 1:
-            raise LibcloudError('could not find new created snapshot with '
-                                'id %s.' % snapshot_id, driver=self)
-        return snapshots[0]
-
-    def attach_volume(self, node, volume, device=None,
-                      ex_delete_with_instance=None):
-        """
-        Attaches volume to node.
-
-        @inherits :class:`NodeDriver.attach_volume`
-
-        :keyword ex_delete_with_instance: if to delete this volume when the
-                                        instance is deleted.
-        :type ex_delete_with_instance: ``bool``
-        """
-        params = {'Action': 'AttachDisk',
-                  'InstanceId': node.id,
-                  'DiskId': volume.id}
-
-        if device:
-            params['Device'] = device
-        if ex_delete_with_instance:
-            params['DeleteWithInstance'] = \
-                str(bool(ex_delete_with_instance)).lower()
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    def detach_volume(self, volume, ex_instance_id=None):
-        """
-        Detaches a volume from a node.
-
-        @inherits :class:`NodeDriver.detach_volume`
-
-        :keyword ex_instance_id: the id of the instance from which the volume
-                                 is detached.
-        :type ex_instance_id: ``str``
-        """
-        params = {'Action': 'DetachDisk',
-                  'DiskId': volume.id}
-
-        if ex_instance_id:
-            params['InstanceId'] = ex_instance_id
-        else:
-            volumes = self.list_volumes(ex_volume_ids=[volume.id])
-            if len(volumes) != 1:
-                raise AttributeError('could not find the instance id '
-                                     'the volume %s attached to, '
-                                     'ex_instance_id is required.' %
-                                     volume.id)
-            params['InstanceId'] = volumes[0].extra['instance_id']
-
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    def destroy_volume(self, volume):
-        params = {'Action': 'DeleteDisk',
-                  'DiskId': volume.id}
-        volumes = self.list_volumes(ex_volume_ids=[volume.id])
-        if len(volumes) != 1:
-            raise LibcloudError('could not find the volume with id %s.' %
-                                volume.id,
-                                driver=self)
-        if volumes[0].state != StorageVolumeState.AVAILABLE:
-            raise LibcloudError('only volume in AVAILABLE state could be '
-                                'destroyed.', driver=self)
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    def destroy_volume_snapshot(self, snapshot):
-        params = {'Action': 'DeleteSnapshot'}
-
-        if snapshot and isinstance(snapshot, VolumeSnapshot):
-            params['SnapshotId'] = snapshot.id
-        else:
-            raise AttributeError('snapshot is required and must be a '
-                                 'VolumeSnapshot')
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    ##
-    # Image management methods
-    ##
-
-    def list_images(self, location=None, ex_image_ids=[], ex_filters=None):
-        """
-        List images on a provider.
-        @inherits :class:`NodeDriver.list_images`
-        :keyword ex_image_ids: a list of image ids to filter the images to
-                               be returned.
-        :type ex_image_ids: ``list`` of ``str``
-        :keyword ex_filters: image attribute and value pairs to filter
-                             images. Only the image which matchs all
-                             the pairs will be returned.
-                             If the filter attribute need a json array value,
-                             use ``list`` object, the driver will convert it.
-        :type   ex_filters: ``dict``
-        """
-
-        if location and isinstance(location, NodeLocation):
-            region = location.id
-        else:
-            region = self.region
-        params = {'Action': 'DescribeImages',
-                  'RegionId': region}
-        if ex_image_ids:
-            if isinstance(ex_image_ids, list):
-                params['ImageId'] = ','.join(ex_image_ids)
-            else:
-                raise AttributeError('ex_image_ids should be a list of '
-                                     'image ids')
-        if ex_filters and isinstance(ex_filters, dict):
-            for key in ex_filters.keys():
-                params[key] = ex_filters[key]
-
-        def _parse_response(resp_body):
-            image_elements = findall(resp_body, 'Images/Image',
-                                     namespace=self.namespace)
-            images = [self._to_image(each) for each in image_elements]
-            return images
-        return self._request_multiple_pages(self.path, params,
-                                            _parse_response)
-
-    def create_image(self, node, name, description=None, ex_snapshot_id=None,
-                     ex_image_version=None, ex_client_token=None):
-        """
-        Creates an image from a system disk snapshot.
-        @inherits :class:`NodeDriver.create_image`
-        :keyword ex_snapshot_id: the id of the snapshot to create the image.
-                                 (required)
-        :type ex_snapshot_id: ``str``
-        :keyword ex_image_version: the version number of the image
-        :type ex_image_version: ``str``
-        :keyword ex_client_token: a token generated by client to identify
-                                each request.
-        :type ex_client_token: ``str``
-        """
-        params = {'Action': 'CreateImage',
-                  'RegionId': self.region}
-        if name:
-            params['ImageName'] = name
-        if description:
-            params['Description'] = description
-        if ex_snapshot_id:
-            params['SnapshotId'] = ex_snapshot_id
-        else:
-            raise AttributeError('ex_snapshot_id is required')
-        if ex_image_version:
-            params['ImageVersion'] = ex_image_version
-        if ex_client_token:
-            params['ClientToken'] = ex_client_token
-
-        resp = self.connection.request(self.path, params)
-        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
-        return self.get_image(image_id=image_id)
-
-    def delete_image(self, node_image):
-        params = {'Action': 'DeleteImage',
-                  'RegionId': self.region,
-                  'ImageId': node_image.id}
-        resp = self.connection.request(self.path, params)
-        return resp.success()
-
-    def get_image(self, image_id, ex_region_id=None):
-        if ex_region_id:
-            region = ex_region_id
-        else:
-            region = self.region
-        location = NodeLocation(id=region, name=None, country=None,
-                                driver=self)
-        images = self.list_images(location, ex_image_ids=[image_id])
-        if len(images) != 1:
-            raise LibcloudError('could not find the image with id %s' %
-                                image_id,
-                                driver=self)
-        return images[0]
-
-    def copy_image(self, source_region, node_image, name, description=None,
-                   ex_destination_region_id=None, ex_client_token=None):
-        """
-        Copies an image from a source region to the destination region.
-        If not provide a destination region, default to the current region.
-        @inherits :class:`NodeDriver.copy_image`
-        :keyword ex_destination_region_id: id of the destination region
-        :type ex_destination_region_id: ``str``
-        :keyword ex_client_token: a token generated by client to identify
-                                each request.
-        :type ex_client_token: ``str``
-        """
-        params = {'Action': 'CopyImage',
-                  'RegionId': source_region,
-                  'ImageId': node_image.id}
-        if ex_destination_region_id is not None:
-            params['DestinationRegionId'] = ex_destination_region_id
-        else:
-            params['DestinationRegionId'] = self.region
-        if name:
-            params['DestinationImageName'] = name
-        if description:
-            params['DestinationDescription'] = description
-        if ex_client_token:
-            params['ClientToken'] = ex_client_token
-        resp = self.connection.request(self.path, params)
-        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
-        return self.get_image(image_id=image_id)
-
-    def _to_nodes(self, object):
-        """
-        Convert response to Node object list
-
-        :param object: parsed response object
-        :return: a list of ``Node``
-        :rtype: ``list``
-        """
-        node_elements = findall(object, 'Instances/Instance', self.namespace)
-        return [self._to_node(el) for el in node_elements]
-
-    def _to_node(self, instance):
-        """
-        Convert an InstanceAttributesType object to ``Node`` object
-
-        :param instance: a xml element represents an instance
-        :return: a ``Node`` object
-        :rtype: ``Node``
-        """
-        _id = findtext(element=instance, xpath='InstanceId',
-                       namespace=self.namespace)
-        name = findtext(element=instance, xpath='InstanceName',
-                        namespace=self.namespace)
-        instance_status = findtext(element=instance, xpath='Status',
-                                   namespace=self.namespace)
-        state = self.NODE_STATE_MAPPING.get(instance_status, NodeState.UNKNOWN)
-
-        def _get_ips(ip_address_els):
-            return [each.text for each in ip_address_els]
-
-        public_ip_els = findall(element=instance,
-                                xpath='PublicIpAddress/IpAddress',
-                                namespace=self.namespace)
-        public_ips = _get_ips(public_ip_els)
-        private_ip_els = findall(element=instance,
-                                 xpath='InnerIpAddress/IpAddress',
-                                 namespace=self.namespace)
-        private_ips = _get_ips(private_ip_els)
-
-        # Extra properties
-        extra = self._get_extra_dict(instance,
-                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['node'])
-        extra['vpc_attributes'] = self._get_vpc_attributes(instance)
-        extra['eip_address'] = self._get_eip_address(instance)
-        extra['operation_locks'] = self._get_operation_locks(instance)
-
-        node = Node(id=_id, name=name, state=state,
-                    public_ips=public_ips, private_ips=private_ips,
-                    driver=self.connection.driver, extra=extra)
-        return node
-
-    def _get_extra_dict(self, element, mapping):
-        """
-        Extract attributes from the element based on rules provided in the
-        mapping dictionary.
-
-        :param      element: Element to parse the values from.
-        :type       element: xml.etree.ElementTree.Element.
-
-        :param      mapping: Dictionary with the extra layout
-        :type       node: :class:`Node`
-
-        :rtype: ``dict``
-        """
-        extra = {}
-        for attribute, values in mapping.items():
-            transform_func = values['transform_func']
-            value = findattr(element=element,
-                             xpath=values['xpath'],
-                             namespace=self.namespace)
-            if value:
-                try:
-                    extra[attribute] = transform_func(value)
-                except Exception:
-                    extra[attribute] = None
-            else:
-                extra[attribute] = value
-
-        return extra
-
-    def _get_vpc_attributes(self, instance):
-        vpcs = findall(instance, xpath='VpcAttributes',
-                       namespace=self.namespace)
-        if len(vpcs) <= 0:
-            return None
-        return self._get_extra_dict(
-            vpcs[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['vpc_attributes'])
-
-    def _get_eip_address(self, instance):
-        eips = findall(instance, xpath='EipAddress',
-                       namespace=self.namespace)
-        if len(eips) <= 0:
-            return None
-        return self._get_extra_dict(
-            eips[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['eip_address_associate'])
-
-    def _get_operation_locks(self, instance):
-        locks = findall(instance, xpath='OperationLocks',
-                        namespace=self.namespace)
-        if len(locks) <= 0:
-            return None
-        return self._get_extra_dict(
-            locks[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['operation_locks'])
-
-    def _wait_until_state(self, nodes, state, wait_period=3, timeout=600):
-        """
-        Block until the provided nodes are in the desired state.
-        :param nodes: List of nodes to wait for
-        :type nodes: ``list`` of :class:`.Node`
-        :param state: desired state
-        :type state: ``NodeState``
-        :param wait_period: How many seconds to wait between each loop
-                            iteration. (default is 3)
-        :type wait_period: ``int``
-        :param timeout: How many seconds to wait before giving up.
-                        (default is 600)
-        :type timeout: ``int``
-        :return: if the nodes are in the desired state.
-        :rtype: ``bool``
-        """
-        start = time.time()
-        end = start + timeout
-        node_ids = [node.id for node in nodes]
-
-        while(time.time() < end):
-            matched_nodes = self.list_nodes(ex_node_ids=node_ids)
-            if len(matched_nodes) > len(node_ids):
-                found_ids = [node.id for node in matched_nodes]
-                msg = ('found multiple nodes with same ids, '
-                       'desired ids: %(ids)s, found ids: %(found_ids)s' %
-                       {'ids': node_ids, 'found_ids': found_ids})
-                raise LibcloudError(value=msg, driver=self)
-            desired_nodes = [node for node in matched_nodes
-                             if node.state == state]
-
-            if len(desired_nodes) == len(node_ids):
-                return True
-            else:
-                time.sleep(wait_period)
-                continue
-
-        raise LibcloudError(value='Timed out after %s seconds' % (timeout),
-                            driver=self)
-
-    def _to_volume(self, element):
-        _id = findtext(element, 'DiskId', namespace=self.namespace)
-        name = findtext(element, 'DiskName', namespace=self.namespace)
-        size = int(findtext(element, 'Size', namespace=self.namespace))
-        status_str = findtext(element, 'Status', namespace=self.namespace)
-        status = self.VOLUME_STATE_MAPPING.get(status_str,
-                                               StorageVolumeState.UNKNOWN)
-
-        extra = self._get_extra_dict(element,
-                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['volume'])
-        extra['operation_locks'] = self._get_operation_locks(element)
-        return StorageVolume(_id, name, size, self, state=status, extra=extra)
-
-    def _list_to_json_array(self, value):
-        try:
-            return json.dumps(value)
-        except Exception:
-            raise AttributeError('could not convert list to json array')
-
-    def _to_snapshot(self, element):
-        _id = findtext(element, 'SnapshotId', namespace=self.namespace)
-        created = findtext(element, 'CreationTime', namespace=self.namespace)
-        status_str = findtext(element, 'Status', namespace=self.namespace)
-        state = self.SNAPSHOT_STATE_MAPPING.get(status_str,
-                                                VolumeSnapshotState.UNKNOWN)
-        extra = self._get_extra_dict(element,
-                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['snapshot'])
-        return VolumeSnapshot(id=_id, driver=self, extra=extra,
-                              created=created, state=state)
-
-    def _to_size(self, element):
-        _id = findtext(element, 'InstanceTypeId', namespace=self.namespace)
-        ram = float(findtext(element, 'MemorySize', namespace=self.namespace))
-        extra = {}
-        extra['cpu_core_count'] = int(findtext(element, 'CpuCoreCount',
-                                               namespace=self.namespace))
-        extra['instance_type_family'] = findtext(element, 'InstanceTypeFamily',
-                                                 namespace=self.namespace)
-        return NodeSize(id=_id, name=_id, ram=ram, disk=None, bandwidth=None,
-                        price=None, driver=self, extra=extra)
-
-    def _to_location(self, element):
-        _id = findtext(element, 'RegionId', namespace=self.namespace)
-        localname = findtext(element, 'LocalName', namespace=self.namespace)
-        return NodeLocation(id=_id, name=localname, country=None, driver=self)
-
-    def _to_image(self, element):
-        _id = findtext(element, 'ImageId', namespace=self.namespace)
-        name = findtext(element, 'ImageName', namespace=self.namespace)
-        extra = self._get_extra_dict(element,
-                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['image'])
-        extra['disk_device_mappings'] = self._get_disk_device_mappings(
-            element.find('DiskDeviceMappings'))
-        return NodeImage(id=_id, name=name, driver=self, extra=extra)
-
-    def _get_disk_device_mappings(self, element):
-        if element is None:
-            return None
-        mapping_element = element.find('DiskDeviceMapping')
-        if mapping_element is not None:
-            return self._get_extra_dict(
-                mapping_element,
-                RESOURCE_EXTRA_ATTRIBUTES_MAP['disk_device_mapping'])
-        return None
-
-    def _to_security_group(self, element):
-        _id = findtext(element, 'SecurityGroupId', namespace=self.namespace)
-        name = findtext(element, 'SecurityGroupName',
-                        namespace=self.namespace)
-        description = findtext(element, 'Description',
-                               namespace=self.namespace)
-        vpc_id = findtext(element, 'VpcId', namespace=self.namespace)
-        creation_time = findtext(element, 'CreationTime',
-                                 namespace=self.namespace)
-        return ECSSecurityGroup(_id, name, description=description,
-                                driver=self, vpc_id=vpc_id,
-                                creation_time=creation_time)
-
-    def _to_zone(self, element):
-        _id = findtext(element, 'ZoneId', namespace=self.namespace)
-        local_name = findtext(element, 'LocalName', namespace=self.namespace)
-        resource_types = findall(element,
-                                 'AvailableResourceCreation/ResourceTypes',
-                                 namespace=self.namespace)
-        instance_types = findall(element,
-                                 'AvailableInstanceTypes/InstanceTypes',
-                                 namespace=self.namespace)
-        disk_categories = findall(element,
-                                  'AvailableDiskCategories/DiskCategories',
-                                  namespace=self.namespace)
-
-        def _text(element):
-            return element.text
-
-        return ECSZone(id=_id, name=local_name, driver=self,
-                       available_resource_types=list(
-                           map(_text, resource_types)),
-                       available_instance_types=list(
-                           map(_text, instance_types)),
-                       available_disk_categories=list(
-                           map(_text, disk_categories)))
-
-    def _get_pagination(self, element):
-        page_number = int(findtext(element, 'PageNumber'))
-        total_count = int(findtext(element, 'TotalCount'))
-        page_size = int(findtext(element, 'PageSize'))
-        return Pagination(total=total_count, size=page_size,
-                          current=page_number)
-
-    def _request_multiple_pages(self, path, params, parse_func):
-        """
-        Request all resources by multiple pages.
-        :param path: the resource path
-        :type path: ``str``
-        :param params: the query parameters
-        :type params: ``dict``
-        :param parse_func: the function object to parse the response body
-        :param type: ``function``
-        :return: list of resource object, if not found any, return []
-        :rtype: ``list``
-        """
-        results = []
-        while True:
-            one_page = self.connection.request(path, params).object
-            resources = parse_func(one_page)
-            results += resources
-            pagination = self._get_pagination(one_page)
-            if pagination.next() is None:
-                break
-            params.update(pagination.to_dict())
-        return results
-
-
-def _parse_bool(value):
-    if isinstance(value, bool):
-        return value
-    if u(value).lower() == 'true':
-        return True
-    return False
+def _parse_bool(value):
+    if isinstance(value, bool):
+        return value
+    if u(value).lower() == 'true':
+        return True
+    return False
 
 
 """
@@ -1388,3 +347,1148 @@ RESOURCE_EXTRA_ATTRIBUTES_MAP = {
         }
     }
 }
+
+
+class ECSConnection(SignedAliyunConnection):
+    """
+    Represents a single connection to the Aliyun ECS Endpoint.
+    """
+
+    version = ECS_API_VERSION
+    host = ECS_API_ENDPOINT
+    responseCls = AliyunXmlResponse
+    service_name = 'ecs'
+
+
+class ECSSecurityGroup(object):
+    """
+    Security group used to control nodes internet and intranet accessibility.
+    """
+    def __init__(self, id, name, description=None, driver=None, vpc_id=None,
+                 creation_time=None):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.driver = driver
+        self.vpc_id = vpc_id
+        self.creation_time = creation_time
+
+    def __repr__(self):
+        return ('<ECSSecurityGroup: id=%s, name=%s, driver=%s ...>' %
+                (self.id, self.name, self.driver.name))
+
+
+class ECSZone(object):
+    """
+    ECSZone used to represent an availability zone in a region.
+    """
+    def __init__(self, id, name, driver=None,
+                 available_resource_types=None,
+                 available_instance_types=None,
+                 available_disk_categories=None):
+        self.id = id
+        self.name = name
+        self.driver = driver
+        self.available_resource_types = available_resource_types
+        self.available_instance_types = available_instance_types
+        self.available_disk_categories = available_disk_categories
+
+    def __repr__(self):
+        return ('<ECSZone: id=%s, name=%s, driver=%s>' %
+                (self.id, self.name, self.driver))
+
+
+class InternetChargeType(object):
+    """
+    Internet connection billing types for Aliyun Nodes.
+    """
+    BY_BANDWIDTH = 'PayByBandwidth'
+    BY_TRAFFIC = 'PayByTraffic'
+
+
+class DiskCategory(object):
+    """
+    Enum defined disk types supported by Aliyun system and data disks.
+    """
+    CLOUD = 'cloud'
+    CLOUD_EFFICIENCY = 'cloud_efficiency'
+    CLOUD_SSD = 'cloud_ssd'
+    EPHEMERAL_SSD = 'ephemeral_ssd'
+
+
+class Pagination(object):
+    """
+    Pagination used to describe the multiple pages results.
+    """
+    def __init__(self, total, size, current):
+        """
+        Create a pagination.
+
+        :param total: the total count of the results
+        :param size: the page size of each page
+        :param current: the current page number, 1-based
+        """
+        self.total = total
+        self.size = size
+        self.current = current
+
+    def next(self):
+        """
+        Switch to the next page.
+        :return: the new pagination or None when no more page
+        :rtype: ``Pagination``
+        """
+        if self.total is None or (self.size * self.current >= self.total):
+            return None
+        self.current += 1
+        return self
+
+    def to_dict(self):
+        return {'PageNumber': self.current,
+                'PageSize': self.size}
+
+    def __repr__(self):
+        return ('<Pagination total=%d, size=%d, current page=%d>' %
+                (self.total, self.size, self.current))
+
+
+class ECSDriver(NodeDriver):
+    """
+    Aliyun ECS node driver.
+
+    Used for Aliyun ECS service.
+    """
+
+    name = 'Aliyun ECS'
+    website = 'https://www.aliyun.com/product/ecs'
+    connectionCls = ECSConnection
+    features = {'create_node': ['password']}
+    namespace = None
+    path = '/'
+
+    internet_charge_types = InternetChargeType
+    disk_categories = DiskCategory
+
+    NODE_STATE_MAPPING = {
+        'Starting': NodeState.PENDING,
+        'Running': NodeState.RUNNING,
+        'Stopping': NodeState.PENDING,
+        'Stopped': NodeState.STOPPED
+    }
+
+    VOLUME_STATE_MAPPING = {
+        'In_use': StorageVolumeState.INUSE,
+        'Available': StorageVolumeState.AVAILABLE,
+        'Attaching': StorageVolumeState.ATTACHING,
+        'Detaching': StorageVolumeState.INUSE,
+        'Creating': StorageVolumeState.CREATING,
+        'ReIniting': StorageVolumeState.CREATING}
+
+    SNAPSHOT_STATE_MAPPING = {
+        'progressing': VolumeSnapshotState.CREATING,
+        'accomplished': VolumeSnapshotState.AVAILABLE,
+        'failed': VolumeSnapshotState.ERROR}
+
+    def list_nodes(self, ex_node_ids=None, ex_filters=None):
+        """
+        List all nodes.
+
+        @inherits: :class:`NodeDriver.create_node`
+
+        :keyword  ex_node_ids: a list of node's ids used to filter nodes.
+                               Only the nodes which's id in this list
+                               will be returned.
+        :type   ex_node_ids: ``list`` of ``str``
+        :keyword  ex_filters: node attribute and value pairs to filter nodes.
+                              Only the nodes which matchs all the pairs will
+                              be returned.
+                              If the filter attribute need a json array value,
+                              use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+
+        params = {'Action': 'DescribeInstances',
+                  'RegionId': self.region}
+
+        if ex_node_ids:
+            if isinstance(ex_node_ids, list):
+                params['InstanceIds'] = self._list_to_json_array(ex_node_ids)
+            else:
+                raise AttributeError('ex_node_ids should be a list of '
+                                     'node ids.')
+
+        if ex_filters:
+            if isinstance(ex_filters, dict):
+                params.update(ex_filters)
+            else:
+                raise AttributeError('ex_filters should be a dict of '
+                                     'node attributes.')
+
+        nodes = self._request_multiple_pages(self.path, params,
+                                             self._to_nodes)
+        return nodes
+
+    def list_sizes(self, location=None):
+        params = {'Action': 'DescribeInstanceTypes'}
+
+        resp_body = self.connection.request(self.path, params).object
+        size_elements = findall(resp_body, 'InstanceTypes/InstanceType',
+                                namespace=self.namespace)
+        sizes = [self._to_size(each) for each in size_elements]
+        return sizes
+
+    def list_locations(self):
+        params = {'Action': 'DescribeRegions'}
+
+        resp_body = self.connection.request(self.path, params).object
+        location_elements = findall(resp_body, 'Regions/Region',
+                                    namespace=self.namespace)
+        locations = [self._to_location(each) for each in location_elements]
+        return locations
+
+    def create_node(self, name, size, image, auth=None,
+                    ex_security_group_id=None, ex_description=None,
+                    ex_internet_charge_type=None,
+                    ex_internet_max_bandwidth_out=None,
+                    ex_internet_max_bandwidth_in=None,
+                    ex_hostname=None, ex_io_optimized=None,
+                    ex_system_disk=None, ex_data_disks=None,
+                    ex_vswitch_id=None, ex_private_ip_address=None,
+                    ex_client_token=None, **kwargs):
+        """
+        @inherits: :class:`NodeDriver.create_node`
+
+        :param name: The name for this new node (required)
+        :type name: ``str``
+
+        :param image: The image to use when creating this node (required)
+        :type image: `NodeImage`
+
+        :param size: The size of the node to create (required)
+        :type size: `NodeSize`
+
+        :keyword auth: Initial authentication information for the node
+                       (optional)
+        :type auth: :class:`NodeAuthSSHKey` or :class:`NodeAuthPassword`
+
+        :keyword ex_security_group_id: The id of the security group the
+                                       new created node is attached to.
+                                       (required)
+        :type ex_security_group_id: ``str``
+
+        :keyword ex_description: A description string for this node (optional)
+        :type ex_description: ``str``
+
+        :keyword ex_internet_charge_type: The internet charge type (optional)
+        :type ex_internet_charge_type: a ``str`` of 'PayByTraffic'
+                                       or 'PayByBandwidth'
+
+        :keyword ex_internet_max_bandwidth_out: The max output bandwidth,
+                                                in Mbps (optional)
+                                                Required for 'PayByTraffic'
+                                                internet charge type
+        :type ex_internet_max_bandwidth_out: a ``int`` in range [0, 100]
+                                             a ``int`` in range [1, 100] for
+                                             'PayByTraffic' internet charge
+                                             type
+
+        :keyword ex_internet_max_bandwidth_in: The max input bandwidth,
+                                               in Mbps (optional)
+        :type ex_internet_max_bandwidth_in: a ``int`` in range [1, 200]
+                                            default to 200 in server side
+
+        :keyword ex_hostname: The hostname for the node (optional)
+        :type ex_hostname: ``str``
+
+        :keyword ex_io_optimized: Whether the node is IO optimized (optional)
+        :type ex_io_optimized: ``boll``
+
+        :keyword ex_system_disk: The system disk for the node (optional)
+        :type ex_system_disk: ``dict``
+
+        :keyword ex_data_disks: The data disks for the node (optional)
+        :type ex_data_disks: a `list` of `dict`
+
+        :keyword ex_vswitch_id: The id of vswitch for a VPC type node
+                                (optional)
+        :type ex_vswitch_id: ``str``
+
+        :keyword ex_private_ip_address: The IP address in private network
+                                        (optional)
+        :type ex_private_ip_address: ``str``
+
+        :keyword ex_client_token: A token generated by client to keep
+                                  requests idempotency (optional)
+        :type keyword ex_client_token: ``str``
+        """
+
+        params = {'Action': 'CreateInstance',
+                  'RegionId': self.region,
+                  'ImageId': image.id,
+                  'InstanceType': size.id,
+                  'InstanceName': name}
+
+        if not ex_security_group_id:
+            raise AttributeError('ex_security_group_id is mandatory')
+        params['SecurityGroupId'] = ex_security_group_id
+
+        if ex_description:
+            params['Description'] = ex_description
+
+        inet_params = self._get_internet_related_params(
+            ex_internet_charge_type,
+            ex_internet_max_bandwidth_in,
+            ex_internet_max_bandwidth_out)
+        if inet_params:
+            params.update(inet_params)
+
+        if ex_hostname:
+            params['HostName'] = ex_hostname
+
+        if auth:
+            auth = self._get_and_check_auth(auth)
+            params['Password'] = auth.password
+
+        if ex_io_optimized is not None:
+            optimized = ex_io_optimized
+            if not isinstance(optimized, bool):
+                optimized = str(optimized).lower() == 'true'
+            params['IoOptimized'] = 'true' if optimized else 'false'
+
+        if ex_system_disk:
+            system_disk = self._get_system_disk(ex_system_disk)
+            if system_disk:
+                params.update(system_disk)
+
+        if ex_data_disks:
+            data_disks = self._get_data_disks(ex_data_disks)
+            if data_disks:
+                params.update(data_disks)
+
+        if ex_vswitch_id:
+            params['VSwitchId'] = ex_vswitch_id
+
+        if ex_private_ip_address:
+            if not ex_vswitch_id:
+                raise AttributeError('must provide ex_private_ip_address  '
+                                     'and ex_vswitch_id at the same time')
+            else:
+                params['PrivateIpAddress'] = ex_private_ip_address
+
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+
+        resp = self.connection.request(self.path, params=params)
+        node_id = findtext(resp.object, xpath='InstanceId',
+                           namespace=self.namespace)
+        nodes = self.list_nodes(ex_node_ids=[node_id])
+        if len(nodes) != 1:
+            raise LibcloudError('could not find the new created node '
+                                'with id %s. ' % node_id,
+                                driver=self)
+        node = nodes[0]
+        self.ex_start_node(node)
+        self._wait_until_state(nodes, NodeState.RUNNING)
+        return node
+
+    def reboot_node(self, node, ex_force_stop=False):
+        """
+        @inherits :class:`NodeDriver.reboot_node`
+        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
+                                otherwise, stop node normally,
+                                default to ``False``
+        :type ex_force_stop: ``bool``
+        """
+        params = {'Action': 'RebootInstance',
+                  'InstanceId': node.id,
+                  'ForceStop': u(ex_force_stop).lower()}
+        resp = self.connection.request(self.path, params=params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.RUNNING)
+
+    def destroy_node(self, node):
+        nodes = self.list_nodes(ex_node_ids=[node.id])
+        if len(nodes) != 1 and node.id != nodes[0].id:
+            raise LibcloudError('could not find the node with id %s.'
+                                % node.id)
+        current = nodes[0]
+        if current.state == NodeState.RUNNING:
+            # stop node first
+            self.ex_stop_node(node)
+            self._wait_until_state(nodes, NodeState.STOPPED)
+        params = {'Action': 'DeleteInstance',
+                  'InstanceId': node.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_start_node(self, node):
+        """
+        Start node to running state.
+
+        :param node: ``Node`` object
+        :return: starting operation result.
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StartInstance',
+                  'InstanceId': node.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.RUNNING)
+
+    def ex_stop_node(self, node, ex_force_stop=False):
+        """
+        Stop a running node.
+
+        :param node: The node to stop
+        :type node: :class:`Node`
+        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
+                                otherwise, stop node normally,
+                                default to ``False``
+        :type ex_force_stop: ``bool``
+        :return: stopping operation result.
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StopInstance',
+                  'InstanceId': node.id,
+                  'ForceStop': u(ex_force_stop).lower()}
+        resp = self.connection.request(self.path, params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.STOPPED)
+
+    def ex_list_security_groups(self, ex_filters=None):
+        """
+        List security groups in the current region.
+
+        :keyword ex_filters: security group attributes to filter results.
+        :type ex_filters: ``dict``
+        :return: a list of defined security groups
+        :rtype: ``list`` of ``ECSSecurityGroup``
+        """
+        params = {'Action': 'DescribeSecurityGroups',
+                  'RegionId': self.region}
+
+        if ex_filters and isinstance(ex_filters, dict):
+            ex_filters.update(params)
+            params = ex_filters
+
+        def _parse_response(resp_object):
+            sg_elements = findall(resp_object, 'SecurityGroups/SecurityGroup',
+                                  namespace=self.namespace)
+            sgs = [self._to_security_group(el) for el in sg_elements]
+            return sgs
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def ex_list_zones(self, region_id=None):
+        """
+        List availability zones in the given region or the current region.
+        :keyword region_id: the id of the region to query zones from
+        :type region_id: ``str``
+        :return: list of zones
+        :rtype: ``list`` of ``ECSZone``
+        """
+        params = {'Action': 'DescribeZones'}
+        if region_id:
+            params['RegionId'] = region_id
+        else:
+            params['RegionId'] = self.region
+        resp_body = self.connection.request(self.path, params).object
+        zone_elements = findall(resp_body, 'Zones/Zone',
+                                namespace=self.namespace)
+        zones = [self._to_zone(el) for el in zone_elements]
+        return zones
+
+    ##
+    # Volume and snapshot management methods
+    ##
+
+    def list_volumes(self, ex_volume_ids=None, ex_filters=None):
+        """
+        List all volumes.
+
+        @inherits: :class:`NodeDriver.list_volumes`
+
+        :keyword  ex_volume_ids: a list of volume's ids used to filter volumes.
+                                 Only the volumes which's id in this list
+                                 will be returned.
+        :type   ex_volume_ids: ``list`` of ``str``
+        :keyword  ex_filters: volume attribute and value pairs to filter
+                              volumes. Only the volumes which matchs all will
+                              be returned.
+                              If the filter attribute need a json array value,
+                              use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+        params = {'Action': 'DescribeDisks',
+                  'RegionId': self.region}
+
+        if ex_volume_ids:
+            if isinstance(ex_volume_ids, list):
+                params['DiskIds'] = self._list_to_json_array(ex_volume_ids)
+            else:
+                raise AttributeError('ex_volume_ids should be a list of '
+                                     'volume ids.')
+
+        if ex_filters:
+            if not isinstance(ex_filters, dict):
+                raise AttributeError('ex_filters should be a dict of '
+                                     'volume attributes.')
+            else:
+                for key in ex_filters.keys():
+                    params[key] = ex_filters[key]
+
+        def _parse_response(resp_object):
+            disk_elements = findall(resp_object, 'Disks/Disk',
+                                    namespace=self.namespace)
+            volumes = [self._to_volume(each) for each in disk_elements]
+            return volumes
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def list_volume_snapshots(self, volume, ex_snapshot_ids=[],
+                              ex_filters=None):
+        """
+        List snapshots for a storage volume.
+
+        @inherites :class:`NodeDriver.list_volume_snapshots`
+        :keyword ex_snapshot_ids: a list of snapshot ids to filter the
+                                  snapshots returned.
+        :type ex_snapshot_ids: ``list`` of ``str``
+        :keyword ex_filters: snapshot attribute and value pairs to filter
+                             snapshots. Only the snapshot which matchs all
+                             the pairs will be returned.
+                             If the filter attribute need a json array value,
+                             use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+        params = {'Action': 'DescribeSnapshots',
+                  'RegionId': self.region}
+
+        if volume:
+            params['DiskId'] = volume.id
+        if ex_snapshot_ids and isinstance(ex_snapshot_ids, list):
+            params['SnapshotIds'] = self._list_to_json_array(ex_snapshot_ids)
+        if ex_filters and isinstance(ex_filters, dict):
+            for key in ex_filters.keys():
+                params[key] = ex_filters[key]
+
+        def _parse_response(resp_body):
+            snapshot_elements = findall(resp_body, 'Snapshots/Snapshot',
+                                        namespace=self.namespace)
+            snapshots = [self._to_snapshot(each) for each in snapshot_elements]
+            return snapshots
+
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def create_volume(self, size, name, location=None, snapshot=None,
+                      ex_zone_id=None, ex_description=None,
+                      ex_disk_category=None, ex_client_token=None):
+        """
+        Create a new volume.
+
+        @inherites :class:`NodeDriver.create_volume`
+        :keyword ex_zone_id: the availability zone id (required)
+        :type ex_zone_id: ``str``
+        :keyword ex_description: volume description
+        :type ex_description: ``unicode``
+        :keyword ex_disk_category: disk category for data disk
+        :type ex_disk_category: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateDisk',
+                  'RegionId': self.region,
+                  'DiskName': name,
+                  'Size': size}
+        if ex_zone_id is None:
+            raise AttributeError('ex_zone_id is required')
+        params['ZoneId'] = ex_zone_id
+
+        if snapshot is not None and isinstance(snapshot, VolumeSnapshot):
+            params['SnapshotId'] = snapshot.id
+        if ex_description:
+            params['Description'] = ex_description
+        if ex_disk_category:
+            params['DiskCategory'] = ex_disk_category
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+        resp = self.connection.request(self.path, params).object
+        volume_id = findtext(resp, 'DiskId', namespace=self.namespace)
+        volumes = self.list_volumes(ex_volume_ids=[volume_id])
+        if len(volumes) != 1:
+            raise LibcloudError('could not find the new create volume '
+                                'with id %s.' % volume_id,
+                                driver=self)
+        return volumes[0]
+
+    def create_volume_snapshot(self, volume, name=None, ex_description=None,
+                               ex_client_token=None):
+        """
+        Creates a snapshot of the storage volume.
+        @inherits :class:`NodeDriver.create_volume_snapshot`
+        :keyword ex_description: description of the snapshot.
+        :type ex_description: ``unicode``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateSnapshot',
+                  'DiskId': volume.id}
+        if name:
+            params['SnapshotName'] = name
+        if ex_description:
+            params['Description'] = ex_description
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+
+        snapshot_elements = self.connection.request(self.path, params).object
+        snapshot_id = findtext(snapshot_elements, 'SnapshotId',
+                               namespace=self.namespace)
+        snapshots = self.list_volume_snapshots(volume=None,
+                                               ex_snapshot_ids=[snapshot_id])
+        if len(snapshots) != 1:
+            raise LibcloudError('could not find new created snapshot with '
+                                'id %s.' % snapshot_id, driver=self)
+        return snapshots[0]
+
+    def attach_volume(self, node, volume, device=None,
+                      ex_delete_with_instance=None):
+        """
+        Attaches volume to node.
+
+        @inherits :class:`NodeDriver.attach_volume`
+
+        :keyword ex_delete_with_instance: if to delete this volume when the
+                                        instance is deleted.
+        :type ex_delete_with_instance: ``bool``
+        """
+        params = {'Action': 'AttachDisk',
+                  'InstanceId': node.id,
+                  'DiskId': volume.id}
+
+        if device:
+            params['Device'] = device
+        if ex_delete_with_instance:
+            params['DeleteWithInstance'] = \
+                str(bool(ex_delete_with_instance)).lower()
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def detach_volume(self, volume, ex_instance_id=None):
+        """
+        Detaches a volume from a node.
+
+        @inherits :class:`NodeDriver.detach_volume`
+
+        :keyword ex_instance_id: the id of the instance from which the volume
+                                 is detached.
+        :type ex_instance_id: ``str``
+        """
+        params = {'Action': 'DetachDisk',
+                  'DiskId': volume.id}
+
+        if ex_instance_id:
+            params['InstanceId'] = ex_instance_id
+        else:
+            volumes = self.list_volumes(ex_volume_ids=[volume.id])
+            if len(volumes) != 1:
+                raise AttributeError('could not find the instance id '
+                                     'the volume %s attached to, '
+                                     'ex_instance_id is required.' %
+                                     volume.id)
+            params['InstanceId'] = volumes[0].extra['instance_id']
+
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def destroy_volume(self, volume):
+        params = {'Action': 'DeleteDisk',
+                  'DiskId': volume.id}
+        volumes = self.list_volumes(ex_volume_ids=[volume.id])
+        if len(volumes) != 1:
+            raise LibcloudError('could not find the volume with id %s.' %
+                                volume.id,
+                                driver=self)
+        if volumes[0].state != StorageVolumeState.AVAILABLE:
+            raise LibcloudError('only volume in AVAILABLE state could be '
+                                'destroyed.', driver=self)
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def destroy_volume_snapshot(self, snapshot):
+        params = {'Action': 'DeleteSnapshot'}
+
+        if snapshot and isinstance(snapshot, VolumeSnapshot):
+            params['SnapshotId'] = snapshot.id
+        else:
+            raise AttributeError('snapshot is required and must be a '
+                                 'VolumeSnapshot')
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    ##
+    # Image management methods
+    ##
+
+    def list_images(self, location=None, ex_image_ids=None, ex_filters=None):
+        """
+        List images on a provider.
+        @inherits :class:`NodeDriver.list_images`
+        :keyword ex_image_ids: a list of image ids to filter the images to
+                               be returned.
+        :type ex_image_ids: ``list`` of ``str``
+        :keyword ex_filters: image attribute and value pairs to filter
+                             images. Only the image which matchs all
+                             the pairs will be returned.
+                             If the filter attribute need a json array value,
+                             use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+
+        if location and isinstance(location, NodeLocation):
+            region = location.id
+        else:
+            region = self.region
+        params = {'Action': 'DescribeImages',
+                  'RegionId': region}
+        if ex_image_ids:
+            if isinstance(ex_image_ids, list):
+                params['ImageId'] = ','.join(ex_image_ids)
+            else:
+                raise AttributeError('ex_image_ids should be a list of '
+                                     'image ids')
+        if ex_filters and isinstance(ex_filters, dict):
+            for key in ex_filters.keys():
+                params[key] = ex_filters[key]
+
+        def _parse_response(resp_body):
+            image_elements = findall(resp_body, 'Images/Image',
+                                     namespace=self.namespace)
+            images = [self._to_image(each) for each in image_elements]
+            return images
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def create_image(self, node, name, description=None, ex_snapshot_id=None,
+                     ex_image_version=None, ex_client_token=None):
+        """
+        Creates an image from a system disk snapshot.
+        @inherits :class:`NodeDriver.create_image`
+        :keyword ex_snapshot_id: the id of the snapshot to create the image.
+                                 (required)
+        :type ex_snapshot_id: ``str``
+        :keyword ex_image_version: the version number of the image
+        :type ex_image_version: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateImage',
+                  'RegionId': self.region}
+        if name:
+            params['ImageName'] = name
+        if description:
+            params['Description'] = description
+        if ex_snapshot_id:
+            params['SnapshotId'] = ex_snapshot_id
+        else:
+            raise AttributeError('ex_snapshot_id is required')
+        if ex_image_version:
+            params['ImageVersion'] = ex_image_version
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+
+        resp = self.connection.request(self.path, params)
+        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
+        return self.get_image(image_id=image_id)
+
+    def delete_image(self, node_image):
+        params = {'Action': 'DeleteImage',
+                  'RegionId': self.region,
+                  'ImageId': node_image.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def get_image(self, image_id, ex_region_id=None):
+        if ex_region_id:
+            region = ex_region_id
+        else:
+            region = self.region
+        location = NodeLocation(id=region, name=None, country=None,
+                                driver=self)
+        images = self.list_images(location, ex_image_ids=[image_id])
+        if len(images) != 1:
+            raise LibcloudError('could not find the image with id %s' %
+                                image_id,
+                                driver=self)
+        return images[0]
+
+    def copy_image(self, source_region, node_image, name, description=None,
+                   ex_destination_region_id=None, ex_client_token=None):
+        """
+        Copies an image from a source region to the destination region.
+        If not provide a destination region, default to the current region.
+        @inherits :class:`NodeDriver.copy_image`
+        :keyword ex_destination_region_id: id of the destination region
+        :type ex_destination_region_id: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CopyImage',
+                  'RegionId': source_region,
+                  'ImageId': node_image.id}
+        if ex_destination_region_id is not None:
+            params['DestinationRegionId'] = ex_destination_region_id
+        else:
+            params['DestinationRegionId'] = self.region
+        if name:
+            params['DestinationImageName'] = name
+        if description:
+            params['DestinationDescription'] = description
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+        resp = self.connection.request(self.path, params)
+        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
+        return self.get_image(image_id=image_id)
+
+    def _to_nodes(self, object):
+        """
+        Convert response to Node object list
+
+        :param object: parsed response object
+        :return: a list of ``Node``
+        :rtype: ``list``
+        """
+        node_elements = findall(object, 'Instances/Instance', self.namespace)
+        return [self._to_node(el) for el in node_elements]
+
+    def _to_node(self, instance):
+        """
+        Convert an InstanceAttributesType object to ``Node`` object
+
+        :param instance: a xml element represents an instance
+        :return: a ``Node`` object
+        :rtype: ``Node``
+        """
+        _id = findtext(element=instance, xpath='InstanceId',
+                       namespace=self.namespace)
+        name = findtext(element=instance, xpath='InstanceName',
+                        namespace=self.namespace)
+        instance_status = findtext(element=instance, xpath='Status',
+                                   namespace=self.namespace)
+        state = self.NODE_STATE_MAPPING.get(instance_status, NodeState.UNKNOWN)
+
+        def _get_ips(ip_address_els):
+            return [each.text for each in ip_address_els]
+
+        public_ip_els = findall(element=instance,
+                                xpath='PublicIpAddress/IpAddress',
+                                namespace=self.namespace)
+        public_ips = _get_ips(public_ip_els)
+        private_ip_els = findall(element=instance,
+                                 xpath='InnerIpAddress/IpAddress',
+                                 namespace=self.namespace)
+        private_ips = _get_ips(private_ip_els)
+
+        # Extra properties
+        extra = self._get_extra_dict(instance,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['node'])
+        extra['vpc_attributes'] = self._get_vpc_attributes(instance)
+        extra['eip_address'] = self._get_eip_address(instance)
+        extra['operation_locks'] = self._get_operation_locks(instance)
+
+        node = Node(id=_id, name=name, state=state,
+                    public_ips=public_ips, private_ips=private_ips,
+                    driver=self.connection.driver, extra=extra)
+        return node
+
+    def _get_extra_dict(self, element, mapping):
+        """
+        Extract attributes from the element based on rules provided in the
+        mapping dictionary.
+
+        :param      element: Element to parse the values from.
+        :type       element: xml.etree.ElementTree.Element.
+
+        :param      mapping: Dictionary with the extra layout
+        :type       node: :class:`Node`
+
+        :rtype: ``dict``
+        """
+        extra = {}
+        for attribute, values in mapping.items():
+            transform_func = values['transform_func']
+            value = findattr(element=element,
+                             xpath=values['xpath'],
+                             namespace=self.namespace)
+            if value:
+                try:
+                    extra[attribute] = transform_func(value)
+                except Exception:
+                    extra[attribute] = None
+            else:
+                extra[attribute] = value
+
+        return extra
+
+    def _get_internet_related_params(self, ex_internet_charge_type,
+                                     ex_internet_max_bandwidth_in,
+                                     ex_internet_max_bandwidth_out):
+        params = {}
+        if ex_internet_charge_type:
+            params['InternetChargeType'] = ex_internet_charge_type
+            if ex_internet_charge_type.lower() == 'paybytraffic':
+                if ex_internet_max_bandwidth_out:
+                    params['InternetMaxBandwidthOut'] = \
+                        ex_internet_max_bandwidth_out
+                else:
+                    raise AttributeError('ex_internet_max_bandwidth_out is '
+                                         'mandatory for PayByTraffic internet'
+                                         ' charge type.')
+
+        if ex_internet_max_bandwidth_in:
+            params['InternetMaxBandwidthIn'] = \
+                ex_internet_max_bandwidth_in
+        return params
+
+    def _get_system_disk(self, ex_system_disk):
+        if not isinstance(ex_system_disk, dict):
+            raise AttributeError('ex_system_disk is not a dict')
+        sys_disk_dict = ex_system_disk
+        key_base = 'SystemDisk.'
+        # TODO(samsong8610): Use a type instead of dict
+        mappings = {'category': 'Category',
+                    'disk_name': 'DiskName',
+                    'description': 'Description'}
+        params = {}
+        for attr in mappings.keys():
+            if attr in sys_disk_dict:
+                params[key_base + mappings[attr]] = sys_disk_dict[attr]
+        return params
+
+    def _get_data_disks(self, ex_data_disks):
+        if isinstance(ex_data_disks, dict):
+            data_disks = [ex_data_disks]
+        elif isinstance(ex_data_disks, list):
+            data_disks = ex_data_disks
+        else:
+            raise AttributeError('ex_data_disks should be a list of dict')
+        # TODO(samsong8610): Use a type instead of dict
+        mappings = {'size': 'Size',
+                    'category': 'Category',
+                    'snapshot_id': 'SnapshotId',
+                    'disk_name': 'DiskName',
+                    'description': 'Description',
+                    'device': 'Device',
+                    'delete_with_instance': 'DeleteWithInstance'}
+        params = {}
+        for idx, disk in enumerate(data_disks):
+            key_base = 'DataDisk.{0}.'.format(idx + 1)
+            for attr in mappings.keys():
+                if attr in disk:
+                    if attr == 'delete_with_instance':
+                        # Convert bool value to str
+                        value = str(disk[attr]).lower()
+                    else:
+                        value = disk[attr]
+                    params[key_base + mappings[attr]] = value
+        return params
+
+    def _get_vpc_attributes(self, instance):
+        vpcs = findall(instance, xpath='VpcAttributes',
+                       namespace=self.namespace)
+        if len(vpcs) <= 0:
+            return None
+        return self._get_extra_dict(
+            vpcs[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['vpc_attributes'])
+
+    def _get_eip_address(self, instance):
+        eips = findall(instance, xpath='EipAddress',
+                       namespace=self.namespace)
+        if len(eips) <= 0:
+            return None
+        return self._get_extra_dict(
+            eips[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['eip_address_associate'])
+
+    def _get_operation_locks(self, instance):
+        locks = findall(instance, xpath='OperationLocks',
+                        namespace=self.namespace)
+        if len(locks) <= 0:
+            return None
+        return self._get_extra_dict(
+            locks[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['operation_locks'])
+
+    def _wait_until_state(self, nodes, state, wait_period=3, timeout=600):
+        """
+        Block until the provided nodes are in the desired state.
+        :param nodes: List of nodes to wait for
+        :type nodes: ``list`` of :class:`.Node`
+        :param state: desired state
+        :type state: ``NodeState``
+        :param wait_period: How many seconds to wait between each loop
+                            iteration. (default is 3)
+        :type wait_period: ``int``
+        :param timeout: How many seconds to wait before giving up.
+                        (default is 600)
+        :type timeout: ``int``
+        :return: if the nodes are in the desired state.
+        :rtype: ``bool``
+        """
+        start = time.time()
+        end = start + timeout
+        node_ids = [node.id for node in nodes]
+
+        while(time.time() < end):
+            matched_nodes = self.list_nodes(ex_node_ids=node_ids)
+            if len(matched_nodes) > len(node_ids):
+                found_ids = [node.id for node in matched_nodes]
+                msg = ('found multiple nodes with same ids, '
+                       'desired ids: %(ids)s, found ids: %(found_ids)s' %
+                       {'ids': node_ids, 'found_ids': found_ids})
+                raise LibcloudError(value=msg, driver=self)
+            desired_nodes = [node for node in matched_nodes
+                             if node.state == state]
+
+            if len(desired_nodes) == len(node_ids):
+                return True
+            else:
+                time.sleep(wait_period)
+                continue
+
+        raise LibcloudError(value='Timed out after %s seconds' % (timeout),
+                            driver=self)
+
+    def _to_volume(self, element):
+        _id = findtext(element, 'DiskId', namespace=self.namespace)
+        name = findtext(element, 'DiskName', namespace=self.namespace)
+        size = int(findtext(element, 'Size', namespace=self.namespace))
+        status_str = findtext(element, 'Status', namespace=self.namespace)
+        status = self.VOLUME_STATE_MAPPING.get(status_str,
+                                               StorageVolumeState.UNKNOWN)
+
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['volume'])
+        extra['operation_locks'] = self._get_operation_locks(element)
+        return StorageVolume(_id, name, size, self, state=status, extra=extra)
+
+    def _list_to_json_array(self, value):
+        try:
+            return json.dumps(value)
+        except Exception:
+            raise AttributeError('could not convert list to json array')
+
+    def _to_snapshot(self, element):
+        _id = findtext(element, 'SnapshotId', namespace=self.namespace)
+        created = findtext(element, 'CreationTime', namespace=self.namespace)
+        status_str = findtext(element, 'Status', namespace=self.namespace)
+        state = self.SNAPSHOT_STATE_MAPPING.get(status_str,
+                                                VolumeSnapshotState.UNKNOWN)
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['snapshot'])
+        return VolumeSnapshot(id=_id, driver=self, extra=extra,
+                              created=created, state=state)
+
+    def _to_size(self, element):
+        _id = findtext(element, 'InstanceTypeId', namespace=self.namespace)
+        ram = float(findtext(element, 'MemorySize', namespace=self.namespace))
+        extra = {}
+        extra['cpu_core_count'] = int(findtext(element, 'CpuCoreCount',
+                                               namespace=self.namespace))
+        extra['instance_type_family'] = findtext(element, 'InstanceTypeFamily',
+                                                 namespace=self.namespace)
+        return NodeSize(id=_id, name=_id, ram=ram, disk=None, bandwidth=None,
+                        price=None, driver=self, extra=extra)
+
+    def _to_location(self, element):
+        _id = findtext(element, 'RegionId', namespace=self.namespace)
+        localname = findtext(element, 'LocalName', namespace=self.namespace)
+        return NodeLocation(id=_id, name=localname, country=None, driver=self)
+
+    def _to_image(self, element):
+        _id = findtext(element, 'ImageId', namespace=self.namespace)
+        name = findtext(element, 'ImageName', namespace=self.namespace)
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['image'])
+        extra['disk_device_mappings'] = self._get_disk_device_mappings(
+            element.find('DiskDeviceMappings'))
+        return NodeImage(id=_id, name=name, driver=self, extra=extra)
+
+    def _get_disk_device_mappings(self, element):
+        if element is None:
+            return None
+        mapping_element = element.find('DiskDeviceMapping')
+        if mapping_element is not None:
+            return self._get

<TRUNCATED>

[04/10] libcloud git commit: [LIBCLOUD-802] Add drivers for Aliyun cloud

Posted by to...@apache.org.
[LIBCLOUD-802] Add drivers for Aliyun cloud

Closes #712

Signed-off-by: Tomaz Muraus <to...@tomaz.me>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/e6002e0a
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/e6002e0a
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/e6002e0a

Branch: refs/heads/trunk
Commit: e6002e0ad443b9762f7a5e6187aeb4617e2874e8
Parents: 6f3279c
Author: xg.song <xg...@venusource.com>
Authored: Mon Dec 28 09:13:45 2015 +0800
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sat Mar 12 13:28:15 2016 -0800

----------------------------------------------------------------------
 example_aliyun_ecs.py                           |   79 +
 example_aliyun_oss.py                           |   82 ++
 example_aliyun_slb.py                           |   55 +
 libcloud/common/aliyun.py                       |  239 +++
 libcloud/compute/drivers/ecs.py                 | 1390 ++++++++++++++++++
 libcloud/compute/providers.py                   |    2 +
 libcloud/compute/types.py                       |    2 +
 libcloud/loadbalancer/drivers/slb.py            |  754 ++++++++++
 libcloud/loadbalancer/providers.py              |    3 +
 libcloud/loadbalancer/types.py                  |    4 +
 libcloud/storage/drivers/oss.py                 | 1069 ++++++++++++++
 libcloud/storage/providers.py                   |    2 +
 libcloud/storage/types.py                       |    2 +
 libcloud/test/common/test_aliyun.py             |   45 +
 .../test/compute/fixtures/ecs/attach_disk.xml   |    4 +
 .../test/compute/fixtures/ecs/copy_image.xml    |    5 +
 .../test/compute/fixtures/ecs/create_disk.xml   |    5 +
 .../test/compute/fixtures/ecs/create_image.xml  |    5 +
 .../compute/fixtures/ecs/create_instance.xml    |    5 +
 .../ecs/create_node_describe_instances.xml      |   56 +
 .../compute/fixtures/ecs/create_snapshot.xml    |    5 +
 .../ecs/create_volume_describe_disks.xml        |   36 +
 .../test/compute/fixtures/ecs/delete_disk.xml   |    4 +
 .../test/compute/fixtures/ecs/delete_image.xml  |    4 +
 .../compute/fixtures/ecs/delete_instance.xml    |    4 +
 .../compute/fixtures/ecs/delete_snapshot.xml    |    4 +
 .../compute/fixtures/ecs/describe_disks.xml     |   62 +
 .../compute/fixtures/ecs/describe_images.xml    |   41 +
 .../fixtures/ecs/describe_instance_types.xml    |   18 +
 .../compute/fixtures/ecs/describe_instances.xml |   56 +
 .../compute/fixtures/ecs/describe_regions.xml   |   42 +
 .../fixtures/ecs/describe_security_groups.xml   |   18 +
 .../compute/fixtures/ecs/describe_snapshots.xml |   25 +
 .../compute/fixtures/ecs/describe_zones.xml     |   42 +
 .../ecs/destroy_node_describe_instances.xml     |   56 +
 .../ecs/destroy_volume_describe_disks.xml       |   36 +
 .../test/compute/fixtures/ecs/detach_disk.xml   |    4 +
 .../ecs/detach_volume_describe_disks.xml        |   36 +
 .../fixtures/ecs/get_image_describe_images.xml  |   10 +
 .../fixtures/ecs/pages_describe_images.xml      |   41 +
 .../ecs/pages_describe_images_page2.xml         |   41 +
 .../compute/fixtures/ecs/reboot_instance.xml    |    4 +
 .../ecs/reboot_node_describe_instances.xml      |   56 +
 .../compute/fixtures/ecs/start_instance.xml     |    4 +
 .../test/compute/fixtures/ecs/stop_instance.xml |    4 +
 .../ecs/stop_node_describe_instances.xml        |   56 +
 libcloud/test/compute/test_ecs.py               |  912 ++++++++++++
 .../fixtures/slb/add_backend_servers.xml        |   11 +
 .../fixtures/slb/create_load_balancer.xml       |   10 +
 .../slb/create_load_balancer_http_listener.xml  |    4 +
 .../fixtures/slb/delete_load_balancer.xml       |    4 +
 .../fixtures/slb/delete_server_certificate.xml  |    4 +
 .../slb/describe_load_balancer_attribute.xml    |   38 +
 .../fixtures/slb/describe_load_balancers.xml    |   20 +
 .../slb/describe_server_certificates.xml        |   15 +
 .../fixtures/slb/remove_backend_servers.xml     |   11 +
 .../slb/set_server_certificate_name.xml         |    4 +
 .../slb/start_load_balancer_listener.xml        |    4 +
 .../fixtures/slb/upload_server_certificate.xml  |    7 +
 libcloud/test/loadbalancer/test_slb.py          |  566 +++++++
 libcloud/test/secrets.py-dist                   |    3 +
 .../fixtures/oss/complete_multipart_upload.xml  |    7 +
 .../oss/ex_iterate_multipart_uploads_p1.xml     |   22 +
 .../oss/ex_iterate_multipart_uploads_p2.xml     |   17 +
 .../fixtures/oss/initiate_multipart_upload.xml  |    6 +
 .../fixtures/oss/list_container_objects.xml     |   33 +
 .../oss/list_container_objects_chinese.xml      |   32 +
 .../oss/list_container_objects_empty.xml        |    9 +
 .../oss/list_container_objects_prefix.xml       |   33 +
 .../storage/fixtures/oss/list_containers.xml    |   19 +
 .../fixtures/oss/list_containers_empty.xml      |    9 +
 libcloud/test/storage/test_oss.py               |  881 +++++++++++
 72 files changed, 7198 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/example_aliyun_ecs.py
----------------------------------------------------------------------
diff --git a/example_aliyun_ecs.py b/example_aliyun_ecs.py
new file mode 100644
index 0000000..87ee13c
--- /dev/null
+++ b/example_aliyun_ecs.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+from libcloud.compute.base import NodeAuthPassword
+
+ECSDriver = get_driver(Provider.ECS)
+
+region = 'cn-hangzhou'
+
+your_access_key_id = ''
+your_access_key_secret = ''
+ecs = ECSDriver(your_access_key_id, your_access_key_secret, region=region)
+
+sizes = ecs.list_sizes()
+small = sizes[1]
+
+locations = ecs.list_locations()
+location = None
+for each in locations:
+    if each.id == region:
+        location = each
+        break
+if location is None:
+    print('could not find cn-qingdao location')
+    sys.exit(-1)
+print(location.name)
+
+images = ecs.list_images()
+print('Found %d images' % len(images))
+for each in images:
+    if 'ubuntu' in each.id.lower():
+        image = each
+        break
+else:
+    image = images[0]
+print('Use image %s' % image)
+
+sgs = ecs.ex_list_security_groups()
+print('Found %d security groups' % len(sgs))
+sg = sgs[0]
+print('Use security group %s' % sg)
+
+nodes = ecs.list_nodes()
+print('Found %d nodes' % len(nodes))
+if len(nodes) == 0:
+    print('Starting create a new node')
+    data_disk = {
+        'size': 5,
+        'category': ecs.disk_categories.CLOUD,
+        'disk_name': 'data_disk1',
+        'delete_with_instance': True}
+
+    auth = NodeAuthPassword('P@$$w0rd')
+
+    node = ecs.create_node(image=image, size=small, name='test',
+                           ex_security_group_id=sg.id,
+                           ex_internet_charge_type=ecs.internet_charge_types.BY_TRAFFIC,
+                           ex_internet_max_bandwidth_out=1,
+                           ex_data_disk=data_disk,
+                           auth=auth)
+    print('Created node %s' % node)
+    nodes = ecs.list_nodes()
+
+for each in nodes:
+    print('Found node %s' % each)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/example_aliyun_oss.py
----------------------------------------------------------------------
diff --git a/example_aliyun_oss.py b/example_aliyun_oss.py
new file mode 100644
index 0000000..0d335ea
--- /dev/null
+++ b/example_aliyun_oss.py
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 libcloud.storage.types import Provider
+from libcloud.storage.providers import get_driver
+
+OSSDriver = get_driver(Provider.OSS)
+
+your_access_key_id = ''
+your_access_key_secret = ''
+oss = OSSDriver(your_access_key_id, your_access_key_secret)
+
+container_name = 'CONTAINER_NAME_FOR_TEST'
+object_name = 'OBJECT_NAME_FOR_TEST'
+local_file_path = 'LOCAL_FILE_FULL_PATH_TO_UPLOAD'
+upload_object_name = 'OBJECT_NAME_FOR_UPLOAD_FILE'
+for container in oss.iterate_containers():
+    print('container: %s' % container)
+
+c1 = oss.get_container(container_name)
+print('Got container %s:' % c1)
+
+objects = c1.list_objects()
+count = len(objects)
+print('Has %d objects' % count)
+
+objects = oss.list_container_objects(c1, ex_prefix='en')
+print('Has %d objects with prefix "en"' % len(objects))
+for each in objects:
+    print(each)
+
+obj = oss.get_object(container_name, object_name)
+print('Got object %s:' % obj)
+
+# Download object
+oss.download_object(obj, object_name, overwrite_existing=True)
+for trunk in oss.download_object_as_stream(obj):
+    print(trunk)
+
+# Upload object
+obj = oss.upload_object(local_file_path, c1, upload_object_name)
+
+# Upload multipart
+uploads = list(oss.ex_iterate_multipart_uploads(c1))
+print('Found %d incompleted uploads' % len(uploads))
+if len(uploads) > 0:
+    oss.ex_abort_all_multipart_uploads(c1)
+    print('Abort them all')
+
+def data_iter(limit):
+    i = 0
+    while True:
+        yield i
+        i += 1
+        if i >= limit:
+            break
+
+print('Starting to upload 1MB using multipart api')
+one_mb = 1024*1024
+obj = oss.upload_object_via_stream(data_iter(one_mb), c1, upload_object_name)
+print('Finish uploading')
+
+# Delete objects
+print('Delete object %s' % obj)
+oss.delete_object(obj)
+
+# Create container
+#c2 = oss.create_container(container_name='20160117')
+#c2 = oss.create_container(container_name='20160117', ex_location='oss-cn-beijing')
+#c2_got = oss.get_container('20160117')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/example_aliyun_slb.py
----------------------------------------------------------------------
diff --git a/example_aliyun_slb.py b/example_aliyun_slb.py
new file mode 100644
index 0000000..991be8c
--- /dev/null
+++ b/example_aliyun_slb.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 libcloud.compute.types import Provider as NodeProvider
+from libcloud.compute.providers import get_driver as get_node_driver
+from libcloud.loadbalancer.providers import get_driver
+from libcloud.loadbalancer.base import Algorithm, Member
+from libcloud.loadbalancer.types import Provider
+
+SLBDriver = get_driver(Provider.SLB)
+ECSDriver = get_node_driver(NodeProvider.ECS)
+
+region = 'cn-hangzhou'
+
+your_access_key_id = ''
+your_access_key_secret = ''
+slb = SLBDriver(your_access_key_id, your_access_key_secret, region=region)
+ecs = ECSDriver(your_access_key_id, your_access_key_secret, region=region)
+
+protos = slb.list_protocols()
+print('Found %d protocols: %s' % (len(protos), protos))
+
+balancers = slb.list_balancers()
+print('Found %d load balancers' % len(balancers))
+print(balancers)
+
+if len(balancers) > 0:
+    b1 = balancers[0]
+    print('Delete %s' % b1)
+    slb.destroy_balancer(b1)
+else:
+    extra = {'AddressType': 'internet',
+             'Bandwidth': 1,
+             'StickySession': 'off',
+             'HealthCheck': 'off'}
+    nodes = ecs.list_nodes()
+    print('Found %d nodes' % len(nodes))
+    members = [Member(node.id, node.public_ips[0], 80, extra={'Weight': 50*(i+1)})
+               for i, node in enumerate(nodes)]
+    new_b = slb.create_balancer('test-balancer', 80, 'http',
+                                Algorithm.WEIGHTED_ROUND_ROBIN, members,
+                                **extra)
+    print('Created balancer %s' % new_b)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/common/aliyun.py
----------------------------------------------------------------------
diff --git a/libcloud/common/aliyun.py b/libcloud/common/aliyun.py
new file mode 100644
index 0000000..4e91dbb
--- /dev/null
+++ b/libcloud/common/aliyun.py
@@ -0,0 +1,239 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 base64
+import hashlib
+import hmac
+import sys
+import time
+import uuid
+
+try:
+    from lxml import etree as ET
+except ImportError:
+    from xml.etree import ElementTree as ET
+
+from libcloud.common.base import ConnectionUserAndKey, XmlResponse
+from libcloud.common.types import MalformedResponseError
+from libcloud.utils.py3 import b, u, urlquote, PY3
+from libcloud.utils.xml import findtext
+
+__all__ = [
+    'AliyunXmlResponse',
+    'AliyunRequestSigner',
+    'AliyunRequestSignerAlgorithmV1_0',
+    'SignedAliyunConnection',
+    'AliyunConnection',
+
+    'SIGNATURE_VERSION_1_0',
+    'DEFAULT_SIGNATURE_VERSION'
+]
+
+SIGNATURE_VERSION_1_0 = '1.0'
+DEFAULT_SIGNATURE_VERSION = SIGNATURE_VERSION_1_0
+
+
+class AliyunXmlResponse(XmlResponse):
+    namespace = None
+
+    def success(self):
+        return self.status >= 200 and self.status < 300
+
+    def parse_body(self):
+        """
+        Each response from Aliyun contains a request id and a host id.
+        The response body is in utf-8 encoding.
+        """
+        if len(self.body) == 0 and not self.parse_zero_length_body:
+            return self.body
+
+        try:
+            if PY3:
+                parser = ET.XMLParser(encoding='utf-8')
+                body = ET.XML(self.body.encode('utf-8'), parser=parser)
+            else:
+                body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError('Failed to parse XML',
+                                         body=self.body,
+                                         driver=self.connection.driver)
+        self.request_id = findtext(element=body, xpath='RequestId',
+                                   namespace=self.namespace)
+        self.host_id = findtext(element=body, xpath='HostId',
+                                namespace=self.namespace)
+        return body
+
+    def parse_error(self):
+        """
+        Parse error responses from Aliyun.
+        """
+        body = super(AliyunXmlResponse, self).parse_error()
+        code, message = self._parse_error_details(element=body)
+        request_id = findtext(element=body, xpath='RequestId',
+                              namespace=self.namespace)
+        host_id = findtext(element=body, xpath='HostId',
+                           namespace=self.namespace)
+        error = {'code': code,
+                 'message': message,
+                 'request_id': request_id,
+                 'host_id': host_id}
+        return u(error)
+
+    def _parse_error_details(self, element):
+        """
+        Parse error code and message from the provided error element.
+
+        :return: ``tuple`` with two elements: (code, message)
+        :rtype: ``tuple``
+        """
+        code = findtext(element=element, xpath='Code',
+                        namespace=self.namespace)
+        message = findtext(element=element, xpath='Message',
+                           namespace=self.namespace)
+
+        return (code, message)
+
+
+class AliyunRequestSigner(object):
+    """
+    Class handles signing the outgoing Aliyun requests.
+    """
+
+    def __init__(self, access_key, access_secret, version):
+        """
+        :param access_key: Access key.
+        :type access_key: ``str``
+
+        :param access_secret: Access secret.
+        :type access_secret: ``str``
+
+        :param version: API version.
+        :type version: ``str``
+        """
+        self.access_key = access_key
+        self.access_secret = access_secret
+        self.version = version
+
+    def get_request_params(self, params, method='GET', path='/'):
+        return params
+
+    def get_request_headers(self, params, headers, method='GET', path='/'):
+        return params, headers
+
+
+class AliyunRequestSignerAlgorithmV1_0(AliyunRequestSigner):
+    """Aliyun request signer using signature version 1.0."""
+    def get_request_params(self, params, method='GET', path='/'):
+        params['Format'] = 'XML'
+        params['Version'] = self.version
+        params['AccessKeyId'] = self.access_key
+        params['SignatureMethod'] = 'HMAC-SHA1'
+        params['SignatureVersion'] = SIGNATURE_VERSION_1_0
+        params['SignatureNonce'] = _get_signature_nonce()
+        # TODO: Support 'ResourceOwnerAccount'
+        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
+                                            time.gmtime())
+        params['Signature'] = self._sign_request(params, method, path)
+        return params
+
+    def _sign_request(self, params, method, path):
+        """
+        Sign Aliyun requests parameters and get the signature.
+
+        StringToSign = HTTPMethod + '&' +
+                       percentEncode('/') + '&' +
+                       percentEncode(CanonicalizedQueryString)
+        """
+        keys = list(params.keys())
+        keys.sort()
+        pairs = []
+        for key in keys:
+            pairs.append('%s=%s' % (_percent_encode(key),
+                                    _percent_encode(params[key])))
+        qs = urlquote('&'.join(pairs), safe='-_.~')
+        string_to_sign = '&'.join((method, urlquote(path, safe=''), qs))
+        b64_hmac = base64.b64encode(
+            hmac.new(b(self._get_access_secret()), b(string_to_sign),
+                     digestmod=hashlib.sha1).digest()
+        )
+
+        return b64_hmac.decode('utf8')
+
+    def _get_access_secret(self):
+        return '%s&' % self.access_secret
+
+
+class AliyunConnection(ConnectionUserAndKey):
+    pass
+
+
+class SignedAliyunConnection(AliyunConnection):
+    def __init__(self, user_id, key, secure=True, host=None, port=None,
+                 url=None, timeout=None, proxy_url=None, retry_delay=None,
+                 backoff=None, signature_version=DEFAULT_SIGNATURE_VERSION):
+        super(SignedAliyunConnection, self).__init__(user_id=user_id, key=key,
+                                                     secure=secure,
+                                                     host=host, port=port,
+                                                     url=url, timeout=timeout,
+                                                     proxy_url=proxy_url,
+                                                     retry_delay=retry_delay,
+                                                     backoff=backoff)
+        self.signature_version = str(signature_version)
+
+        if self.signature_version == '1.0':
+            signer_cls = AliyunRequestSignerAlgorithmV1_0
+        else:
+            raise ValueError('Unsupported signature_version: %s' %
+                             signature_version)
+
+        self.signer = signer_cls(access_key=self.user_id,
+                                 access_secret=self.key,
+                                 version=self.version)
+
+    def add_default_params(self, params):
+        params = self.signer.get_request_params(params=params,
+                                                method=self.method,
+                                                path=self.action)
+        return params
+
+
+def _percent_encode(encode_str):
+    """
+    Encode string to utf8, quote for url and replace '+' with %20,
+    '*' with %2A and keep '~' not converted.
+
+    :param src_str: ``str`` in the same encoding with sys.stdin,
+                    default to encoding cp936.
+    :return: ``str`` represents the encoded result
+    :rtype: ``str``
+    """
+    encoding = sys.stdin.encoding or 'cp936'
+    decoded = str(encode_str)
+    if PY3:
+        if isinstance(encode_str, bytes):
+            decoded = encode_str.decode(encoding)
+    else:
+        decoded = str(encode_str).decode(encoding)
+
+    res = urlquote(
+        decoded.encode('utf8'), '')
+    res = res.replace('+', '%20')
+    res = res.replace('*', '%2A')
+    res = res.replace('%7E', '~')
+    return res
+
+
+def _get_signature_nonce():
+    return str(uuid.uuid4())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/compute/drivers/ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ecs.py b/libcloud/compute/drivers/ecs.py
new file mode 100644
index 0000000..2e5e59e
--- /dev/null
+++ b/libcloud/compute/drivers/ecs.py
@@ -0,0 +1,1390 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+"""
+Node driver for Aliyun.
+"""
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+import time
+
+from libcloud.common.aliyun import AliyunXmlResponse, SignedAliyunConnection
+from libcloud.common.types import LibcloudError
+from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeSize, \
+    StorageVolume, VolumeSnapshot, NodeLocation
+from libcloud.compute.types import NodeState, StorageVolumeState, \
+    VolumeSnapshotState
+from libcloud.utils.py3 import _real_unicode as u
+from libcloud.utils.xml import findall, findattr, findtext
+
+__all__ = [
+    'ECS_API_VERSION',
+    'ECSDriver'
+]
+
+ECS_API_VERSION = '2014-05-26'
+ECS_API_ENDPOINT = 'ecs.aliyuncs.com'
+DEFAULT_SIGNATURE_VERSION = '1.0'
+
+
+class ECSConnection(SignedAliyunConnection):
+    """
+    Represents a single connection to the Aliyun ECS Endpoint.
+    """
+
+    version = ECS_API_VERSION
+    host = ECS_API_ENDPOINT
+    responseCls = AliyunXmlResponse
+    service_name = 'ecs'
+
+
+class ECSSecurityGroup(object):
+    """
+    Security group used to control nodes internet and intranet accessibility.
+    """
+    def __init__(self, id, name, description=None, driver=None, vpc_id=None,
+                 creation_time=None):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.driver = driver
+        self.vpc_id = vpc_id
+        self.creation_time = creation_time
+
+    def __repr__(self):
+        return ('<ECSSecurityGroup: id=%s, name=%s, driver=%s ...>' %
+                (self.id, self.name, self.driver.name))
+
+
+class ECSZone(object):
+    """
+    ECSZone used to represent an availability zone in a region.
+    """
+    def __init__(self, id, name, driver=None,
+                 available_resource_types=None,
+                 available_instance_types=None,
+                 available_disk_categories=None):
+        self.id = id
+        self.name = name
+        self.driver = driver
+        self.available_resource_types = available_resource_types
+        self.available_instance_types = available_instance_types
+        self.available_disk_categories = available_disk_categories
+
+    def __repr__(self):
+        return ('<ECSZone: id=%s, name=%s, driver=%s>' %
+                (self.id, self.name, self.driver))
+
+
+class InternetChargeType(object):
+    """
+    Internet connection billing types for Aliyun Nodes.
+    """
+    BY_BANDWIDTH = 'PayByBandwidth'
+    BY_TRAFFIC = 'PayByTraffic'
+
+
+class DiskCategory(object):
+    """
+    Enum defined disk types supported by Aliyun system and data disks.
+    """
+    CLOUD = 'cloud'
+    CLOUD_EFFICIENCY = 'cloud_efficiency'
+    CLOUD_SSD = 'cloud_ssd'
+    EPHEMERAL_SSD = 'ephemeral_ssd'
+
+
+class Pagination(object):
+    """
+    Pagination used to describe the multiple pages results.
+    """
+    def __init__(self, total, size, current):
+        """
+        Create a pagination.
+
+        :param total: the total count of the results
+        :param size: the page size of each page
+        :param current: the current page number, 1-based
+        """
+        self.total = total
+        self.size = size
+        self.current = current
+
+    def next(self):
+        """
+        Switch to the next page.
+        :return: the new pagination or None when no more page
+        :rtype: ``Pagination``
+        """
+        if self.total is None or (self.size * self.current >= self.total):
+            return None
+        self.current += 1
+        return self
+
+    def to_dict(self):
+        return {'PageNumber': self.current,
+                'PageSize': self.size}
+
+    def __repr__(self):
+        return ('<Pagination total=%d, size=%d, current page=%d>' %
+                (self.total, self.size, self.current))
+
+
+class ECSDriver(NodeDriver):
+    """
+    Aliyun ECS node driver.
+
+    Used for Aliyun ECS service.
+    """
+
+    name = 'Aliyun ECS'
+    website = 'https://www.aliyun.com/product/ecs'
+    connectionCls = ECSConnection
+    features = {'create_node': ['password']}
+    namespace = None
+    path = '/'
+
+    internet_charge_types = InternetChargeType
+    disk_categories = DiskCategory
+
+    NODE_STATE_MAPPING = {
+        'Starting': NodeState.PENDING,
+        'Running': NodeState.RUNNING,
+        'Stopping': NodeState.PENDING,
+        'Stopped': NodeState.STOPPED
+    }
+
+    VOLUME_STATE_MAPPING = {
+        'In_use': StorageVolumeState.INUSE,
+        'Available': StorageVolumeState.AVAILABLE,
+        'Attaching': StorageVolumeState.ATTACHING,
+        'Detaching': StorageVolumeState.INUSE,
+        'Creating': StorageVolumeState.CREATING,
+        'ReIniting': StorageVolumeState.CREATING}
+
+    SNAPSHOT_STATE_MAPPING = {
+        'progressing': VolumeSnapshotState.CREATING,
+        'accomplished': VolumeSnapshotState.AVAILABLE,
+        'failed': VolumeSnapshotState.ERROR}
+
+    def list_nodes(self, ex_node_ids=[], ex_filters=None):
+        """
+        List all nodes.
+
+        @inherits: :class:`NodeDriver.create_node`
+
+        :keyword  ex_node_ids: a list of node's ids used to filter nodes.
+                               Only the nodes which's id in this list
+                               will be returned.
+        :type   ex_node_ids: ``list`` of ``str``
+        :keyword  ex_filters: node attribute and value pairs to filter nodes.
+                              Only the nodes which matchs all the pairs will
+                              be returned.
+                              If the filter attribute need a json array value,
+                              use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+
+        params = {'Action': 'DescribeInstances',
+                  'RegionId': self.region}
+
+        if ex_node_ids:
+            if isinstance(ex_node_ids, list):
+                params['InstanceIds'] = json.dumps(ex_node_ids)
+            else:
+                raise AttributeError('ex_node_ids should be a list of '
+                                     'node ids.')
+
+        if ex_filters:
+            if isinstance(ex_filters, dict):
+                params.update(ex_filters)
+            else:
+                raise AttributeError('ex_filters should be a dict of '
+                                     'node attributes.')
+
+        nodes = self._request_multiple_pages(self.path, params,
+                                             self._to_nodes)
+        return nodes
+
+    def list_sizes(self, location=None):
+        params = {'Action': 'DescribeInstanceTypes'}
+
+        resp_body = self.connection.request(self.path, params).object
+        size_elements = findall(resp_body, 'InstanceTypes/InstanceType',
+                                namespace=self.namespace)
+        sizes = [self._to_size(each) for each in size_elements]
+        return sizes
+
+    def list_locations(self):
+        params = {'Action': 'DescribeRegions'}
+
+        resp_body = self.connection.request(self.path, params).object
+        location_elements = findall(resp_body, 'Regions/Region',
+                                    namespace=self.namespace)
+        locations = [self._to_location(each) for each in location_elements]
+        return locations
+
+    def create_node(self, **kwargs):
+        name = kwargs['name']
+        size = kwargs['size']
+        image = kwargs['image']
+
+        params = {'Action': 'CreateInstance',
+                  'RegionId': self.region,
+                  'ImageId': image.id,
+                  'InstanceType': size.id,
+                  'InstanceName': name}
+
+        if 'ex_security_group_id' not in kwargs:
+            raise AttributeError('ex_security_group_id is mandatory')
+        params['SecurityGroupId'] = kwargs['ex_security_group_id']
+
+        if 'ex_description' in kwargs:
+            params['Description'] = kwargs['ex_description']
+
+        if 'ex_internet_charge_type' in kwargs:
+            params['InternetChargeType'] = kwargs['ex_internet_charge_type']
+            if kwargs['ex_internet_charge_type'].lower() == 'paybytraffic':
+                if 'ex_internet_max_bandwidth_out' not in kwargs:
+                    raise AttributeError('ex_internet_max_bandwidth_out is '
+                                         'mandatory for PayByTraffic internet'
+                                         ' charge type.')
+                else:
+                    params['InternetMaxBandwidthOut'] = \
+                        kwargs['ex_internet_max_bandwidth_out']
+
+        if 'ex_internet_max_bandwidth_in' in kwargs:
+            params['InternetMaxBandwidthIn'] = \
+                kwargs['ex_internet_max_bandwidth_in']
+
+        if 'ex_hostname' in kwargs:
+            params['HostName'] = kwargs['ex_hostname']
+
+        if 'auth' in kwargs:
+            auth = self._get_and_check_auth(kwargs['auth'])
+            params['Password'] = auth.password
+
+        if 'ex_io_optimized' in kwargs:
+            optimized = kwargs['ex_io_optimized']
+            if not isinstance(optimized, bool):
+                optimized = str(optimized).lower() == 'true'
+            params['IoOptimized'] = 'true' if optimized else 'false'
+
+        if 'ex_system_disk' in kwargs:
+            if not isinstance(kwargs['ex_system_disk'], dict):
+                raise AttributeError('ex_system_disk is not a dict')
+            sys_disk_dict = kwargs['ex_system_disk']
+            key_base = 'SystemDisk.'
+            mappings = {'category': 'Category',
+                        'disk_name': 'DiskName',
+                        'description': 'Description'}
+            for attr in mappings.keys():
+                if attr in sys_disk_dict:
+                    params[key_base + mappings[attr]] = sys_disk_dict[attr]
+
+        if 'ex_data_disk' in kwargs:
+            if isinstance(kwargs['ex_data_disk'], dict):
+                data_disks = [kwargs['ex_data_disk']]
+            elif isinstance(kwargs['ex_data_disk'], list):
+                data_disks = kwargs['ex_data_disk']
+            disk_key_base = 'DataDisk.'
+            mappings = {'size': 'Size',
+                        'category': 'Category',
+                        'snapshot_id': 'SnapshotId',
+                        'disk_name': 'DiskName',
+                        'description': 'Description',
+                        'device': 'Device',
+                        'delete_with_instance': 'DeleteWithInstance'}
+            for idx, disk in enumerate(data_disks):
+                key_base = disk_key_base + str(idx + 1) + '.'
+                for attr in mappings.keys():
+                    if attr in disk:
+                        if attr == 'delete_with_instance':
+                            # Convert bool value to str
+                            value = str(disk[attr]).lower()
+                        else:
+                            value = disk[attr]
+                        params[key_base + mappings[attr]] = value
+
+        if 'ex_vswitch_id' in kwargs:
+            params['VSwitchId'] = kwargs['ex_vswitch_id']
+
+        if 'ex_private_ip_address' in kwargs:
+            if 'ex_vswitch_id' not in kwargs:
+                raise AttributeError('must provide ex_private_ip_address  '
+                                     'and ex_vswitch_id at the same time')
+            else:
+                params['PrivateIpAddress'] = kwargs['ex_private_ip_address']
+
+        if 'ex_client_token' in kwargs:
+            params['ClientToken'] = kwargs['ex_client_token']
+
+        resp = self.connection.request(self.path, params=params)
+        node_id = findtext(resp.object, xpath='InstanceId',
+                           namespace=self.namespace)
+        nodes = self.list_nodes(ex_node_ids=[node_id])
+        if len(nodes) != 1:
+            raise LibcloudError('could not find the new created node '
+                                'with id %s. ' % node_id,
+                                driver=self)
+        node = nodes[0]
+        self.ex_start_node(node)
+        self._wait_until_state(nodes, NodeState.RUNNING)
+        return node
+
+    def reboot_node(self, node, ex_force_stop=False):
+        """
+        @inherits :class:`NodeDriver.reboot_node`
+        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
+                                otherwise, stop node normally,
+                                default to ``False``
+        :type ex_force_stop: ``bool``
+        """
+        params = {'Action': 'RebootInstance',
+                  'InstanceId': node.id,
+                  'ForceStop': u(ex_force_stop).lower()}
+        resp = self.connection.request(self.path, params=params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.RUNNING)
+
+    def destroy_node(self, node):
+        nodes = self.list_nodes(ex_node_ids=[node.id])
+        if len(nodes) != 1 and node.id != nodes[0].id:
+            raise LibcloudError('could not find the node with id %s.'
+                                % node.id)
+        current = nodes[0]
+        if current.state == NodeState.RUNNING:
+            # stop node first
+            self.ex_stop_node(node)
+            self._wait_until_state(nodes, NodeState.STOPPED)
+        params = {'Action': 'DeleteInstance',
+                  'InstanceId': node.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_start_node(self, node):
+        """
+        Start node to running state.
+
+        :param node: ``Node`` object
+        :return: starting operation result.
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StartInstance',
+                  'InstanceId': node.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.RUNNING)
+
+    def ex_stop_node(self, node, ex_force_stop=False):
+        """
+        Stop a running node.
+
+        :param node: The node to stop
+        :type node: :class:`Node`
+        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
+                                otherwise, stop node normally,
+                                default to ``False``
+        :type ex_force_stop: ``bool``
+        :return: stopping operation result.
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StopInstance',
+                  'InstanceId': node.id,
+                  'ForceStop': u(ex_force_stop).lower()}
+        resp = self.connection.request(self.path, params)
+        return resp.success() and \
+            self._wait_until_state([node], NodeState.STOPPED)
+
+    def ex_list_security_groups(self, ex_filters=None):
+        """
+        List security groups in the current region.
+
+        :keyword ex_filters: security group attributes to filter results.
+        :type ex_filters: ``dict``
+        :return: a list of defined security groups
+        :rtype: ``list`` of ``ECSSecurityGroup``
+        """
+        params = {'Action': 'DescribeSecurityGroups',
+                  'RegionId': self.region}
+
+        if ex_filters and isinstance(ex_filters, dict):
+            ex_filters.update(params)
+            params = ex_filters
+
+        def _parse_response(resp_object):
+            sg_elements = findall(resp_object, 'SecurityGroups/SecurityGroup',
+                                  namespace=self.namespace)
+            sgs = [self._to_security_group(el) for el in sg_elements]
+            return sgs
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def ex_list_zones(self, region_id=None):
+        """
+        List availability zones in the given region or the current region.
+        :keyword region_id: the id of the region to query zones from
+        :type region_id: ``str``
+        :return: list of zones
+        :rtype: ``list`` of ``ECSZone``
+        """
+        params = {'Action': 'DescribeZones'}
+        if region_id:
+            params['RegionId'] = region_id
+        else:
+            params['RegionId'] = self.region
+        resp_body = self.connection.request(self.path, params).object
+        zone_elements = findall(resp_body, 'Zones/Zone',
+                                namespace=self.namespace)
+        zones = [self._to_zone(el) for el in zone_elements]
+        return zones
+
+    ##
+    # Volume and snapshot management methods
+    ##
+
+    def list_volumes(self, ex_volume_ids=[], ex_filters=None):
+        """
+        List all volumes.
+
+        @inherits: :class:`NodeDriver.list_volumes`
+
+        :keyword  ex_volume_ids: a list of volume's ids used to filter volumes.
+                                 Only the volumes which's id in this list
+                                 will be returned.
+        :type   ex_volume_ids: ``list`` of ``str``
+        :keyword  ex_filters: volume attribute and value pairs to filter
+                              volumes. Only the volumes which matchs all will
+                              be returned.
+                              If the filter attribute need a json array value,
+                              use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+        params = {'Action': 'DescribeDisks',
+                  'RegionId': self.region}
+
+        if ex_volume_ids:
+            if isinstance(ex_volume_ids, list):
+                params['DiskIds'] = json.dumps(ex_volume_ids)
+            else:
+                raise AttributeError('ex_volume_ids should be a list of '
+                                     'volume ids.')
+
+        if ex_filters:
+            if not isinstance(ex_filters, dict):
+                raise AttributeError('ex_filters should be a dict of '
+                                     'volume attributes.')
+            else:
+                for key in ex_filters.keys():
+                    params[key] = ex_filters[key]
+
+        def _parse_response(resp_object):
+            disk_elements = findall(resp_object, 'Disks/Disk',
+                                    namespace=self.namespace)
+            volumes = [self._to_volume(each) for each in disk_elements]
+            return volumes
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def list_volume_snapshots(self, volume, ex_snapshot_ids=[],
+                              ex_filters=None):
+        """
+        List snapshots for a storage volume.
+
+        @inherites :class:`NodeDriver.list_volume_snapshots`
+        :keyword ex_snapshot_ids: a list of snapshot ids to filter the
+                                  snapshots returned.
+        :type ex_snapshot_ids: ``list`` of ``str``
+        :keyword ex_filters: snapshot attribute and value pairs to filter
+                             snapshots. Only the snapshot which matchs all
+                             the pairs will be returned.
+                             If the filter attribute need a json array value,
+                             use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+        params = {'Action': 'DescribeSnapshots',
+                  'RegionId': self.region}
+
+        if volume:
+            params['DiskId'] = volume.id
+        if ex_snapshot_ids and isinstance(ex_snapshot_ids, list):
+            params['SnapshotIds'] = self._list_to_json_array(ex_snapshot_ids)
+        if ex_filters and isinstance(ex_filters, dict):
+            for key in ex_filters.keys():
+                params[key] = ex_filters[key]
+
+        def _parse_response(resp_body):
+            snapshot_elements = findall(resp_body, 'Snapshots/Snapshot',
+                                        namespace=self.namespace)
+            snapshots = [self._to_snapshot(each) for each in snapshot_elements]
+            return snapshots
+
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def create_volume(self, size, name, location=None, snapshot=None,
+                      ex_zone_id=None, ex_description=None,
+                      ex_disk_category=None, ex_client_token=None):
+        """
+        Create a new volume.
+
+        @inherites :class:`NodeDriver.create_volume`
+        :keyword ex_zone_id: the availability zone id (required)
+        :type ex_zone_id: ``str``
+        :keyword ex_description: volume description
+        :type ex_description: ``unicode``
+        :keyword ex_disk_category: disk category for data disk
+        :type ex_disk_category: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateDisk',
+                  'RegionId': self.region,
+                  'DiskName': name,
+                  'Size': size}
+        if ex_zone_id is None:
+            raise AttributeError('ex_zone_id is required')
+        params['ZoneId'] = ex_zone_id
+
+        if snapshot is not None and isinstance(snapshot, VolumeSnapshot):
+            params['SnapshotId'] = snapshot.id
+        if ex_description:
+            params['Description'] = ex_description
+        if ex_disk_category:
+            params['DiskCategory'] = ex_disk_category
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+        resp = self.connection.request(self.path, params).object
+        volume_id = findtext(resp, 'DiskId', namespace=self.namespace)
+        volumes = self.list_volumes(ex_volume_ids=[volume_id])
+        if len(volumes) != 1:
+            raise LibcloudError('could not find the new create volume '
+                                'with id %s.' % volume_id,
+                                driver=self)
+        return volumes[0]
+
+    def create_volume_snapshot(self, volume, name=None, ex_description=None,
+                               ex_client_token=None):
+        """
+        Creates a snapshot of the storage volume.
+        @inherits :class:`NodeDriver.create_volume_snapshot`
+        :keyword ex_description: description of the snapshot.
+        :type ex_description: ``unicode``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateSnapshot',
+                  'DiskId': volume.id}
+        if name:
+            params['SnapshotName'] = name
+        if ex_description:
+            params['Description'] = ex_description
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+
+        snapshot_elements = self.connection.request(self.path, params).object
+        snapshot_id = findtext(snapshot_elements, 'SnapshotId',
+                               namespace=self.namespace)
+        snapshots = self.list_volume_snapshots(volume=None,
+                                               ex_snapshot_ids=[snapshot_id])
+        if len(snapshots) != 1:
+            raise LibcloudError('could not find new created snapshot with '
+                                'id %s.' % snapshot_id, driver=self)
+        return snapshots[0]
+
+    def attach_volume(self, node, volume, device=None,
+                      ex_delete_with_instance=None):
+        """
+        Attaches volume to node.
+
+        @inherits :class:`NodeDriver.attach_volume`
+
+        :keyword ex_delete_with_instance: if to delete this volume when the
+                                        instance is deleted.
+        :type ex_delete_with_instance: ``bool``
+        """
+        params = {'Action': 'AttachDisk',
+                  'InstanceId': node.id,
+                  'DiskId': volume.id}
+
+        if device:
+            params['Device'] = device
+        if ex_delete_with_instance:
+            params['DeleteWithInstance'] = \
+                str(bool(ex_delete_with_instance)).lower()
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def detach_volume(self, volume, ex_instance_id=None):
+        """
+        Detaches a volume from a node.
+
+        @inherits :class:`NodeDriver.detach_volume`
+
+        :keyword ex_instance_id: the id of the instance from which the volume
+                                 is detached.
+        :type ex_instance_id: ``str``
+        """
+        params = {'Action': 'DetachDisk',
+                  'DiskId': volume.id}
+
+        if ex_instance_id:
+            params['InstanceId'] = ex_instance_id
+        else:
+            volumes = self.list_volumes(ex_volume_ids=[volume.id])
+            if len(volumes) != 1:
+                raise AttributeError('could not find the instance id '
+                                     'the volume %s attached to, '
+                                     'ex_instance_id is required.' %
+                                     volume.id)
+            params['InstanceId'] = volumes[0].extra['instance_id']
+
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def destroy_volume(self, volume):
+        params = {'Action': 'DeleteDisk',
+                  'DiskId': volume.id}
+        volumes = self.list_volumes(ex_volume_ids=[volume.id])
+        if len(volumes) != 1:
+            raise LibcloudError('could not find the volume with id %s.' %
+                                volume.id,
+                                driver=self)
+        if volumes[0].state != StorageVolumeState.AVAILABLE:
+            raise LibcloudError('only volume in AVAILABLE state could be '
+                                'destroyed.', driver=self)
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def destroy_volume_snapshot(self, snapshot):
+        params = {'Action': 'DeleteSnapshot'}
+
+        if snapshot and isinstance(snapshot, VolumeSnapshot):
+            params['SnapshotId'] = snapshot.id
+        else:
+            raise AttributeError('snapshot is required and must be a '
+                                 'VolumeSnapshot')
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    ##
+    # Image management methods
+    ##
+
+    def list_images(self, location=None, ex_image_ids=[], ex_filters=None):
+        """
+        List images on a provider.
+        @inherits :class:`NodeDriver.list_images`
+        :keyword ex_image_ids: a list of image ids to filter the images to
+                               be returned.
+        :type ex_image_ids: ``list`` of ``str``
+        :keyword ex_filters: image attribute and value pairs to filter
+                             images. Only the image which matchs all
+                             the pairs will be returned.
+                             If the filter attribute need a json array value,
+                             use ``list`` object, the driver will convert it.
+        :type   ex_filters: ``dict``
+        """
+
+        if location and isinstance(location, NodeLocation):
+            region = location.id
+        else:
+            region = self.region
+        params = {'Action': 'DescribeImages',
+                  'RegionId': region}
+        if ex_image_ids:
+            if isinstance(ex_image_ids, list):
+                params['ImageId'] = ','.join(ex_image_ids)
+            else:
+                raise AttributeError('ex_image_ids should be a list of '
+                                     'image ids')
+        if ex_filters and isinstance(ex_filters, dict):
+            for key in ex_filters.keys():
+                params[key] = ex_filters[key]
+
+        def _parse_response(resp_body):
+            image_elements = findall(resp_body, 'Images/Image',
+                                     namespace=self.namespace)
+            images = [self._to_image(each) for each in image_elements]
+            return images
+        return self._request_multiple_pages(self.path, params,
+                                            _parse_response)
+
+    def create_image(self, node, name, description=None, ex_snapshot_id=None,
+                     ex_image_version=None, ex_client_token=None):
+        """
+        Creates an image from a system disk snapshot.
+        @inherits :class:`NodeDriver.create_image`
+        :keyword ex_snapshot_id: the id of the snapshot to create the image.
+                                 (required)
+        :type ex_snapshot_id: ``str``
+        :keyword ex_image_version: the version number of the image
+        :type ex_image_version: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CreateImage',
+                  'RegionId': self.region}
+        if name:
+            params['ImageName'] = name
+        if description:
+            params['Description'] = description
+        if ex_snapshot_id:
+            params['SnapshotId'] = ex_snapshot_id
+        else:
+            raise AttributeError('ex_snapshot_id is required')
+        if ex_image_version:
+            params['ImageVersion'] = ex_image_version
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+
+        resp = self.connection.request(self.path, params)
+        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
+        return self.get_image(image_id=image_id)
+
+    def delete_image(self, node_image):
+        params = {'Action': 'DeleteImage',
+                  'RegionId': self.region,
+                  'ImageId': node_image.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def get_image(self, image_id, ex_region_id=None):
+        if ex_region_id:
+            region = ex_region_id
+        else:
+            region = self.region
+        location = NodeLocation(id=region, name=None, country=None,
+                                driver=self)
+        images = self.list_images(location, ex_image_ids=[image_id])
+        if len(images) != 1:
+            raise LibcloudError('could not find the image with id %s' %
+                                image_id,
+                                driver=self)
+        return images[0]
+
+    def copy_image(self, source_region, node_image, name, description=None,
+                   ex_destination_region_id=None, ex_client_token=None):
+        """
+        Copies an image from a source region to the destination region.
+        If not provide a destination region, default to the current region.
+        @inherits :class:`NodeDriver.copy_image`
+        :keyword ex_destination_region_id: id of the destination region
+        :type ex_destination_region_id: ``str``
+        :keyword ex_client_token: a token generated by client to identify
+                                each request.
+        :type ex_client_token: ``str``
+        """
+        params = {'Action': 'CopyImage',
+                  'RegionId': source_region,
+                  'ImageId': node_image.id}
+        if ex_destination_region_id is not None:
+            params['DestinationRegionId'] = ex_destination_region_id
+        else:
+            params['DestinationRegionId'] = self.region
+        if name:
+            params['DestinationImageName'] = name
+        if description:
+            params['DestinationDescription'] = description
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
+        resp = self.connection.request(self.path, params)
+        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
+        return self.get_image(image_id=image_id)
+
+    def _to_nodes(self, object):
+        """
+        Convert response to Node object list
+
+        :param object: parsed response object
+        :return: a list of ``Node``
+        :rtype: ``list``
+        """
+        node_elements = findall(object, 'Instances/Instance', self.namespace)
+        return [self._to_node(el) for el in node_elements]
+
+    def _to_node(self, instance):
+        """
+        Convert an InstanceAttributesType object to ``Node`` object
+
+        :param instance: a xml element represents an instance
+        :return: a ``Node`` object
+        :rtype: ``Node``
+        """
+        _id = findtext(element=instance, xpath='InstanceId',
+                       namespace=self.namespace)
+        name = findtext(element=instance, xpath='InstanceName',
+                        namespace=self.namespace)
+        instance_status = findtext(element=instance, xpath='Status',
+                                   namespace=self.namespace)
+        state = self.NODE_STATE_MAPPING.get(instance_status, NodeState.UNKNOWN)
+
+        def _get_ips(ip_address_els):
+            return [each.text for each in ip_address_els]
+
+        public_ip_els = findall(element=instance,
+                                xpath='PublicIpAddress/IpAddress',
+                                namespace=self.namespace)
+        public_ips = _get_ips(public_ip_els)
+        private_ip_els = findall(element=instance,
+                                 xpath='InnerIpAddress/IpAddress',
+                                 namespace=self.namespace)
+        private_ips = _get_ips(private_ip_els)
+
+        # Extra properties
+        extra = self._get_extra_dict(instance,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['node'])
+        extra['vpc_attributes'] = self._get_vpc_attributes(instance)
+        extra['eip_address'] = self._get_eip_address(instance)
+        extra['operation_locks'] = self._get_operation_locks(instance)
+
+        node = Node(id=_id, name=name, state=state,
+                    public_ips=public_ips, private_ips=private_ips,
+                    driver=self.connection.driver, extra=extra)
+        return node
+
+    def _get_extra_dict(self, element, mapping):
+        """
+        Extract attributes from the element based on rules provided in the
+        mapping dictionary.
+
+        :param      element: Element to parse the values from.
+        :type       element: xml.etree.ElementTree.Element.
+
+        :param      mapping: Dictionary with the extra layout
+        :type       node: :class:`Node`
+
+        :rtype: ``dict``
+        """
+        extra = {}
+        for attribute, values in mapping.items():
+            transform_func = values['transform_func']
+            value = findattr(element=element,
+                             xpath=values['xpath'],
+                             namespace=self.namespace)
+            if value:
+                try:
+                    extra[attribute] = transform_func(value)
+                except Exception:
+                    extra[attribute] = None
+            else:
+                extra[attribute] = value
+
+        return extra
+
+    def _get_vpc_attributes(self, instance):
+        vpcs = findall(instance, xpath='VpcAttributes',
+                       namespace=self.namespace)
+        if len(vpcs) <= 0:
+            return None
+        return self._get_extra_dict(
+            vpcs[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['vpc_attributes'])
+
+    def _get_eip_address(self, instance):
+        eips = findall(instance, xpath='EipAddress',
+                       namespace=self.namespace)
+        if len(eips) <= 0:
+            return None
+        return self._get_extra_dict(
+            eips[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['eip_address_associate'])
+
+    def _get_operation_locks(self, instance):
+        locks = findall(instance, xpath='OperationLocks',
+                        namespace=self.namespace)
+        if len(locks) <= 0:
+            return None
+        return self._get_extra_dict(
+            locks[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['operation_locks'])
+
+    def _wait_until_state(self, nodes, state, wait_period=3, timeout=600):
+        """
+        Block until the provided nodes are in the desired state.
+        :param nodes: List of nodes to wait for
+        :type nodes: ``list`` of :class:`.Node`
+        :param state: desired state
+        :type state: ``NodeState``
+        :param wait_period: How many seconds to wait between each loop
+                            iteration. (default is 3)
+        :type wait_period: ``int``
+        :param timeout: How many seconds to wait before giving up.
+                        (default is 600)
+        :type timeout: ``int``
+        :return: if the nodes are in the desired state.
+        :rtype: ``bool``
+        """
+        start = time.time()
+        end = start + timeout
+        node_ids = [node.id for node in nodes]
+
+        while(time.time() < end):
+            matched_nodes = self.list_nodes(ex_node_ids=node_ids)
+            if len(matched_nodes) > len(node_ids):
+                found_ids = [node.id for node in matched_nodes]
+                msg = ('found multiple nodes with same ids, '
+                       'desired ids: %(ids)s, found ids: %(found_ids)s' %
+                       {'ids': node_ids, 'found_ids': found_ids})
+                raise LibcloudError(value=msg, driver=self)
+            desired_nodes = [node for node in matched_nodes
+                             if node.state == state]
+
+            if len(desired_nodes) == len(node_ids):
+                return True
+            else:
+                time.sleep(wait_period)
+                continue
+
+        raise LibcloudError(value='Timed out after %s seconds' % (timeout),
+                            driver=self)
+
+    def _to_volume(self, element):
+        _id = findtext(element, 'DiskId', namespace=self.namespace)
+        name = findtext(element, 'DiskName', namespace=self.namespace)
+        size = int(findtext(element, 'Size', namespace=self.namespace))
+        status_str = findtext(element, 'Status', namespace=self.namespace)
+        status = self.VOLUME_STATE_MAPPING.get(status_str,
+                                               StorageVolumeState.UNKNOWN)
+
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['volume'])
+        extra['operation_locks'] = self._get_operation_locks(element)
+        return StorageVolume(_id, name, size, self, state=status, extra=extra)
+
+    def _list_to_json_array(self, value):
+        try:
+            return json.dumps(value)
+        except Exception:
+            raise AttributeError('could not convert list to json array')
+
+    def _to_snapshot(self, element):
+        _id = findtext(element, 'SnapshotId', namespace=self.namespace)
+        created = findtext(element, 'CreationTime', namespace=self.namespace)
+        status_str = findtext(element, 'Status', namespace=self.namespace)
+        state = self.SNAPSHOT_STATE_MAPPING.get(status_str,
+                                                VolumeSnapshotState.UNKNOWN)
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['snapshot'])
+        return VolumeSnapshot(id=_id, driver=self, extra=extra,
+                              created=created, state=state)
+
+    def _to_size(self, element):
+        _id = findtext(element, 'InstanceTypeId', namespace=self.namespace)
+        ram = float(findtext(element, 'MemorySize', namespace=self.namespace))
+        extra = {}
+        extra['cpu_core_count'] = int(findtext(element, 'CpuCoreCount',
+                                               namespace=self.namespace))
+        extra['instance_type_family'] = findtext(element, 'InstanceTypeFamily',
+                                                 namespace=self.namespace)
+        return NodeSize(id=_id, name=_id, ram=ram, disk=None, bandwidth=None,
+                        price=None, driver=self, extra=extra)
+
+    def _to_location(self, element):
+        _id = findtext(element, 'RegionId', namespace=self.namespace)
+        localname = findtext(element, 'LocalName', namespace=self.namespace)
+        return NodeLocation(id=_id, name=localname, country=None, driver=self)
+
+    def _to_image(self, element):
+        _id = findtext(element, 'ImageId', namespace=self.namespace)
+        name = findtext(element, 'ImageName', namespace=self.namespace)
+        extra = self._get_extra_dict(element,
+                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['image'])
+        extra['disk_device_mappings'] = self._get_disk_device_mappings(
+            element.find('DiskDeviceMappings'))
+        return NodeImage(id=_id, name=name, driver=self, extra=extra)
+
+    def _get_disk_device_mappings(self, element):
+        if element is None:
+            return None
+        mapping_element = element.find('DiskDeviceMapping')
+        if mapping_element is not None:
+            return self._get_extra_dict(
+                mapping_element,
+                RESOURCE_EXTRA_ATTRIBUTES_MAP['disk_device_mapping'])
+        return None
+
+    def _to_security_group(self, element):
+        _id = findtext(element, 'SecurityGroupId', namespace=self.namespace)
+        name = findtext(element, 'SecurityGroupName',
+                        namespace=self.namespace)
+        description = findtext(element, 'Description',
+                               namespace=self.namespace)
+        vpc_id = findtext(element, 'VpcId', namespace=self.namespace)
+        creation_time = findtext(element, 'CreationTime',
+                                 namespace=self.namespace)
+        return ECSSecurityGroup(_id, name, description=description,
+                                driver=self, vpc_id=vpc_id,
+                                creation_time=creation_time)
+
+    def _to_zone(self, element):
+        _id = findtext(element, 'ZoneId', namespace=self.namespace)
+        local_name = findtext(element, 'LocalName', namespace=self.namespace)
+        resource_types = findall(element,
+                                 'AvailableResourceCreation/ResourceTypes',
+                                 namespace=self.namespace)
+        instance_types = findall(element,
+                                 'AvailableInstanceTypes/InstanceTypes',
+                                 namespace=self.namespace)
+        disk_categories = findall(element,
+                                  'AvailableDiskCategories/DiskCategories',
+                                  namespace=self.namespace)
+
+        def _text(element):
+            return element.text
+
+        return ECSZone(id=_id, name=local_name, driver=self,
+                       available_resource_types=list(
+                           map(_text, resource_types)),
+                       available_instance_types=list(
+                           map(_text, instance_types)),
+                       available_disk_categories=list(
+                           map(_text, disk_categories)))
+
+    def _get_pagination(self, element):
+        page_number = int(findtext(element, 'PageNumber'))
+        total_count = int(findtext(element, 'TotalCount'))
+        page_size = int(findtext(element, 'PageSize'))
+        return Pagination(total=total_count, size=page_size,
+                          current=page_number)
+
+    def _request_multiple_pages(self, path, params, parse_func):
+        """
+        Request all resources by multiple pages.
+        :param path: the resource path
+        :type path: ``str``
+        :param params: the query parameters
+        :type params: ``dict``
+        :param parse_func: the function object to parse the response body
+        :param type: ``function``
+        :return: list of resource object, if not found any, return []
+        :rtype: ``list``
+        """
+        results = []
+        while True:
+            one_page = self.connection.request(path, params).object
+            resources = parse_func(one_page)
+            results += resources
+            pagination = self._get_pagination(one_page)
+            if pagination.next() is None:
+                break
+            params.update(pagination.to_dict())
+        return results
+
+
+def _parse_bool(value):
+    if isinstance(value, bool):
+        return value
+    if u(value).lower() == 'true':
+        return True
+    return False
+
+
+"""
+Define the extra dictionary for specific resources
+"""
+RESOURCE_EXTRA_ATTRIBUTES_MAP = {
+    'node': {
+        'description': {
+            'xpath': 'Description',
+            'transform_func': u
+        },
+        'image_id': {
+            'xpath': 'ImageId',
+            'transform_func': u
+        },
+        'zone_id': {
+            'xpath': 'ZoneId',
+            'transform_func': u
+        },
+        'instance_type': {
+            'xpath': 'InstanceType',
+            'transform_func': u
+        },
+        'instance_type_family': {
+            'xpath': 'InstanceTypeFamily',
+            'transform_func': u
+        },
+        'hostname': {
+            'xpath': 'HostName',
+            'transform_func': u
+        },
+        'serial_number': {
+            'xpath': 'SerialNumber',
+            'transform_func': u
+        },
+        'internet_charge_type': {
+            'xpath': 'InternetChargeType',
+            'transform_func': u
+        },
+        'creation_time': {
+            'xpath': 'CreationTime',
+            'transform_func': u
+        },
+        'instance_network_type': {
+            'xpath': 'InstanceNetworkType',
+            'transform_func': u
+        },
+        'instance_charge_type': {
+            'xpath': 'InstanceChargeType',
+            'transform_func': u
+        },
+        'device_available': {
+            'xpath': 'DeviceAvailable',
+            'transform_func': u
+        },
+        'io_optimized': {
+            'xpath': 'IoOptimized',
+            'transform_func': u
+        },
+        'expired_time': {
+            'xpath': 'ExpiredTime',
+            'transform_func': u
+        }
+    },
+    'vpc_attributes': {
+        'vpc_id': {
+            'xpath': 'VpcId',
+            'transform_func': u
+        },
+        'vswitch_id': {
+            'xpath': 'VSwitchId',
+            'transform_func': u
+        },
+        'private_ip_address': {
+            'xpath': 'PrivateIpAddress/IpAddress',
+            'transform_func': u
+        },
+        'nat_ip_address': {
+            'xpath': 'NatIpAddress',
+            'transform_func': u
+        }
+    },
+    'eip_address_associate': {
+        'allocation_id': {
+            'xpath': 'AllocationId',
+            'transform_func': u
+        },
+        'ip_address': {
+            'xpath': 'IpAddress',
+            'transform_func': u
+        },
+        'bandwidth': {
+            'xpath': 'Bandwidth',
+            'transform_func': int
+        },
+        'internet_charge_type': {
+            'xpath': 'InternetChargeType',
+            'transform_func': u
+        }
+    },
+    'operation_locks': {
+        'lock_reason': {
+            'xpath': 'LockReason',
+            'transform_func': u
+        }
+    },
+    'volume': {
+        'region_id': {
+            'xpath': 'RegionId',
+            'transform_func': u
+        },
+        'zone_id': {
+            'xpath': 'ZoneId',
+            'transform_func': u
+        },
+        'description': {
+            'xpath': 'Description',
+            'transform_func': u
+        },
+        'type': {
+            'xpath': 'Type',
+            'transform_func': u
+        },
+        'category': {
+            'xpath': 'Category',
+            'transform_func': u
+        },
+        'image_id': {
+            'xpath': 'ImageId',
+            'transform_func': u
+        },
+        'source_snapshot_id': {
+            'xpath': 'SourceSnapshotId',
+            'transform_func': u
+        },
+        'product_code': {
+            'xpath': 'ProductCode',
+            'transform_func': u
+        },
+        'portable': {
+            'xpath': 'Portable',
+            'transform_func': _parse_bool
+        },
+        'instance_id': {
+            'xpath': 'InstanceId',
+            'transform_func': u
+        },
+        'device': {
+            'xpath': 'Device',
+            'transform_func': u
+        },
+        'delete_with_instance': {
+            'xpath': 'DeleteWithInstance',
+            'transform_func': _parse_bool
+        },
+        'enable_auto_snapshot': {
+            'xpath': 'EnableAutoSnapshot',
+            'transform_func': _parse_bool
+        },
+        'creation_time': {
+            'xpath': 'CreationTime',
+            'transform_func': u
+        },
+        'attached_time': {
+            'xpath': 'AttachedTime',
+            'transform_func': u
+        },
+        'detached_time': {
+            'xpath': 'DetachedTime',
+            'transform_func': u
+        },
+        'disk_charge_type': {
+            'xpath': 'DiskChargeType',
+            'transform_func': u
+        }
+    },
+    'snapshot': {
+        'snapshot_name': {
+            'xpath': 'SnapshotName',
+            'transform_func': u
+        },
+        'description': {
+            'xpath': 'Description',
+            'transform_func': u
+        },
+        'progress': {
+            'xpath': 'Progress',
+            'transform_func': u
+        },
+        'source_disk_id': {
+            'xpath': 'SourceDiskId',
+            'transform_func': u
+        },
+        'source_disk_size': {
+            'xpath': 'SourceDiskSize',
+            'transform_func': int
+        },
+        'source_disk_type': {
+            'xpath': 'SourceDiskType',
+            'transform_func': u
+        },
+        'product_code': {
+            'xpath': 'ProductCode',
+            'transform_func': u
+        },
+        'usage': {
+            'xpath': 'Usage',
+            'transform_func': u
+        }
+    },
+    'image': {
+        'image_version': {
+            'xpath': 'ImageVersion',
+            'transform_func': u
+        },
+        'os_type': {
+            'xpath': 'OSType',
+            'transform_func': u
+        },
+        'platform': {
+            'xpath': 'Platform',
+            'transform_func': u
+        },
+        'architecture': {
+            'xpath': 'Architecture',
+            'transform_func': u
+        },
+        'description': {
+            'xpath': 'Description',
+            'transform_func': u
+        },
+        'size': {
+            'xpath': 'Size',
+            'transform_func': int
+        },
+        'image_owner_alias': {
+            'xpath': 'ImageOwnerAlias',
+            'transform_func': u
+        },
+        'os_name': {
+            'xpath': 'OSName',
+            'transform_func': u
+        },
+        'product_code': {
+            'xpath': 'ProductCode',
+            'transform_func': u
+        },
+        'is_subscribed': {
+            'xpath': 'IsSubscribed',
+            'transform_func': _parse_bool
+        },
+        'progress': {
+            'xpath': 'Progress',
+            'transform_func': u
+        },
+        'creation_time': {
+            'xpath': 'CreationTime',
+            'transform_func': u
+        },
+        'usage': {
+            'xpath': 'Usage',
+            'transform_func': u
+        },
+        'is_copied': {
+            'xpath': 'IsCopied',
+            'transform_func': _parse_bool
+        }
+    },
+    'disk_device_mapping': {
+        'snapshot_id': {
+            'xpath': 'SnapshotId',
+            'transform_func': u
+        },
+        'size': {
+            'xpath': 'Size',
+            'transform_func': int
+        },
+        'device': {
+            'xpath': 'Device',
+            'transform_func': u
+        },
+        'format': {
+            'xpath': 'Format',
+            'transform_func': u
+        },
+        'import_oss_bucket': {
+            'xpath': 'ImportOSSBucket',
+            'transform_func': u
+        },
+        'import_oss_object': {
+            'xpath': 'ImportOSSObject',
+            'transform_func': u
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index 2d4d5c6..9ac944c 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -173,6 +173,8 @@ DRIVERS = {
     ('libcloud.compute.drivers.ciscoccs', 'CiscoCCSNodeDriver'),
     Provider.NTTA:
     ('libcloud.compute.drivers.ntta', 'NTTAmericaNodeDriver'),
+    Provider.ECS:
+    ('libcloud.compute.drivers.ecs', 'ECSDriver'),
 
     # Deprecated
     Provider.CLOUDSIGMA_US:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 32b640c..34884e0 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -100,6 +100,7 @@ class Provider(Type):
     :cvar VULTR: vultr driver.
     :cvar AZURE: Azure driver.
     :cvar AURORACOMPUTE: Aurora Compute driver.
+    :cvar ECS: Aliyun ECS driver.
     """
     AZURE = 'azure'
     DUMMY = 'dummy'
@@ -159,6 +160,7 @@ class Provider(Type):
     NTTA = 'ntta'
     MEDONE = 'medone'
     CISCOCCS = 'ciscoccs'
+    ECS = 'ecs'
 
     # OpenStack based providers
     HPCLOUD = 'hpcloud'


[03/10] libcloud git commit: [LIBCLOUD-802] Add drivers for Aliyun cloud

Posted by to...@apache.org.
http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/loadbalancer/drivers/slb.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/drivers/slb.py b/libcloud/loadbalancer/drivers/slb.py
new file mode 100644
index 0000000..a7eab40
--- /dev/null
+++ b/libcloud/loadbalancer/drivers/slb.py
@@ -0,0 +1,754 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+__all__ = [
+    'SLBDriver'
+]
+
+import sys
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from libcloud.common.aliyun import AliyunXmlResponse, SignedAliyunConnection
+from libcloud.common.types import LibcloudError
+from libcloud.loadbalancer.types import State
+from libcloud.loadbalancer.base import Algorithm, Driver, LoadBalancer, Member
+from libcloud.utils.misc import ReprMixin
+from libcloud.utils.py3 import u
+from libcloud.utils.xml import findattr, findtext, findall
+
+
+SLB_API_VERSION = '2014-05-15'
+SLB_API_HOST = 'slb.aliyuncs.com'
+DEFAULT_SIGNATURE_VERSION = '1.0'
+
+
+STATE_MAPPINGS = {
+    'inactive': State.UNKNOWN,
+    'active': State.RUNNING,
+    'locked': State.PENDING
+}
+
+
+RESOURCE_EXTRA_ATTRIBUTES_MAP = {
+    'balancer': {
+        'create_timestamp': {
+            'xpath': 'CreateTimeStamp',
+            'transform_func': int
+        },
+        'address_type': {
+            'xpath': 'AddressType',
+            'transform_func': u
+        },
+        'region_id': {
+            'xpath': 'RegionId',
+            'transform_func': u
+        },
+        'region_id_alias': {
+            'xpath': 'RegionIdAlias',
+            'transform_func': u
+        },
+        'create_time': {
+            'xpath': 'CreateTime',
+            'transform_func': u
+        },
+        'master_zone_id': {
+            'xpath': 'MasterZoneId',
+            'transform_func': u
+        },
+        'slave_zone_id': {
+            'xpath': 'SlaveZoneId',
+            'transform_func': u
+        },
+        'network_type': {
+            'xpath': 'NetworkType',
+            'transform_func': u
+        }
+    }
+}
+
+
+SLB_SCHEDULER_TO_ALGORITHM = {
+    'wrr': Algorithm.WEIGHTED_ROUND_ROBIN,
+    'wlc': Algorithm.WEIGHTED_LEAST_CONNECTIONS
+}
+
+
+ALGORITHM_TO_SLB_SCHEDULER = {
+    Algorithm.WEIGHTED_ROUND_ROBIN: 'wrr',
+    Algorithm.WEIGHTED_LEAST_CONNECTIONS: 'wlc'
+}
+
+
+class SLBConnection(SignedAliyunConnection):
+    version = SLB_API_VERSION
+    host = SLB_API_HOST
+    responseCls = AliyunXmlResponse
+    service_name = 'slb'
+
+
+class SLBLoadBalancerAttribute(object):
+    """
+    This class used to get listeners and backend servers related to a balancer
+    listeners is a ``list`` of ``dict``, each element contains
+    'ListenerPort' and 'ListenerProtocol' keys.
+    backend_servers is a ``list`` of ``dict``, each element contains
+    'ServerId' and 'Weight' keys.
+    """
+    def __init__(self, balancer, listeners, backend_servers, extra=None):
+        self.balancer = balancer
+        self.listeners = listeners or []
+        self.backend_servers = backend_servers or []
+        self.extra = extra or {}
+
+    def is_listening(self, port):
+        for listener in self.listeners:
+            if listener.get('ListenerPort') == port:
+                return True
+        return False
+
+    def is_attached(self, member):
+        for server in self.backend_servers:
+            if server.get('Serverid') == member.id:
+                return True
+        return False
+
+    def __repr__(self):
+        return ('<SLBLoadBalancerAttribute id=%s, ports=%s, servers=%s ...>' %
+                (self.balancer.id, self.listeners, self.backend_servers))
+
+
+class SLBLoadBalancerListener(ReprMixin, object):
+    """
+    Base SLB load balancer listener class
+    """
+    _repr_attributes = ['port', 'backend_port', 'scheduler', 'bandwidth']
+    action = None
+    option_keys = []
+
+    def __init__(self, port, backend_port, algorithm, bandwidth, extra=None):
+        self.port = port
+        self.backend_port = backend_port
+        self.scheduler = ALGORITHM_TO_SLB_SCHEDULER.get(algorithm, 'wrr')
+        self.bandwidth = bandwidth
+        self.extra = extra or {}
+
+    @classmethod
+    def create(cls, port, backend_port, algorithm, bandwidth, extra=None):
+        return cls(port, backend_port, algorithm, bandwidth, extra=extra)
+
+    def get_create_params(self):
+        params = self.get_required_params()
+        options = self.get_optional_params()
+        options.update(params)
+        return options
+
+    def get_required_params(self):
+        params = {'Action': self.action,
+                  'ListenerPort': self.port,
+                  'BackendServerPort': self.backend_port,
+                  'Scheduler': self.scheduler,
+                  'Bandwidth': self.bandwidth}
+        return params
+
+    def get_optional_params(self):
+        options = {}
+        for option in self.option_keys:
+            if self.extra and option in self.extra:
+                options[option] = self.extra[option]
+        return options
+
+
+class SLBLoadBalancerHttpListener(SLBLoadBalancerListener):
+    """
+    This class represents a rule to route http request to the backends.
+    """
+    action = 'CreateLoadBalancerHTTPListener'
+    option_keys = ['XForwardedFor', 'StickySessionType', 'CookieTimeout',
+                   'Cookie', 'HealthCheckDomain', 'HealthCheckURI',
+                   'HealthCheckConnectPort', 'HealthyThreshold',
+                   'UnhealthyThreshold', 'HealthCheckTimeout',
+                   'HealthCheckInterval', 'HealthCheckHttpCode']
+
+    def __init__(self, port, backend_port, algorithm, bandwidth,
+                 sticky_session, health_check, extra=None):
+        super(SLBLoadBalancerHttpListener, self).__init__(
+            port, backend_port, algorithm, bandwidth, extra=extra)
+        self.sticky_session = sticky_session
+        self.health_check = health_check
+
+    def get_required_params(self):
+        params = super(SLBLoadBalancerHttpListener,
+                       self).get_required_params()
+        params['StickySession'] = self.sticky_session
+        params['HealthCheck'] = self.health_check
+        return params
+
+    @classmethod
+    def create(cls, port, backend_port, algorithm, bandwidth, extra={}):
+        if 'StickySession' not in extra:
+            raise AttributeError('StickySession is required')
+        if 'HealthCheck' not in extra:
+            raise AttributeError('HealthCheck is required')
+        sticky_session = extra['StickySession']
+        health_check = extra['HealthCheck']
+        return cls(port, backend_port, algorithm, bandwidth, sticky_session,
+                   health_check, extra=extra)
+
+
+class SLBLoadBalancerHttpsListener(SLBLoadBalancerListener):
+    """
+    This class represents a rule to route https request to the backends.
+    """
+    action = 'CreateLoadBalancerHTTPSListener'
+    option_keys = ['XForwardedFor', 'StickySessionType', 'CookieTimeout',
+                   'Cookie', 'HealthCheckDomain', 'HealthCheckURI',
+                   'HealthCheckConnectPort', 'HealthyThreshold',
+                   'UnhealthyThreshold', 'HealthCheckTimeout',
+                   'HealthCheckInterval', 'HealthCheckHttpCode']
+
+    def __init__(self, port, backend_port, algorithm, bandwidth,
+                 sticky_session, health_check, certificate_id, extra=None):
+        super(SLBLoadBalancerHttpsListener, self).__init__(
+            port, backend_port, algorithm, bandwidth, extra=extra)
+        self.sticky_session = sticky_session
+        self.health_check = health_check
+        self.certificate_id = certificate_id
+
+    def get_required_params(self):
+        params = super(SLBLoadBalancerHttpsListener,
+                       self).get_required_params()
+        params['StickySession'] = self.sticky_session
+        params['HealthCheck'] = self.health_check
+        params['ServerCertificateId'] = self.certificate_id
+        return params
+
+    @classmethod
+    def create(cls, port, backend_port, algorithm, bandwidth, extra={}):
+        if 'StickySession' not in extra:
+            raise AttributeError('StickySession is required')
+        if 'HealthCheck' not in extra:
+            raise AttributeError('HealthCheck is required')
+        if 'ServerCertificateId' not in extra:
+            raise AttributeError('ServerCertificateId is required')
+        sticky_session = extra['StickySession']
+        health_check = extra['HealthCheck']
+        certificate_id = extra['ServerCertificateId']
+        return cls(port, backend_port, algorithm, bandwidth, sticky_session,
+                   health_check, certificate_id, extra=extra)
+
+
+class SLBLoadBalancerTcpListener(SLBLoadBalancerListener):
+    """
+    This class represents a rule to route tcp request to the backends.
+    """
+    action = 'CreateLoadBalancerTCPListener'
+    option_keys = ['PersistenceTimeout', 'HealthCheckType',
+                   'HealthCheckDomain', 'HealthCheckURI',
+                   'HealthCheckConnectPort', 'HealthyThreshold',
+                   'UnhealthyThreshold', 'HealthCheckConnectTimeout',
+                   'HealthCheckInterval', 'HealthCheckHttpCode']
+
+
+class SLBLoadBalancerUdpListener(SLBLoadBalancerTcpListener):
+    """
+    This class represents a rule to route udp request to the backends.
+    """
+    action = 'CreateLoadBalancerUDPListener'
+    option_keys = ['PersistenceTimeout', 'HealthCheckConnectPort',
+                   'HealthyThreshold', 'UnhealthyThreshold',
+                   'HealthCheckConnectTimeout', 'HealthCheckInterval']
+
+
+class SLBServerCertificate(ReprMixin, object):
+    _repr_attributes = ['id', 'name', 'fingerprint']
+
+    def __init__(self, id, name, fingerprint):
+        self.id = id
+        self.name = name
+        self.fingerprint = fingerprint
+
+
+PROTOCOL_TO_LISTENER_MAP = {
+    'http': SLBLoadBalancerHttpListener,
+    'https': SLBLoadBalancerHttpsListener,
+    'tcp': SLBLoadBalancerTcpListener,
+    'udp': SLBLoadBalancerUdpListener
+}
+
+
+class SLBDriver(Driver):
+    """
+    Aliyun SLB load balancer driver.
+    """
+    name = 'Aliyun Server Load Balancer'
+    website = 'https://www.aliyun.com/product/slb'
+    connectionCls = SLBConnection
+    path = '/'
+    namespace = None
+
+    _VALUE_TO_ALGORITHM_MAP = SLB_SCHEDULER_TO_ALGORITHM
+
+    _ALGORITHM_TO_VALUE_MAP = ALGORITHM_TO_SLB_SCHEDULER
+
+    def __init__(self, access_id, secret, region):
+        super(SLBDriver, self).__init__(access_id, secret)
+        self.region = region
+
+    def list_protocols(self):
+        return list(PROTOCOL_TO_LISTENER_MAP.keys())
+
+    def list_balancers(self, ex_balancer_ids=[], ex_filters=None):
+        """
+        List all loadbalancers
+        @inherits :class:`Driver.list_balancers`
+        :keyword ex_balancer_ids: a list of balancer ids to filter results
+                                  Only balancers which's id in this list
+                                  will be returned
+        :type ex_balancer_ids: ``list``
+        :keyword ex_filters: attributes to filter results. Only balancers
+                             which have all the desired attributes
+                             and values will be returned
+        :type ex_filters: ``dict``
+        """
+        params = {'Action': 'DescribeLoadBalancers',
+                  'RegionId': self.region}
+        if ex_balancer_ids and isinstance(ex_balancer_ids, list):
+            params['LoadBalancerId'] = ','.join(ex_balancer_ids)
+
+        if ex_filters and isinstance(ex_filters, dict):
+            ex_filters.update(params)
+            params = ex_filters
+        resp_body = self.connection.request(self.path, params=params).object
+        return self._to_balancers(resp_body)
+
+    def create_balancer(self, name, port, protocol, algorithm, members,
+                        **kwargs):
+        # 1.Create load balancer
+        params = {'Action': 'CreateLoadBalancer',
+                  'RegionId': self.region}
+        if name:
+            params['LoadBalancerName'] = name
+        if not port:
+            raise AttributeError('port is required')
+        if not protocol:
+            # NOTE(samsong8610): Use http listener as default
+            protocol = 'http'
+        if protocol not in PROTOCOL_TO_LISTENER_MAP:
+            raise AttributeError('unsupport protocol %s' % protocol)
+
+        extra_param_keys = [
+            'AddressType',
+            'VSwitchId',
+            'InternetChargeType',
+            'Bandwidth',
+            'ClientToken',
+            'MasterZoneId',
+            'SlaveZoneId'
+        ]
+        extra = self._get_extra_params(extra_param_keys, kwargs)
+        # Bandwidth in range [1, 1000] Mbps
+        bandwidth = -1
+        if 'Bandwidth' in extra and extra['Bandwidth']:
+            try:
+                bandwidth = int(extra['Bandwidth'])
+            except ValueError:
+                raise AttributeError('Bandwidth should be a integer in '
+                                     'range [1, 1000].')
+
+        charge_type = extra.get('InternetChargeType', None)
+        if charge_type and charge_type.lower() == 'paybybandwidth':
+            if bandwidth == -1:
+                raise AttributeError('PayByBandwidth need Bandwidth be set')
+        params.update(extra)
+
+        if members and isinstance(members, list):
+            backend_ports = [member.port for member in members]
+            if len(set(backend_ports)) != 1:
+                raise AttributeError('the ports of members should be unique')
+            # NOTE(samsong8610): If members do not provide backend port,
+            #                    default to listening port
+            backend_port = backend_ports[0] or port
+        else:
+            backend_port = port
+
+        balancer = None
+        try:
+            resp_body = self.connection.request(self.path, params).object
+            balancer = self._to_balancer(resp_body)
+            balancer.port = port
+
+            # 2.Add backend servers
+            if members is None:
+                members = []
+            for member in members:
+                self.balancer_attach_member(balancer, member)
+            # 3.Create listener
+            # NOTE(samsong8610): Assume only create a listener which uses all
+            #                    the bandwidth.
+            self.ex_create_listener(balancer, backend_port, protocol,
+                                    algorithm, bandwidth, **kwargs)
+            self.ex_start_listener(balancer, port)
+            return balancer
+        except Exception:
+            e = sys.exc_info()[1]
+            if balancer is not None:
+                try:
+                    self.destroy_balancer(balancer)
+                except Exception:
+                    pass
+            raise e
+
+    def destroy_balancer(self, balancer):
+        params = {'Action': 'DeleteLoadBalancer',
+                  'LoadBalancerId': balancer.id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def get_balancer(self, balancer_id):
+        balancers = self.list_balancers(ex_balancer_ids=[balancer_id])
+        if len(balancers) != 1:
+            raise LibcloudError('could not find load balancer with id %s' %
+                                balancer_id)
+        return balancers[0]
+
+    def balancer_attach_compute_node(self, balancer, node):
+        if len(node.public_ips) > 0:
+            ip = node.public_ips[0]
+        else:
+            ip = node.private_ips[0]
+        member = Member(id=node.id, ip=ip, port=balancer.port)
+        return self.balancer_attach_member(balancer, member)
+
+    def balancer_attach_member(self, balancer, member):
+        params = {'Action': 'AddBackendServers',
+                  'LoadBalancerId': balancer.id}
+        if member and isinstance(member, Member):
+            params['BackendServers'] = self._to_servers_json([member])
+        self.connection.request(self.path, params)
+        return member
+
+    def balancer_detach_member(self, balancer, member):
+        params = {'Action': 'RemoveBackendServers',
+                  'LoadBalancerId': balancer.id}
+        if member and isinstance(member, Member):
+            params['BackendServers'] = self._list_to_json([member.id])
+        self.connection.request(self.path, params)
+        return member
+
+    def balancer_list_members(self, balancer):
+        attribute = self.ex_get_balancer_attribute(balancer)
+        members = [Member(server['ServerId'], None, None, balancer=balancer,
+                          extra={'Weight': server['Weight']})
+                   for server in attribute.backend_servers]
+        return members
+
+    def ex_get_balancer_attribute(self, balancer):
+        """
+        Get balancer attribute
+        :param balancer: the balancer to get attribute
+        :type balancer: ``LoadBalancer``
+        :return: the balancer attribute
+        :rtype: ``SLBLoadBalancerAttribute``
+        """
+        params = {'Action': 'DescribeLoadBalancerAttribute',
+                  'LoadBalancerId': balancer.id}
+        resp_body = self.connection.request(self.path, params).object
+        attribute = self._to_balancer_attribute(resp_body)
+        return attribute
+
+    def ex_list_listeners(self, balancer):
+        """
+        Get all listener related to the given balancer
+        :param balancer: the balancer to list listeners
+        :type balancer: ``LoadBalancer``
+        :return: a list of listeners
+        :rtype: ``list`` of ``SLBLoadBalancerListener``
+        """
+        attribute = self.ex_get_balancer_attribute(balancer)
+        listeners = [SLBLoadBalancerListener(each['ListenerPort'], None,
+                                             None, None)
+                     for each in attribute.listeners]
+        return listeners
+
+    def ex_create_listener(self, balancer, backend_port, protocol, algorithm,
+                           bandwidth, **kwargs):
+        """
+        Create load balancer listening rule.
+        :param balancer: the balancer which the rule belongs to.
+                         The listener created will listen on the port of the
+                         the balancer as default. 'ListenerPort' in kwargs
+                         will *OVERRIDE* it.
+        :type balancer: ``LoadBalancer``
+        :param backend_port: the backend server port
+        :type backend_port: ``int``
+        :param protocol: the balancer protocol, default to http
+        :type protocol: ``str``
+        :param algorithm: the balancer routing algorithm
+        :type algorithm: ``Algorithm``
+        :param bandwidth: the listener bandwidth limits
+        :type bandwidth: ``str``
+        :return: the created listener
+        :rtype: ``SLBLoadBalancerListener``
+        """
+        cls = PROTOCOL_TO_LISTENER_MAP.get(protocol,
+                                           SLBLoadBalancerHttpListener)
+        if 'ListenerPort' in kwargs:
+            port = kwargs['ListenerPort']
+        else:
+            port = balancer.port
+        listener = cls.create(port, backend_port, algorithm,
+                              bandwidth, extra=kwargs)
+        params = listener.get_create_params()
+        params['LoadBalancerId'] = balancer.id
+        params['RegionId'] = self.region
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_start_listener(self, balancer, port):
+        """
+        Start balancer's listener listening the given port.
+        :param balancer: a load balancer
+        :type balancer: ``LoadBalancer``
+        :param port: listening port
+        :type port: ``int``
+        :return: whether operation is success
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StartLoadBalancerListener',
+                  'LoadBalancerId': balancer.id,
+                  'ListenerPort': port}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_stop_listener(self, balancer, port):
+        """
+        Stop balancer's listener listening the given port.
+        :param balancer: a load balancer
+        :type balancer: ``LoadBalancer``
+        :param port: listening port
+        :type port: ``int``
+        :return: whether operation is success
+        :rtype: ``bool``
+        """
+        params = {'Action': 'StopLoadBalancerListener',
+                  'LoadBalancerId': balancer.id,
+                  'ListenerPort': port}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_upload_certificate(self, name, server_certificate,
+                              private_key):
+        """
+        Upload certificate and private key for https load balancer listener
+        :param name: the certificate name
+        :type name: ``str``
+        :param server_certificate: the content of the certificate to upload
+                                   in PEM format
+        :type server_certificate: ``str``
+        :param private_key: the content of the private key to upload
+                            in PEM format
+        :type private_key: ``str``
+        :return: new created certificate info
+        :rtype: ``SLBServerCertificate``
+        """
+        params = {'Action': 'UploadServerCertificate',
+                  'RegionId': self.region,
+                  'ServerCertificate': server_certificate,
+                  'PrivateKey': private_key}
+        if name:
+            params['ServerCertificateName'] = name
+        resp_body = self.connection.request(self.path, params).object
+        return self._to_server_certificate(resp_body)
+
+    def ex_list_certificates(self, certificate_ids=[]):
+        """
+        List all server certificates
+        :param certificate_ids: certificate ids to filter results
+        :type certificate_ids: ``str``
+        :return: certificates
+        :rtype: ``SLBServerCertificate``
+        """
+        params = {'Action': 'DescribeServerCertificates',
+                  'RegionId': self.region}
+        if certificate_ids and isinstance(certificate_ids, list):
+            params['ServerCertificateId'] = ','.join(certificate_ids)
+
+        resp_body = self.connection.request(self.path, params).object
+        cert_elements = findall(resp_body,
+                                'ServerCertificates/ServerCertificate',
+                                namespace=self.namespace)
+        certificates = [self._to_server_certificate(el)
+                        for el in cert_elements]
+        return certificates
+
+    def ex_delete_certificate(self, certificate_id):
+        """
+        Delete the given server certificate
+        :param certificate_id: the id of the certificate to delete
+        :type certificate_id: ``str``
+        :return: whether process is success
+        :rtype: ``bool``
+        """
+        params = {'Action': 'DeleteServerCertificate',
+                  'RegionId': self.region,
+                  'ServerCertificateId': certificate_id}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def ex_set_certificate_name(self, certificate_id, name):
+        """
+        Set server certificate name.
+        :param certificate_id: the id of the server certificate to update
+        :type certificate_id: ``str``
+        :param name: the new name
+        :type name: ``str``
+        :return: whether updating is success
+        :rtype: ``bool``
+        """
+        params = {'Action': 'SetServerCertificateName',
+                  'RegionId': self.region,
+                  'ServerCertificateId': certificate_id,
+                  'ServerCertificateName': name}
+        resp = self.connection.request(self.path, params)
+        return resp.success()
+
+    def _to_balancers(self, element):
+        xpath = 'LoadBalancers/LoadBalancer'
+        return [self._to_balancer(el)
+                for el in findall(element=element, xpath=xpath,
+                                  namespace=self.namespace)]
+
+    def _to_balancer(self, el):
+        _id = findtext(element=el, xpath='LoadBalancerId',
+                       namespace=self.namespace)
+        name = findtext(element=el, xpath='LoadBalancerName',
+                        namespace=self.namespace)
+        status = findtext(element=el, xpath='LoadBalancerStatus',
+                          namespace=self.namespace)
+        state = STATE_MAPPINGS.get(status, State.UNKNOWN)
+        address = findtext(element=el, xpath='Address',
+                           namespace=self.namespace)
+        extra = self._get_extra_dict(
+            el, RESOURCE_EXTRA_ATTRIBUTES_MAP['balancer'])
+
+        balancer = LoadBalancer(id=_id, name=name, state=state, ip=address,
+                                port=None, driver=self, extra=extra)
+        return balancer
+
+    def _create_list_params(self, params, items, label):
+        """
+        return parameter list
+        """
+        if isinstance(items, str):
+            items = [items]
+        for index, item in enumerate(items):
+            params[label % (index + 1)] = item
+        return params
+
+    def _get_extra_dict(self, element, mapping):
+        """
+        Extract attributes from the element based on rules provided in the
+        mapping dictionary.
+
+        :param      element: Element to parse the values from.
+        :type       element: xml.etree.ElementTree.Element.
+
+        :param      mapping: Dictionary with the extra layout
+        :type       node: :class:`Node`
+
+        :rtype: ``dict``
+        """
+        extra = {}
+        for attribute, values in mapping.items():
+            transform_func = values['transform_func']
+            value = findattr(element=element,
+                             xpath=values['xpath'],
+                             namespace=self.namespace)
+            if value:
+                try:
+                    extra[attribute] = transform_func(value)
+                except Exception:
+                    extra[attribute] = None
+            else:
+                extra[attribute] = value
+
+        return extra
+
+    def _to_servers_json(self, members):
+        servers = []
+        for each in members:
+            server = {'ServerId': each.id,
+                      'Weight': '100'}
+            if 'Weight' in each.extra:
+                server['Weight'] = each.extra['Weight']
+            servers.append(server)
+        try:
+            return json.dumps(servers)
+        except Exception:
+            raise AttributeError('could not convert member to backend server')
+
+    def _to_balancer_attribute(self, element):
+        balancer = self._to_balancer(element)
+        port_proto_elements = findall(
+            element, 'ListenerPortsAndProtocol/ListenerPortAndProtocol',
+            namespace=self.namespace)
+        if len(port_proto_elements) > 0:
+            listeners = [self._to_port_and_protocol(el)
+                         for el in port_proto_elements]
+        else:
+            port_elements = findall(element, 'ListenerPorts/ListenerPort',
+                                    namespace=self.namespace)
+            listeners = [{'ListenerPort': el.text, 'ListenerProtocol': 'http'}
+                         for el in port_elements]
+        server_elements = findall(element,
+                                  'BackendServers/BackendServer',
+                                  namespace=self.namespace)
+        backend_servers = [self._to_server_and_weight(el)
+                           for el in server_elements]
+        return SLBLoadBalancerAttribute(balancer, listeners, backend_servers)
+
+    def _to_port_and_protocol(self, el):
+        port = findtext(el, 'ListenerPort', namespace=self.namespace)
+        protocol = findtext(el, 'ListenerProtocol', namespace=self.namespace)
+        return {'ListenerPort': port, 'ListenerProtocol': protocol}
+
+    def _to_server_and_weight(self, el):
+        server_id = findtext(el, 'ServerId', namespace=self.namespace)
+        weight = findtext(el, 'Weight', namespace=self.namespace)
+        return {'ServerId': server_id, 'Weight': weight}
+
+    def _to_server_certificate(self, el):
+        _id = findtext(el, 'ServerCertificateId', namespace=self.namespace)
+        name = findtext(el, 'ServerCertificateName', namespace=self.namespace)
+        fingerprint = findtext(el, 'Fingerprint', namespace=self.namespace)
+        return SLBServerCertificate(id=_id, name=name,
+                                    fingerprint=fingerprint)
+
+    def _get_extra_params(self, extra_param_keys, kwargs):
+        params = {}
+        for key in extra_param_keys:
+            if key in kwargs:
+                params[key] = kwargs[key]
+        return params
+
+    def _list_to_json(self, value):
+        try:
+            return json.dumps(value)
+        except Exception:
+            return '[]'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/loadbalancer/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/providers.py b/libcloud/loadbalancer/providers.py
index a4ff090..b619c9d 100644
--- a/libcloud/loadbalancer/providers.py
+++ b/libcloud/loadbalancer/providers.py
@@ -42,6 +42,9 @@ DRIVERS = {
     ('libcloud.loadbalancer.drivers.softlayer', 'SoftlayerLBDriver'),
     Provider.DIMENSIONDATA:
     ('libcloud.loadbalancer.drivers.dimensiondata', 'DimensionDataLBDriver'),
+    Provider.SLB:
+    ('libcloud.loadbalancer.drivers.slb', 'SLBDriver'),
+
     # Deprecated
     Provider.RACKSPACE_US:
     ('libcloud.loadbalancer.drivers.rackspace', 'RackspaceLBDriver'),

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/loadbalancer/types.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/types.py b/libcloud/loadbalancer/types.py
index c7e390b..0d5deec 100644
--- a/libcloud/loadbalancer/types.py
+++ b/libcloud/loadbalancer/types.py
@@ -32,6 +32,9 @@ class LibcloudLBImmutableError(LibcloudLBError):
 
 
 class Provider(object):
+    """
+    :cvar SLB: Aliyun SLB loadbalancer driver
+    """
     RACKSPACE = 'rackspace'
     GOGRID = 'gogrid'
     NINEFOLD = 'ninefold'
@@ -41,6 +44,7 @@ class Provider(object):
     GCE = 'gce'
     SOFTLAYER = 'softlayer'
     DIMENSIONDATA = 'dimensiondata'
+    SLB = 'slb'
 
     # Deprecated
     RACKSPACE_US = 'rackspace_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/storage/drivers/oss.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/drivers/oss.py b/libcloud/storage/drivers/oss.py
new file mode 100644
index 0000000..84df44f
--- /dev/null
+++ b/libcloud/storage/drivers/oss.py
@@ -0,0 +1,1069 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 base64
+import codecs
+import hmac
+import os
+import time
+import sys
+from hashlib import sha1
+
+try:
+    from lxml import etree as ET
+except ImportError:
+    from xml.etree import ElementTree as ET
+
+try:
+    from lxml.etree import Element, SubElement
+except ImportError:
+    from xml.etree.ElementTree import Element, SubElement
+
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import urlquote
+from libcloud.utils.py3 import urlencode
+from libcloud.utils.py3 import b
+from libcloud.utils.py3 import tostring
+from libcloud.utils.py3 import PY3
+from libcloud.utils.xml import fixxpath, findtext
+from libcloud.utils.files import guess_file_mime_type, read_in_chunks, \
+    exhaust_iterator
+from libcloud.common.types import InvalidCredsError, LibcloudError
+from libcloud.common.base import ConnectionUserAndKey, RawResponse, \
+    XmlResponse
+from libcloud.common.types import MalformedResponseError
+from libcloud.storage.base import Object, Container, StorageDriver, \
+    DEFAULT_CONTENT_TYPE
+from libcloud.storage.types import ContainerError
+from libcloud.storage.types import ContainerIsNotEmptyError
+from libcloud.storage.types import InvalidContainerNameError
+from libcloud.storage.types import ContainerDoesNotExistError
+from libcloud.storage.types import ObjectDoesNotExistError
+from libcloud.storage.types import ObjectHashMismatchError
+
+__all__ = [
+    'OSSStorageDriver',
+    'OSSMultipartUpload',
+
+    'EXPIRATION_SECONDS',
+    'CHUNK_SIZE',
+    'MAX_UPLOADS_PER_RESPONSE'
+]
+
+GMT_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
+EXPIRATION_SECONDS = 15 * 60
+
+# OSS multi-part chunks must be great than 100KB except the last one
+CHUNK_SIZE = 100 * 1024
+
+# Desired number of items in each response inside a paginated request in
+# ex_iterate_multipart_uploads.
+MAX_UPLOADS_PER_RESPONSE = 1000
+
+
+class OSSResponse(XmlResponse):
+    namespace = None
+    valid_response_codes = [httplib.NOT_FOUND, httplib.CONFLICT,
+                            httplib.BAD_REQUEST]
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299 or i in self.valid_response_codes
+
+    def parse_body(self):
+        """
+        OSSResponse body is in utf-8 encoding.
+        """
+        if len(self.body) == 0 and not self.parse_zero_length_body:
+            return self.body
+
+        try:
+            if PY3:
+                parser = ET.XMLParser(encoding='utf-8')
+                body = ET.XML(self.body.encode('utf-8'), parser=parser)
+            else:
+                body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError('Failed to parse XML',
+                                         body=self.body,
+                                         driver=self.connection.driver)
+        return body
+
+    def parse_error(self):
+        if self.status in [httplib.UNAUTHORIZED, httplib.FORBIDDEN]:
+            raise InvalidCredsError(self.body)
+        elif self.status == httplib.MOVED_PERMANENTLY:
+            raise LibcloudError('This bucket is located in a different ' +
+                                'region. Please use the correct driver.',
+                                driver=OSSStorageDriver)
+        elif self.status == httplib.METHOD_NOT_ALLOWED:
+            raise LibcloudError('The method is not allowed. Status code: %d, '
+                                'headers: %s' % (self.status, self.headers))
+        raise LibcloudError('Unknown error. Status code: %d, body: %s' %
+                            (self.status, self.body),
+                            driver=OSSStorageDriver)
+
+
+class OSSRawResponse(OSSResponse, RawResponse):
+    pass
+
+
+class OSSConnection(ConnectionUserAndKey):
+    """
+    Represents a single connection to the Aliyun OSS Endpoint
+    """
+
+    _domain = 'aliyuncs.com'
+    _default_location = 'oss'
+    responseCls = OSSResponse
+    rawResponseCls = OSSRawResponse
+
+    @staticmethod
+    def _get_auth_signature(method, headers, params, expires, secret_key, path,
+                            vendor_prefix):
+        """
+        Signature = base64(hmac-sha1(AccessKeySecret,
+          VERB + "\n"
+          + CONTENT-MD5 + "\n"
+          + CONTENT-TYPE + "\n"
+          + EXPIRES + "\n"
+          + CanonicalizedOSSHeaders
+          + CanonicalizedResource))
+        """
+        special_headers = {'content-md5': '',
+                           'content-type': '',
+                           'expires': ''}
+        vendor_headers = {}
+
+        for key, value in list(headers.items()):
+            key_lower = key.lower()
+            if key_lower in special_headers:
+                special_headers[key_lower] = value.strip()
+            elif key_lower.startswith(vendor_prefix):
+                vendor_headers[key_lower] = value.strip()
+
+        if expires:
+            special_headers['expires'] = str(expires)
+
+        buf = [method]
+        for _, value in sorted(special_headers.items()):
+            buf.append(value)
+        string_to_sign = '\n'.join(buf)
+
+        buf = []
+        for key, value in sorted(vendor_headers.items()):
+            buf.append('%s:%s' % (key, value))
+        header_string = '\n'.join(buf)
+
+        values_to_sign = []
+        for value in [string_to_sign, header_string, path]:
+            if value:
+                values_to_sign.append(value)
+
+        string_to_sign = '\n'.join(values_to_sign)
+        b64_hmac = base64.b64encode(
+            hmac.new(b(secret_key), b(string_to_sign), digestmod=sha1).digest()
+        )
+        return b64_hmac
+
+    @staticmethod
+    def _get_expires(params):
+        """
+        Get expires timeout seconds from parameters.
+        """
+        expires = None
+        if 'expires' in params:
+            expires = params['expires']
+        elif 'Expires' in params:
+            expires = params['Expires']
+        if expires:
+            try:
+                return int(expires)
+            except Exception:
+                pass
+        return int(time.time()) + EXPIRATION_SECONDS
+
+    def add_default_params(self, params):
+        expires_at = self._get_expires(params)
+        expires = str(expires_at)
+        params['OSSAccessKeyId'] = self.user_id
+        params['Expires'] = expires
+        return params
+
+    def add_default_headers(self, headers):
+        headers['Date'] = time.strftime(GMT_TIME_FORMAT, time.gmtime())
+        return headers
+
+    def pre_connect_hook(self, params, headers):
+        if self._container:
+            path = '/%s%s' % (self._container.name, self.action)
+        else:
+            path = self.action
+        params['Signature'] = self._get_auth_signature(
+            method=self.method, headers=headers, params=params,
+            expires=params['Expires'], secret_key=self.key, path=path,
+            vendor_prefix=self.driver.http_vendor_prefix)
+        return params, headers
+
+    def request(self, action, params=None, data=None, headers=None,
+                method='GET', raw=False, container=None):
+        self.host = '%s.%s' % (self._default_location, self._domain)
+        self._container = container
+        if container and container.name:
+            if 'location' in container.extra:
+                self.host = '%s.%s.%s' % (container.name,
+                                          container.extra['location'],
+                                          self._domain)
+            else:
+                self.host = '%s.%s' % (container.name, self.host)
+        return super(OSSConnection, self).request(action=action,
+                                                  params=params,
+                                                  data=data,
+                                                  headers=headers,
+                                                  method=method,
+                                                  raw=raw)
+
+
+class OSSMultipartUpload(object):
+    """
+    Class representing an Aliyun OSS multipart upload
+    """
+
+    def __init__(self, key, id, initiated):
+        """
+        Class representing an Aliyun OSS multipart upload
+
+        :param key: The object/key that was being uploaded
+        :type key: ``str``
+
+        :param id: The upload id assigned by Aliyun
+        :type id: ``str``
+
+        :param initiated: The date/time at which the upload was started
+        :type created_at: ``str``
+        """
+        self.key = key
+        self.id = id
+        self.initiated = initiated
+
+    def __repr__(self):
+        return ('<OSSMultipartUpload: key=%s>' % (self.key))
+
+
+class OSSStorageDriver(StorageDriver):
+    name = 'Aliyun OSS'
+    website = 'http://www.aliyun.com/product/oss'
+    connectionCls = OSSConnection
+    hash_type = 'md5'
+    supports_chunked_encoding = False
+    supports_multipart_upload = True
+    namespace = None
+    http_vendor_prefix = 'x-oss-'
+
+    def iterate_containers(self):
+        response = self.connection.request('/')
+        if response.status == httplib.OK:
+            containers = self._to_containers(obj=response.object,
+                                             xpath='Buckets/Bucket')
+            return containers
+
+        raise LibcloudError('Unexpected status code: %s' % (response.status),
+                            driver=self)
+
+    def list_container_objects(self, container, ex_prefix=None):
+        """
+        Return a list of objects for the given container.
+
+        :param container: Container instance.
+        :type container: :class:`Container`
+
+        :keyword ex_prefix: Only return objects starting with ex_prefix
+        :type ex_prefix: ``str``
+
+        :return: A list of Object instances.
+        :rtype: ``list`` of :class:`Object`
+        """
+        return list(self.iterate_container_objects(container,
+                    ex_prefix=ex_prefix))
+
+    def iterate_container_objects(self, container, ex_prefix=None):
+        """
+        Return a generator of objects for the given container.
+
+        :param container: Container instance
+        :type container: :class:`Container`
+
+        :keyword ex_prefix: Only return objects starting with ex_prefix
+        :type ex_prefix: ``str``
+
+        :return: A generator of Object instances.
+        :rtype: ``generator`` of :class:`Object`
+        """
+        params = {}
+        if ex_prefix:
+            params['prefix'] = ex_prefix
+
+        last_key = None
+        exhausted = False
+
+        while not exhausted:
+            if last_key:
+                params['marker'] = last_key
+
+            response = self.connection.request('/',
+                                               params=params,
+                                               container=container)
+
+            if response.status != httplib.OK:
+                raise LibcloudError('Unexpected status code: %s' %
+                                    (response.status), driver=self)
+
+            objects = self._to_objs(obj=response.object,
+                                    xpath='Contents', container=container)
+            is_truncated = response.object.findtext(fixxpath(
+                xpath='IsTruncated', namespace=self.namespace)).lower()
+            exhausted = (is_truncated == 'false')
+
+            last_key = None
+            for obj in objects:
+                last_key = obj.name
+                yield obj
+
+    def get_container(self, container_name):
+        for container in self.iterate_containers():
+            if container.name == container_name:
+                return container
+        raise ContainerDoesNotExistError(value=None,
+                                         driver=self,
+                                         container_name=container_name)
+
+    def get_object(self, container_name, object_name):
+        container = self.get_container(container_name=container_name)
+        object_path = self._get_object_path(container, object_name)
+        response = self.connection.request(object_path,
+                                           method='HEAD',
+                                           container=container)
+
+        if response.status == httplib.OK:
+            obj = self._headers_to_object(object_name=object_name,
+                                          container=container,
+                                          headers=response.headers)
+            return obj
+
+        raise ObjectDoesNotExistError(value=None, driver=self,
+                                      object_name=object_name)
+
+    def create_container(self, container_name, ex_location=None):
+        """
+        @inherits :class:`StorageDriver.create_container`
+
+        :keyword ex_location: The desired location where to create container
+        :type keyword: ``str``
+        """
+        extra = None
+        if ex_location:
+            root = Element('CreateBucketConfiguration')
+            child = SubElement(root, 'LocationConstraint')
+            child.text = ex_location
+
+            data = tostring(root)
+            extra = {'location': ex_location}
+        else:
+            data = ''
+
+        container = Container(name=container_name, extra=extra, driver=self)
+        response = self.connection.request('/',
+                                           data=data,
+                                           method='PUT',
+                                           container=container)
+
+        if response.status == httplib.OK:
+            return container
+        elif response.status == httplib.CONFLICT:
+            raise InvalidContainerNameError(
+                value='Container with this name already exists. The name must '
+                      'be unique among all the containers in the system',
+                container_name=container_name, driver=self)
+        elif response.status == httplib.BAD_REQUEST:
+            raise ContainerError(
+                value='Bad request when creating container: %s' %
+                      response.body,
+                container_name=container_name, driver=self)
+
+        raise LibcloudError('Unexpected status code: %s' % (response.status),
+                            driver=self)
+
+    def delete_container(self, container):
+        # Note: All the objects in the container must be deleted first
+        response = self.connection.request('/',
+                                           method='DELETE',
+                                           container=container)
+        if response.status == httplib.NO_CONTENT:
+            return True
+        elif response.status == httplib.CONFLICT:
+            raise ContainerIsNotEmptyError(
+                value='Container must be empty before it can be deleted.',
+                container_name=container.name, driver=self)
+        elif response.status == httplib.NOT_FOUND:
+            raise ContainerDoesNotExistError(value=None,
+                                             driver=self,
+                                             container_name=container.name)
+
+        return False
+
+    def download_object(self, obj, destination_path, overwrite_existing=False,
+                        delete_on_failure=True):
+        obj_path = self._get_object_path(obj.container, obj.name)
+
+        response = self.connection.request(obj_path,
+                                           method='GET',
+                                           raw=True,
+                                           container=obj.container)
+
+        return self._get_object(obj=obj, callback=self._save_object,
+                                response=response,
+                                callback_kwargs={
+                                    'obj': obj,
+                                    'response': response.response,
+                                    'destination_path': destination_path,
+                                    'overwrite_existing': overwrite_existing,
+                                    'delete_on_failure': delete_on_failure},
+                                success_status_code=httplib.OK)
+
+    def download_object_as_stream(self, obj, chunk_size=None):
+        obj_path = self._get_object_path(obj.container, obj.name)
+        response = self.connection.request(obj_path,
+                                           method='GET',
+                                           raw=True,
+                                           container=obj.container)
+
+        return self._get_object(obj=obj, callback=read_in_chunks,
+                                response=response,
+                                callback_kwargs={'iterator': response.response,
+                                                 'chunk_size': chunk_size},
+                                success_status_code=httplib.OK)
+
+    def upload_object(self, file_path, container, object_name, extra=None,
+                      verify_hash=True, headers=None):
+        upload_func = self._upload_file
+        upload_func_kwargs = {'file_path': file_path}
+
+        return self._put_object(container=container, object_name=object_name,
+                                upload_func=upload_func,
+                                upload_func_kwargs=upload_func_kwargs,
+                                extra=extra, file_path=file_path,
+                                verify_hash=verify_hash)
+
+    def upload_object_via_stream(self, iterator, container, object_name,
+                                 extra=None, headers=None):
+        method = 'PUT'
+        params = None
+
+        if self.supports_multipart_upload:
+            # Initiate the multipart request and get an upload id
+            upload_func = self._upload_multipart
+            upload_func_kwargs = {'iterator': iterator,
+                                  'container': container,
+                                  'object_name': object_name}
+            method = 'POST'
+            iterator = iter('')
+            params = 'uploads'
+
+        elif self.supports_chunked_encoding:
+            upload_func = self._stream_data
+            upload_func_kwargs = {'iterator': iterator}
+        else:
+            # In this case, we have to load the entire object to
+            # memory and send it as normal data
+            upload_func = self._upload_data
+            upload_func_kwargs = {}
+
+        return self._put_object(container=container, object_name=object_name,
+                                upload_func=upload_func,
+                                upload_func_kwargs=upload_func_kwargs,
+                                extra=extra, method=method, query_args=params,
+                                iterator=iterator, verify_hash=False)
+
+    def delete_object(self, obj):
+        object_path = self._get_object_path(obj.container, obj.name)
+        response = self.connection.request(object_path, method='DELETE',
+                                           container=obj.container)
+        if response.status == httplib.NO_CONTENT:
+            return True
+        elif response.status == httplib.NOT_FOUND:
+            raise ObjectDoesNotExistError(value=None, driver=self,
+                                          object_name=obj.name)
+
+        return False
+
+    def ex_iterate_multipart_uploads(self, container, prefix=None,
+                                     delimiter=None,
+                                     max_uploads=MAX_UPLOADS_PER_RESPONSE):
+        """
+        Extension method for listing all in-progress OSS multipart uploads.
+
+        Each multipart upload which has not been committed or aborted is
+        considered in-progress.
+
+        :param container: The container holding the uploads
+        :type container: :class:`Container`
+
+        :keyword prefix: Print only uploads of objects with this prefix
+        :type prefix: ``str``
+
+        :keyword delimiter: The object/key names are grouped based on
+            being split by this delimiter
+        :type delimiter: ``str``
+
+        :keyword max_uploads: The max uplod items returned for one request
+        :type max_uploads: ``int``
+
+        :return: A generator of OSSMultipartUpload instances.
+        :rtype: ``generator`` of :class:`OSSMultipartUpload`
+        """
+
+        if not self.supports_multipart_upload:
+            raise LibcloudError('Feature not supported', driver=self)
+
+        request_path = '/?uploads'
+        params = {'max-uploads': max_uploads}
+
+        if prefix:
+            params['prefix'] = prefix
+
+        if delimiter:
+            params['delimiter'] = delimiter
+
+        def finder(node, text):
+            return node.findtext(fixxpath(xpath=text,
+                                          namespace=self.namespace))
+
+        while True:
+            response = self.connection.request(request_path, params=params,
+                                               container=container)
+
+            if response.status != httplib.OK:
+                raise LibcloudError('Error fetching multipart uploads. '
+                                    'Got code: %s' % response.status,
+                                    driver=self)
+
+            body = response.parse_body()
+            # pylint: disable=maybe-no-member
+            for node in body.findall(fixxpath(xpath='Upload',
+                                              namespace=self.namespace)):
+
+                key = finder(node, 'Key')
+                upload_id = finder(node, 'UploadId')
+                initiated = finder(node, 'Initiated')
+
+                yield OSSMultipartUpload(key, upload_id, initiated)
+
+            # Check if this is the last entry in the listing
+            # pylint: disable=maybe-no-member
+            is_truncated = body.findtext(fixxpath(xpath='IsTruncated',
+                                                  namespace=self.namespace))
+
+            if is_truncated.lower() == 'false':
+                break
+
+            # Provide params for the next request
+            upload_marker = body.findtext(fixxpath(xpath='NextUploadIdMarker',
+                                                   namespace=self.namespace))
+            key_marker = body.findtext(fixxpath(xpath='NextKeyMarker',
+                                                namespace=self.namespace))
+
+            params['key-marker'] = key_marker
+            params['upload-id-marker'] = upload_marker
+
+    def ex_abort_all_multipart_uploads(self, container, prefix=None):
+        """
+        Extension method for removing all partially completed OSS multipart
+        uploads.
+
+        :param container: The container holding the uploads
+        :type container: :class:`Container`
+
+        :keyword prefix: Delete only uploads of objects with this prefix
+        :type prefix: ``str``
+        """
+
+        # Iterate through the container and delete the upload ids
+        for upload in self.ex_iterate_multipart_uploads(container, prefix,
+                                                        delimiter=None):
+            object_path = self._get_object_path(container, upload.key)
+            self._abort_multipart(object_path, upload.id, container=container)
+
+    def _clean_object_name(self, name):
+        name = urlquote(name)
+        return name
+
+    def _put_object(self, container, object_name, upload_func,
+                    upload_func_kwargs, method='PUT', query_args=None,
+                    extra=None, file_path=None, iterator=None,
+                    verify_hash=False):
+        """
+        Create an object and upload data using the given function.
+        """
+        headers = {}
+        extra = extra or {}
+
+        content_type = extra.get('content_type', None)
+        meta_data = extra.get('meta_data', None)
+        acl = extra.get('acl', None)
+
+        if meta_data:
+            for key, value in list(meta_data.items()):
+                key = self.http_vendor_prefix + 'meta-%s' % (key)
+                headers[key] = value
+
+        if acl:
+            if acl not in ['public-read', 'private', 'public-read-write']:
+                raise AttributeError('invalid acl value: %s' % acl)
+            headers[self.http_vendor_prefix + 'object-acl'] = acl
+
+        request_path = self._get_object_path(container, object_name)
+
+        if query_args:
+            request_path = '?'.join((request_path, query_args))
+
+        # TODO: Let the underlying exceptions bubble up and capture the SIGPIPE
+        # here.
+        # SIGPIPE is thrown if the provided container does not exist or the
+        # user does not have correct permission
+        result_dict = self._upload_object(
+            object_name=object_name, content_type=content_type,
+            upload_func=upload_func, upload_func_kwargs=upload_func_kwargs,
+            request_path=request_path, request_method=method,
+            headers=headers, file_path=file_path, iterator=iterator,
+            container=container)
+
+        response = result_dict['response']
+        bytes_transferred = result_dict['bytes_transferred']
+        headers = response.headers
+        response = response.response
+        server_hash = headers['etag'].replace('"', '')
+
+        if (verify_hash and result_dict['data_hash'].upper() != server_hash):
+            raise ObjectHashMismatchError(
+                value='MD5 hash checksum does not match',
+                object_name=object_name, driver=self)
+        elif response.status == httplib.OK:
+            obj = Object(
+                name=object_name, size=bytes_transferred, hash=server_hash,
+                extra={'acl': acl}, meta_data=meta_data, container=container,
+                driver=self)
+
+            return obj
+        else:
+            raise LibcloudError(
+                'Unexpected status code, status_code=%s' % (response.status),
+                driver=self)
+
+    def _upload_multipart(self, response, data, iterator, container,
+                          object_name, calculate_hash=True):
+        """
+        Callback invoked for uploading data to OSS using Aliyun's
+        multipart upload mechanism
+
+        :param response: Response object from the initial POST request
+        :type response: :class:`OSSRawResponse`
+
+        :param data: Any data from the initial POST request
+        :type data: ``str``
+
+        :param iterator: The generator for fetching the upload data
+        :type iterator: ``generator``
+
+        :param container: The container owning the object to which data is
+            being uploaded
+        :type container: :class:`Container`
+
+        :param object_name: The name of the object to which we are uploading
+        :type object_name: ``str``
+
+        :keyword calculate_hash: Indicates if we must calculate the data hash
+        :type calculate_hash: ``bool``
+
+        :return: A tuple of (status, checksum, bytes transferred)
+        :rtype: ``tuple``
+        """
+
+        object_path = self._get_object_path(container, object_name)
+
+        # Get the upload id from the response xml
+        response.body = response.response.read()
+        body = response.parse_body()
+        upload_id = body.find(fixxpath(xpath='UploadId',
+                                       namespace=self.namespace)).text
+
+        try:
+            # Upload the data through the iterator
+            result = self._upload_from_iterator(iterator, object_path,
+                                                upload_id, calculate_hash,
+                                                container=container)
+            (chunks, data_hash, bytes_transferred) = result
+
+            # Commit the chunk info and complete the upload
+            etag = self._commit_multipart(object_path, upload_id, chunks,
+                                          container=container)
+        except Exception:
+            exc = sys.exc_info()[1]
+            # Amazon provides a mechanism for aborting an upload.
+            self._abort_multipart(object_path, upload_id, container=container)
+            raise exc
+
+        # Modify the response header of the first request. This is used
+        # by other functions once the callback is done
+        response.headers['etag'] = etag
+
+        return (True, data_hash, bytes_transferred)
+
+    def _upload_from_iterator(self, iterator, object_path, upload_id,
+                              calculate_hash=True, container=None):
+        """
+        Uploads data from an interator in fixed sized chunks to OSS
+
+        :param iterator: The generator for fetching the upload data
+        :type iterator: ``generator``
+
+        :param object_path: The path of the object to which we are uploading
+        :type object_name: ``str``
+
+        :param upload_id: The upload id allocated for this multipart upload
+        :type upload_id: ``str``
+
+        :keyword calculate_hash: Indicates if we must calculate the data hash
+        :type calculate_hash: ``bool``
+
+        :keyword container: the container object to upload object to
+        :type container: :class:`Container`
+
+        :return: A tuple of (chunk info, checksum, bytes transferred)
+        :rtype: ``tuple``
+        """
+
+        data_hash = None
+        if calculate_hash:
+            data_hash = self._get_hash_function()
+
+        bytes_transferred = 0
+        count = 1
+        chunks = []
+        params = {'uploadId': upload_id}
+
+        # Read the input data in chunk sizes suitable for AWS
+        for data in read_in_chunks(iterator, chunk_size=CHUNK_SIZE,
+                                   fill_size=True, yield_empty=True):
+            bytes_transferred += len(data)
+
+            if calculate_hash:
+                data_hash.update(data)
+
+            chunk_hash = self._get_hash_function()
+            chunk_hash.update(data)
+            chunk_hash = base64.b64encode(chunk_hash.digest()).decode('utf-8')
+
+            # OSS will calculate hash of the uploaded data and
+            # check this header.
+            headers = {'Content-MD5': chunk_hash}
+            params['partNumber'] = count
+
+            request_path = '?'.join((object_path, urlencode(params)))
+
+            resp = self.connection.request(request_path, method='PUT',
+                                           data=data, headers=headers,
+                                           container=container)
+
+            if resp.status != httplib.OK:
+                raise LibcloudError('Error uploading chunk', driver=self)
+
+            server_hash = resp.headers['etag']
+
+            # Keep this data for a later commit
+            chunks.append((count, server_hash))
+            count += 1
+
+        if calculate_hash:
+            data_hash = data_hash.hexdigest()
+
+        return (chunks, data_hash, bytes_transferred)
+
+    def _commit_multipart(self, object_path, upload_id, chunks,
+                          container=None):
+        """
+        Makes a final commit of the data.
+
+        :param object_path: Server side object path.
+        :type object_path: ``str``
+
+        :param upload_id: ID of the multipart upload.
+        :type upload_id: ``str``
+
+        :param upload_id: A list of (chunk_number, chunk_hash) tuples.
+        :type upload_id: ``list``
+
+        :keyword container: The container owning the object to which data is
+            being uploaded
+        :type container: :class:`Container`
+        """
+
+        root = Element('CompleteMultipartUpload')
+
+        for (count, etag) in chunks:
+            part = SubElement(root, 'Part')
+            part_no = SubElement(part, 'PartNumber')
+            part_no.text = str(count)
+
+            etag_id = SubElement(part, 'ETag')
+            etag_id.text = str(etag)
+
+        data = tostring(root)
+
+        params = {'uploadId': upload_id}
+        request_path = '?'.join((object_path, urlencode(params)))
+        response = self.connection.request(request_path, data=data,
+                                           method='POST', container=container)
+
+        if response.status != httplib.OK:
+            element = response.object
+            # pylint: disable=maybe-no-member
+            code, message = response._parse_error_details(element=element)
+            msg = 'Error in multipart commit: %s (%s)' % (message, code)
+            raise LibcloudError(msg, driver=self)
+
+        # Get the server's etag to be passed back to the caller
+        body = response.parse_body()
+        server_hash = body.find(fixxpath(xpath='ETag',
+                                         namespace=self.namespace)).text
+        return server_hash
+
+    def _abort_multipart(self, object_path, upload_id, container=None):
+        """
+        Aborts an already initiated multipart upload
+
+        :param object_path: Server side object path.
+        :type object_path: ``str``
+
+        :param upload_id: ID of the multipart upload.
+        :type upload_id: ``str``
+
+        :keyword container: The container owning the object to which data is
+            being uploaded
+        :type container: :class:`Container`
+        """
+
+        params = {'uploadId': upload_id}
+        request_path = '?'.join((object_path, urlencode(params)))
+        resp = self.connection.request(request_path, method='DELETE',
+                                       container=container)
+
+        if resp.status != httplib.NO_CONTENT:
+            raise LibcloudError('Error in multipart abort. status_code=%d' %
+                                (resp.status), driver=self)
+
+    def _upload_object(self, object_name, content_type, upload_func,
+                       upload_func_kwargs, request_path, request_method='PUT',
+                       headers=None, file_path=None, iterator=None,
+                       container=None):
+        """
+        Helper function for setting common request headers and calling the
+        passed in callback which uploads an object.
+        """
+        headers = headers or {}
+
+        if file_path and not os.path.exists(file_path):
+            raise OSError('File %s does not exist' % (file_path))
+
+        if iterator is not None and not hasattr(iterator, 'next') and not \
+                hasattr(iterator, '__next__'):
+            raise AttributeError('iterator object must implement next() ' +
+                                 'method.')
+
+        if not content_type:
+            if file_path:
+                name = file_path
+            else:
+                name = object_name
+            content_type, _ = guess_file_mime_type(name)
+
+            if not content_type:
+                if self.strict_mode:
+                    raise AttributeError('File content-type could not be '
+                                         'guessed and no content_type value '
+                                         'is provided')
+                else:
+                    # Fallback to a content-type
+                    content_type = DEFAULT_CONTENT_TYPE
+
+        file_size = None
+
+        if iterator:
+            if self.supports_chunked_encoding:
+                headers['Transfer-Encoding'] = 'chunked'
+                upload_func_kwargs['chunked'] = True
+            else:
+                # Chunked transfer encoding is not supported. Need to buffer
+                # all the data in memory so we can determine file size.
+                iterator = read_in_chunks(
+                    iterator=iterator)
+                data = exhaust_iterator(iterator=iterator)
+
+                file_size = len(data)
+                upload_func_kwargs['data'] = data
+        else:
+            file_size = os.path.getsize(file_path)
+            upload_func_kwargs['chunked'] = False
+
+        if file_size is not None and 'Content-Length' not in headers:
+            headers['Content-Length'] = file_size
+
+        headers['Content-Type'] = content_type
+        response = self.connection.request(request_path,
+                                           method=request_method, data=None,
+                                           headers=headers, raw=True,
+                                           container=container)
+
+        upload_func_kwargs['response'] = response
+        success, data_hash, bytes_transferred = upload_func(
+            **upload_func_kwargs)
+
+        if not success:
+            raise LibcloudError(
+                value='Object upload failed, Perhaps a timeout?', driver=self)
+
+        result_dict = {'response': response, 'data_hash': data_hash,
+                       'bytes_transferred': bytes_transferred}
+        return result_dict
+
+    def _to_containers(self, obj, xpath):
+        for element in obj.findall(fixxpath(xpath=xpath,
+                                   namespace=self.namespace)):
+            yield self._to_container(element)
+
+    def _to_container(self, element):
+        extra = {
+            'creation_date': findtext(element=element, xpath='CreationDate',
+                                      namespace=self.namespace),
+            'location': findtext(element=element, xpath='Location',
+                                 namespace=self.namespace)
+        }
+
+        container = Container(name=findtext(element=element, xpath='Name',
+                                            namespace=self.namespace),
+                              extra=extra,
+                              driver=self
+                              )
+
+        return container
+
+    def _to_objs(self, obj, xpath, container):
+        return [self._to_obj(element, container) for element in
+                obj.findall(fixxpath(xpath=xpath, namespace=self.namespace))]
+
+    def _to_obj(self, element, container):
+        owner_id = findtext(element=element, xpath='Owner/ID',
+                            namespace=self.namespace)
+        owner_display_name = findtext(element=element,
+                                      xpath='Owner/DisplayName',
+                                      namespace=self.namespace)
+        meta_data = {'owner': {'id': owner_id,
+                               'display_name': self._safe_decode(
+                                   owner_display_name)}}
+        last_modified = findtext(element=element,
+                                 xpath='LastModified',
+                                 namespace=self.namespace)
+        extra = {'last_modified': last_modified}
+
+        name = self._safe_decode(findtext(element=element, xpath='Key',
+                                          namespace=self.namespace))
+        obj = Object(name=name,
+                     size=int(findtext(element=element, xpath='Size',
+                                       namespace=self.namespace)),
+                     hash=findtext(element=element, xpath='ETag',
+                                   namespace=self.namespace).replace('"', ''),
+                     extra=extra,
+                     meta_data=meta_data,
+                     container=container,
+                     driver=self
+                     )
+
+        return obj
+
+    def _safe_decode(self, encoded):
+        """
+        Decode it as an escaped string and then treate the content as
+        UTF-8 encoded.
+        """
+        try:
+            if encoded:
+                unescaped, _ign = codecs.escape_decode(encoded)
+                return unescaped.decode('utf-8')
+            return encoded
+        except Exception:
+            return encoded
+
+    def _get_container_path(self, container):
+        """
+        Return a container path
+
+        :param container: Container instance
+        :type  container: :class:`Container`
+
+        :return: A path for this container.
+        :rtype: ``str``
+        """
+        return '/%s' % (container.name)
+
+    def _get_object_path(self, container, object_name):
+        """
+        Return an object's path.
+        Aliyun OSS api puts the container name in the host,
+        so ignore container here.
+
+        :param container: Container instance
+        :type  container: :class:`Container`
+
+        :param object_name: Object name
+        :type  object_name: :class:`str`
+
+        :return: A  path for this object.
+        :rtype: ``str``
+        """
+        object_name_cleaned = self._clean_object_name(object_name)
+        object_path = '/%s' % object_name_cleaned
+        return object_path
+
+    def _headers_to_object(self, object_name, container, headers):
+        hash = headers['etag'].replace('"', '')
+        extra = {'content_type': headers['content-type'],
+                 'etag': headers['etag']}
+        meta_data = {}
+
+        if 'last-modified' in headers:
+            extra['last_modified'] = headers['last-modified']
+
+        for key, value in headers.items():
+            if not key.lower().startswith(self.http_vendor_prefix + 'meta-'):
+                continue
+
+            key = key.replace(self.http_vendor_prefix + 'meta-', '')
+            meta_data[key] = value
+
+        obj = Object(name=object_name, size=int(headers['content-length']),
+                     hash=hash, extra=extra,
+                     meta_data=meta_data,
+                     container=container,
+                     driver=self)
+        return obj

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/storage/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/providers.py b/libcloud/storage/providers.py
index f02588c..0bf1e41 100644
--- a/libcloud/storage/providers.py
+++ b/libcloud/storage/providers.py
@@ -58,6 +58,8 @@ DRIVERS = {
     ('libcloud.storage.drivers.auroraobjects', 'AuroraObjectsStorageDriver'),
     Provider.BACKBLAZE_B2:
     ('libcloud.storage.drivers.backblaze_b2', 'BackblazeB2StorageDriver'),
+    Provider.OSS:
+    ('libcloud.storage.drivers.oss', 'OSSStorageDriver'),
 
     # Deprecated
     Provider.CLOUDFILES_US:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/storage/types.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/types.py b/libcloud/storage/types.py
index 837897f..9c536e3 100644
--- a/libcloud/storage/types.py
+++ b/libcloud/storage/types.py
@@ -43,6 +43,7 @@ class Provider(object):
     :cvar NIMBUS: Nimbus.io driver
     :cvar LOCAL: Local storage driver
     :cvar AURORAOBJECTS: AuroraObjects storage driver
+    :cvar OSS: Aliyun OSS storage driver
     """
     DUMMY = 'dummy'
     S3 = 's3'
@@ -64,6 +65,7 @@ class Provider(object):
     KTUCLOUD = 'ktucloud'
     AURORAOBJECTS = 'auroraobjects'
     BACKBLAZE_B2 = 'backblaze_b2'
+    OSS = 'oss'
 
     # Deperecated
     CLOUDFILES_US = 'cloudfiles_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/common/test_aliyun.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_aliyun.py b/libcloud/test/common/test_aliyun.py
new file mode 100644
index 0000000..da071ba
--- /dev/null
+++ b/libcloud/test/common/test_aliyun.py
@@ -0,0 +1,45 @@
+import sys
+import unittest
+
+from libcloud.common import aliyun
+from libcloud.common.aliyun import AliyunRequestSignerAlgorithmV1_0
+from libcloud.test import LibcloudTestCase
+
+
+class AliyunRequestSignerAlgorithmV1_0TestCase(LibcloudTestCase):
+
+    def setUp(self):
+        self.signer = AliyunRequestSignerAlgorithmV1_0('testid', 'testsecret',
+                                                       '1.0')
+
+    def test_sign_request(self):
+        params = {'TimeStamp': '2012-12-26T10:33:56Z',
+                  'Format': 'XML',
+                  'AccessKeyId': 'testid',
+                  'Action': 'DescribeRegions',
+                  'SignatureMethod': 'HMAC-SHA1',
+                  'RegionId': 'region1',
+                  'SignatureNonce': 'NwDAxvLU6tFE0DVb',
+                  'Version': '2014-05-26',
+                  'SignatureVersion': '1.0'}
+        method = 'GET'
+        path = '/'
+
+        expected = 'K9fCVP6Jrklpd3rLYKh1pfrrFNo='
+        self.assertEqual(expected,
+                         self.signer._sign_request(params, method, path))
+
+
+class AliyunCommonTestCase(LibcloudTestCase):
+
+    def test_percent_encode(self):
+        data = {
+            'abc': 'abc',
+            ' *~': '%20%2A~'
+        }
+        for key in data:
+            self.assertEqual(data[key], aliyun._percent_encode(key))
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/attach_disk.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/attach_disk.xml b/libcloud/test/compute/fixtures/ecs/attach_disk.xml
new file mode 100644
index 0000000..cfce115
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/attach_disk.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<AttachDiskResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</AttachDiskResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/copy_image.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/copy_image.xml b/libcloud/test/compute/fixtures/ecs/copy_image.xml
new file mode 100644
index 0000000..a25cba5
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/copy_image.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<CopyImageResponse>
+	<ImageId>i-28n7dkvov</ImageId>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</CopyImageResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_disk.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_disk.xml b/libcloud/test/compute/fixtures/ecs/create_disk.xml
new file mode 100644
index 0000000..9ba554e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_disk.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<CreateDiskResponse>
+	<DiskId>i-28n7dkvov</DiskId>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</CreateDiskResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_image.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_image.xml b/libcloud/test/compute/fixtures/ecs/create_image.xml
new file mode 100644
index 0000000..f25bc69
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_image.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<CreateImageResponse>
+	<ImageId>i-28n7dkvov</ImageId>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</CreateImageResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_instance.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_instance.xml b/libcloud/test/compute/fixtures/ecs/create_instance.xml
new file mode 100644
index 0000000..b9699bd
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_instance.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<CreateInstanceResponse>
+	<InstanceId>i-28n7dkvov</InstanceId>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</CreateInstanceResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_node_describe_instances.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_node_describe_instances.xml b/libcloud/test/compute/fixtures/ecs/create_node_describe_instances.xml
new file mode 100644
index 0000000..24eae7e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_node_describe_instances.xml
@@ -0,0 +1,56 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeInstancesResponse>
+	<PageNumber>1</PageNumber>
+	<TotalCount>1</TotalCount>
+	<PageSize>10</PageSize>
+	<RequestId>CA75EE06-D5F7-433C-870B-5042EED6C1DC</RequestId>
+	<Instances>
+		<Instance>
+			<ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+			<InnerIpAddress>
+				<IpAddress>10.163.197.74</IpAddress>
+			</InnerIpAddress>
+			<InstanceTypeFamily>ecs.t1</InstanceTypeFamily>
+			<VlanId></VlanId>
+			<InstanceId>i-28n7dkvov</InstanceId>
+			<EipAddress>
+				<IpAddress></IpAddress>
+				<AllocationId></AllocationId>
+				<InternetChargeType></InternetChargeType>
+			</EipAddress>
+			<InternetMaxBandwidthIn>-1</InternetMaxBandwidthIn>
+			<ZoneId>cn-qingdao-b</ZoneId>
+			<InternetChargeType>PayByTraffic</InternetChargeType>
+			<SerialNumber>ca0122d9-374d-4fce-9fc0-71f7c3eaf1c3</SerialNumber>
+			<IoOptimized>false</IoOptimized>
+			<Memory>1024</Memory>
+			<Cpu>1</Cpu>
+			<VpcAttributes>
+				<NatIpAddress></NatIpAddress>
+				<PrivateIpAddress />
+				<VSwitchId></VSwitchId>
+				<VpcId></VpcId>
+			</VpcAttributes>
+			<InternetMaxBandwidthOut>1</InternetMaxBandwidthOut>
+			<DeviceAvailable>true</DeviceAvailable>
+			<SecurityGroupIds>
+				<SecurityGroupId>sg-28ou0f3xa</SecurityGroupId>
+			</SecurityGroupIds>
+			<InstanceName>iZ28n7dkvovZ</InstanceName>
+			<Description></Description>
+			<InstanceNetworkType>classic</InstanceNetworkType>
+			<PublicIpAddress>
+				<IpAddress>114.215.124.73</IpAddress>
+			</PublicIpAddress>
+			<HostName>iZ28n7dkvovZ</HostName>
+			<InstanceType>ecs.t1.small</InstanceType>
+			<CreationTime>2015-12-27T07:35Z</CreationTime>
+			<Status>Running</Status>
+			<ClusterId></ClusterId>
+			<RegionId>cn-qingdao</RegionId>
+			<OperationLocks />
+			<InstanceChargeType>PostPaid</InstanceChargeType>
+			<ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+		</Instance>
+	</Instances>
+</DescribeInstancesResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_snapshot.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_snapshot.xml b/libcloud/test/compute/fixtures/ecs/create_snapshot.xml
new file mode 100644
index 0000000..54793b1
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_snapshot.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<CreateSnapshotResponse>
+	<SnapshotId>i-28n7dkvov</SnapshotId>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</CreateSnapshotResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/create_volume_describe_disks.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/create_volume_describe_disks.xml b/libcloud/test/compute/fixtures/ecs/create_volume_describe_disks.xml
new file mode 100644
index 0000000..d25c9a1
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/create_volume_describe_disks.xml
@@ -0,0 +1,36 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DescribeDisksResponse>
+    <Disks>
+        <Disk>
+	    <DiskChargeType>PostPaid</DiskChargeType>
+            <DeleteAutoSnapshot>true</DeleteAutoSnapshot>
+            <DeleteWithInstance>true</DeleteWithInstance>
+            <EnableAutoSnapshot>true</EnableAutoSnapshot>
+            <Category>cloud</Category>
+            <Description>Description</Description>
+            <DiskName>ubuntu1404sys</DiskName>
+            <Size>5</Size>
+            <Type>system</Type>
+            <InstanceId>i-28whl2nj2</InstanceId>
+            <CreationTime>2014-07-23T02:44:06Z</CreationTime>
+            <ImageId>ubuntu1404_64_20G_aliaegis_20150325.vhd</ImageId>
+            <ZoneId>cn-qingdao-b</ZoneId>
+            <AttachedTime>2016-01-04T15:02:17Z</AttachedTime>
+            <DetachedTime></DetachedTime>
+	    <Device>/dev/xvda</Device>
+            <OperationLocks></OperationLocks>
+            <Portable>false</Portable>
+            <ProductCode></ProductCode>
+            <RegionId>cn-qingdao</RegionId>
+            <DiskId>d-28zfrmo13</DiskId>
+            <SourceSnapshotId></SourceSnapshotId>
+            <Status>In_use</Status>
+	    <OperationLocks />
+	    <ExpiredTime>2999-09-08T16:00Z</ExpiredTime>
+        </Disk>
+    </Disks>
+    <PageNumber>1</PageNumber>
+    <PageSize>10</PageSize>
+    <TotalCount>1</TotalCount>
+    <RequestId>ED5CF6DD-71CA-462C-9C94-A61A78A01479</RequestId>
+</DescribeDisksResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/delete_disk.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/delete_disk.xml b/libcloud/test/compute/fixtures/ecs/delete_disk.xml
new file mode 100644
index 0000000..d83b677
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/delete_disk.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DeleteDiskResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</DeleteDiskResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/delete_image.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/delete_image.xml b/libcloud/test/compute/fixtures/ecs/delete_image.xml
new file mode 100644
index 0000000..9c0382f
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/delete_image.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DeleteImageResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</DeleteImageResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/delete_instance.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/delete_instance.xml b/libcloud/test/compute/fixtures/ecs/delete_instance.xml
new file mode 100644
index 0000000..b1f017c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/delete_instance.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DeleteInstanceResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</DeleteInstanceResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e6002e0a/libcloud/test/compute/fixtures/ecs/delete_snapshot.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ecs/delete_snapshot.xml b/libcloud/test/compute/fixtures/ecs/delete_snapshot.xml
new file mode 100644
index 0000000..7fac47b
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ecs/delete_snapshot.xml
@@ -0,0 +1,4 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<DeleteSnapshotResponse>
+	<RequestId>DA38B11A-9D6D-420B-942F-95D68606C4FC</RequestId>
+</DeleteSnapshotResponse>


[05/10] libcloud git commit: [LIBCLOUD-802] Refactor codes according to the project convention

Posted by to...@apache.org.
http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index 9ac944c..dfe7e5d 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -173,7 +173,7 @@ DRIVERS = {
     ('libcloud.compute.drivers.ciscoccs', 'CiscoCCSNodeDriver'),
     Provider.NTTA:
     ('libcloud.compute.drivers.ntta', 'NTTAmericaNodeDriver'),
-    Provider.ECS:
+    Provider.ALIYUN_ECS:
     ('libcloud.compute.drivers.ecs', 'ECSDriver'),
 
     # Deprecated

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 34884e0..226966f 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -100,7 +100,7 @@ class Provider(Type):
     :cvar VULTR: vultr driver.
     :cvar AZURE: Azure driver.
     :cvar AURORACOMPUTE: Aurora Compute driver.
-    :cvar ECS: Aliyun ECS driver.
+    :cvar ALIYUN_ECS: Aliyun ECS driver.
     """
     AZURE = 'azure'
     DUMMY = 'dummy'
@@ -160,7 +160,7 @@ class Provider(Type):
     NTTA = 'ntta'
     MEDONE = 'medone'
     CISCOCCS = 'ciscoccs'
-    ECS = 'ecs'
+    ALIYUN_ECS = 'ecs'
 
     # OpenStack based providers
     HPCLOUD = 'hpcloud'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/loadbalancer/drivers/slb.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/drivers/slb.py b/libcloud/loadbalancer/drivers/slb.py
index a7eab40..03ca6be 100644
--- a/libcloud/loadbalancer/drivers/slb.py
+++ b/libcloud/loadbalancer/drivers/slb.py
@@ -14,6 +14,7 @@
 # limitations under the License.
 
 __all__ = [
+    'SLB_API_VERSION',
     'SLBDriver'
 ]
 
@@ -313,19 +314,23 @@ class SLBDriver(Driver):
     def list_protocols(self):
         return list(PROTOCOL_TO_LISTENER_MAP.keys())
 
-    def list_balancers(self, ex_balancer_ids=[], ex_filters=None):
+    def list_balancers(self, ex_balancer_ids=None, ex_filters=None):
         """
         List all loadbalancers
+
         @inherits :class:`Driver.list_balancers`
+
         :keyword ex_balancer_ids: a list of balancer ids to filter results
                                   Only balancers which's id in this list
                                   will be returned
-        :type ex_balancer_ids: ``list``
+        :type ex_balancer_ids: ``list`` of ``str``
+
         :keyword ex_filters: attributes to filter results. Only balancers
                              which have all the desired attributes
                              and values will be returned
         :type ex_filters: ``dict``
         """
+
         params = {'Action': 'DescribeLoadBalancers',
                   'RegionId': self.region}
         if ex_balancer_ids and isinstance(ex_balancer_ids, list):
@@ -338,7 +343,41 @@ class SLBDriver(Driver):
         return self._to_balancers(resp_body)
 
     def create_balancer(self, name, port, protocol, algorithm, members,
+                        ex_bandwidth=None, ex_internet_charge_type=None,
+                        ex_address_type=None, ex_vswitch_id=None,
+                        ex_master_zone_id=None, ex_slave_zone_id=None,
+                        ex_client_token=None,
                         **kwargs):
+        """
+        Create a new load balancer instance
+
+        @inherits: :class:`Driver.create_balancer`
+
+        :keyword ex_bandwidth: The max bandwidth limit for `paybybandwidth`
+                               internet charge type, in Mbps unit
+        :type ex_bandwidth: ``int`` in range [1, 1000]
+
+        :keyword ex_internet_charge_type: The internet charge type
+        :type ex_internet_charge_type: a ``str`` of `paybybandwidth`
+                                       or `paybytraffic`
+
+        :keyword ex_address_type: The listening IP address type
+        :type ex_address_type: a ``str`` of `internet` or `intranet`
+
+        :keyword ex_vswitch_id: The vswitch id in a VPC network
+        :type ex_vswitch_id: ``str``
+
+        :keyword ex_master_zone_id: The id of the master availability zone
+        :type ex_master_zone_id: ``str``
+
+        :keyword ex_slave_zone_id: The id of the slave availability zone
+        :type ex_slave_zone_id: ``str``
+
+        :keyword ex_client_token: The token generated by client to
+                                  identify requests
+        :type ex_client_token: ``str``
+        """
+
         # 1.Create load balancer
         params = {'Action': 'CreateLoadBalancer',
                   'RegionId': self.region}
@@ -352,30 +391,39 @@ class SLBDriver(Driver):
         if protocol not in PROTOCOL_TO_LISTENER_MAP:
             raise AttributeError('unsupport protocol %s' % protocol)
 
-        extra_param_keys = [
-            'AddressType',
-            'VSwitchId',
-            'InternetChargeType',
-            'Bandwidth',
-            'ClientToken',
-            'MasterZoneId',
-            'SlaveZoneId'
-        ]
-        extra = self._get_extra_params(extra_param_keys, kwargs)
         # Bandwidth in range [1, 1000] Mbps
         bandwidth = -1
-        if 'Bandwidth' in extra and extra['Bandwidth']:
+        if ex_bandwidth:
             try:
-                bandwidth = int(extra['Bandwidth'])
+                bandwidth = int(ex_bandwidth)
             except ValueError:
-                raise AttributeError('Bandwidth should be a integer in '
+                raise AttributeError('ex_bandwidth should be a integer in '
                                      'range [1, 1000].')
+            params['Bandwidth'] = bandwidth
 
-        charge_type = extra.get('InternetChargeType', None)
-        if charge_type and charge_type.lower() == 'paybybandwidth':
-            if bandwidth == -1:
-                raise AttributeError('PayByBandwidth need Bandwidth be set')
-        params.update(extra)
+        if ex_internet_charge_type:
+            if ex_internet_charge_type.lower() == 'paybybandwidth':
+                if bandwidth == -1:
+                    raise AttributeError('PayByBandwidth internet charge type'
+                                         ' need ex_bandwidth be set')
+            params['InternetChargeType'] = ex_internet_charge_type
+
+        if ex_address_type:
+            if ex_address_type.lower() not in ('internet', 'intranet'):
+                raise AttributeError('ex_address_type should be "internet" '
+                                     'or "intranet"')
+            params['AddressType'] = ex_address_type
+
+        if ex_vswitch_id:
+            params['VSwitchId'] = ex_vswitch_id
+
+        if ex_master_zone_id:
+            params['MasterZoneId'] = ex_master_zone_id
+        if ex_slave_zone_id:
+            params['SlaveZoneId'] = ex_slave_zone_id
+
+        if ex_client_token:
+            params['ClientToken'] = ex_client_token
 
         if members and isinstance(members, list):
             backend_ports = [member.port for member in members]
@@ -461,11 +509,14 @@ class SLBDriver(Driver):
     def ex_get_balancer_attribute(self, balancer):
         """
         Get balancer attribute
+
         :param balancer: the balancer to get attribute
         :type balancer: ``LoadBalancer``
+
         :return: the balancer attribute
         :rtype: ``SLBLoadBalancerAttribute``
         """
+
         params = {'Action': 'DescribeLoadBalancerAttribute',
                   'LoadBalancerId': balancer.id}
         resp_body = self.connection.request(self.path, params).object
@@ -475,11 +526,14 @@ class SLBDriver(Driver):
     def ex_list_listeners(self, balancer):
         """
         Get all listener related to the given balancer
+
         :param balancer: the balancer to list listeners
         :type balancer: ``LoadBalancer``
+
         :return: a list of listeners
         :rtype: ``list`` of ``SLBLoadBalancerListener``
         """
+
         attribute = self.ex_get_balancer_attribute(balancer)
         listeners = [SLBLoadBalancerListener(each['ListenerPort'], None,
                                              None, None)
@@ -490,22 +544,29 @@ class SLBDriver(Driver):
                            bandwidth, **kwargs):
         """
         Create load balancer listening rule.
+
         :param balancer: the balancer which the rule belongs to.
                          The listener created will listen on the port of the
                          the balancer as default. 'ListenerPort' in kwargs
                          will *OVERRIDE* it.
         :type balancer: ``LoadBalancer``
+
         :param backend_port: the backend server port
         :type backend_port: ``int``
+
         :param protocol: the balancer protocol, default to http
         :type protocol: ``str``
+
         :param algorithm: the balancer routing algorithm
         :type algorithm: ``Algorithm``
+
         :param bandwidth: the listener bandwidth limits
         :type bandwidth: ``str``
+
         :return: the created listener
         :rtype: ``SLBLoadBalancerListener``
         """
+
         cls = PROTOCOL_TO_LISTENER_MAP.get(protocol,
                                            SLBLoadBalancerHttpListener)
         if 'ListenerPort' in kwargs:
@@ -523,13 +584,17 @@ class SLBDriver(Driver):
     def ex_start_listener(self, balancer, port):
         """
         Start balancer's listener listening the given port.
+
         :param balancer: a load balancer
         :type balancer: ``LoadBalancer``
+
         :param port: listening port
         :type port: ``int``
+
         :return: whether operation is success
         :rtype: ``bool``
         """
+
         params = {'Action': 'StartLoadBalancerListener',
                   'LoadBalancerId': balancer.id,
                   'ListenerPort': port}
@@ -539,13 +604,17 @@ class SLBDriver(Driver):
     def ex_stop_listener(self, balancer, port):
         """
         Stop balancer's listener listening the given port.
+
         :param balancer: a load balancer
         :type balancer: ``LoadBalancer``
+
         :param port: listening port
         :type port: ``int``
+
         :return: whether operation is success
         :rtype: ``bool``
         """
+
         params = {'Action': 'StopLoadBalancerListener',
                   'LoadBalancerId': balancer.id,
                   'ListenerPort': port}
@@ -556,17 +625,22 @@ class SLBDriver(Driver):
                               private_key):
         """
         Upload certificate and private key for https load balancer listener
+
         :param name: the certificate name
         :type name: ``str``
+
         :param server_certificate: the content of the certificate to upload
                                    in PEM format
         :type server_certificate: ``str``
+
         :param private_key: the content of the private key to upload
                             in PEM format
         :type private_key: ``str``
+
         :return: new created certificate info
         :rtype: ``SLBServerCertificate``
         """
+
         params = {'Action': 'UploadServerCertificate',
                   'RegionId': self.region,
                   'ServerCertificate': server_certificate,
@@ -579,11 +653,14 @@ class SLBDriver(Driver):
     def ex_list_certificates(self, certificate_ids=[]):
         """
         List all server certificates
+
         :param certificate_ids: certificate ids to filter results
         :type certificate_ids: ``str``
+
         :return: certificates
         :rtype: ``SLBServerCertificate``
         """
+
         params = {'Action': 'DescribeServerCertificates',
                   'RegionId': self.region}
         if certificate_ids and isinstance(certificate_ids, list):
@@ -600,11 +677,14 @@ class SLBDriver(Driver):
     def ex_delete_certificate(self, certificate_id):
         """
         Delete the given server certificate
+
         :param certificate_id: the id of the certificate to delete
         :type certificate_id: ``str``
+
         :return: whether process is success
         :rtype: ``bool``
         """
+
         params = {'Action': 'DeleteServerCertificate',
                   'RegionId': self.region,
                   'ServerCertificateId': certificate_id}
@@ -614,13 +694,17 @@ class SLBDriver(Driver):
     def ex_set_certificate_name(self, certificate_id, name):
         """
         Set server certificate name.
+
         :param certificate_id: the id of the server certificate to update
         :type certificate_id: ``str``
+
         :param name: the new name
         :type name: ``str``
+
         :return: whether updating is success
         :rtype: ``bool``
         """
+
         params = {'Action': 'SetServerCertificateName',
                   'RegionId': self.region,
                   'ServerCertificateId': certificate_id,
@@ -740,13 +824,6 @@ class SLBDriver(Driver):
         return SLBServerCertificate(id=_id, name=name,
                                     fingerprint=fingerprint)
 
-    def _get_extra_params(self, extra_param_keys, kwargs):
-        params = {}
-        for key in extra_param_keys:
-            if key in kwargs:
-                params[key] = kwargs[key]
-        return params
-
     def _list_to_json(self, value):
         try:
             return json.dumps(value)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/loadbalancer/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/providers.py b/libcloud/loadbalancer/providers.py
index b619c9d..f60e6d3 100644
--- a/libcloud/loadbalancer/providers.py
+++ b/libcloud/loadbalancer/providers.py
@@ -42,7 +42,7 @@ DRIVERS = {
     ('libcloud.loadbalancer.drivers.softlayer', 'SoftlayerLBDriver'),
     Provider.DIMENSIONDATA:
     ('libcloud.loadbalancer.drivers.dimensiondata', 'DimensionDataLBDriver'),
-    Provider.SLB:
+    Provider.ALIYUN_SLB:
     ('libcloud.loadbalancer.drivers.slb', 'SLBDriver'),
 
     # Deprecated

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/loadbalancer/types.py
----------------------------------------------------------------------
diff --git a/libcloud/loadbalancer/types.py b/libcloud/loadbalancer/types.py
index 0d5deec..45d6c6a 100644
--- a/libcloud/loadbalancer/types.py
+++ b/libcloud/loadbalancer/types.py
@@ -33,7 +33,7 @@ class LibcloudLBImmutableError(LibcloudLBError):
 
 class Provider(object):
     """
-    :cvar SLB: Aliyun SLB loadbalancer driver
+    :cvar ALIYUN_SLB: Aliyun SLB loadbalancer driver
     """
     RACKSPACE = 'rackspace'
     GOGRID = 'gogrid'
@@ -44,7 +44,7 @@ class Provider(object):
     GCE = 'gce'
     SOFTLAYER = 'softlayer'
     DIMENSIONDATA = 'dimensiondata'
-    SLB = 'slb'
+    ALIYUN_SLB = 'slb'
 
     # Deprecated
     RACKSPACE_US = 'rackspace_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/storage/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/providers.py b/libcloud/storage/providers.py
index 0bf1e41..1cb31e4 100644
--- a/libcloud/storage/providers.py
+++ b/libcloud/storage/providers.py
@@ -58,7 +58,7 @@ DRIVERS = {
     ('libcloud.storage.drivers.auroraobjects', 'AuroraObjectsStorageDriver'),
     Provider.BACKBLAZE_B2:
     ('libcloud.storage.drivers.backblaze_b2', 'BackblazeB2StorageDriver'),
-    Provider.OSS:
+    Provider.ALIYUN_OSS:
     ('libcloud.storage.drivers.oss', 'OSSStorageDriver'),
 
     # Deprecated

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/storage/types.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/types.py b/libcloud/storage/types.py
index 9c536e3..247e59f 100644
--- a/libcloud/storage/types.py
+++ b/libcloud/storage/types.py
@@ -43,7 +43,7 @@ class Provider(object):
     :cvar NIMBUS: Nimbus.io driver
     :cvar LOCAL: Local storage driver
     :cvar AURORAOBJECTS: AuroraObjects storage driver
-    :cvar OSS: Aliyun OSS storage driver
+    :cvar ALIYUN_OSS: Aliyun OSS storage driver
     """
     DUMMY = 'dummy'
     S3 = 's3'
@@ -65,7 +65,7 @@ class Provider(object):
     KTUCLOUD = 'ktucloud'
     AURORAOBJECTS = 'auroraobjects'
     BACKBLAZE_B2 = 'backblaze_b2'
-    OSS = 'oss'
+    ALIYUN_OSS = 'oss'
 
     # Deperecated
     CLOUDFILES_US = 'cloudfiles_us'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/test/compute/test_ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_ecs.py b/libcloud/test/compute/test_ecs.py
index 96e482e..495f7b2 100644
--- a/libcloud/test/compute/test_ecs.py
+++ b/libcloud/test/compute/test_ecs.py
@@ -160,7 +160,7 @@ class ECSDriverTestCase(LibcloudTestCase):
         node = self.driver.create_node(name=self.name, image=self.fake_image,
                                        size=self.fake_size,
                                        ex_security_group_id='sg-28ou0f3xa',
-                                       ex_data_disk=self.data_disk)
+                                       ex_data_disks=self.data_disk)
         self.assertIsNotNone(node)
 
     def test_list_sizes(self):

http://git-wip-us.apache.org/repos/asf/libcloud/blob/ae4c482b/libcloud/test/loadbalancer/test_slb.py
----------------------------------------------------------------------
diff --git a/libcloud/test/loadbalancer/test_slb.py b/libcloud/test/loadbalancer/test_slb.py
index 926aed0..0325cb2 100644
--- a/libcloud/test/loadbalancer/test_slb.py
+++ b/libcloud/test/loadbalancer/test_slb.py
@@ -114,11 +114,11 @@ class SLBDriverTestCases(unittest.TestCase):
         self.protocol = 'http'
         self.algorithm = Algorithm.WEIGHTED_ROUND_ROBIN
         self.extra = {
-            'AddressType': 'internet',
-            'InternetChargeType': 'paybytraffic',
-            'Bandwidth': 1,
-            'MasterZoneId': 'cn-hangzhou-d',
-            'SlaveZoneId': 'cn-hangzhou-b',
+            'ex_address_type': 'internet',
+            'ex_internet_charge_type': 'paybytraffic',
+            'ex_bandwidth': 1,
+            'ex_master_zone_id': 'cn-hangzhou-d',
+            'ex_slave_zone_id': 'cn-hangzhou-b',
             'StickySession': 'on',
             'HealthCheck': 'on'}
         self.members = [Member('node1', None, None)]
@@ -153,12 +153,12 @@ class SLBDriverTestCases(unittest.TestCase):
     def test_create_balancer_bandwidth_value_error(self):
         self.assertRaises(AttributeError, self.driver.create_balancer,
                           None, 80, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
-                          None, Bandwidth='NAN')
+                          None, ex_bandwidth='NAN')
 
     def test_create_balancer_paybybandwidth_without_bandwidth_exception(self):
         self.assertRaises(AttributeError, self.driver.create_balancer,
                           None, 80, 'http', Algorithm.WEIGHTED_ROUND_ROBIN,
-                          None, InternetChargeType='paybybandwidth')
+                          None, ex_internet_charge_type='paybybandwidth')
 
     def test_balancer_list_members(self):
         balancer = self.driver.get_balancer(balancer_id='tests')
@@ -312,15 +312,15 @@ class SLBMockHttp(MockHttpTestCase):
     def _CreateLoadBalancer(self, method, url, body, headers):
         params = {'RegionId': self.test.region,
                   'LoadBalancerName': self.test.name}
-        balancer_keys = [
-            'AddressType',
-            'InternetChargeType',
-            'Bandwidth',
-            'MasterZoneId',
-            'SlaveZoneId'
-        ]
+        balancer_keys = {
+            'AddressType': 'ex_address_type',
+            'InternetChargeType': 'ex_internet_charge_type',
+            'Bandwidth': 'ex_bandwidth',
+            'MasterZoneId': 'ex_master_zone_id',
+            'SlaveZoneId': 'ex_slave_zone_id'
+        }
         for key in balancer_keys:
-            params[key] = str(self.test.extra[key])
+            params[key] = str(self.test.extra[balancer_keys[key]])
 
         self.assertUrlContainsQueryParams(url, params)
         body = self.fixtures.load('create_load_balancer.xml')