[LIBCLOUD-802] Add drivers for Aliyun cloud Closes #712
Signed-off-by: Tomaz Muraus <[email protected]> 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 <[email protected]> Authored: Mon Dec 28 09:13:45 2015 +0800 Committer: Tomaz Muraus <[email protected]> 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'
