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