1# Licensed to the Apache Software Foundation (ASF) under one or more
2# contributor license agreements.  See the NOTICE file distributed with
3# this work for additional information regarding copyright ownership.
4# The ASF licenses this file to You under the Apache License, Version 2.0
5# (the "License"); you may not use this file except in compliance with
6# the License.  You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""
17Node driver for Aliyun.
18"""
19
20try:
21    import simplejson as json
22except ImportError:
23    import json
24import time
25
26from libcloud.common.aliyun import AliyunXmlResponse, SignedAliyunConnection
27from libcloud.common.types import LibcloudError
28from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeSize, \
29    StorageVolume, VolumeSnapshot, NodeLocation
30from libcloud.compute.types import NodeState, StorageVolumeState, \
31    VolumeSnapshotState
32from libcloud.utils.py3 import _real_unicode as u
33from libcloud.utils.xml import findall, findattr, findtext
34
35__all__ = [
36    'DiskCategory',
37    'InternetChargeType',
38    'ECS_API_VERSION',
39    'ECSDriver',
40    'ECSSecurityGroup',
41    'ECSZone'
42]
43
44ECS_API_VERSION = '2014-05-26'
45ECS_API_ENDPOINT = 'ecs.aliyuncs.com'
46DEFAULT_SIGNATURE_VERSION = '1.0'
47
48
49def _parse_bool(value):
50    if isinstance(value, bool):
51        return value
52    if u(value).lower() == 'true':
53        return True
54    return False
55
56
57"""
58Define the extra dictionary for specific resources
59"""
60RESOURCE_EXTRA_ATTRIBUTES_MAP = {
61    'node': {
62        'description': {
63            'xpath': 'Description',
64            'transform_func': u
65        },
66        'image_id': {
67            'xpath': 'ImageId',
68            'transform_func': u
69        },
70        'zone_id': {
71            'xpath': 'ZoneId',
72            'transform_func': u
73        },
74        'instance_type': {
75            'xpath': 'InstanceType',
76            'transform_func': u
77        },
78        'instance_type_family': {
79            'xpath': 'InstanceTypeFamily',
80            'transform_func': u
81        },
82        'hostname': {
83            'xpath': 'HostName',
84            'transform_func': u
85        },
86        'serial_number': {
87            'xpath': 'SerialNumber',
88            'transform_func': u
89        },
90        'internet_charge_type': {
91            'xpath': 'InternetChargeType',
92            'transform_func': u
93        },
94        'creation_time': {
95            'xpath': 'CreationTime',
96            'transform_func': u
97        },
98        'instance_network_type': {
99            'xpath': 'InstanceNetworkType',
100            'transform_func': u
101        },
102        'instance_charge_type': {
103            'xpath': 'InstanceChargeType',
104            'transform_func': u
105        },
106        'device_available': {
107            'xpath': 'DeviceAvailable',
108            'transform_func': u
109        },
110        'io_optimized': {
111            'xpath': 'IoOptimized',
112            'transform_func': u
113        },
114        'expired_time': {
115            'xpath': 'ExpiredTime',
116            'transform_func': u
117        }
118    },
119    'vpc_attributes': {
120        'vpc_id': {
121            'xpath': 'VpcId',
122            'transform_func': u
123        },
124        'vswitch_id': {
125            'xpath': 'VSwitchId',
126            'transform_func': u
127        },
128        'private_ip_address': {
129            'xpath': 'PrivateIpAddress/IpAddress',
130            'transform_func': u
131        },
132        'nat_ip_address': {
133            'xpath': 'NatIpAddress',
134            'transform_func': u
135        }
136    },
137    'eip_address_associate': {
138        'allocation_id': {
139            'xpath': 'AllocationId',
140            'transform_func': u
141        },
142        'ip_address': {
143            'xpath': 'IpAddress',
144            'transform_func': u
145        },
146        'bandwidth': {
147            'xpath': 'Bandwidth',
148            'transform_func': int
149        },
150        'internet_charge_type': {
151            'xpath': 'InternetChargeType',
152            'transform_func': u
153        }
154    },
155    'operation_locks': {
156        'lock_reason': {
157            'xpath': 'LockReason',
158            'transform_func': u
159        }
160    },
161    'volume': {
162        'region_id': {
163            'xpath': 'RegionId',
164            'transform_func': u
165        },
166        'zone_id': {
167            'xpath': 'ZoneId',
168            'transform_func': u
169        },
170        'description': {
171            'xpath': 'Description',
172            'transform_func': u
173        },
174        'type': {
175            'xpath': 'Type',
176            'transform_func': u
177        },
178        'category': {
179            'xpath': 'Category',
180            'transform_func': u
181        },
182        'image_id': {
183            'xpath': 'ImageId',
184            'transform_func': u
185        },
186        'source_snapshot_id': {
187            'xpath': 'SourceSnapshotId',
188            'transform_func': u
189        },
190        'product_code': {
191            'xpath': 'ProductCode',
192            'transform_func': u
193        },
194        'portable': {
195            'xpath': 'Portable',
196            'transform_func': _parse_bool
197        },
198        'instance_id': {
199            'xpath': 'InstanceId',
200            'transform_func': u
201        },
202        'device': {
203            'xpath': 'Device',
204            'transform_func': u
205        },
206        'delete_with_instance': {
207            'xpath': 'DeleteWithInstance',
208            'transform_func': _parse_bool
209        },
210        'enable_auto_snapshot': {
211            'xpath': 'EnableAutoSnapshot',
212            'transform_func': _parse_bool
213        },
214        'creation_time': {
215            'xpath': 'CreationTime',
216            'transform_func': u
217        },
218        'attached_time': {
219            'xpath': 'AttachedTime',
220            'transform_func': u
221        },
222        'detached_time': {
223            'xpath': 'DetachedTime',
224            'transform_func': u
225        },
226        'disk_charge_type': {
227            'xpath': 'DiskChargeType',
228            'transform_func': u
229        }
230    },
231    'snapshot': {
232        'snapshot_name': {
233            'xpath': 'SnapshotName',
234            'transform_func': u
235        },
236        'description': {
237            'xpath': 'Description',
238            'transform_func': u
239        },
240        'progress': {
241            'xpath': 'Progress',
242            'transform_func': u
243        },
244        'source_disk_id': {
245            'xpath': 'SourceDiskId',
246            'transform_func': u
247        },
248        'source_disk_size': {
249            'xpath': 'SourceDiskSize',
250            'transform_func': int
251        },
252        'source_disk_type': {
253            'xpath': 'SourceDiskType',
254            'transform_func': u
255        },
256        'product_code': {
257            'xpath': 'ProductCode',
258            'transform_func': u
259        },
260        'usage': {
261            'xpath': 'Usage',
262            'transform_func': u
263        }
264    },
265    'image': {
266        'image_version': {
267            'xpath': 'ImageVersion',
268            'transform_func': u
269        },
270        'os_type': {
271            'xpath': 'OSType',
272            'transform_func': u
273        },
274        'platform': {
275            'xpath': 'Platform',
276            'transform_func': u
277        },
278        'architecture': {
279            'xpath': 'Architecture',
280            'transform_func': u
281        },
282        'description': {
283            'xpath': 'Description',
284            'transform_func': u
285        },
286        'size': {
287            'xpath': 'Size',
288            'transform_func': int
289        },
290        'image_owner_alias': {
291            'xpath': 'ImageOwnerAlias',
292            'transform_func': u
293        },
294        'os_name': {
295            'xpath': 'OSName',
296            'transform_func': u
297        },
298        'product_code': {
299            'xpath': 'ProductCode',
300            'transform_func': u
301        },
302        'is_subscribed': {
303            'xpath': 'IsSubscribed',
304            'transform_func': _parse_bool
305        },
306        'progress': {
307            'xpath': 'Progress',
308            'transform_func': u
309        },
310        'creation_time': {
311            'xpath': 'CreationTime',
312            'transform_func': u
313        },
314        'usage': {
315            'xpath': 'Usage',
316            'transform_func': u
317        },
318        'is_copied': {
319            'xpath': 'IsCopied',
320            'transform_func': _parse_bool
321        }
322    },
323    'disk_device_mapping': {
324        'snapshot_id': {
325            'xpath': 'SnapshotId',
326            'transform_func': u
327        },
328        'size': {
329            'xpath': 'Size',
330            'transform_func': int
331        },
332        'device': {
333            'xpath': 'Device',
334            'transform_func': u
335        },
336        'format': {
337            'xpath': 'Format',
338            'transform_func': u
339        },
340        'import_oss_bucket': {
341            'xpath': 'ImportOSSBucket',
342            'transform_func': u
343        },
344        'import_oss_object': {
345            'xpath': 'ImportOSSObject',
346            'transform_func': u
347        }
348    }
349}
350
351
352class ECSConnection(SignedAliyunConnection):
353    """
354    Represents a single connection to the Aliyun ECS Endpoint.
355    """
356
357    api_version = ECS_API_VERSION
358    host = ECS_API_ENDPOINT
359    responseCls = AliyunXmlResponse
360    service_name = 'ecs'
361
362
363class ECSSecurityGroup(object):
364    """
365    Security group used to control nodes internet and intranet accessibility.
366    """
367    def __init__(self, id, name, description=None, driver=None, vpc_id=None,
368                 creation_time=None):
369        self.id = id
370        self.name = name
371        self.description = description
372        self.driver = driver
373        self.vpc_id = vpc_id
374        self.creation_time = creation_time
375
376    def __repr__(self):
377        return ('<ECSSecurityGroup: id=%s, name=%s, driver=%s ...>' %
378                (self.id, self.name, self.driver.name))
379
380
381class ECSSecurityGroupAttribute(object):
382
383    """
384    Security group attribute.
385    """
386    def __init__(self, ip_protocol=None, port_range=None,
387                 source_group_id=None, policy=None, nic_type=None):
388        self.ip_protocol = ip_protocol
389        self.port_range = port_range
390        self.source_group_id = source_group_id
391        self.policy = policy
392        self.nic_type = nic_type
393
394    def __repr__(self):
395        return ('<ECSSecurityGroupAttribute: ip_protocol=%s ...>' %
396                (self.ip_protocol))
397
398
399class ECSZone(object):
400    """
401    ECSZone used to represent an availability zone in a region.
402    """
403    def __init__(self, id, name, driver=None,
404                 available_resource_types=None,
405                 available_instance_types=None,
406                 available_disk_categories=None):
407        self.id = id
408        self.name = name
409        self.driver = driver
410        self.available_resource_types = available_resource_types
411        self.available_instance_types = available_instance_types
412        self.available_disk_categories = available_disk_categories
413
414    def __repr__(self):
415        return ('<ECSZone: id=%s, name=%s, driver=%s>' %
416                (self.id, self.name, self.driver))
417
418
419class InternetChargeType(object):
420    """
421    Internet connection billing types for Aliyun Nodes.
422    """
423    BY_BANDWIDTH = 'PayByBandwidth'
424    BY_TRAFFIC = 'PayByTraffic'
425
426
427class DiskCategory(object):
428    """
429    Enum defined disk types supported by Aliyun system and data disks.
430    """
431    CLOUD = 'cloud'
432    CLOUD_EFFICIENCY = 'cloud_efficiency'
433    CLOUD_SSD = 'cloud_ssd'
434    EPHEMERAL_SSD = 'ephemeral_ssd'
435
436
437class Pagination(object):
438    """
439    Pagination used to describe the multiple pages results.
440    """
441    def __init__(self, total, size, current):
442        """
443        Create a pagination.
444
445        :param total: the total count of the results
446        :param size: the page size of each page
447        :param current: the current page number, 1-based
448        """
449        self.total = total
450        self.size = size
451        self.current = current
452
453    def next(self):
454        """
455        Switch to the next page.
456        :return: the new pagination or None when no more page
457        :rtype: ``Pagination``
458        """
459        if self.total is None or (self.size * self.current >= self.total):
460            return None
461        self.current += 1
462        return self
463
464    def to_dict(self):
465        return {'PageNumber': self.current,
466                'PageSize': self.size}
467
468    def __repr__(self):
469        return ('<Pagination total=%d, size=%d, current page=%d>' %
470                (self.total, self.size, self.current))
471
472
473class ECSDriver(NodeDriver):
474    """
475    Aliyun ECS node driver.
476
477    Used for Aliyun ECS service.
478
479    TODO:
480    Get guest OS root password
481    Adjust internet bandwidth settings
482    Manage security groups and rules
483    """
484
485    name = 'Aliyun ECS'
486    website = 'https://www.aliyun.com/product/ecs'
487    connectionCls = ECSConnection
488    features = {'create_node': ['password']}
489    namespace = None
490    path = '/'
491
492    internet_charge_types = InternetChargeType
493    disk_categories = DiskCategory
494
495    NODE_STATE_MAPPING = {
496        'Starting': NodeState.PENDING,
497        'Running': NodeState.RUNNING,
498        'Stopping': NodeState.PENDING,
499        'Stopped': NodeState.STOPPED
500    }
501
502    VOLUME_STATE_MAPPING = {
503        'In_use': StorageVolumeState.INUSE,
504        'Available': StorageVolumeState.AVAILABLE,
505        'Attaching': StorageVolumeState.ATTACHING,
506        'Detaching': StorageVolumeState.INUSE,
507        'Creating': StorageVolumeState.CREATING,
508        'ReIniting': StorageVolumeState.CREATING}
509
510    SNAPSHOT_STATE_MAPPING = {
511        'progressing': VolumeSnapshotState.CREATING,
512        'accomplished': VolumeSnapshotState.AVAILABLE,
513        'failed': VolumeSnapshotState.ERROR}
514
515    def list_nodes(self, ex_node_ids=None, ex_filters=None):
516        """
517        List all nodes.
518
519        @inherits: :class:`NodeDriver.create_node`
520
521        :keyword  ex_node_ids: a list of node's ids used to filter nodes.
522                               Only the nodes which's id in this list
523                               will be returned.
524        :type   ex_node_ids: ``list`` of ``str``
525        :keyword  ex_filters: node attribute and value pairs to filter nodes.
526                              Only the nodes which matchs all the pairs will
527                              be returned.
528                              If the filter attribute need a json array value,
529                              use ``list`` object, the driver will convert it.
530        :type   ex_filters: ``dict``
531        """
532
533        params = {'Action': 'DescribeInstances',
534                  'RegionId': self.region}
535
536        if ex_node_ids:
537            if isinstance(ex_node_ids, list):
538                params['InstanceIds'] = self._list_to_json_array(ex_node_ids)
539            else:
540                raise AttributeError('ex_node_ids should be a list of '
541                                     'node ids.')
542
543        if ex_filters:
544            if isinstance(ex_filters, dict):
545                params.update(ex_filters)
546            else:
547                raise AttributeError('ex_filters should be a dict of '
548                                     'node attributes.')
549
550        nodes = self._request_multiple_pages(self.path, params,
551                                             self._to_nodes)
552        return nodes
553
554    def list_sizes(self, location=None):
555        params = {'Action': 'DescribeInstanceTypes'}
556
557        resp_body = self.connection.request(self.path, params).object
558        size_elements = findall(resp_body, 'InstanceTypes/InstanceType',
559                                namespace=self.namespace)
560        sizes = [self._to_size(each) for each in size_elements]
561        return sizes
562
563    def list_locations(self):
564        params = {'Action': 'DescribeRegions'}
565
566        resp_body = self.connection.request(self.path, params).object
567        location_elements = findall(resp_body, 'Regions/Region',
568                                    namespace=self.namespace)
569        locations = [self._to_location(each) for each in location_elements]
570        return locations
571
572    def create_node(self, name, size, image, auth=None,
573                    ex_security_group_id=None, ex_description=None,
574                    ex_internet_charge_type=None,
575                    ex_internet_max_bandwidth_out=None,
576                    ex_internet_max_bandwidth_in=None,
577                    ex_hostname=None, ex_io_optimized=None,
578                    ex_system_disk=None, ex_data_disks=None,
579                    ex_vswitch_id=None, ex_private_ip_address=None,
580                    ex_client_token=None, **kwargs):
581        """
582        @inherits: :class:`NodeDriver.create_node`
583
584        :param name: The name for this new node (required)
585        :type name: ``str``
586
587        :param image: The image to use when creating this node (required)
588        :type image: `NodeImage`
589
590        :param size: The size of the node to create (required)
591        :type size: `NodeSize`
592
593        :keyword auth: Initial authentication information for the node
594                       (optional)
595        :type auth: :class:`NodeAuthSSHKey` or :class:`NodeAuthPassword`
596
597        :keyword ex_security_group_id: The id of the security group the
598                                       new created node is attached to.
599                                       (required)
600        :type ex_security_group_id: ``str``
601
602        :keyword ex_description: A description string for this node (optional)
603        :type ex_description: ``str``
604
605        :keyword ex_internet_charge_type: The internet charge type (optional)
606        :type ex_internet_charge_type: a ``str`` of 'PayByTraffic'
607                                       or 'PayByBandwidth'
608
609        :keyword ex_internet_max_bandwidth_out: The max output bandwidth,
610                                                in Mbps (optional)
611                                                Required for 'PayByTraffic'
612                                                internet charge type
613        :type ex_internet_max_bandwidth_out: a ``int`` in range [0, 100]
614                                             a ``int`` in range [1, 100] for
615                                             'PayByTraffic' internet charge
616                                             type
617
618        :keyword ex_internet_max_bandwidth_in: The max input bandwidth,
619                                               in Mbps (optional)
620        :type ex_internet_max_bandwidth_in: a ``int`` in range [1, 200]
621                                            default to 200 in server side
622
623        :keyword ex_hostname: The hostname for the node (optional)
624        :type ex_hostname: ``str``
625
626        :keyword ex_io_optimized: Whether the node is IO optimized (optional)
627        :type ex_io_optimized: ``boll``
628
629        :keyword ex_system_disk: The system disk for the node (optional)
630        :type ex_system_disk: ``dict``
631
632        :keyword ex_data_disks: The data disks for the node (optional)
633        :type ex_data_disks: a `list` of `dict`
634
635        :keyword ex_vswitch_id: The id of vswitch for a VPC type node
636                                (optional)
637        :type ex_vswitch_id: ``str``
638
639        :keyword ex_private_ip_address: The IP address in private network
640                                        (optional)
641        :type ex_private_ip_address: ``str``
642
643        :keyword ex_client_token: A token generated by client to keep
644                                  requests idempotency (optional)
645        :type keyword ex_client_token: ``str``
646        """
647
648        params = {'Action': 'CreateInstance',
649                  'RegionId': self.region,
650                  'ImageId': image.id,
651                  'InstanceType': size.id,
652                  'InstanceName': name}
653
654        if not ex_security_group_id:
655            raise AttributeError('ex_security_group_id is mandatory')
656        params['SecurityGroupId'] = ex_security_group_id
657
658        if ex_description:
659            params['Description'] = ex_description
660
661        inet_params = self._get_internet_related_params(
662            ex_internet_charge_type,
663            ex_internet_max_bandwidth_in,
664            ex_internet_max_bandwidth_out)
665        if inet_params:
666            params.update(inet_params)
667
668        if ex_hostname:
669            params['HostName'] = ex_hostname
670
671        if auth:
672            auth = self._get_and_check_auth(auth)
673            params['Password'] = auth.password
674
675        if ex_io_optimized is not None:
676            optimized = ex_io_optimized
677            if isinstance(optimized, bool):
678                optimized = 'optimized' if optimized else 'none'
679            params['IoOptimized'] = optimized
680
681        if ex_system_disk:
682            system_disk = self._get_system_disk(ex_system_disk)
683            if system_disk:
684                params.update(system_disk)
685
686        if ex_data_disks:
687            data_disks = self._get_data_disks(ex_data_disks)
688            if data_disks:
689                params.update(data_disks)
690
691        if ex_vswitch_id:
692            params['VSwitchId'] = ex_vswitch_id
693
694        if ex_private_ip_address:
695            if not ex_vswitch_id:
696                raise AttributeError('must provide ex_private_ip_address  '
697                                     'and ex_vswitch_id at the same time')
698            else:
699                params['PrivateIpAddress'] = ex_private_ip_address
700
701        if ex_client_token:
702            params['ClientToken'] = ex_client_token
703
704        resp = self.connection.request(self.path, params=params)
705        node_id = findtext(resp.object, xpath='InstanceId',
706                           namespace=self.namespace)
707        nodes = self.list_nodes(ex_node_ids=[node_id])
708        if len(nodes) != 1:
709            raise LibcloudError('could not find the new created node '
710                                'with id %s. ' % node_id,
711                                driver=self)
712        node = nodes[0]
713        self.ex_start_node(node)
714        self._wait_until_state(nodes, NodeState.RUNNING)
715        return node
716
717    def reboot_node(self, node, ex_force_stop=False):
718        """
719        Reboot the given node
720
721        @inherits :class:`NodeDriver.reboot_node`
722
723        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
724                                otherwise, stop node normally,
725                                default to ``False``
726        :type ex_force_stop: ``bool``
727        """
728        params = {'Action': 'RebootInstance',
729                  'InstanceId': node.id,
730                  'ForceStop': u(ex_force_stop).lower()}
731        resp = self.connection.request(self.path, params=params)
732        return resp.success() and \
733            self._wait_until_state([node], NodeState.RUNNING)
734
735    def destroy_node(self, node):
736        nodes = self.list_nodes(ex_node_ids=[node.id])
737        if len(nodes) != 1 and node.id != nodes[0].id:
738            raise LibcloudError('could not find the node with id %s.'
739                                % node.id)
740        current = nodes[0]
741        if current.state == NodeState.RUNNING:
742            # stop node first
743            self.ex_stop_node(node)
744            self._wait_until_state(nodes, NodeState.STOPPED)
745        params = {'Action': 'DeleteInstance',
746                  'InstanceId': node.id}
747        resp = self.connection.request(self.path, params)
748        return resp.success()
749
750    def start_node(self, node):
751        """
752        Start node to running state.
753
754        :param node: the ``Node`` object to start
755        :type node: ``Node``
756
757        :return: starting operation result.
758        :rtype: ``bool``
759        """
760        params = {'Action': 'StartInstance',
761                  'InstanceId': node.id}
762        resp = self.connection.request(self.path, params)
763        return resp.success() and \
764            self._wait_until_state([node], NodeState.RUNNING)
765
766    def stop_node(self, node, ex_force_stop=False):
767        """
768        Stop a running node.
769
770        :param node: The node to stop
771        :type node: :class:`Node`
772
773        :keyword ex_force_stop: if ``True``, stop node force (maybe lose data)
774                                otherwise, stop node normally,
775                                default to ``False``
776        :type ex_force_stop: ``bool``
777
778        :return: stopping operation result.
779        :rtype: ``bool``
780        """
781        params = {'Action': 'StopInstance',
782                  'InstanceId': node.id,
783                  'ForceStop': u(ex_force_stop).lower()}
784        resp = self.connection.request(self.path, params)
785        return resp.success() and \
786            self._wait_until_state([node], NodeState.STOPPED)
787
788    def ex_start_node(self, node):
789        # NOTE: This method is here for backward compatibility reasons after
790        # this method was promoted to be part of the standard compute API in
791        # Libcloud v2.7.0
792        return self.start_node(node=node)
793
794    def ex_stop_node(self, node, ex_force_stop=False):
795        # NOTE: This method is here for backward compatibility reasons after
796        # this method was promoted to be part of the standard compute API in
797        # Libcloud v2.7.0
798        return self.stop_node(node=node, ex_force_stop=ex_force_stop)
799
800    def ex_create_security_group(self, description=None, client_token=None):
801        """
802        Create a new security group.
803
804        :keyword description: security group description
805        :type description: ``unicode``
806
807        :keyword client_token: a token generated by client to identify
808                                  each request.
809        :type client_token: ``str``
810        """
811        params = {'Action': 'CreateSecurityGroup',
812                  'RegionId': self.region}
813
814        if description:
815            params['Description'] = description
816        if client_token:
817            params['ClientToken'] = client_token
818        resp = self.connection.request(self.path, params)
819        return findtext(resp.object, 'SecurityGroupId',
820                        namespace=self.namespace)
821
822    def ex_delete_security_group_by_id(self, group_id=None):
823        """
824        Delete a new security group.
825
826        :keyword group_id: security group id
827        :type group_id: ``str``
828        """
829        params = {'Action': 'DeleteSecurityGroup',
830                  'RegionId': self.region,
831                  'SecurityGroupId': group_id}
832        resp = self.connection.request(self.path, params)
833        return resp.success()
834
835    def ex_modify_security_group_by_id(
836            self,
837            group_id=None,
838            name=None,
839            description=None):
840        """
841        Modify a new security group.
842        :keyword group_id: id of the security group
843        :type group_id: ``str``
844        :keyword name: new name of the security group
845        :type name: ``unicode``
846        :keyword description: new description of the security group
847        :type description: ``unicode``
848        """
849
850        params = {'Action': 'ModifySecurityGroupAttribute',
851                  'RegionId': self.region}
852        if not group_id:
853            raise AttributeError('group_id is required')
854        params["SecurityGroupId"] = group_id
855
856        if name:
857            params["SecurityGroupName"] = name
858        if description:
859            params["Description"] = description
860
861        resp = self.connection.request(self.path, params)
862        return resp.success()
863
864    def ex_list_security_groups(self, ex_filters=None):
865        """
866        List security groups in the current region.
867
868        :keyword ex_filters: security group attributes to filter results.
869        :type ex_filters: ``dict``
870
871        :return: a list of defined security groups
872        :rtype: ``list`` of ``ECSSecurityGroup``
873        """
874        params = {'Action': 'DescribeSecurityGroups',
875                  'RegionId': self.region}
876
877        if ex_filters and isinstance(ex_filters, dict):
878            ex_filters.update(params)
879            params = ex_filters
880
881        def _parse_response(resp_object):
882            sg_elements = findall(resp_object, 'SecurityGroups/SecurityGroup',
883                                  namespace=self.namespace)
884            sgs = [self._to_security_group(el) for el in sg_elements]
885            return sgs
886        return self._request_multiple_pages(self.path, params,
887                                            _parse_response)
888
889    def ex_list_security_group_attributes(self, group_id=None,
890                                          nic_type='internet'):
891        """
892        List security group attributes in the current region.
893
894        :keyword group_id: security group id.
895        :type group_id: ``str``
896
897        :keyword nic_type: internet|intranet.
898        :type nic_type: ``str``
899
900        :return: a list of defined security group Attributes
901        :rtype: ``list`` of ``ECSSecurityGroupAttribute``
902        """
903        params = {'Action': 'DescribeSecurityGroupAttribute',
904                  'RegionId': self.region,
905                  'NicType': nic_type}
906
907        if group_id is None:
908            raise AttributeError('group_id is required')
909        params['SecurityGroupId'] = group_id
910
911        resp_object = self.connection.request(self.path, params).object
912        sga_elements = findall(resp_object, 'Permissions/Permission',
913                               namespace=self.namespace)
914        return [self._to_security_group_attribute(el) for el in sga_elements]
915
916    def ex_join_security_group(self, node, group_id=None):
917        """
918        Join a node into security group.
919
920        :param node: The node to join security group
921        :type node: :class:`Node`
922
923        :param group_id: security group id.
924        :type group_id: ``str``
925
926
927        :return: join operation result.
928        :rtype: ``bool``
929        """
930        if group_id is None:
931            raise AttributeError('group_id is required')
932
933        if node.state != NodeState.RUNNING and \
934           node.state != NodeState.STOPPED:
935            raise LibcloudError('The node state with id % s need\
936                                be running or stopped .' % node.id)
937
938        params = {'Action': 'JoinSecurityGroup',
939                  'InstanceId': node.id,
940                  'SecurityGroupId': group_id}
941        resp = self.connection.request(self.path, params)
942        return resp.success()
943
944    def ex_leave_security_group(self, node, group_id=None):
945        """
946        Leave a node from security group.
947
948        :param node: The node to leave security group
949        :type node: :class:`Node`
950
951        :param group_id: security group id.
952        :type group_id: ``str``
953
954
955        :return: leave operation result.
956        :rtype: ``bool``
957        """
958        if group_id is None:
959            raise AttributeError('group_id is required')
960
961        if node.state != NodeState.RUNNING and \
962           node.state != NodeState.STOPPED:
963            raise LibcloudError('The node state with id % s need\
964                                be running or stopped .' % node.id)
965
966        params = {'Action': 'LeaveSecurityGroup',
967                  'InstanceId': node.id,
968                  'SecurityGroupId': group_id}
969        resp = self.connection.request(self.path, params)
970        return resp.success()
971
972    def ex_list_zones(self, region_id=None):
973        """
974        List availability zones in the given region or the current region.
975
976        :keyword region_id: the id of the region to query zones from
977        :type region_id: ``str``
978
979        :return: list of zones
980        :rtype: ``list`` of ``ECSZone``
981        """
982        params = {'Action': 'DescribeZones'}
983        if region_id:
984            params['RegionId'] = region_id
985        else:
986            params['RegionId'] = self.region
987        resp_body = self.connection.request(self.path, params).object
988        zone_elements = findall(resp_body, 'Zones/Zone',
989                                namespace=self.namespace)
990        zones = [self._to_zone(el) for el in zone_elements]
991        return zones
992    ##
993    # Volume and snapshot management methods
994    ##
995
996    def list_volumes(self, ex_volume_ids=None, ex_filters=None):
997        """
998        List all volumes.
999
1000        @inherits: :class:`NodeDriver.list_volumes`
1001
1002        :keyword ex_volume_ids: a list of volume's ids used to filter volumes.
1003                                Only the volumes which's id in this list
1004                                will be returned.
1005        :type ex_volume_ids: ``list`` of ``str``
1006
1007        :keyword ex_filters: volume attribute and value pairs to filter
1008                             volumes. Only the volumes which matchs all will
1009                             be returned.
1010                             If the filter attribute need a json array value,
1011                             use ``list`` object, the driver will convert it.
1012        :type ex_filters: ``dict``
1013        """
1014        params = {'Action': 'DescribeDisks',
1015                  'RegionId': self.region}
1016
1017        if ex_volume_ids:
1018            if isinstance(ex_volume_ids, list):
1019                params['DiskIds'] = self._list_to_json_array(ex_volume_ids)
1020            else:
1021                raise AttributeError('ex_volume_ids should be a list of '
1022                                     'volume ids.')
1023
1024        if ex_filters:
1025            if not isinstance(ex_filters, dict):
1026                raise AttributeError('ex_filters should be a dict of '
1027                                     'volume attributes.')
1028            else:
1029                for key in ex_filters.keys():
1030                    params[key] = ex_filters[key]
1031
1032        def _parse_response(resp_object):
1033            disk_elements = findall(resp_object, 'Disks/Disk',
1034                                    namespace=self.namespace)
1035            volumes = [self._to_volume(each) for each in disk_elements]
1036            return volumes
1037        return self._request_multiple_pages(self.path, params,
1038                                            _parse_response)
1039
1040    def list_volume_snapshots(self, volume, ex_snapshot_ids=[],
1041                              ex_filters=None):
1042        """
1043        List snapshots for a storage volume.
1044
1045        @inherites :class:`NodeDriver.list_volume_snapshots`
1046
1047        :keyword ex_snapshot_ids: a list of snapshot ids to filter the
1048                                  snapshots returned.
1049        :type ex_snapshot_ids: ``list`` of ``str``
1050
1051        :keyword ex_filters: snapshot attribute and value pairs to filter
1052                             snapshots. Only the snapshot which matchs all
1053                             the pairs will be returned.
1054                             If the filter attribute need a json array value,
1055                             use ``list`` object, the driver will convert it.
1056        :type ex_filters: ``dict``
1057        """
1058        params = {'Action': 'DescribeSnapshots',
1059                  'RegionId': self.region}
1060
1061        if volume:
1062            params['DiskId'] = volume.id
1063        if ex_snapshot_ids and isinstance(ex_snapshot_ids, list):
1064            params['SnapshotIds'] = self._list_to_json_array(ex_snapshot_ids)
1065        if ex_filters and isinstance(ex_filters, dict):
1066            for key in ex_filters.keys():
1067                params[key] = ex_filters[key]
1068
1069        def _parse_response(resp_body):
1070            snapshot_elements = findall(resp_body, 'Snapshots/Snapshot',
1071                                        namespace=self.namespace)
1072            snapshots = [self._to_snapshot(each) for each in snapshot_elements]
1073            return snapshots
1074
1075        return self._request_multiple_pages(self.path, params,
1076                                            _parse_response)
1077
1078    def create_volume(self, size, name, location=None, snapshot=None,
1079                      ex_zone_id=None, ex_description=None,
1080                      ex_disk_category=None, ex_client_token=None):
1081        """
1082        Create a new volume.
1083
1084        @inherites :class:`NodeDriver.create_volume`
1085
1086        :keyword ex_zone_id: the availability zone id (required)
1087        :type ex_zone_id: ``str``
1088
1089        :keyword ex_description: volume description
1090        :type ex_description: ``unicode``
1091
1092        :keyword ex_disk_category: disk category for data disk
1093        :type ex_disk_category: ``str``
1094
1095        :keyword ex_client_token: a token generated by client to identify
1096                                  each request.
1097        :type ex_client_token: ``str``
1098        """
1099        params = {'Action': 'CreateDisk',
1100                  'RegionId': self.region,
1101                  'DiskName': name,
1102                  'Size': size}
1103        if ex_zone_id is None:
1104            raise AttributeError('ex_zone_id is required')
1105        params['ZoneId'] = ex_zone_id
1106
1107        if snapshot is not None and isinstance(snapshot, VolumeSnapshot):
1108            params['SnapshotId'] = snapshot.id
1109        if ex_description:
1110            params['Description'] = ex_description
1111        if ex_disk_category:
1112            params['DiskCategory'] = ex_disk_category
1113        if ex_client_token:
1114            params['ClientToken'] = ex_client_token
1115        resp = self.connection.request(self.path, params).object
1116        volume_id = findtext(resp, 'DiskId', namespace=self.namespace)
1117        volumes = self.list_volumes(ex_volume_ids=[volume_id])
1118        if len(volumes) != 1:
1119            raise LibcloudError('could not find the new create volume '
1120                                'with id %s.' % volume_id,
1121                                driver=self)
1122        return volumes[0]
1123
1124    def create_volume_snapshot(self, volume, name=None, ex_description=None,
1125                               ex_client_token=None):
1126        """
1127        Creates a snapshot of the storage volume.
1128
1129        @inherits :class:`NodeDriver.create_volume_snapshot`
1130
1131        :keyword ex_description: description of the snapshot.
1132        :type ex_description: ``unicode``
1133
1134        :keyword ex_client_token: a token generated by client to identify
1135                                  each request.
1136        :type ex_client_token: ``str``
1137        """
1138        params = {'Action': 'CreateSnapshot',
1139                  'DiskId': volume.id}
1140        if name:
1141            params['SnapshotName'] = name
1142        if ex_description:
1143            params['Description'] = ex_description
1144        if ex_client_token:
1145            params['ClientToken'] = ex_client_token
1146
1147        snapshot_elements = self.connection.request(self.path, params).object
1148        snapshot_id = findtext(snapshot_elements, 'SnapshotId',
1149                               namespace=self.namespace)
1150        snapshots = self.list_volume_snapshots(volume=None,
1151                                               ex_snapshot_ids=[snapshot_id])
1152        if len(snapshots) != 1:
1153            raise LibcloudError('could not find new created snapshot with '
1154                                'id %s.' % snapshot_id, driver=self)
1155        return snapshots[0]
1156
1157    def attach_volume(self, node, volume, device=None,
1158                      ex_delete_with_instance=None):
1159        """
1160        Attaches volume to node.
1161
1162        @inherits :class:`NodeDriver.attach_volume`
1163
1164        :keyword device: device path allocated for this attached volume
1165        :type device: ``str`` between /dev/xvdb to xvdz,
1166                      if empty, allocated by the system
1167        :keyword ex_delete_with_instance: if to delete this volume when the
1168                                          instance is deleted.
1169        :type ex_delete_with_instance: ``bool``
1170        """
1171        params = {'Action': 'AttachDisk',
1172                  'InstanceId': node.id,
1173                  'DiskId': volume.id}
1174
1175        if device:
1176            params['Device'] = device
1177        if ex_delete_with_instance:
1178            params['DeleteWithInstance'] = \
1179                str(bool(ex_delete_with_instance)).lower()
1180        resp = self.connection.request(self.path, params)
1181        return resp.success()
1182
1183    def detach_volume(self, volume, ex_instance_id=None):
1184        """
1185        Detaches a volume from a node.
1186
1187        @inherits :class:`NodeDriver.detach_volume`
1188
1189        :keyword ex_instance_id: the id of the instance from which the volume
1190                                 is detached.
1191        :type ex_instance_id: ``str``
1192        """
1193        params = {'Action': 'DetachDisk',
1194                  'DiskId': volume.id}
1195
1196        if ex_instance_id:
1197            params['InstanceId'] = ex_instance_id
1198        else:
1199            volumes = self.list_volumes(ex_volume_ids=[volume.id])
1200            if len(volumes) != 1:
1201                raise AttributeError('could not find the instance id '
1202                                     'the volume %s attached to, '
1203                                     'ex_instance_id is required.' %
1204                                     volume.id)
1205            params['InstanceId'] = volumes[0].extra['instance_id']
1206
1207        resp = self.connection.request(self.path, params)
1208        return resp.success()
1209
1210    def destroy_volume(self, volume):
1211        params = {'Action': 'DeleteDisk',
1212                  'DiskId': volume.id}
1213        volumes = self.list_volumes(ex_volume_ids=[volume.id])
1214        if len(volumes) != 1:
1215            raise LibcloudError('could not find the volume with id %s.' %
1216                                volume.id,
1217                                driver=self)
1218        if volumes[0].state != StorageVolumeState.AVAILABLE:
1219            raise LibcloudError('only volume in AVAILABLE state could be '
1220                                'destroyed.', driver=self)
1221        resp = self.connection.request(self.path, params)
1222        return resp.success()
1223
1224    def destroy_volume_snapshot(self, snapshot):
1225        params = {'Action': 'DeleteSnapshot'}
1226
1227        if snapshot and isinstance(snapshot, VolumeSnapshot):
1228            params['SnapshotId'] = snapshot.id
1229        else:
1230            raise AttributeError('snapshot is required and must be a '
1231                                 'VolumeSnapshot')
1232        resp = self.connection.request(self.path, params)
1233        return resp.success()
1234
1235    ##
1236    # Image management methods
1237    ##
1238
1239    def list_images(self, location=None, ex_image_ids=None, ex_filters=None):
1240        """
1241        List images on a provider.
1242
1243        @inherits :class:`NodeDriver.list_images`
1244
1245        :keyword ex_image_ids: a list of image ids to filter the images to
1246                               be returned.
1247        :type ex_image_ids: ``list`` of ``str``
1248
1249        :keyword ex_filters: image attribute and value pairs to filter
1250                             images. Only the image which matchs all
1251                             the pairs will be returned.
1252                             If the filter attribute need a json array value,
1253                             use ``list`` object, the driver will convert it.
1254        :type ex_filters: ``dict``
1255        """
1256
1257        if location and isinstance(location, NodeLocation):
1258            region = location.id
1259        else:
1260            region = self.region
1261        params = {'Action': 'DescribeImages',
1262                  'RegionId': region}
1263        if ex_image_ids:
1264            if isinstance(ex_image_ids, list):
1265                params['ImageId'] = ','.join(ex_image_ids)
1266            else:
1267                raise AttributeError('ex_image_ids should be a list of '
1268                                     'image ids')
1269        if ex_filters and isinstance(ex_filters, dict):
1270            for key in ex_filters.keys():
1271                params[key] = ex_filters[key]
1272
1273        def _parse_response(resp_body):
1274            image_elements = findall(resp_body, 'Images/Image',
1275                                     namespace=self.namespace)
1276            images = [self._to_image(each) for each in image_elements]
1277            return images
1278        return self._request_multiple_pages(self.path, params,
1279                                            _parse_response)
1280
1281    def create_image(self, node, name, description=None, ex_snapshot_id=None,
1282                     ex_image_version=None, ex_client_token=None):
1283        """
1284        Creates an image from a system disk snapshot.
1285
1286        @inherits :class:`NodeDriver.create_image`
1287
1288        :keyword ex_snapshot_id: the id of the snapshot to create the image.
1289                                 (required)
1290        :type ex_snapshot_id: ``str``
1291
1292        :keyword ex_image_version: the version number of the image
1293        :type ex_image_version: ``str``
1294
1295        :keyword ex_client_token: a token generated by client to identify
1296                                  each request.
1297        :type ex_client_token: ``str``
1298        """
1299        params = {'Action': 'CreateImage',
1300                  'RegionId': self.region}
1301        if name:
1302            params['ImageName'] = name
1303        if description:
1304            params['Description'] = description
1305        if ex_snapshot_id:
1306            params['SnapshotId'] = ex_snapshot_id
1307        else:
1308            raise AttributeError('ex_snapshot_id is required')
1309        if ex_image_version:
1310            params['ImageVersion'] = ex_image_version
1311        if ex_client_token:
1312            params['ClientToken'] = ex_client_token
1313
1314        resp = self.connection.request(self.path, params)
1315        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
1316        return self.get_image(image_id=image_id)
1317
1318    def delete_image(self, node_image):
1319        params = {'Action': 'DeleteImage',
1320                  'RegionId': self.region,
1321                  'ImageId': node_image.id}
1322        resp = self.connection.request(self.path, params)
1323        return resp.success()
1324
1325    def get_image(self, image_id, ex_region_id=None):
1326        if ex_region_id:
1327            region = ex_region_id
1328        else:
1329            region = self.region
1330        location = NodeLocation(id=region, name=None, country=None,
1331                                driver=self)
1332        images = self.list_images(location, ex_image_ids=[image_id])
1333        if len(images) != 1:
1334            raise LibcloudError('could not find the image with id %s' %
1335                                image_id,
1336                                driver=self)
1337        return images[0]
1338
1339    def copy_image(self, source_region, node_image, name, description=None,
1340                   ex_destination_region_id=None, ex_client_token=None):
1341        """
1342        Copies an image from a source region to the destination region.
1343        If not provide a destination region, default to the current region.
1344
1345        @inherits :class:`NodeDriver.copy_image`
1346
1347        :keyword ex_destination_region_id: id of the destination region
1348        :type ex_destination_region_id: ``str``
1349
1350        :keyword ex_client_token: a token generated by client to identify
1351                                  each request.
1352        :type ex_client_token: ``str``
1353        """
1354        params = {'Action': 'CopyImage',
1355                  'RegionId': source_region,
1356                  'ImageId': node_image.id}
1357        if ex_destination_region_id is not None:
1358            params['DestinationRegionId'] = ex_destination_region_id
1359        else:
1360            params['DestinationRegionId'] = self.region
1361        if name:
1362            params['DestinationImageName'] = name
1363        if description:
1364            params['DestinationDescription'] = description
1365        if ex_client_token:
1366            params['ClientToken'] = ex_client_token
1367        resp = self.connection.request(self.path, params)
1368        image_id = findtext(resp.object, 'ImageId', namespace=self.namespace)
1369        return self.get_image(image_id=image_id)
1370
1371    def create_public_ip(self, instance_id):
1372        """
1373        Create public ip.
1374
1375        :keyword instance_id: instance id for allocating public ip.
1376        :type    instance_id: ``str``
1377
1378        :return public ip
1379        :rtype ``str``
1380        """
1381        params = {'Action': 'AllocatePublicIpAddress',
1382                  'InstanceId': instance_id}
1383
1384        resp = self.connection.request(self.path, params=params)
1385        return findtext(resp.object, 'IpAddress',
1386                        namespace=self.namespace)
1387
1388    def _to_nodes(self, object):
1389        """
1390        Convert response to Node object list
1391
1392        :param object: parsed response object
1393        :return: a list of ``Node``
1394        :rtype: ``list``
1395        """
1396        node_elements = findall(object, 'Instances/Instance', self.namespace)
1397        return [self._to_node(el) for el in node_elements]
1398
1399    def _to_node(self, instance):
1400        """
1401        Convert an InstanceAttributesType object to ``Node`` object
1402
1403        :param instance: a xml element represents an instance
1404        :return: a ``Node`` object
1405        :rtype: ``Node``
1406        """
1407        _id = findtext(element=instance, xpath='InstanceId',
1408                       namespace=self.namespace)
1409        name = findtext(element=instance, xpath='InstanceName',
1410                        namespace=self.namespace)
1411        instance_status = findtext(element=instance, xpath='Status',
1412                                   namespace=self.namespace)
1413        state = self.NODE_STATE_MAPPING.get(instance_status, NodeState.UNKNOWN)
1414
1415        def _get_ips(ip_address_els):
1416            return [each.text for each in ip_address_els]
1417
1418        public_ip_els = findall(element=instance,
1419                                xpath='PublicIpAddress/IpAddress',
1420                                namespace=self.namespace)
1421        public_ips = _get_ips(public_ip_els)
1422        private_ip_els = findall(element=instance,
1423                                 xpath='InnerIpAddress/IpAddress',
1424                                 namespace=self.namespace)
1425        private_ips = _get_ips(private_ip_els)
1426
1427        # Extra properties
1428        extra = self._get_extra_dict(instance,
1429                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['node'])
1430        extra['vpc_attributes'] = self._get_vpc_attributes(instance)
1431        extra['eip_address'] = self._get_eip_address(instance)
1432        extra['operation_locks'] = self._get_operation_locks(instance)
1433
1434        node = Node(id=_id, name=name, state=state,
1435                    public_ips=public_ips, private_ips=private_ips,
1436                    driver=self.connection.driver, extra=extra)
1437        return node
1438
1439    def _get_extra_dict(self, element, mapping):
1440        """
1441        Extract attributes from the element based on rules provided in the
1442        mapping dictionary.
1443
1444        :param      element: Element to parse the values from.
1445        :type       element: xml.etree.ElementTree.Element.
1446
1447        :param      mapping: Dictionary with the extra layout
1448        :type       node: :class:`Node`
1449
1450        :rtype: ``dict``
1451        """
1452        extra = {}
1453        for attribute, values in mapping.items():
1454            transform_func = values['transform_func']
1455            value = findattr(element=element,
1456                             xpath=values['xpath'],
1457                             namespace=self.namespace)
1458            if value:
1459                try:
1460                    extra[attribute] = transform_func(value)
1461                except Exception:
1462                    extra[attribute] = None
1463            else:
1464                extra[attribute] = value
1465
1466        return extra
1467
1468    def _get_internet_related_params(self, ex_internet_charge_type,
1469                                     ex_internet_max_bandwidth_in,
1470                                     ex_internet_max_bandwidth_out):
1471        params = {}
1472        if ex_internet_charge_type:
1473            params['InternetChargeType'] = ex_internet_charge_type
1474            if ex_internet_charge_type.lower() == 'paybytraffic':
1475                if ex_internet_max_bandwidth_out:
1476                    params['InternetMaxBandwidthOut'] = \
1477                        ex_internet_max_bandwidth_out
1478                else:
1479                    raise AttributeError('ex_internet_max_bandwidth_out is '
1480                                         'mandatory for PayByTraffic internet'
1481                                         ' charge type.')
1482            elif ex_internet_max_bandwidth_out:
1483                params['InternetMaxBandwidthOut'] = \
1484                    ex_internet_max_bandwidth_out
1485
1486        if ex_internet_max_bandwidth_in:
1487            params['InternetMaxBandwidthIn'] = \
1488                ex_internet_max_bandwidth_in
1489        return params
1490
1491    def _get_system_disk(self, ex_system_disk):
1492        if not isinstance(ex_system_disk, dict):
1493            raise AttributeError('ex_system_disk is not a dict')
1494        sys_disk_dict = ex_system_disk
1495        key_base = 'SystemDisk.'
1496        # TODO(samsong8610): Use a type instead of dict
1497        mappings = {'category': 'Category',
1498                    'disk_name': 'DiskName',
1499                    'description': 'Description'}
1500        params = {}
1501        for attr in mappings.keys():
1502            if attr in sys_disk_dict:
1503                params[key_base + mappings[attr]] = sys_disk_dict[attr]
1504        return params
1505
1506    def _get_data_disks(self, ex_data_disks):
1507        if isinstance(ex_data_disks, dict):
1508            data_disks = [ex_data_disks]
1509        elif isinstance(ex_data_disks, list):
1510            data_disks = ex_data_disks
1511        else:
1512            raise AttributeError('ex_data_disks should be a list of dict')
1513        # TODO(samsong8610): Use a type instead of dict
1514        mappings = {'size': 'Size',
1515                    'category': 'Category',
1516                    'snapshot_id': 'SnapshotId',
1517                    'disk_name': 'DiskName',
1518                    'description': 'Description',
1519                    'device': 'Device',
1520                    'delete_with_instance': 'DeleteWithInstance'}
1521        params = {}
1522        for idx, disk in enumerate(data_disks):
1523            key_base = 'DataDisk.{0}.'.format(idx + 1)
1524            for attr in mappings.keys():
1525                if attr in disk:
1526                    if attr == 'delete_with_instance':
1527                        # Convert bool value to str
1528                        value = str(disk[attr]).lower()
1529                    else:
1530                        value = disk[attr]
1531                    params[key_base + mappings[attr]] = value
1532        return params
1533
1534    def _get_vpc_attributes(self, instance):
1535        vpcs = findall(instance, xpath='VpcAttributes',
1536                       namespace=self.namespace)
1537        if len(vpcs) <= 0:
1538            return None
1539        return self._get_extra_dict(
1540            vpcs[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['vpc_attributes'])
1541
1542    def _get_eip_address(self, instance):
1543        eips = findall(instance, xpath='EipAddress',
1544                       namespace=self.namespace)
1545        if len(eips) <= 0:
1546            return None
1547        return self._get_extra_dict(
1548            eips[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['eip_address_associate'])
1549
1550    def _get_operation_locks(self, instance):
1551        locks = findall(instance, xpath='OperationLocks',
1552                        namespace=self.namespace)
1553        if len(locks) <= 0:
1554            return None
1555        return self._get_extra_dict(
1556            locks[0], RESOURCE_EXTRA_ATTRIBUTES_MAP['operation_locks'])
1557
1558    def _wait_until_state(self, nodes, state, wait_period=3, timeout=600):
1559        """
1560        Block until the provided nodes are in the desired state.
1561        :param nodes: List of nodes to wait for
1562        :type nodes: ``list`` of :class:`.Node`
1563        :param state: desired state
1564        :type state: ``NodeState``
1565        :param wait_period: How many seconds to wait between each loop
1566                            iteration. (default is 3)
1567        :type wait_period: ``int``
1568        :param timeout: How many seconds to wait before giving up.
1569                        (default is 600)
1570        :type timeout: ``int``
1571        :return: if the nodes are in the desired state.
1572        :rtype: ``bool``
1573        """
1574        start = time.time()
1575        end = start + timeout
1576        node_ids = [node.id for node in nodes]
1577
1578        while(time.time() < end):
1579            matched_nodes = self.list_nodes(ex_node_ids=node_ids)
1580            if len(matched_nodes) > len(node_ids):
1581                found_ids = [node.id for node in matched_nodes]
1582                msg = ('found multiple nodes with same ids, '
1583                       'desired ids: %(ids)s, found ids: %(found_ids)s' %
1584                       {'ids': node_ids, 'found_ids': found_ids})
1585                raise LibcloudError(value=msg, driver=self)
1586            desired_nodes = [node for node in matched_nodes
1587                             if node.state == state]
1588
1589            if len(desired_nodes) == len(node_ids):
1590                return True
1591            else:
1592                time.sleep(wait_period)
1593                continue
1594
1595        raise LibcloudError(value='Timed out after %s seconds' % (timeout),
1596                            driver=self)
1597
1598    def _to_volume(self, element):
1599        _id = findtext(element, 'DiskId', namespace=self.namespace)
1600        name = findtext(element, 'DiskName', namespace=self.namespace)
1601        size = int(findtext(element, 'Size', namespace=self.namespace))
1602        status_str = findtext(element, 'Status', namespace=self.namespace)
1603        status = self.VOLUME_STATE_MAPPING.get(status_str,
1604                                               StorageVolumeState.UNKNOWN)
1605
1606        extra = self._get_extra_dict(element,
1607                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['volume'])
1608        extra['operation_locks'] = self._get_operation_locks(element)
1609        return StorageVolume(_id, name, size, self, state=status, extra=extra)
1610
1611    def _list_to_json_array(self, value):
1612        try:
1613            return json.dumps(value)
1614        except Exception:
1615            raise AttributeError('could not convert list to json array')
1616
1617    def _to_snapshot(self, element):
1618        _id = findtext(element, 'SnapshotId', namespace=self.namespace)
1619        created = findtext(element, 'CreationTime', namespace=self.namespace)
1620        status_str = findtext(element, 'Status', namespace=self.namespace)
1621        state = self.SNAPSHOT_STATE_MAPPING.get(status_str,
1622                                                VolumeSnapshotState.UNKNOWN)
1623        extra = self._get_extra_dict(element,
1624                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['snapshot'])
1625        return VolumeSnapshot(id=_id, driver=self, extra=extra,
1626                              created=created, state=state)
1627
1628    def _to_size(self, element):
1629        _id = findtext(element, 'InstanceTypeId', namespace=self.namespace)
1630        ram = float(findtext(element, 'MemorySize', namespace=self.namespace))
1631        extra = {}
1632        extra['cpu_core_count'] = int(findtext(element, 'CpuCoreCount',
1633                                               namespace=self.namespace))
1634        extra['instance_type_family'] = findtext(element, 'InstanceTypeFamily',
1635                                                 namespace=self.namespace)
1636        return NodeSize(id=_id, name=_id, ram=ram, disk=None, bandwidth=None,
1637                        price=None, driver=self, extra=extra)
1638
1639    def _to_location(self, element):
1640        _id = findtext(element, 'RegionId', namespace=self.namespace)
1641        localname = findtext(element, 'LocalName', namespace=self.namespace)
1642        return NodeLocation(id=_id, name=localname, country=None, driver=self)
1643
1644    def _to_image(self, element):
1645        _id = findtext(element, 'ImageId', namespace=self.namespace)
1646        name = findtext(element, 'ImageName', namespace=self.namespace)
1647        extra = self._get_extra_dict(element,
1648                                     RESOURCE_EXTRA_ATTRIBUTES_MAP['image'])
1649        extra['disk_device_mappings'] = self._get_disk_device_mappings(
1650            element.find('DiskDeviceMappings'))
1651        return NodeImage(id=_id, name=name, driver=self, extra=extra)
1652
1653    def _get_disk_device_mappings(self, element):
1654        if element is None:
1655            return None
1656        mapping_element = element.find('DiskDeviceMapping')
1657        if mapping_element is not None:
1658            return self._get_extra_dict(
1659                mapping_element,
1660                RESOURCE_EXTRA_ATTRIBUTES_MAP['disk_device_mapping'])
1661        return None
1662
1663    def _to_security_group(self, element):
1664        _id = findtext(element, 'SecurityGroupId', namespace=self.namespace)
1665        name = findtext(element, 'SecurityGroupName',
1666                        namespace=self.namespace)
1667        description = findtext(element, 'Description',
1668                               namespace=self.namespace)
1669        vpc_id = findtext(element, 'VpcId', namespace=self.namespace)
1670        creation_time = findtext(element, 'CreationTime',
1671                                 namespace=self.namespace)
1672        return ECSSecurityGroup(_id, name, description=description,
1673                                driver=self, vpc_id=vpc_id,
1674                                creation_time=creation_time)
1675
1676    def _to_security_group_attribute(self, element):
1677        ip_protocol = findtext(element, 'IpProtocol', namespace=self.namespace)
1678        port_range = findtext(element, 'PortRange', namespace=self.namespace)
1679        source_group_id = findtext(element, 'SourceGroupId',
1680                                   namespace=self.namespace)
1681        policy = findtext(element, 'Policy', namespace=self.namespace)
1682        nic_type = findtext(element, 'NicType', namespace=self.namespace)
1683        return ECSSecurityGroupAttribute(ip_protocol=ip_protocol,
1684                                         port_range=port_range,
1685                                         source_group_id=source_group_id,
1686                                         policy=policy, nic_type=nic_type)
1687
1688    def _to_zone(self, element):
1689        _id = findtext(element, 'ZoneId', namespace=self.namespace)
1690        local_name = findtext(element, 'LocalName', namespace=self.namespace)
1691        resource_types = findall(element,
1692                                 'AvailableResourceCreation/ResourceTypes',
1693                                 namespace=self.namespace)
1694        instance_types = findall(element,
1695                                 'AvailableInstanceTypes/InstanceTypes',
1696                                 namespace=self.namespace)
1697        disk_categories = findall(element,
1698                                  'AvailableDiskCategories/DiskCategories',
1699                                  namespace=self.namespace)
1700
1701        def _text(element):
1702            return element.text
1703
1704        return ECSZone(id=_id, name=local_name, driver=self,
1705                       available_resource_types=list(
1706                           map(_text, resource_types)),
1707                       available_instance_types=list(
1708                           map(_text, instance_types)),
1709                       available_disk_categories=list(
1710                           map(_text, disk_categories)))
1711
1712    def _get_pagination(self, element):
1713        page_number = int(findtext(element, 'PageNumber'))
1714        total_count = int(findtext(element, 'TotalCount'))
1715        page_size = int(findtext(element, 'PageSize'))
1716        return Pagination(total=total_count, size=page_size,
1717                          current=page_number)
1718
1719    def _request_multiple_pages(self, path, params, parse_func):
1720        """
1721        Request all resources by multiple pages.
1722        :param path: the resource path
1723        :type path: ``str``
1724        :param params: the query parameters
1725        :type params: ``dict``
1726        :param parse_func: the function object to parse the response body
1727        :param type: ``function``
1728        :return: list of resource object, if not found any, return []
1729        :rtype: ``list``
1730        """
1731        results = []
1732        while True:
1733            one_page = self.connection.request(path, params).object
1734            resources = parse_func(one_page)
1735            results += resources
1736            pagination = self._get_pagination(one_page)
1737            if pagination.next() is None:
1738                break
1739            params.update(pagination.to_dict())
1740        return results
1741