Source code for libcloud.compute.drivers.ec2

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Amazon EC2, Eucalyptus, Nimbus and Outscale drivers.
"""

from typing import List

import re
import base64
import copy
import warnings
import time

from libcloud.utils.py3 import ET
from libcloud.utils.py3 import b, basestring, ensure_string

from libcloud.utils.xml import fixxpath, findtext, findattr, findall
from libcloud.utils.publickey import get_pubkey_ssh2_fingerprint
from libcloud.utils.publickey import get_pubkey_comment
from libcloud.utils.iso8601 import parse_date
from libcloud.common.aws import AWSBaseResponse, SignedAWSConnection
from libcloud.common.aws import DEFAULT_SIGNATURE_VERSION
from libcloud.common.types import (InvalidCredsError, MalformedResponseError,
                                   LibcloudError)
from libcloud.compute.providers import Provider
from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize
from libcloud.compute.base import NodeImage, StorageVolume, VolumeSnapshot
from libcloud.compute.base import KeyPair
from libcloud.compute.types import NodeState, KeyPairDoesNotExistError, \
    StorageVolumeState, VolumeSnapshotState
from libcloud.compute.constants import INSTANCE_TYPES, REGION_DETAILS

__all__ = [
    'API_VERSION',
    'NAMESPACE',
    'INSTANCE_TYPES',
    'OUTSCALE_INSTANCE_TYPES',
    'OUTSCALE_SAS_REGION_DETAILS',
    'OUTSCALE_INC_REGION_DETAILS',
    'DEFAULT_EUCA_API_VERSION',
    'EUCA_NAMESPACE',

    'EC2NodeDriver',
    'BaseEC2NodeDriver',

    'NimbusNodeDriver',
    'EucNodeDriver',

    'OutscaleSASNodeDriver',
    'OutscaleINCNodeDriver',

    'EC2NodeLocation',
    'EC2ReservedNode',
    'EC2SecurityGroup',
    'EC2ImportSnapshotTask',
    'EC2PlacementGroup',
    'EC2Network',
    'EC2NetworkSubnet',
    'EC2NetworkInterface',
    'EC2RouteTable',
    'EC2Route',
    'EC2SubnetAssociation',
    'ExEC2AvailabilityZone',

    'IdempotentParamError'
]

API_VERSION = '2016-11-15'
NAMESPACE = 'http://ec2.amazonaws.com/doc/%s/' % (API_VERSION)

# Eucalyptus Constants
DEFAULT_EUCA_API_VERSION = '3.3.0'
EUCA_NAMESPACE = 'http://msgs.eucalyptus.com/%s' % (DEFAULT_EUCA_API_VERSION)

# Outscale Constants
DEFAULT_OUTSCALE_API_VERSION = '2016-04-01'
OUTSCALE_NAMESPACE = 'http://api.outscale.com/wsdl/fcuext/2014-04-15/'

# Add Nimbus region
REGION_DETAILS['nimbus'] = {
    # Nimbus clouds have 3 EC2-style instance types but their particular
    # RAM allocations are configured by the admin
    'country': 'custom',
    'signature_version': '2',
    'instance_types': [
        'm1.small',
        'm1.large',
        'm1.xlarge'
    ]
}

"""
Sizes must be hardcoded because Outscale doesn't provide an API to fetch them.
Outscale cloud instances share some names with EC2 but have different
specifications so declare them in another constant.
"""
OUTSCALE_INSTANCE_TYPES = {
    't1.micro': {
        'id': 't1.micro',
        'name': 'Micro Instance',
        'ram': 615,
        'disk': 0,
        'bandwidth': None
    },
    'm1.small': {
        'id': 'm1.small',
        'name': 'Standard Small Instance',
        'ram': 1740,
        'disk': 150,
        'bandwidth': None
    },
    'm1.medium': {
        'id': 'm1.medium',
        'name': 'Standard Medium Instance',
        'ram': 3840,
        'disk': 420,
        'bandwidth': None
    },
    'm1.large': {
        'id': 'm1.large',
        'name': 'Standard Large Instance',
        'ram': 7680,
        'disk': 840,
        'bandwidth': None
    },
    'm1.xlarge': {
        'id': 'm1.xlarge',
        'name': 'Standard Extra Large Instance',
        'ram': 15360,
        'disk': 1680,
        'bandwidth': None
    },
    'c1.medium': {
        'id': 'c1.medium',
        'name': 'Compute Optimized Medium Instance',
        'ram': 1740,
        'disk': 340,
        'bandwidth': None
    },
    'c1.xlarge': {
        'id': 'c1.xlarge',
        'name': 'Compute Optimized Extra Large Instance',
        'ram': 7168,
        'disk': 1680,
        'bandwidth': None
    },
    'c3.large': {
        'id': 'c3.large',
        'name': 'Compute Optimized Large Instance',
        'ram': 3840,
        'disk': 32,
        'bandwidth': None
    },
    'c3.xlarge': {
        'id': 'c3.xlarge',
        'name': 'Compute Optimized Extra Large Instance',
        'ram': 7168,
        'disk': 80,
        'bandwidth': None
    },
    'c3.2xlarge': {
        'id': 'c3.2xlarge',
        'name': 'Compute Optimized Double Extra Large Instance',
        'ram': 15359,
        'disk': 160,
        'bandwidth': None
    },
    'c3.4xlarge': {
        'id': 'c3.4xlarge',
        'name': 'Compute Optimized Quadruple Extra Large Instance',
        'ram': 30720,
        'disk': 320,
        'bandwidth': None
    },
    'c3.8xlarge': {
        'id': 'c3.8xlarge',
        'name': 'Compute Optimized Eight Extra Large Instance',
        'ram': 61440,
        'disk': 640,
        'bandwidth': None
    },
    'm2.xlarge': {
        'id': 'm2.xlarge',
        'name': 'High Memory Extra Large Instance',
        'ram': 17510,
        'disk': 420,
        'bandwidth': None
    },
    'm2.2xlarge': {
        'id': 'm2.2xlarge',
        'name': 'High Memory Double Extra Large Instance',
        'ram': 35020,
        'disk': 840,
        'bandwidth': None
    },
    'm2.4xlarge': {
        'id': 'm2.4xlarge',
        'name': 'High Memory Quadruple Extra Large Instance',
        'ram': 70042,
        'disk': 1680,
        'bandwidth': None
    },
    'nv1.small': {
        'id': 'nv1.small',
        'name': 'GPU Small Instance',
        'ram': 1739,
        'disk': 150,
        'bandwidth': None
    },
    'nv1.medium': {
        'id': 'nv1.medium',
        'name': 'GPU Medium Instance',
        'ram': 3839,
        'disk': 420,
        'bandwidth': None
    },
    'nv1.large': {
        'id': 'nv1.large',
        'name': 'GPU Large Instance',
        'ram': 7679,
        'disk': 840,
        'bandwidth': None
    },
    'nv1.xlarge': {
        'id': 'nv1.xlarge',
        'name': 'GPU Extra Large Instance',
        'ram': 15358,
        'disk': 1680,
        'bandwidth': None
    },
    'g2.2xlarge': {
        'id': 'g2.2xlarge',
        'name': 'GPU Double Extra Large Instance',
        'ram': 15360,
        'disk': 60,
        'bandwidth': None
    },
    'cc1.4xlarge': {
        'id': 'cc1.4xlarge',
        'name': 'Cluster Compute Quadruple Extra Large Instance',
        'ram': 24576,
        'disk': 1680,
        'bandwidth': None
    },
    'cc2.8xlarge': {
        'id': 'cc2.8xlarge',
        'name': 'Cluster Compute Eight Extra Large Instance',
        'ram': 65536,
        'disk': 3360,
        'bandwidth': None
    },
    'hi1.xlarge': {
        'id': 'hi1.xlarge',
        'name': 'High Storage Extra Large Instance',
        'ram': 15361,
        'disk': 1680,
        'bandwidth': None
    },
    'm3.xlarge': {
        'id': 'm3.xlarge',
        'name': 'High Storage Optimized Extra Large Instance',
        'ram': 15357,
        'disk': 0,
        'bandwidth': None
    },
    'm3.2xlarge': {
        'id': 'm3.2xlarge',
        'name': 'High Storage Optimized Double Extra Large Instance',
        'ram': 30720,
        'disk': 0,
        'bandwidth': None
    },
    'm3s.xlarge': {
        'id': 'm3s.xlarge',
        'name': 'High Storage Optimized Extra Large Instance',
        'ram': 15359,
        'disk': 0,
        'bandwidth': None
    },
    'm3s.2xlarge': {
        'id': 'm3s.2xlarge',
        'name': 'High Storage Optimized Double Extra Large Instance',
        'ram': 30719,
        'disk': 0,
        'bandwidth': None
    },
    'cr1.8xlarge': {
        'id': 'cr1.8xlarge',
        'name': 'Memory Optimized Eight Extra Large Instance',
        'ram': 249855,
        'disk': 240,
        'bandwidth': None
    },
    'os1.2xlarge': {
        'id': 'os1.2xlarge',
        'name': 'Memory Optimized, High Storage, Passthrough NIC Double Extra '
                'Large Instance',
        'ram': 65536,
        'disk': 60,
        'bandwidth': None
    },
    'os1.4xlarge': {
        'id': 'os1.4xlarge',
        'name': 'Memory Optimized, High Storage, Passthrough NIC Quadruple Ext'
                'ra Large Instance',
        'ram': 131072,
        'disk': 120,
        'bandwidth': None
    },
    'os1.8xlarge': {
        'id': 'os1.8xlarge',
        'name': 'Memory Optimized, High Storage, Passthrough NIC Eight Extra L'
                'arge Instance',
        'ram': 249856,
        'disk': 500,
        'bandwidth': None
    },
    'oc1.4xlarge': {
        'id': 'oc1.4xlarge',
        'name': 'Outscale Quadruple Extra Large Instance',
        'ram': 24575,
        'disk': 1680,
        'bandwidth': None
    },
    'oc2.8xlarge': {
        'id': 'oc2.8xlarge',
        'name': 'Outscale Eight Extra Large Instance',
        'ram': 65535,
        'disk': 3360,
        'bandwidth': None
    }
}


"""
The function manipulating Outscale cloud regions will be overridden because
Outscale instances types are in a separate dict so also declare Outscale cloud
regions in some other constants.
"""
OUTSCALE_SAS_REGION_DETAILS = {
    'eu-west-3': {
        'endpoint': 'api-ppd.outscale.com',
        'api_name': 'osc_sas_eu_west_3',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'eu-west-1': {
        'endpoint': 'api.eu-west-1.outscale.com',
        'api_name': 'osc_sas_eu_west_1',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'eu-west-2': {
        'endpoint': 'fcu.eu-west-2.outscale.com',
        'api_name': 'osc_sas_eu_west_2',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'us-east-1': {
        'endpoint': 'api.us-east-1.outscale.com',
        'api_name': 'osc_sas_us_east_1',
        'country': 'USA',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'us-east-2': {
        'endpoint': 'fcu.us-east-2.outscale.com',
        'api_name': 'osc_sas_us_east_2',
        'country': 'USA',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    }
}


OUTSCALE_INC_REGION_DETAILS = {
    'eu-west-1': {
        'endpoint': 'api.eu-west-1.outscale.com',
        'api_name': 'osc_inc_eu_west_1',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'p2.xlarge',
            'p2.8xlarge',
            'p2.16xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'eu-west-2': {
        'endpoint': 'fcu.eu-west-2.outscale.com',
        'api_name': 'osc_inc_eu_west_2',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'eu-west-3': {
        'endpoint': 'api-ppd.outscale.com',
        'api_name': 'osc_inc_eu_west_3',
        'country': 'FRANCE',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'us-east-1': {
        'endpoint': 'api.us-east-1.outscale.com',
        'api_name': 'osc_inc_us_east_1',
        'country': 'USA',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    },
    'us-east-2': {
        'endpoint': 'fcu.us-east-2.outscale.com',
        'api_name': 'osc_inc_us_east_2',
        'country': 'USA',
        'instance_types': [
            't1.micro',
            'm1.small',
            'm1.medium',
            'm1.large',
            'm1.xlarge',
            'c1.medium',
            'c1.xlarge',
            'm2.xlarge',
            'm2.2xlarge',
            'm2.4xlarge',
            'nv1.small',
            'nv1.medium',
            'nv1.large',
            'nv1.xlarge',
            'cc1.4xlarge',
            'cc2.8xlarge',
            'm3.xlarge',
            'm3.2xlarge',
            'cr1.8xlarge',
            'os1.8xlarge'
        ]
    }
}


"""
Define the extra dictionary for specific resources
"""
RESOURCE_EXTRA_ATTRIBUTES_MAP = {
    'ebs_instance_block_device': {
        'attach_time': {
            'xpath': 'ebs/attachTime',
            'transform_func': parse_date
        },
        'delete': {
            'xpath': 'ebs/deleteOnTermination',
            'transform_func': str
        },
        'status': {
            'xpath': 'ebs/status',
            'transform_func': str
        },
        'volume_id': {
            'xpath': 'ebs/volumeId',
            'transform_func': str
        }
    },
    'ebs_volume': {
        'snapshot_id': {
            'xpath': 'ebs/snapshotId',
            'transform_func': str
        },
        'volume_id': {
            'xpath': 'ebs/volumeId',
            'transform_func': str
        },
        'volume_size': {
            'xpath': 'ebs/volumeSize',
            'transform_func': int
        },
        'delete': {
            'xpath': 'ebs/deleteOnTermination',
            'transform_func': str
        },
        'volume_type': {
            'xpath': 'ebs/volumeType',
            'transform_func': str
        },
        'iops': {
            'xpath': 'ebs/iops',
            'transform_func': int
        }
    },
    'elastic_ip': {
        'allocation_id': {
            'xpath': 'allocationId',
            'transform_func': str,
        },
        'association_id': {
            'xpath': 'associationId',
            'transform_func': str,
        },
        'interface_id': {
            'xpath': 'networkInterfaceId',
            'transform_func': str,
        },
        'owner_id': {
            'xpath': 'networkInterfaceOwnerId',
            'transform_func': str,
        },
        'private_ip': {
            'xpath': 'privateIp',
            'transform_func': str,
        }
    },
    'image': {
        'state': {
            'xpath': 'imageState',
            'transform_func': str
        },
        'owner_id': {
            'xpath': 'imageOwnerId',
            'transform_func': str
        },
        'owner_alias': {
            'xpath': 'imageOwnerAlias',
            'transform_func': str
        },
        'is_public': {
            'xpath': 'isPublic',
            'transform_func': str
        },
        'architecture': {
            'xpath': 'architecture',
            'transform_func': str
        },
        'image_type': {
            'xpath': 'imageType',
            'transform_func': str
        },
        'image_location': {
            'xpath': 'imageLocation',
            'transform_func': str
        },
        'platform': {
            'xpath': 'platform',
            'transform_func': str
        },
        'description': {
            'xpath': 'description',
            'transform_func': str
        },
        'root_device_type': {
            'xpath': 'rootDeviceType',
            'transform_func': str
        },
        'virtualization_type': {
            'xpath': 'virtualizationType',
            'transform_func': str
        },
        'hypervisor': {
            'xpath': 'hypervisor',
            'transform_func': str
        },
        'kernel_id': {
            'xpath': 'kernelId',
            'transform_func': str
        },
        'ramdisk_id': {
            'xpath': 'ramdiskId',
            'transform_func': str
        },
        'ena_support': {
            'xpath': 'enaSupport',
            'transform_func': str
        },
        'sriov_net_support': {
            'xpath': 'sriovNetSupport',
            'transform_func': str
        }
    },
    'network': {
        'state': {
            'xpath': 'state',
            'transform_func': str
        },
        'dhcp_options_id': {
            'xpath': 'dhcpOptionsId',
            'transform_func': str
        },
        'instance_tenancy': {
            'xpath': 'instanceTenancy',
            'transform_func': str
        },
        'is_default': {
            'xpath': 'isDefault',
            'transform_func': str
        }
    },
    'network_interface': {
        'subnet_id': {
            'xpath': 'subnetId',
            'transform_func': str
        },
        'vpc_id': {
            'xpath': 'vpcId',
            'transform_func': str
        },
        'zone': {
            'xpath': 'availabilityZone',
            'transform_func': str
        },
        'description': {
            'xpath': 'description',
            'transform_func': str
        },
        'owner_id': {
            'xpath': 'ownerId',
            'transform_func': str
        },
        'mac_address': {
            'xpath': 'macAddress',
            'transform_func': str
        },
        'private_dns_name': {
            'xpath': 'privateIpAddressesSet/privateDnsName',
            'transform_func': str
        },
        'source_dest_check': {
            'xpath': 'sourceDestCheck',
            'transform_func': str
        }
    },
    'network_interface_attachment': {
        'attachment_id': {
            'xpath': 'attachment/attachmentId',
            'transform_func': str
        },
        'instance_id': {
            'xpath': 'attachment/instanceId',
            'transform_func': str
        },
        'owner_id': {
            'xpath': 'attachment/instanceOwnerId',
            'transform_func': str
        },
        'device_index': {
            'xpath': 'attachment/deviceIndex',
            'transform_func': int
        },
        'status': {
            'xpath': 'attachment/status',
            'transform_func': str
        },
        'attach_time': {
            'xpath': 'attachment/attachTime',
            'transform_func': parse_date
        },
        'delete': {
            'xpath': 'attachment/deleteOnTermination',
            'transform_func': str
        }
    },
    'node': {
        'availability': {
            'xpath': 'placement/availabilityZone',
            'transform_func': str
        },
        'architecture': {
            'xpath': 'architecture',
            'transform_func': str
        },
        'client_token': {
            'xpath': 'clientToken',
            'transform_func': str
        },
        'dns_name': {
            'xpath': 'dnsName',
            'transform_func': str
        },
        'hypervisor': {
            'xpath': 'hypervisor',
            'transform_func': str
        },
        'iam_profile': {
            'xpath': 'iamInstanceProfile/id',
            'transform_func': str
        },
        'image_id': {
            'xpath': 'imageId',
            'transform_func': str
        },
        'instance_id': {
            'xpath': 'instanceId',
            'transform_func': str
        },
        'instance_lifecycle': {
            'xpath': 'instanceLifecycle',
            'transform_func': str
        },
        'instance_tenancy': {
            'xpath': 'placement/tenancy',
            'transform_func': str
        },
        'instance_type': {
            'xpath': 'instanceType',
            'transform_func': str
        },
        'key_name': {
            'xpath': 'keyName',
            'transform_func': str
        },
        'launch_index': {
            'xpath': 'amiLaunchIndex',
            'transform_func': int
        },
        'launch_time': {
            'xpath': 'launchTime',
            'transform_func': str
        },
        'kernel_id': {
            'xpath': 'kernelId',
            'transform_func': str
        },
        'monitoring': {
            'xpath': 'monitoring/state',
            'transform_func': str
        },
        'platform': {
            'xpath': 'platform',
            'transform_func': str
        },
        'private_dns': {
            'xpath': 'privateDnsName',
            'transform_func': str
        },
        'ramdisk_id': {
            'xpath': 'ramdiskId',
            'transform_func': str
        },
        'root_device_type': {
            'xpath': 'rootDeviceType',
            'transform_func': str
        },
        'root_device_name': {
            'xpath': 'rootDeviceName',
            'transform_func': str
        },
        'reason': {
            'xpath': 'reason',
            'transform_func': str
        },
        'source_dest_check': {
            'xpath': 'sourceDestCheck',
            'transform_func': str
        },
        'status': {
            'xpath': 'instanceState/name',
            'transform_func': str
        },
        'subnet_id': {
            'xpath': 'subnetId',
            'transform_func': str
        },
        'virtualization_type': {
            'xpath': 'virtualizationType',
            'transform_func': str
        },
        'ebs_optimized': {
            'xpath': 'ebsOptimized',
            'transform_func': str
        },
        'vpc_id': {
            'xpath': 'vpcId',
            'transform_func': str
        }
    },
    'reserved_node': {
        'instance_type': {
            'xpath': 'instanceType',
            'transform_func': str
        },
        'availability': {
            'xpath': 'availabilityZone',
            'transform_func': str
        },
        'start': {
            'xpath': 'start',
            'transform_func': str
        },
        'end': {
            'xpath': 'end',
            'transform_func': str
        },
        'duration': {
            'xpath': 'duration',
            'transform_func': int
        },
        'usage_price': {
            'xpath': 'usagePrice',
            'transform_func': float
        },
        'fixed_price': {
            'xpath': 'fixedPrice',
            'transform_func': float
        },
        'instance_count': {
            'xpath': 'instanceCount',
            'transform_func': int
        },
        'description': {
            'xpath': 'productDescription',
            'transform_func': str
        },
        'instance_tenancy': {
            'xpath': 'instanceTenancy',
            'transform_func': str
        },
        'currency_code': {
            'xpath': 'currencyCode',
            'transform_func': str
        },
        'offering_type': {
            'xpath': 'offeringType',
            'transform_func': str
        }
    },
    'security_group': {
        'vpc_id': {
            'xpath': 'vpcId',
            'transform_func': str
        },
        'description': {
            'xpath': 'groupDescription',
            'transform_func': str
        },
        'owner_id': {
            'xpath': 'ownerId',
            'transform_func': str
        }
    },
    'snapshot': {
        'volume_id': {
            'xpath': 'volumeId',
            'transform_func': str
        },
        'state': {
            'xpath': 'status',
            'transform_func': str
        },
        'description': {
            'xpath': 'description',
            'transform_func': str
        },
        'progress': {
            'xpath': 'progress',
            'transform_func': str
        },
        'start_time': {
            'xpath': 'startTime',
            'transform_func': parse_date
        }
    },
    'subnet': {
        'cidr_block': {
            'xpath': 'cidrBlock',
            'transform_func': str
        },
        'available_ips': {
            'xpath': 'availableIpAddressCount',
            'transform_func': int
        },
        'zone': {
            'xpath': 'availabilityZone',
            'transform_func': str
        },
        'vpc_id': {
            'xpath': 'vpcId',
            'transform_func': str
        }
    },
    'volume': {
        'device': {
            'xpath': 'attachmentSet/item/device',
            'transform_func': str
        },
        'snapshot_id': {
            'xpath': 'snapshotId',
            'transform_func': lambda v: str(v) or None
        },
        'iops': {
            'xpath': 'iops',
            'transform_func': int
        },
        'zone': {
            'xpath': 'availabilityZone',
            'transform_func': str
        },
        'create_time': {
            'xpath': 'createTime',
            'transform_func': parse_date
        },
        'state': {
            'xpath': 'status',
            'transform_func': str
        },
        'encrypted': {
            'xpath': 'encrypted',
            'transform_func': lambda x: {'true': True, 'false': False}.get(x)
        },
        'attach_time': {
            'xpath': 'attachmentSet/item/attachTime',
            'transform_func': parse_date
        },
        'attachment_status': {
            'xpath': 'attachmentSet/item/status',
            'transform_func': str
        },
        'instance_id': {
            'xpath': 'attachmentSet/item/instanceId',
            'transform_func': str
        },
        'delete': {
            'xpath': 'attachmentSet/item/deleteOnTermination',
            'transform_func': str
        },
        'volume_type': {
            'xpath': 'volumeType',
            'transform_func': str
        }
    },
    'route_table': {
        'vpc_id': {
            'xpath': 'vpcId',
            'transform_func': str
        }
    }
}

VOLUME_MODIFICATION_ATTRIBUTE_MAP = {
    'end_time': {
        'xpath': 'endTime',
        'transform_func': parse_date
    },
    'modification_state': {
        'xpath': 'modificationState',
        'transform_func': str
    },
    'original_iops': {
        'xpath': 'originalIops',
        'transform_func': int
    },
    'original_size': {
        'xpath': 'originalSize',
        'transform_func': int
    },
    'original_volume_type': {
        'xpath': 'originalVolumeType',
        'transform_func': str
    },
    'progress': {
        'xpath': 'progress',
        'transform_func': int
    },
    'start_time': {
        'xpath': 'startTime',
        'transform_func': parse_date
    },
    'status_message': {
        'xpath': 'statusMessage',
        'transform_func': str
    },
    'target_iops': {
        'xpath': 'targetIops',
        'transform_func': int
    },
    'target_size': {
        'xpath': 'targetSize',
        'transform_func': int
    },
    'target_volume_type': {
        'xpath': 'targetVolumeType',
        'transform_func': str
    },
    'volume_id': {
        'xpath': 'volumeId',
        'transform_func': str
    }
}

VALID_EC2_REGIONS = REGION_DETAILS.keys()
VALID_EC2_REGIONS = [r for r in VALID_EC2_REGIONS if r != 'nimbus']
VALID_VOLUME_TYPES = ['standard', 'io1', 'gp2', 'st1', 'sc1']


[docs]class EC2NodeLocation(NodeLocation): def __init__(self, id, name, country, driver, availability_zone): super(EC2NodeLocation, self).__init__(id, name, country, driver) self.availability_zone = availability_zone def __repr__(self): return (('<EC2NodeLocation: id=%s, name=%s, country=%s, ' 'availability_zone=%s driver=%s>') % (self.id, self.name, self.country, self.availability_zone, self.driver.name))
class EC2Response(AWSBaseResponse): """ EC2 specific response parsing and error handling. """ def parse_error(self): err_list = [] # Okay, so for Eucalyptus, you can get a 403, with no body, # if you are using the wrong user/password. msg = "Failure: 403 Forbidden" if self.status == 403 and self.body[:len(msg)] == msg: raise InvalidCredsError(msg) try: body = ET.XML(self.body) except Exception: raise MalformedResponseError("Failed to parse XML", body=self.body, driver=EC2NodeDriver) for err in body.findall('Errors/Error'): code, message = list(err) err_list.append('%s: %s' % (code.text, message.text)) if code.text == 'InvalidClientTokenId': raise InvalidCredsError(err_list[-1]) if code.text == 'SignatureDoesNotMatch': raise InvalidCredsError(err_list[-1]) if code.text == 'AuthFailure': raise InvalidCredsError(err_list[-1]) if code.text == 'OptInRequired': raise InvalidCredsError(err_list[-1]) if code.text == 'IdempotentParameterMismatch': raise IdempotentParamError(err_list[-1]) if code.text == 'InvalidKeyPair.NotFound': # TODO: Use connection context instead match = re.match(r'.*\'(.+?)\'.*', message.text) if match: name = match.groups()[0] else: name = None raise KeyPairDoesNotExistError(name=name, driver=self.connection.driver) return '\n'.join(err_list) class EC2Connection(SignedAWSConnection): """ Represents a single connection to the EC2 Endpoint. """ version = API_VERSION host = REGION_DETAILS['us-east-1']['endpoint'] responseCls = EC2Response service_name = 'ec2'
[docs]class ExEC2AvailabilityZone(object): """ Extension class which stores information about an EC2 availability zone. Note: This class is EC2 specific. """ def __init__(self, name, zone_state, region_name): self.name = name self.zone_state = zone_state self.region_name = region_name def __repr__(self): return (('<ExEC2AvailabilityZone: name=%s, zone_state=%s, ' 'region_name=%s>') % (self.name, self.zone_state, self.region_name))
[docs]class EC2ReservedNode(Node): """ Class which stores information about EC2 reserved instances/nodes Inherits from Node and passes in None for name and private/public IPs Note: This class is EC2 specific. """ def __init__(self, id, state, driver, size=None, image=None, extra=None): super(EC2ReservedNode, self).__init__(id=id, name=None, state=state, public_ips=None, private_ips=None, driver=driver, extra=extra) def __repr__(self): return (('<EC2ReservedNode: id=%s>') % (self.id))
[docs]class EC2SecurityGroup(object): """ Represents information about a Security group Note: This class is EC2 specific. """ def __init__(self, id, name, ingress_rules, egress_rules, extra=None): self.id = id self.name = name self.ingress_rules = ingress_rules self.egress_rules = egress_rules self.extra = extra or {} def __repr__(self): return (('<EC2SecurityGroup: id=%s, name=%s') % (self.id, self.name))
[docs]class EC2ImportSnapshotTask(object): """ Represents information about a describe_import_snapshot_task. Note: This class is EC2 specific. """ def __init__(self, status, snapshotId): self.status = status self.snapshotId = snapshotId def __repr__(self): return (('<EC2SecurityGroup: status=%s, snapshotId=%s') % (self.status, self.snapshotId))
[docs]class EC2PlacementGroup(object): """ Represents information about a Placement Grous Note: This class is EC2 specific. """ def __init__(self, name, state, strategy='cluster', extra=None): self.name = name self.strategy = strategy self.extra = extra or {} def __repr__(self): return '<EC2PlacementGroup: name=%s, state=%s>' % (self.name, self.strategy)
[docs]class EC2Network(object): """ Represents information about a VPC (Virtual Private Cloud) network Note: This class is EC2 specific. """ def __init__(self, id, name, cidr_block, extra=None): self.id = id self.name = name self.cidr_block = cidr_block self.extra = extra or {} def __repr__(self): return (('<EC2Network: id=%s, name=%s') % (self.id, self.name))
[docs]class EC2NetworkSubnet(object): """ Represents information about a VPC (Virtual Private Cloud) subnet Note: This class is EC2 specific. """ def __init__(self, id, name, state, extra=None): self.id = id self.name = name self.state = state self.extra = extra or {} def __repr__(self): return (('<EC2NetworkSubnet: id=%s, name=%s') % (self.id, self.name))
[docs]class EC2NetworkInterface(object): """ Represents information about a VPC network interface Note: This class is EC2 specific. The state parameter denotes the current status of the interface. Valid values for state are attaching, attached, detaching and detached. """ def __init__(self, id, name, state, extra=None): self.id = id self.name = name self.state = state self.extra = extra or {} def __repr__(self): return (('<EC2NetworkInterface: id=%s, name=%s') % (self.id, self.name))
class ElasticIP(object): """ Represents information about an elastic IP address :param ip: The elastic IP address :type ip: ``str`` :param domain: The domain that the IP resides in (EC2-Classic/VPC). EC2 classic is represented with standard and VPC is represented with vpc. :type domain: ``str`` :param instance_id: The identifier of the instance which currently has the IP associated. :type instance_id: ``str`` Note: This class is used to support both EC2 and VPC IPs. For VPC specific attributes are stored in the extra dict to make promotion to the base API easier. """ def __init__(self, ip, domain, instance_id, extra=None): self.ip = ip self.domain = domain self.instance_id = instance_id self.extra = extra or {} def __repr__(self): return (('<ElasticIP: ip=%s, domain=%s, instance_id=%s>') % (self.ip, self.domain, self.instance_id)) class VPCInternetGateway(object): """ Class which stores information about VPC Internet Gateways. Note: This class is VPC specific. """ def __init__(self, id, name, vpc_id, state, driver, extra=None): self.id = id self.name = name self.vpc_id = vpc_id self.state = state self.extra = extra or {} def __repr__(self): return (('<VPCInternetGateway: id=%s>') % (self.id))
[docs]class EC2RouteTable(object): """ Class which stores information about VPC Route Tables. Note: This class is VPC specific. """ def __init__(self, id, name, routes, subnet_associations, propagating_gateway_ids, extra=None): """ :param id: The ID of the route table. :type id: ``str`` :param name: The name of the route table. :type name: ``str`` :param routes: A list of routes in the route table. :type routes: ``list`` of :class:`EC2Route` :param subnet_associations: A list of associations between the route table and one or more subnets. :type subnet_associations: ``list`` of :class:`EC2SubnetAssociation` :param propagating_gateway_ids: The list of IDs of any virtual private gateways propagating the routes. :type propagating_gateway_ids: ``list`` """ self.id = id self.name = name self.routes = routes self.subnet_associations = subnet_associations self.propagating_gateway_ids = propagating_gateway_ids self.extra = extra or {} def __repr__(self): return (('<EC2RouteTable: id=%s>') % (self.id))
[docs]class EC2Route(object): """ Class which stores information about a Route. Note: This class is VPC specific. """ def __init__(self, cidr, gateway_id, instance_id, owner_id, interface_id, state, origin, vpc_peering_connection_id): """ :param cidr: The CIDR block used for the destination match. :type cidr: ``str`` :param gateway_id: The ID of a gateway attached to the VPC. :type gateway_id: ``str`` :param instance_id: The ID of a NAT instance in the VPC. :type instance_id: ``str`` :param owner_id: The AWS account ID of the owner of the instance. :type owner_id: ``str`` :param interface_id: The ID of the network interface. :type interface_id: ``str`` :param state: The state of the route (active | blackhole). :type state: ``str`` :param origin: Describes how the route was created. :type origin: ``str`` :param vpc_peering_connection_id: The ID of the VPC peering connection. :type vpc_peering_connection_id: ``str`` """ self.cidr = cidr self.gateway_id = gateway_id self.instance_id = instance_id self.owner_id = owner_id self.interface_id = interface_id self.state = state self.origin = origin self.vpc_peering_connection_id = vpc_peering_connection_id def __repr__(self): return (('<EC2Route: cidr=%s>') % (self.cidr))
[docs]class EC2SubnetAssociation(object): """ Class which stores information about Route Table associated with a given Subnet in a VPC Note: This class is VPC specific. """ def __init__(self, id, route_table_id, subnet_id, main=False): """ :param id: The ID of the subnet association in the VPC. :type id: ``str`` :param route_table_id: The ID of a route table in the VPC. :type route_table_id: ``str`` :param subnet_id: The ID of a subnet in the VPC. :type subnet_id: ``str`` :param main: If true, means this is a main VPC route table. :type main: ``bool`` """ self.id = id self.route_table_id = route_table_id self.subnet_id = subnet_id self.main = main def __repr__(self): return (('<EC2SubnetAssociation: id=%s>') % (self.id))
class EC2VolumeModification(object): """ Describes the modification status of an EBS volume. If the volume has never been modified, some element values will be null. """ def __init__(self, end_time=None, modification_state=None, original_iops=None, original_size=None, original_volume_type=None, progress=None, start_time=None, status_message=None, target_iops=None, target_size=None, target_volume_type=None, volume_id=None): self.end_time = end_time self.modification_state = modification_state self.original_iops = original_iops self.original_size = original_size self.original_volume_type = original_volume_type self.progress = progress self.start_time = start_time self.status_message = status_message self.target_iops = target_iops self.target_size = target_size self.target_volume_type = target_volume_type self.volume_id = volume_id def __repr__(self): return (('<EC2VolumeModification: end_time=%s, modification_state=%s, ' 'original_iops=%s, original_size=%s, ' 'original_volume_type=%s, progress=%s, start_time=%s, ' 'status_message=%s, target_iops=%s, target_size=%s, ' 'target_volume_type=%s, volume_id=%s>') % (self.end_time, self.modification_state, self.original_iops, self.original_size, self.original_volume_type, self.progress, self.start_time, self.status_message, self.target_iops, self.target_size, self.target_volume_type, self.volume_id))
[docs]class BaseEC2NodeDriver(NodeDriver): """ Base Amazon EC2 node driver. Used for main EC2 and other derivate driver classes to inherit from it. """ connectionCls = EC2Connection features = {'create_node': ['ssh_key']} path = '/' region_name = '' country = '' signature_version = DEFAULT_SIGNATURE_VERSION NODE_STATE_MAP = { 'pending': NodeState.PENDING, 'running': NodeState.RUNNING, 'shutting-down': NodeState.UNKNOWN, 'terminated': NodeState.TERMINATED } # http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Volume.html VOLUME_STATE_MAP = { 'available': StorageVolumeState.AVAILABLE, 'in-use': StorageVolumeState.INUSE, 'error': StorageVolumeState.ERROR, 'creating': StorageVolumeState.CREATING, 'deleting': StorageVolumeState.DELETING, 'deleted': StorageVolumeState.DELETED, 'error_deleting': StorageVolumeState.ERROR } SNAPSHOT_STATE_MAP = { 'pending': VolumeSnapshotState.CREATING, 'completed': VolumeSnapshotState.AVAILABLE, 'error': VolumeSnapshotState.ERROR, }
[docs] def list_nodes(self, ex_node_ids=None, ex_filters=None): # type: (str, str) -> List[Node] """ Lists all nodes. Ex_node_ids parameter is used to filter the list of nodes that should be returned. Only the nodes with the corresponding node IDs will be returned. :param ex_node_ids: List of ``node.id`` :type ex_node_ids: ``list`` of ``str`` :param ex_filters: The filters so that the list includes information for certain nodes only. :type ex_filters: ``dict`` :rtype: ``list`` of :class:`Node` """ params = {'Action': 'DescribeInstances'} if ex_node_ids: params.update(self._pathlist('InstanceId', ex_node_ids)) if ex_filters: params.update(self._build_filters(ex_filters)) elem = self.connection.request(self.path, params=params).object nodes = [] for rs in findall(element=elem, xpath='reservationSet/item', namespace=NAMESPACE): nodes += self._to_nodes(rs, 'instancesSet/item') nodes_elastic_ips_mappings = self.ex_describe_addresses(nodes) for node in nodes: ips = nodes_elastic_ips_mappings[node.id] node.public_ips.extend(ips) return nodes
[docs] def list_sizes(self, location=None): available_types = REGION_DETAILS[self.region_name]['instance_types'] sizes = [] for instance_type in available_types: attributes = INSTANCE_TYPES[instance_type] attributes = copy.deepcopy(attributes) try: price = self._get_size_price(size_id=instance_type) attributes['price'] = price except KeyError: attributes['price'] = None # pricing not available sizes.append(NodeSize(driver=self, **attributes)) return sizes
[docs] def list_images(self, location=None, ex_image_ids=None, ex_owner=None, ex_executableby=None, ex_filters=None): """ Lists all images @inherits: :class:`NodeDriver.list_images` Ex_image_ids parameter is used to filter the list of images that should be returned. Only the images with the corresponding image IDs will be returned. Ex_owner parameter is used to filter the list of images that should be returned. Only the images with the corresponding owner will be returned. Valid values: amazon|aws-marketplace|self|all|aws id Ex_executableby parameter describes images for which the specified user has explicit launch permissions. The user can be an AWS account ID, self to return images for which the sender of the request has explicit launch permissions, or all to return images with public launch permissions. Valid values: all|self|aws id Ex_filters parameter is used to filter the list of images that should be returned. Only images matching the filter will be returned. :param ex_image_ids: List of ``NodeImage.id`` :type ex_image_ids: ``list`` of ``str`` :param ex_owner: Owner name :type ex_owner: ``str`` :param ex_executableby: Executable by :type ex_executableby: ``str`` :param ex_filters: Filter by :type ex_filters: ``dict`` :rtype: ``list`` of :class:`NodeImage` """ params = {'Action': 'DescribeImages'} if ex_owner: params.update({'Owner.1': ex_owner}) if ex_executableby: params.update({'ExecutableBy.1': ex_executableby}) if ex_image_ids: for index, image_id in enumerate(ex_image_ids): index += 1 params.update({'ImageId.%s' % (index): image_id}) if ex_filters: params.update(self._build_filters(ex_filters)) images = self._to_images( self.connection.request(self.path, params=params).object ) return images
[docs] def get_image(self, image_id): """ Gets an image based on an image_id. :param image_id: Image identifier :type image_id: ``str`` :return: A NodeImage object :rtype: :class:`NodeImage` """ images = self.list_images(ex_image_ids=[image_id]) image = images[0] return image
[docs] def list_locations(self): locations = [] iterator = enumerate(self.ex_list_availability_zones()) for index, availability_zone in iterator: locations.append(EC2NodeLocation( index, availability_zone.name, self.country, self, availability_zone) ) return locations
[docs] def list_volumes(self, node=None, ex_filters=None): """ List volumes that are attached to a node, if specified and those that satisfy the filters, if specified. :param node: The node to which the volumes are attached. :type node: :class:`Node` :param ex_filters: The dictionary of additional filters. :type ex_filters: ``dict`` :return: The list of volumes that match the criteria. :rtype: ``list`` of :class:`StorageVolume` """ params = { 'Action': 'DescribeVolumes', } if not ex_filters: ex_filters = {} if node: ex_filters['attachment.instance-id'] = node.id if node or ex_filters: params.update(self._build_filters(ex_filters)) response = self.connection.request(self.path, params=params).object volumes = [self._to_volume(el) for el in response.findall( fixxpath(xpath='volumeSet/item', namespace=NAMESPACE)) ] return volumes
[docs] def create_node(self, name, size, image, location=None, auth=None, ex_keyname=None, ex_userdata=None, ex_security_groups=None, ex_securitygroup=None, ex_security_group_ids=None, ex_metadata=None, ex_mincount=1, ex_maxcount=1, ex_clienttoken=None, ex_blockdevicemappings=None, ex_iamprofile=None, ex_ebs_optimized=None, ex_subnet=None, ex_placement_group=None, ex_assign_public_ip=False, ex_terminate_on_shutdown=False): """ Create a new EC2 node. Reference: http://bit.ly/8ZyPSy [docs.amazonwebservices.com] @inherits: :class:`NodeDriver.create_node` :keyword ex_keyname: The name of the key pair :type ex_keyname: ``str`` :keyword ex_userdata: User data :type ex_userdata: ``str`` :keyword ex_security_groups: A list of names of security groups to assign to the node. :type ex_security_groups: ``list`` :keyword ex_security_group_ids: A list of ids of security groups to assign to the node.[for VPC nodes only] :type ex_security_group_ids: ``list`` :keyword ex_metadata: Key/Value metadata to associate with a node :type ex_metadata: ``dict`` :keyword ex_mincount: Minimum number of instances to launch :type ex_mincount: ``int`` :keyword ex_maxcount: Maximum number of instances to launch :type ex_maxcount: ``int`` :keyword ex_clienttoken: Unique identifier to ensure idempotency :type ex_clienttoken: ``str`` :keyword ex_blockdevicemappings: ``list`` of ``dict`` block device mappings. :type ex_blockdevicemappings: ``list`` of ``dict`` :keyword ex_iam_profile: Name or ARN of IAM profile :type ex_iam_profile: ``str`` :keyword ex_ebs_optimized: EBS-Optimized if True :type ex_ebs_optimized: ``bool`` :keyword ex_subnet: The subnet to launch the instance into. :type ex_subnet: :class:`.EC2Subnet` :keyword ex_placement_group: The name of the placement group to launch the instance into. :type ex_placement_group: ``str`` :keyword ex_assign_public_ip: If True, the instance will be assigned a public ip address. Note : It takes takes a short while for the instance to be assigned the public ip so the node returned will NOT have the public ip assigned yet. :type ex_assign_public_ip: ``bool`` :keyword ex_terminate_on_shutdown: Indicates if the instance should be terminated instead of just shut down when using the operating systems command for system shutdown. :type ex_terminate_on_shutdown: ``bool`` """ params = { 'Action': 'RunInstances', 'ImageId': image.id, 'MinCount': str(ex_mincount), 'MaxCount': str(ex_maxcount), 'InstanceType': size.id } if ex_terminate_on_shutdown: params["InstanceInitiatedShutdownBehavior"] = "terminate" if ex_security_groups and ex_securitygroup: raise ValueError('You can only supply ex_security_groups or' ' ex_securitygroup') # ex_securitygroup is here for backward compatibility security_groups = ex_security_groups or ex_securitygroup if security_groups: if not isinstance(security_groups, (tuple, list)): security_groups = [security_groups] for sig in range(len(security_groups)): params['SecurityGroup.%d' % (sig + 1,)] =\ security_groups[sig] if ex_security_group_ids and not ex_subnet: raise ValueError('You can only supply ex_security_group_ids' ' combinated with ex_subnet') security_group_ids = ex_security_group_ids security_group_id_params = {} if security_group_ids: if not isinstance(security_group_ids, (tuple, list)): security_group_ids = [security_group_ids] for sig in range(len(security_group_ids)): security_group_id_params['SecurityGroupId.%d' % (sig + 1,)] =\ security_group_ids[sig] if location: availability_zone = getattr(location, 'availability_zone', None) if availability_zone: if availability_zone.region_name != self.region_name: raise AttributeError('Invalid availability zone: %s' % (availability_zone.name)) params['Placement.AvailabilityZone'] = availability_zone.name if auth and ex_keyname: raise AttributeError('Cannot specify auth and ex_keyname together') if auth: auth = self._get_and_check_auth(auth) # pylint: disable=no-member key = self.ex_find_or_import_keypair_by_key_material(auth.pubkey) params['KeyName'] = key['keyName'] if ex_keyname: params['KeyName'] = ex_keyname if ex_userdata: params['UserData'] = base64.b64encode(b('ex_userdata'))\ .decode('utf-8') if ex_clienttoken: params['ClientToken'] = ex_clienttoken if ex_blockdevicemappings: params.update(self._get_block_device_mapping_params( ex_blockdevicemappings)) if ex_iamprofile: if not isinstance(ex_iamprofile, basestring): raise AttributeError('ex_iamprofile not string') if ex_iamprofile.startswith('arn:aws:iam:'): params['IamInstanceProfile.Arn'] = ex_iamprofile else: params['IamInstanceProfile.Name'] = ex_iamprofile if ex_ebs_optimized: params['EbsOptimized'] = ex_ebs_optimized subnet_id = None if ex_subnet: subnet_id = ex_subnet.id if ex_placement_group: params['Placement.GroupName'] = ex_placement_group assign_public_ip = ex_assign_public_ip # In the event that a public ip is requested a NetworkInterface # needs to be specified. Some properties that would # normally be at the root (security group ids and subnet id) # need to be moved to the level of the NetworkInterface because # the NetworkInterface is no longer created implicitly if assign_public_ip: root_key = 'NetworkInterface.1.' params[root_key + 'AssociatePublicIpAddress'] = "true" # This means that when the instance is terminated, the # NetworkInterface we created for the instance will be # deleted automatically params[root_key + 'DeleteOnTermination'] = "true" # Required to be 0 if we are associating a public ip params[root_key + 'DeviceIndex'] = "0" if subnet_id: params[root_key + 'SubnetId'] = subnet_id for key, security_group_id in security_group_id_params.items(): key = root_key + key params[key] = security_group_id else: params.update(security_group_id_params) if subnet_id: params['SubnetId'] = subnet_id # Specify tags at instance creation time tags = {'Name': name} if ex_metadata: tags.update(ex_metadata) tagspec_root = 'TagSpecification.1.' params[tagspec_root + 'ResourceType'] = 'instance' tag_nr = 1 for k, v in tags.items(): tag_root = tagspec_root + 'Tag.%d.' % tag_nr params[tag_root + 'Key'] = k params[tag_root + 'Value'] = v tag_nr += 1 object = self.connection.request(self.path, params=params).object nodes = self._to_nodes(object, 'instancesSet/item') for node in nodes: node.name = name node.extra.update({'tags': tags}) if len(nodes) == 1: return nodes[0] else: return nodes
[docs] def reboot_node(self, node): params = {'Action': 'RebootInstances'} params.update(self._pathlist('InstanceId', [node.id])) res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def destroy_node(self, node): params = {'Action': 'TerminateInstances'} params.update(self._pathlist('InstanceId', [node.id])) res = self.connection.request(self.path, params=params).object return self._get_terminate_boolean(res)
[docs] def create_volume(self, size, name, location=None, snapshot=None, ex_volume_type='standard', ex_iops=None, ex_encrypted=False, ex_kms_key_id=None): """ Create a new volume. :param size: Size of volume in gigabytes (required) :type size: ``int`` :param name: Name of the volume to be created :type name: ``str`` :param location: Which data center to create a volume in. If empty, undefined behavior will be selected. (optional) :type location: :class:`.NodeLocation` :param snapshot: Snapshot from which to create the new volume. (optional) :type snapshot: :class:`.VolumeSnapshot` :param location: Datacenter in which to create a volume in. :type location: :class:`.ExEC2AvailabilityZone` :param ex_volume_type: Type of volume to create. :type ex_volume_type: ``str`` :param iops: The number of I/O operations per second (IOPS) that the volume supports. Only used if ex_volume_type is io1. :type iops: ``int`` :param ex_encrypted: Specifies whether the volume should be encrypted. :type ex_encrypted: ``bool`` :param ex_kms_key_id: The full ARN of the AWS Key Management Service (AWS KMS) customer master key (CMK) to use when creating the encrypted volume. Example: arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123 -456a-a12b-a123b4cd56ef. Only used if encrypted is set to True. :type ex_kms_key_id: ``str`` :return: The newly created volume. :rtype: :class:`StorageVolume` """ params = { 'Action': 'CreateVolume', 'Size': str(size)} if ex_volume_type and ex_volume_type not in VALID_VOLUME_TYPES: raise ValueError('Invalid volume type specified: %s' % (ex_volume_type)) if snapshot: params['SnapshotId'] = snapshot.id if location is not None: params['AvailabilityZone'] = location.availability_zone.name if ex_volume_type: params['VolumeType'] = ex_volume_type if ex_volume_type == 'io1' and ex_iops: params['Iops'] = ex_iops if ex_encrypted: params['Encrypted'] = 1 if ex_kms_key_id is not None: params['KmsKeyId'] = ex_kms_key_id volume = self._to_volume( self.connection.request(self.path, params=params).object, name=name) if self.ex_create_tags(volume, {'Name': name}): volume.extra['tags']['Name'] = name return volume
[docs] def attach_volume(self, node, volume, device): params = { 'Action': 'AttachVolume', 'VolumeId': volume.id, 'InstanceId': node.id, 'Device': device} self.connection.request(self.path, params=params) return True
[docs] def detach_volume(self, volume, ex_force=False): params = { 'Action': 'DetachVolume', 'VolumeId': volume.id} if ex_force: params['Force'] = 1 self.connection.request(self.path, params=params) return True
[docs] def destroy_volume(self, volume): params = { 'Action': 'DeleteVolume', 'VolumeId': volume.id} response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
[docs] def create_volume_snapshot(self, volume, name=None, ex_metadata=None): """ Create snapshot from volume :param volume: Instance of ``StorageVolume`` :type volume: ``StorageVolume`` :param name: Name of snapshot (optional) :type name: ``str`` :keyword ex_metadata: The Key/Value metadata to associate with a snapshot (optional) :type ex_metadata: ``dict`` :rtype: :class:`VolumeSnapshot` """ params = { 'Action': 'CreateSnapshot', 'VolumeId': volume.id, } if name: params.update({ 'Description': name, }) if ex_metadata is None: ex_metadata = {} response = self.connection.request(self.path, params=params).object snapshot = self._to_snapshot(response, name) ex_metadata.update(**{'Name': name} if name else {}) if self.ex_create_tags(snapshot, ex_metadata): snapshot.extra['tags'] = ex_metadata return snapshot
[docs] def list_volume_snapshots(self, volume): return [snapshot for snapshot in self.list_snapshots(owner='self') if snapshot.extra["volume_id"] == volume.id]
[docs] def list_snapshots(self, snapshot=None, owner=None): """ Describes all snapshots. :param snapshot: If provided, only returns snapshot information for the provided snapshot. :param owner: The owner of the snapshot: self|amazon|ID :type owner: ``str`` :rtype: ``list`` of :class:`VolumeSnapshot` """ params = { 'Action': 'DescribeSnapshots', } if snapshot: params.update({ 'SnapshotId.1': snapshot.id, }) if owner: params.update({ 'Owner.1': owner, }) response = self.connection.request(self.path, params=params).object snapshots = self._to_snapshots(response) return snapshots
[docs] def destroy_volume_snapshot(self, snapshot): params = { 'Action': 'DeleteSnapshot', 'SnapshotId': snapshot.id } response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
# Key pair management methods
[docs] def list_key_pairs(self): params = { 'Action': 'DescribeKeyPairs' } response = self.connection.request(self.path, params=params) elems = findall(element=response.object, xpath='keySet/item', namespace=NAMESPACE) key_pairs = self._to_key_pairs(elems=elems) return key_pairs
[docs] def get_key_pair(self, name): params = { 'Action': 'DescribeKeyPairs', 'KeyName': name } response = self.connection.request(self.path, params=params) elems = findall(element=response.object, xpath='keySet/item', namespace=NAMESPACE) key_pair = self._to_key_pairs(elems=elems)[0] return key_pair
[docs] def create_key_pair(self, name): params = { 'Action': 'CreateKeyPair', 'KeyName': name } response = self.connection.request(self.path, params=params) elem = response.object key_pair = self._to_key_pair(elem=elem) return key_pair
[docs] def import_key_pair_from_string(self, name, key_material): base64key = ensure_string(base64.b64encode(b(key_material))) params = { 'Action': 'ImportKeyPair', 'KeyName': name, 'PublicKeyMaterial': base64key } response = self.connection.request(self.path, params=params) elem = response.object key_pair = self._to_key_pair(elem=elem) return key_pair
[docs] def delete_key_pair(self, key_pair): params = { 'Action': 'DeleteKeyPair', 'KeyName': key_pair.name } res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def copy_image(self, image, source_region, name=None, description=None): """ Copy an Amazon Machine Image from the specified source region to the current region. @inherits: :class:`NodeDriver.copy_image` :param source_region: The region where the image resides :type source_region: ``str`` :param image: Instance of class NodeImage :type image: :class:`NodeImage` :param name: The name of the new image :type name: ``str`` :param description: The description of the new image :type description: ``str`` :return: Instance of class ``NodeImage`` :rtype: :class:`NodeImage` """ params = {'Action': 'CopyImage', 'SourceRegion': source_region, 'SourceImageId': image.id} if name is not None: params['Name'] = name if description is not None: params['Description'] = description image = self._to_image( self.connection.request(self.path, params=params).object) return image
[docs] def create_image(self, node, name, description=None, reboot=False, block_device_mapping=None): """ Create an Amazon Machine Image based off of an EBS-backed instance. @inherits: :class:`NodeDriver.create_image` :param node: Instance of ``Node`` :type node: :class: `Node` :param name: The name for the new image :type name: ``str`` :param block_device_mapping: A dictionary of the disk layout An example of this dict is included below. :type block_device_mapping: ``list`` of ``dict`` :param reboot: Whether or not to shutdown the instance before creation. Amazon calls this NoReboot and sets it to false by default to ensure a clean image. :type reboot: ``bool`` :param description: An optional description for the new image :type description: ``str`` An example block device mapping dictionary is included: mapping = [{'VirtualName': None, 'Ebs': {'VolumeSize': 10, 'VolumeType': 'standard', 'DeleteOnTermination': 'true'}, 'DeviceName': '/dev/sda1'}] :return: Instance of class ``NodeImage`` :rtype: :class:`NodeImage` """ params = {'Action': 'CreateImage', 'InstanceId': node.id, 'Name': name, 'NoReboot': not reboot} if description is not None: params['Description'] = description if block_device_mapping is not None: params.update(self._get_block_device_mapping_params( block_device_mapping)) image = self._to_image( self.connection.request(self.path, params=params).object) return image
[docs] def delete_image(self, image): """ Deletes an image at Amazon given a NodeImage object @inherits: :class:`NodeDriver.delete_image` :param image: Instance of ``NodeImage`` :type image: :class: `NodeImage` :rtype: ``bool`` """ params = {'Action': 'DeregisterImage', 'ImageId': image.id} response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
[docs] def start_node(self, node): """ Starts the node by passing in the node object, does not work with instance store backed instances. :param node: The node to be used :type node: :class:`Node` :rtype: ``bool`` """ params = {'Action': 'StartInstances'} params.update(self._pathlist('InstanceId', [node.id])) res = self.connection.request(self.path, params=params).object return self._get_state_boolean(res)
[docs] def stop_node(self, node): """ Stops the node by passing in the node object, does not work with instance store backed instances :param node: The node to be used :type node: :class:`Node` :rtype: ``bool`` """ params = {'Action': 'StopInstances'} params.update(self._pathlist('InstanceId', [node.id])) res = self.connection.request(self.path, params=params).object return self._get_state_boolean(res)
[docs] def ex_create_placement_group(self, name): """ Creates a new placement group. :param name: The name for the new placement group :type name: ``str`` :rtype: ``bool`` """ params = {'Action': 'CreatePlacementGroup', 'Strategy': 'cluster', 'GroupName': name} response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
[docs] def ex_delete_placement_group(self, name): """ Deletes a placement group. :param name: The placement group name :type name: ``str`` :rtype: ``bool`` """ params = {'Action': 'DeletePlacementGroup', 'GroupName': name} response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
[docs] def ex_import_snapshot(self, client_data=None, client_token=None, description=None, disk_container=None, dry_run=None, role_name=None): """ Imports a disk into an EBS snapshot. More information can be found at https://goo.gl/sbXkYA. :param client_data: Describes the client specific data (optional) :type client_data: ``dict`` :param client_token: The token to enable idempotency for VM import requests.(optional) :type client_token: ``str`` :param description: The description string for the import snapshot task.(optional) :type description: ``str`` :param disk_container:The disk container object for the import snapshot request. :type disk_container:``dict`` :param dry_run: Checks whether you have the permission for the action, without actually making the request, and provides an error response.(optional) :type dry_run: ``bool`` :param role_name: The name of the role to use when not using the default role, 'vmimport'.(optional) :type role_name: ``str`` :rtype: :class: ``VolumeSnapshot`` """ params = {'Action': 'ImportSnapshot'} # TODO: This method isn't defined anywhere? # if client_data is not None: # params.update(self._get_client_date_params(client_data)) if client_token is not None: params['ClientToken'] = client_token if description is not None: params['Description'] = description if disk_container is not None: params.update(self._get_disk_container_params(disk_container)) if dry_run is not None: params['DryRun'] = dry_run if role_name is not None: params['RoleName'] = role_name importSnapshot = self.connection.request(self.path, params=params).object importTaskId = findtext(element=importSnapshot, xpath='importTaskId', namespace=NAMESPACE) volumeSnapshot = self._wait_for_import_snapshot_completion( import_task_id=importTaskId, timeout=1800, interval=15) return volumeSnapshot
def _wait_for_import_snapshot_completion(self, import_task_id, timeout=1800, interval=15): """ It waits for import snapshot to be completed :param import_task_id: Import task Id for the current Import Snapshot Task :type import_task_id: ``str`` :param timeout: Timeout value for snapshot generation :type timeout: ``float`` :param interval: Time interval for repetative describe import snapshot tasks requests :type interval: ``float`` :rtype: :class:``VolumeSnapshot`` """ start_time = time.time() snapshotId = None while snapshotId is None: if (time.time() - start_time >= timeout): raise Exception('Timeout while waiting ' 'for import task Id %s' % import_task_id) res = self.ex_describe_import_snapshot_tasks(import_task_id) snapshotId = res.snapshotId if snapshotId is None: time.sleep(interval) volumeSnapshot = VolumeSnapshot(snapshotId, driver=self) return volumeSnapshot
[docs] def ex_describe_import_snapshot_tasks(self, import_task_id, dry_run=None): """ Describes your import snapshot tasks. More information can be found at https://goo.gl/CI0MdS. :param import_task_id: Import task Id for the current Import Snapshot Task :type import_task_id: ``str`` :param dry_run: Checks whether you have the permission for the action, without actually making the request, and provides an error response.(optional) :type dry_run: ``bool`` :rtype: :class:``DescribeImportSnapshotTasks Object`` """ params = {'Action': 'DescribeImportSnapshotTasks'} if dry_run is not None: params['DryRun'] = dry_run # This can be extended for multiple import snapshot tasks params['ImportTaskId.1'] = import_task_id res = self._to_import_snapshot_task( self.connection.request(self.path, params=params).object ) return res
[docs] def ex_list_placement_groups(self, names=None): """ A list of placement groups. :param names: Placement Group names :type names: ``list`` of ``str`` :rtype: ``list`` of :class:`.EC2PlacementGroup` """ names = names or [] params = {'Action': 'DescribePlacementGroups'} for index, name in enumerate(names): params['GroupName.%s' % index + 1] = name response = self.connection.request(self.path, params=params).object return self._to_placement_groups(response)
[docs] def ex_register_image(self, name, description=None, architecture=None, image_location=None, root_device_name=None, block_device_mapping=None, kernel_id=None, ramdisk_id=None, virtualization_type=None, ena_support=None, billing_products=None, sriov_net_support=None): """ Registers an Amazon Machine Image based off of an EBS-backed instance. Can also be used to create images from snapshots. More information can be found at http://goo.gl/hqZq0a. :param name: The name for the AMI being registered :type name: ``str`` :param description: The description of the AMI (optional) :type description: ``str`` :param architecture: The architecture of the AMI (i386/x86_64) (optional) :type architecture: ``str`` :param image_location: The location of the AMI within Amazon S3 Required if registering an instance store-backed AMI :type image_location: ``str`` :param root_device_name: The device name for the root device Required if registering an EBS-backed AMI :type root_device_name: ``str`` :param block_device_mapping: A dictionary of the disk layout (optional) :type block_device_mapping: ``dict`` :param kernel_id: Kernel id for AMI (optional) :type kernel_id: ``str`` :param ramdisk_id: RAM disk for AMI (optional) :type ramdisk_id: ``str`` :param virtualization_type: The type of virtualization for the AMI you are registering, paravirt or hvm (optional) :type virtualization_type: ``str`` :param ena_support: Enable enhanced networking with Elastic Network Adapter for the AMI :type ena_support: ``bool`` :param billing_products: The billing product codes :type billing_products: ''list'' :param sriov_net_support: Set to "simple" to enable enhanced networking with the Intel 82599 Virtual Function interface :type sriov_net_support: ``str`` :rtype: :class:`NodeImage` """ params = {'Action': 'RegisterImage', 'Name': name} if description is not None: params['Description'] = description if architecture is not None: params['Architecture'] = architecture if image_location is not None: params['ImageLocation'] = image_location if root_device_name is not None: params['RootDeviceName'] = root_device_name if block_device_mapping is not None: params.update(self._get_block_device_mapping_params( block_device_mapping)) if kernel_id is not None: params['KernelId'] = kernel_id if ramdisk_id is not None: params['RamDiskId'] = ramdisk_id if virtualization_type is not None: params['VirtualizationType'] = virtualization_type if ena_support is not None: params['EnaSupport'] = ena_support if billing_products is not None: params.update(self._get_billing_product_params( billing_products)) if sriov_net_support is not None: params['SriovNetSupport'] = sriov_net_support image = self._to_image( self.connection.request(self.path, params=params).object ) return image
[docs] def ex_list_networks(self, network_ids=None, filters=None): """ Returns a list of :class:`EC2Network` objects for the current region. :param network_ids: Returns only networks matching the provided network IDs. If not specified, a list of all the networks in the corresponding region is returned. :type network_ids: ``list`` :param filters: The filters so that the list returned includes information for certain networks only. :type filters: ``dict`` :rtype: ``list`` of :class:`EC2Network` """ params = {'Action': 'DescribeVpcs'} if network_ids: params.update(self._pathlist('VpcId', network_ids)) if filters: params.update(self._build_filters(filters)) return self._to_networks( self.connection.request(self.path, params=params).object )
[docs] def ex_create_network(self, cidr_block, name=None, instance_tenancy='default'): """ Create a network/VPC :param cidr_block: The CIDR block assigned to the network :type cidr_block: ``str`` :param name: An optional name for the network :type name: ``str`` :param instance_tenancy: The allowed tenancy of instances launched into the VPC. Valid values: default/dedicated :type instance_tenancy: ``str`` :return: Dictionary of network properties :rtype: ``dict`` """ params = {'Action': 'CreateVpc', 'CidrBlock': cidr_block, 'InstanceTenancy': instance_tenancy} response = self.connection.request(self.path, params=params).object element = response.findall(fixxpath(xpath='vpc', namespace=NAMESPACE))[0] network = self._to_network(element, name) if name and self.ex_create_tags(network, {'Name': name}): network.extra['tags']['Name'] = name return network
[docs] def ex_delete_network(self, vpc): """ Deletes a network/VPC. :param vpc: VPC to delete. :type vpc: :class:`.EC2Network` :rtype: ``bool`` """ params = {'Action': 'DeleteVpc', 'VpcId': vpc.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_list_subnets(self, subnet_ids=None, filters=None): """ Returns a list of :class:`EC2NetworkSubnet` objects for the current region. :param subnet_ids: Returns only subnets matching the provided subnet IDs. If not specified, a list of all the subnets in the corresponding region is returned. :type subnet_ids: ``list`` :param filters: The filters so that the list returned includes information for certain subnets only. :type filters: ``dict`` :rtype: ``list`` of :class:`EC2NetworkSubnet` """ params = {'Action': 'DescribeSubnets'} if subnet_ids: params.update(self._pathlist('SubnetId', subnet_ids)) if filters: params.update(self._build_filters(filters)) return self._to_subnets( self.connection.request(self.path, params=params).object )
[docs] def ex_create_subnet(self, vpc_id, cidr_block, availability_zone, name=None): """ Creates a network subnet within a VPC. :param vpc_id: The ID of the VPC that the subnet should be associated with :type vpc_id: ``str`` :param cidr_block: The CIDR block assigned to the subnet :type cidr_block: ``str`` :param availability_zone: The availability zone where the subnet should reside :type availability_zone: ``str`` :param name: An optional name for the network :type name: ``str`` :rtype: :class: `EC2NetworkSubnet` """ params = {'Action': 'CreateSubnet', 'VpcId': vpc_id, 'CidrBlock': cidr_block, 'AvailabilityZone': availability_zone} response = self.connection.request(self.path, params=params).object element = response.findall(fixxpath(xpath='subnet', namespace=NAMESPACE))[0] subnet = self._to_subnet(element, name) if name and self.ex_create_tags(subnet, {'Name': name}): subnet.extra['tags']['Name'] = name return subnet
[docs] def ex_modify_subnet_attribute(self, subnet, attribute='auto_public_ip', value=False): """ Modifies a subnet attribute. You can only modify one attribute at a time. :param subnet: The subnet to delete :type subnet: :class:`.EC2NetworkSubnet` :param attribute: The attribute to set on the subnet; one of: ``'auto_public_ip'``: Automatically allocate a public IP address when a server is created ``'auto_ipv6'``: Automatically assign an IPv6 address when a server is created :type attribute: ``str`` :param value: The value to set the subnet attribute to (defaults to ``False``) :type value: ``bool`` :rtype: ``bool`` """ params = {'Action': 'ModifySubnetAttribute', 'SubnetId': subnet.id} if attribute == 'auto_public_ip': params['MapPublicIpOnLaunch.Value'] = value elif attribute == 'auto_ipv6': params['AssignIpv6AddressOnCreation.Value'] = value else: raise ValueError('Unsupported attribute: %s' % (attribute)) res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_delete_subnet(self, subnet): """ Deletes a VPC subnet. :param subnet: The subnet to delete :type subnet: :class:`.EC2NetworkSubnet` :rtype: ``bool`` """ params = {'Action': 'DeleteSubnet', 'SubnetId': subnet.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_list_security_groups(self): """ Lists existing Security Groups. @note: This is a non-standard extension API, and only works for EC2. :rtype: ``list`` of ``str`` """ params = {'Action': 'DescribeSecurityGroups'} response = self.connection.request(self.path, params=params).object groups = [] for group in findall(element=response, xpath='securityGroupInfo/item', namespace=NAMESPACE): name = findtext(element=group, xpath='groupName', namespace=NAMESPACE) groups.append(name) return groups
[docs] def ex_get_security_groups(self, group_ids=None, group_names=None, filters=None): """ Returns a list of :class:`EC2SecurityGroup` objects for the current region. :param group_ids: Returns only groups matching the provided group IDs. :type group_ids: ``list`` :param group_names: Returns only groups matching the provided group names. :type group_ids: ``list`` :param filters: The filters so that the list returned includes information for specific security groups only. :type filters: ``dict`` :rtype: ``list`` of :class:`EC2SecurityGroup` """ params = {'Action': 'DescribeSecurityGroups'} if group_ids: params.update(self._pathlist('GroupId', group_ids)) if group_names: for name_idx, group_name in enumerate(group_names): name_idx += 1 # We want 1-based indexes name_key = 'GroupName.%s' % (name_idx) params[name_key] = group_name if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params) return self._to_security_groups(response.object)
[docs] def ex_create_security_group(self, name, description, vpc_id=None): """ Creates a new Security Group in EC2-Classic or a targeted VPC. :param name: The name of the security group to create. This must be unique. :type name: ``str`` :param description: Human readable description of a Security Group. :type description: ``str`` :param vpc_id: Optional identifier for VPC networks :type vpc_id: ``str`` :rtype: ``dict`` """ params = {'Action': 'CreateSecurityGroup', 'GroupName': name, 'GroupDescription': description} if vpc_id is not None: params['VpcId'] = vpc_id response = self.connection.request(self.path, params=params).object group_id = findattr(element=response, xpath='groupId', namespace=NAMESPACE) return { 'group_id': group_id }
[docs] def ex_delete_security_group_by_id(self, group_id): """ Deletes a new Security Group using the group ID. :param group_id: The ID of the security group :type group_id: ``str`` :rtype: ``bool`` """ params = {'Action': 'DeleteSecurityGroup', 'GroupId': group_id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_delete_security_group_by_name(self, group_name): """ Deletes a new Security Group using the group name. :param group_name: The name of the security group :type group_name: ``str`` :rtype: ``bool`` """ params = {'Action': 'DeleteSecurityGroup', 'GroupName': group_name} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_delete_security_group(self, name): """ A wrapper method which calls ex_delete_security_group_by_name. :param name: The name of the security group :type name: ``str`` :rtype: ``bool`` """ return self.ex_delete_security_group_by_name(name)
[docs] def ex_authorize_security_group(self, name, from_port, to_port, cidr_ip, protocol='tcp'): """ Edit a Security Group to allow specific traffic. @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the security group to edit :type name: ``str`` :param from_port: The beginning of the port range to open :type from_port: ``str`` :param to_port: The end of the port range to open :type to_port: ``str`` :param cidr_ip: The ip to allow traffic for. :type cidr_ip: ``str`` :param protocol: tcp/udp/icmp :type protocol: ``str`` :rtype: ``bool`` """ params = {'Action': 'AuthorizeSecurityGroupIngress', 'GroupName': name, 'IpProtocol': protocol, 'FromPort': str(from_port), 'ToPort': str(to_port), 'CidrIp': cidr_ip} try: res = self.connection.request( self.path, params=params.copy()).object return self._get_boolean(res) except Exception as e: if e.args[0].find('InvalidPermission.Duplicate') == -1: raise e
[docs] def ex_authorize_security_group_ingress(self, id, from_port, to_port, cidr_ips=None, group_pairs=None, protocol='tcp', description=None): """ Edit a Security Group to allow specific ingress traffic using CIDR blocks or either a group ID, group name or user ID (account). :param id: The id of the security group to edit :type id: ``str`` :param from_port: The beginning of the port range to open :type from_port: ``int`` :param to_port: The end of the port range to open :type to_port: ``int`` :param cidr_ips: The list of IP ranges to allow traffic for. :type cidr_ips: ``list`` :param group_pairs: Source user/group pairs to allow traffic for. More info can be found at http://goo.gl/stBHJF EC2 Classic Example: To allow access from any system associated with the default group on account 1234567890 [{'group_name': 'default', 'user_id': '1234567890'}] VPC example: To allow access from any system associated with security group sg-47ad482e on your own account [{'group_id': ' sg-47ad482e'}] :type group_pairs: ``list`` of ``dict`` :param protocol: tcp/udp/icmp :type protocol: ``str`` :param description: description to be added to the rules inserted :type description: ``str`` :rtype: ``bool`` """ params = self._get_common_security_group_params(id, protocol, from_port, to_port, cidr_ips, group_pairs, description) params["Action"] = 'AuthorizeSecurityGroupIngress' res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_authorize_security_group_egress(self, id, from_port, to_port, cidr_ips, group_pairs=None, protocol='tcp'): """ Edit a Security Group to allow specific egress traffic using CIDR blocks or either a group ID, group name or user ID (account). This call is not supported for EC2 classic and only works for VPC groups. :param id: The id of the security group to edit :type id: ``str`` :param from_port: The beginning of the port range to open :type from_port: ``int`` :param to_port: The end of the port range to open :type to_port: ``int`` :param cidr_ips: The list of ip ranges to allow traffic for. :type cidr_ips: ``list`` :param group_pairs: Source user/group pairs to allow traffic for. More info can be found at http://goo.gl/stBHJF EC2 Classic Example: To allow access from any system associated with the default group on account 1234567890 [{'group_name': 'default', 'user_id': '1234567890'}] VPC Example: Allow access from any system associated with security group sg-47ad482e on your own account [{'group_id': ' sg-47ad482e'}] :type group_pairs: ``list`` of ``dict`` :param protocol: tcp/udp/icmp :type protocol: ``str`` :rtype: ``bool`` """ params = self._get_common_security_group_params(id, protocol, from_port, to_port, cidr_ips, group_pairs) params["Action"] = 'AuthorizeSecurityGroupEgress' res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_revoke_security_group_ingress(self, id, from_port, to_port, cidr_ips=None, group_pairs=None, protocol='tcp'): """ Edits a Security Group to revoke specific ingress traffic using CIDR blocks or either a group ID, group name or user ID (account). :param id: The ID of the security group to edit :type id: ``str`` :param from_port: The beginning of the port range to open :type from_port: ``int`` :param to_port: The end of the port range to open :type to_port: ``int`` :param cidr_ips: The list of ip ranges to allow traffic for. :type cidr_ips: ``list`` :param group_pairs: Source user/group pairs to allow traffic for. More info can be found at http://goo.gl/stBHJF EC2 Classic Example: To allow access from any system associated with the default group on account 1234567890 [{'group_name': 'default', 'user_id': '1234567890'}] VPC Example: Allow access from any system associated with security group sg-47ad482e on your own account [{'group_id': ' sg-47ad482e'}] :type group_pairs: ``list`` of ``dict`` :param protocol: tcp/udp/icmp :type protocol: ``str`` :rtype: ``bool`` """ params = self._get_common_security_group_params(id, protocol, from_port, to_port, cidr_ips, group_pairs) params["Action"] = 'RevokeSecurityGroupIngress' res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_revoke_security_group_egress(self, id, from_port, to_port, cidr_ips=None, group_pairs=None, protocol='tcp'): """ Edit a Security Group to revoke specific egress traffic using CIDR blocks or either a group ID, group name or user ID (account). This call is not supported for EC2 classic and only works for VPC groups. :param id: The id of the security group to edit :type id: ``str`` :param from_port: The beginning of the port range to open :type from_port: ``int`` :param to_port: The end of the port range to open :type to_port: ``int`` :param cidr_ips: The list of ip ranges to allow traffic for. :type cidr_ips: ``list`` :param group_pairs: Source user/group pairs to allow traffic for. More info can be found at http://goo.gl/stBHJF EC2 Classic Example: To allow access from any system associated with the default group on account 1234567890 [{'group_name': 'default', 'user_id': '1234567890'}] VPC Example: Allow access from any system associated with security group sg-47ad482e on your own account [{'group_id': ' sg-47ad482e'}] :type group_pairs: ``list`` of ``dict`` :param protocol: tcp/udp/icmp :type protocol: ``str`` :rtype: ``bool`` """ params = self._get_common_security_group_params(id, protocol, from_port, to_port, cidr_ips, group_pairs) params['Action'] = 'RevokeSecurityGroupEgress' res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_authorize_security_group_permissive(self, name): """ Edit a Security Group to allow all traffic. @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the security group to edit :type name: ``str`` :rtype: ``list`` of ``str`` """ results = [] params = {'Action': 'AuthorizeSecurityGroupIngress', 'GroupName': name, 'IpProtocol': 'tcp', 'FromPort': '0', 'ToPort': '65535', 'CidrIp': '0.0.0.0/0'} try: results.append( self.connection.request(self.path, params=params.copy()).object ) except Exception as e: if e.args[0].find("InvalidPermission.Duplicate") == -1: raise e params['IpProtocol'] = 'udp' try: results.append( self.connection.request(self.path, params=params.copy()).object ) except Exception as e: if e.args[0].find("InvalidPermission.Duplicate") == -1: raise e params.update({'IpProtocol': 'icmp', 'FromPort': '-1', 'ToPort': '-1'}) try: results.append( self.connection.request(self.path, params=params.copy()).object ) except Exception as e: if e.args[0].find("InvalidPermission.Duplicate") == -1: raise e return results
[docs] def ex_list_availability_zones(self, only_available=True): """ Returns a list of :class:`ExEC2AvailabilityZone` objects for the current region. Note: This is an extension method and is only available for EC2 driver. :keyword only_available: If true, returns only availability zones with state 'available' :type only_available: ``str`` :rtype: ``list`` of :class:`ExEC2AvailabilityZone` """ params = {'Action': 'DescribeAvailabilityZones'} filters = {'region-name': self.region_name} if only_available: filters['state'] = 'available' params.update(self._build_filters(filters)) result = self.connection.request(self.path, params=params.copy()).object availability_zones = [] for element in findall(element=result, xpath='availabilityZoneInfo/item', namespace=NAMESPACE): name = findtext(element=element, xpath='zoneName', namespace=NAMESPACE) zone_state = findtext(element=element, xpath='zoneState', namespace=NAMESPACE) region_name = findtext(element=element, xpath='regionName', namespace=NAMESPACE) availability_zone = ExEC2AvailabilityZone( name=name, zone_state=zone_state, region_name=region_name ) availability_zones.append(availability_zone) return availability_zones
[docs] def ex_describe_tags(self, resource): """ Returns a dictionary of tags for a resource (e.g. Node or StorageVolume). :param resource: The resource to be used :type resource: any resource class, such as :class:`Node,` :class:`StorageVolume,` or :class:NodeImage` :return: A dictionary of Node tags :rtype: ``dict`` """ params = {'Action': 'DescribeTags'} filters = { 'resource-id': resource.id } params.update(self._build_filters(filters)) result = self.connection.request(self.path, params=params).object return self._get_resource_tags(result)
[docs] def ex_create_tags(self, resource, tags): """ Creates tags for a resource (Node or StorageVolume). :param resource: The resource to be tagged :type resource: :class:`Node` or :class:`StorageVolume` or :class:`VolumeSnapshot` :param tags: A dictionary or other mapping of strings to strings, associating tag names with tag values. :type tags: ``dict`` :rtype: ``bool`` """ if not tags: return params = {'Action': 'CreateTags', 'ResourceId.0': resource.id} for i, key in enumerate(tags): params['Tag.%d.Key' % i] = key params['Tag.%d.Value' % i] = tags[key] res = self.connection.request(self.path, params=params.copy()).object return self._get_boolean(res)
[docs] def ex_delete_tags(self, resource, tags): """ Deletes tags from a resource. :param resource: The resource to be tagged :type resource: :class:`Node` or :class:`StorageVolume` :param tags: A dictionary or other mapping of strings to strings, specifying the tag names and tag values to be deleted. :type tags: ``dict`` :rtype: ``bool`` """ if not tags: return params = {'Action': 'DeleteTags', 'ResourceId.0': resource.id} for i, key in enumerate(tags): params['Tag.%d.Key' % i] = key if tags[key] is not None: params['Tag.%d.Value' % i] = tags[key] res = self.connection.request(self.path, params=params.copy()).object return self._get_boolean(res)
[docs] def ex_get_metadata_for_node(self, node): """ Returns the metadata associated with the node. :param node: Node instance :type node: :class:`Node` :return: A dictionary or other mapping of strings to strings, associating tag names with tag values. :rtype tags: ``dict`` """ return node.extra['tags']
[docs] def ex_allocate_address(self, domain='standard'): """ Allocate a new Elastic IP address for EC2 classic or VPC :param domain: The domain to allocate the new address in (standard/vpc) :type domain: ``str`` :return: Instance of ElasticIP :rtype: :class:`ElasticIP` """ params = {'Action': 'AllocateAddress'} if domain == 'vpc': params['Domain'] = domain response = self.connection.request(self.path, params=params).object return self._to_address(response, only_associated=False)
[docs] def ex_release_address(self, elastic_ip, domain=None): """ Releases an Elastic IP address using the IP (EC2-Classic) or using the allocation ID (VPC). :param elastic_ip: Elastic IP instance :type elastic_ip: :class:`ElasticIP` :param domain: The domain where the IP resides (vpc only) :type domain: ``str`` :return: True on success, False otherwise. :rtype: ``bool`` """ params = {'Action': 'ReleaseAddress'} if domain is not None and domain != 'vpc': raise AttributeError('Domain can only be set to vpc') if domain is None: params['PublicIp'] = elastic_ip.ip else: params['AllocationId'] = elastic_ip.extra['allocation_id'] response = self.connection.request(self.path, params=params).object return self._get_boolean(response)
[docs] def ex_describe_all_addresses(self, only_associated=False): """ Returns all the Elastic IP addresses for this account optionally, returns only addresses associated with nodes. :param only_associated: If true, return only the addresses that are associated with an instance. :type only_associated: ``bool`` :return: List of Elastic IP addresses. :rtype: ``list`` of :class:`ElasticIP` """ params = {'Action': 'DescribeAddresses'} response = self.connection.request(self.path, params=params).object # We will send our only_associated boolean over to # shape how the return data is sent back return self._to_addresses(response, only_associated)
[docs] def ex_associate_address_with_node(self, node, elastic_ip, domain=None): """ Associate an Elastic IP address with a particular node. :param node: Node instance :type node: :class:`Node` :param elastic_ip: Elastic IP instance :type elastic_ip: :class:`ElasticIP` :param domain: The domain where the IP resides (vpc only) :type domain: ``str`` :return: A string representation of the association ID which is required for VPC disassociation. EC2/standard addresses return None :rtype: ``None`` or ``str`` """ params = {'Action': 'AssociateAddress', 'InstanceId': node.id} if domain is not None and domain != 'vpc': raise AttributeError('Domain can only be set to vpc') if domain is None: params.update({'PublicIp': elastic_ip.ip}) else: params.update({'AllocationId': elastic_ip.extra['allocation_id']}) response = self.connection.request(self.path, params=params).object association_id = findtext(element=response, xpath='associationId', namespace=NAMESPACE) return association_id
[docs] def ex_associate_addresses(self, node, elastic_ip, domain=None): """ Note: This method has been deprecated in favor of the ex_associate_address_with_node method. """ return self.ex_associate_address_with_node(node=node, elastic_ip=elastic_ip, domain=domain)
[docs] def ex_disassociate_address(self, elastic_ip, domain=None): """ Disassociates an Elastic IP address using the IP (EC2-Classic) or the association ID (VPC). :param elastic_ip: ElasticIP instance :type elastic_ip: :class:`ElasticIP` :param domain: The domain where the IP resides (vpc only) :type domain: ``str`` :return: True on success, False otherwise. :rtype: ``bool`` """ params = {'Action': 'DisassociateAddress'} if domain is not None and domain != 'vpc': raise AttributeError('Domain can only be set to vpc') if domain is None: params['PublicIp'] = elastic_ip.ip else: params['AssociationId'] = elastic_ip.extra['association_id'] res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_describe_addresses(self, nodes): """ Returns Elastic IP addresses for all the nodes in the provided list. :param nodes: A list of :class:`Node` instances :type nodes: ``list`` of :class:`Node` :return: Dictionary where a key is a node ID and the value is a list with the Elastic IP addresses associated with this node. :rtype: ``dict`` """ if not nodes: return {} params = {'Action': 'DescribeAddresses'} if len(nodes) == 1: self._add_instance_filter(params, nodes[0]) result = self.connection.request(self.path, params=params).object node_instance_ids = [node.id for node in nodes] nodes_elastic_ip_mappings = {} # We will set only_associated to True so that we only get back # IPs which are associated with instances only_associated = True for node_id in node_instance_ids: nodes_elastic_ip_mappings.setdefault(node_id, []) for addr in self._to_addresses(result, only_associated): instance_id = addr.instance_id if node_id == instance_id: nodes_elastic_ip_mappings[instance_id].append( addr.ip) return nodes_elastic_ip_mappings
[docs] def ex_describe_addresses_for_node(self, node): """ Returns a list of Elastic IP Addresses associated with this node. :param node: Node instance :type node: :class:`Node` :return: List Elastic IP Addresses attached to this node. :rtype: ``list`` of ``str`` """ node_elastic_ips = self.ex_describe_addresses([node]) return node_elastic_ips[node.id]
# Network interface management methods
[docs] def ex_list_network_interfaces(self): """ Returns all network interfaces. :return: List of EC2NetworkInterface instances :rtype: ``list`` of :class `EC2NetworkInterface` """ params = {'Action': 'DescribeNetworkInterfaces'} return self._to_interfaces( self.connection.request(self.path, params=params).object )
[docs] def ex_create_network_interface(self, subnet, name=None, description=None, private_ip_address=None): """ Create a network interface within a VPC subnet. :param subnet: EC2NetworkSubnet instance :type subnet: :class:`EC2NetworkSubnet` :param name: Optional name of the interface :type name: ``str`` :param description: Optional description of the network interface :type description: ``str`` :param private_ip_address: Optional address to assign as the primary private IP address of the interface. If one is not provided then Amazon will automatically auto-assign an available IP. EC2 allows assignment of multiple IPs, but this will be the primary. :type private_ip_address: ``str`` :return: EC2NetworkInterface instance :rtype: :class `EC2NetworkInterface` """ params = {'Action': 'CreateNetworkInterface', 'SubnetId': subnet.id} if description: params['Description'] = description if private_ip_address: params['PrivateIpAddress'] = private_ip_address response = self.connection.request(self.path, params=params).object element = response.findall(fixxpath(xpath='networkInterface', namespace=NAMESPACE))[0] interface = self._to_interface(element, name) if name and self.ex_create_tags(interface, {'Name': name}): interface.extra['tags']['Name'] = name return interface
[docs] def ex_delete_network_interface(self, network_interface): """ Deletes a network interface. :param network_interface: EC2NetworkInterface instance :type network_interface: :class:`EC2NetworkInterface` :rtype: ``bool`` """ params = {'Action': 'DeleteNetworkInterface', 'NetworkInterfaceId': network_interface.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_attach_network_interface_to_node(self, network_interface, node, device_index): """ Attach a network interface to an instance. :param network_interface: EC2NetworkInterface instance :type network_interface: :class:`EC2NetworkInterface` :param node: Node instance :type node: :class:`Node` :param device_index: The interface device index :type device_index: ``int`` :return: String representation of the attachment id. This is required to detach the interface. :rtype: ``str`` """ params = {'Action': 'AttachNetworkInterface', 'NetworkInterfaceId': network_interface.id, 'InstanceId': node.id, 'DeviceIndex': device_index} response = self.connection.request(self.path, params=params).object attachment_id = findattr(element=response, xpath='attachmentId', namespace=NAMESPACE) return attachment_id
[docs] def ex_detach_network_interface(self, attachment_id, force=False): """ Detach a network interface from an instance. :param attachment_id: The attachment ID associated with the interface :type attachment_id: ``str`` :param force: Forces the detachment. :type force: ``bool`` :return: ``True`` on successful detachment, ``False`` otherwise. :rtype: ``bool`` """ params = {'Action': 'DetachNetworkInterface', 'AttachmentId': attachment_id} if force: params['Force'] = True res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_modify_instance_attribute(self, node, attributes): """ Modify node attributes. A list of valid attributes can be found at http://goo.gl/gxcj8 :param node: Node instance :type node: :class:`Node` :param attributes: Dictionary with node attributes :type attributes: ``dict`` :return: True on success, False otherwise. :rtype: ``bool`` """ attributes = attributes or {} attributes.update({'InstanceId': node.id}) params = {'Action': 'ModifyInstanceAttribute'} params.update(attributes) res = self.connection.request(self.path, params=params.copy()).object return self._get_boolean(res)
[docs] def ex_modify_snapshot_attribute(self, snapshot, attributes): """ Modify Snapshot attributes. :param snapshot: VolumeSnapshot instance :type snanpshot: :class:`VolumeSnapshot` :param attributes: Dictionary with snapshot attributes :type attributes: ``dict`` :return: True on success, False otherwise. :rtype: ``bool`` """ attributes = attributes or {} attributes.update({'SnapshotId': snapshot.id}) params = {'Action': 'ModifySnapshotAttribute'} params.update(attributes) res = self.connection.request(self.path, params=params.copy()).object return self._get_boolean(res)
[docs] def ex_modify_image_attribute(self, image, attributes): """ Modifies image attributes. :param image: NodeImage instance :type image: :class:`NodeImage` :param attributes: A dictionary with node attributes :type attributes: ``dict`` :return: True on success, False otherwise. :rtype: ``bool`` """ attributes = attributes or {} attributes.update({'ImageId': image.id}) params = {'Action': 'ModifyImageAttribute'} params.update(attributes) res = self.connection.request(self.path, params=params.copy()).object return self._get_boolean(res)
[docs] def ex_change_node_size(self, node, new_size): """ Change the node size. Note: Node must be turned of before changing the size. :param node: Node instance :type node: :class:`Node` :param new_size: NodeSize instance :type new_size: :class:`NodeSize` :return: True on success, False otherwise. :rtype: ``bool`` """ if 'instancetype' in node.extra: current_instance_type = node.extra['instancetype'] if current_instance_type == new_size.id: raise ValueError('New instance size is the same as' + 'the current one') attributes = {'InstanceType.Value': new_size.id} return self.ex_modify_instance_attribute(node=node, attributes=attributes)
[docs] def ex_start_node(self, node): # NOTE: This method is here for backward compatibility reasons after # this method was promoted to be part of the standard compute API in # Libcloud v2.7.0 return self.start_node(node=node)
[docs] def ex_stop_node(self, node): # NOTE: This method is here for backward compatibility reasons after # this method was promoted to be part of the standard compute API in # Libcloud v2.7.0 return self.stop_node(node=node)
[docs] def ex_get_console_output(self, node): """ Gets console output for the node. :param node: Node which should be used :type node: :class:`Node` :return: A dictionary with the following keys: - instance_id (``str``) - timestamp (``datetime.datetime``) - last output timestamp - output (``str``) - console output :rtype: ``dict`` """ params = { 'Action': 'GetConsoleOutput', 'InstanceId': node.id } response = self.connection.request(self.path, params=params).object timestamp = findattr(element=response, xpath='timestamp', namespace=NAMESPACE) encoded_string = findattr(element=response, xpath='output', namespace=NAMESPACE) timestamp = parse_date(timestamp) if encoded_string: output = base64.b64decode(b(encoded_string)).decode('utf-8') else: # No console output output = None return {'instance_id': node.id, 'timestamp': timestamp, 'output': output}
[docs] def ex_list_reserved_nodes(self): """ Lists all reserved instances/nodes which can be purchased from Amazon for one or three year terms. Reservations are made at a region level and reduce the hourly charge for instances. More information can be found at http://goo.gl/ulXCC7. :rtype: ``list`` of :class:`.EC2ReservedNode` """ params = {'Action': 'DescribeReservedInstances'} response = self.connection.request(self.path, params=params).object return self._to_reserved_nodes(response, 'reservedInstancesSet/item')
# Account specific methods
[docs] def ex_get_limits(self): """ Retrieve account resource limits. :rtype: ``dict`` """ attributes = ['max-instances', 'max-elastic-ips', 'vpc-max-elastic-ips'] params = {} params['Action'] = 'DescribeAccountAttributes' for index, attribute in enumerate(attributes): params['AttributeName.%s' % (index)] = attribute response = self.connection.request(self.path, params=params) data = response.object elems = data.findall(fixxpath(xpath='accountAttributeSet/item', namespace=NAMESPACE)) result = {'resource': {}} for elem in elems: name = findtext(element=elem, xpath='attributeName', namespace=NAMESPACE) value = findtext(element=elem, xpath='attributeValueSet/item/attributeValue', namespace=NAMESPACE) result['resource'][name] = int(value) return result
# Deprecated extension methods
[docs] def ex_list_keypairs(self): """ Lists all the keypair names and fingerprints. :rtype: ``list`` of ``dict`` """ warnings.warn('This method has been deprecated in favor of ' 'list_key_pairs method') key_pairs = self.list_key_pairs() result = [] for key_pair in key_pairs: item = { 'keyName': key_pair.name, 'keyFingerprint': key_pair.fingerprint, } result.append(item) return result
[docs] def ex_describe_all_keypairs(self): """ Returns names for all the available key pairs. @note: This is a non-standard extension API, and only works for EC2. :rtype: ``list`` of ``str`` """ names = [key_pair.name for key_pair in self.list_key_pairs()] return names
[docs] def ex_describe_keypairs(self, name): """ Here for backward compatibility. """ return self.ex_describe_keypair(name=name)
[docs] def ex_describe_keypair(self, name): """ Describes a keypair by name. @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the keypair to describe. :type name: ``str`` :rtype: ``dict`` """ params = { 'Action': 'DescribeKeyPairs', 'KeyName.1': name } response = self.connection.request(self.path, params=params).object key_name = findattr(element=response, xpath='keySet/item/keyName', namespace=NAMESPACE) fingerprint = findattr(element=response, xpath='keySet/item/keyFingerprint', namespace=NAMESPACE).strip() return { 'keyName': key_name, 'keyFingerprint': fingerprint }
[docs] def ex_create_keypair(self, name): """ Creates a new keypair @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the keypair to Create. This must be unique, otherwise an InvalidKeyPair.Duplicate exception is raised. :type name: ``str`` :rtype: ``dict`` """ warnings.warn('This method has been deprecated in favor of ' 'create_key_pair method') key_pair = self.create_key_pair(name=name) result = { 'keyMaterial': key_pair.private_key, 'keyFingerprint': key_pair.fingerprint } return result
[docs] def ex_delete_keypair(self, keypair): """ Deletes a key pair by name. @note: This is a non-standard extension API, and only works with EC2. :param keypair: The name of the keypair to delete. :type keypair: ``str`` :rtype: ``bool`` """ warnings.warn('This method has been deprecated in favor of ' 'delete_key_pair method') keypair = KeyPair(name=keypair, public_key=None, fingerprint=None, driver=self) return self.delete_key_pair(keypair)
[docs] def ex_import_keypair_from_string(self, name, key_material): """ Imports a new public key where the public key is passed in as a string. @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the public key to import. This must be unique, otherwise an InvalidKeyPair.Duplicate exception is raised. :type name: ``str`` :param key_material: The contents of a public key file. :type key_material: ``str`` :rtype: ``dict`` """ warnings.warn('This method has been deprecated in favor of ' 'import_key_pair_from_string method') key_pair = self.import_key_pair_from_string(name=name, key_material=key_material) result = { 'keyName': key_pair.name, 'keyFingerprint': key_pair.fingerprint } return result
[docs] def ex_import_keypair(self, name, keyfile): """ Imports a new public key where the public key is passed via a filename. @note: This is a non-standard extension API, and only works for EC2. :param name: The name of the public key to import. This must be unique, otherwise an InvalidKeyPair. Duplicate exception is raised. :type name: ``str`` :param keyfile: The filename with the path of the public key to import. :type keyfile: ``str`` :rtype: ``dict`` """ warnings.warn('This method has been deprecated in favor of ' 'import_key_pair_from_file method') key_pair = self.import_key_pair_from_file(name=name, key_file_path=keyfile) result = { 'keyName': key_pair.name, 'keyFingerprint': key_pair.fingerprint } return result
[docs] def ex_find_or_import_keypair_by_key_material(self, pubkey): """ Given a public key, look it up in the EC2 KeyPair database. If it exists, return any information we have about it. Otherwise, create it. Keys that are created are named based on their comment and fingerprint. :rtype: ``dict`` """ key_fingerprint = get_pubkey_ssh2_fingerprint(pubkey) key_comment = get_pubkey_comment(pubkey, default='unnamed') key_name = '%s-%s' % (key_comment, key_fingerprint) key_pairs = self.list_key_pairs() key_pairs = [key_pair for key_pair in key_pairs if key_pair.fingerprint == key_fingerprint] if len(key_pairs) >= 1: key_pair = key_pairs[0] result = { 'keyName': key_pair.name, 'keyFingerprint': key_pair.fingerprint } else: result = self.ex_import_keypair_from_string(key_name, pubkey) return result
[docs] def ex_list_internet_gateways(self, gateway_ids=None, filters=None): """ Describes available Internet gateways and whether or not they are attached to a VPC. These are required for VPC nodes to communicate over the Internet. :param gateway_ids: Returns only Internet gateways matching the provided Internet gateway IDs. If not specified, a list of all the Internet gateways in the corresponding region is returned. :type gateway_ids: ``list`` :param filters: The filters so the list returned inclues information for certain gateways only. :type filters: ``dict`` :rtype: ``list`` of :class:`.VPCInternetGateway` """ params = {'Action': 'DescribeInternetGateways'} if gateway_ids: params.update(self._pathlist('InternetGatewayId', gateway_ids)) if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params).object return self._to_internet_gateways(response, 'internetGatewaySet/item')
[docs] def ex_create_internet_gateway(self, name=None): """ Delete a VPC Internet gateway :rtype: ``bool`` """ params = {'Action': 'CreateInternetGateway'} resp = self.connection.request(self.path, params=params).object element = resp.findall(fixxpath(xpath='internetGateway', namespace=NAMESPACE)) gateway = self._to_internet_gateway(element[0], name) if name and self.ex_create_tags(gateway, {'Name': name}): gateway.extra['tags']['Name'] = name return gateway
[docs] def ex_delete_internet_gateway(self, gateway): """ Deletes a VPC Internet gateway. :param gateway: The gateway to delete :type gateway: :class:`.VPCInternetGateway` :rtype: ``bool`` """ params = {'Action': 'DeleteInternetGateway', 'InternetGatewayId': gateway.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_attach_internet_gateway(self, gateway, network): """ Attach an Internet gateway to a VPC :param gateway: The gateway to attach :type gateway: :class:`.VPCInternetGateway` :param network: The VPC network to attach to :type network: :class:`.EC2Network` :rtype: ``bool`` """ params = {'Action': 'AttachInternetGateway', 'InternetGatewayId': gateway.id, 'VpcId': network.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_detach_internet_gateway(self, gateway, network): """ Detaches an Internet gateway from a VPC. :param gateway: The gateway to detach :type gateway: :class:`.VPCInternetGateway` :param network: The VPC network to detach from :type network: :class:`.EC2Network` :rtype: ``bool`` """ params = {'Action': 'DetachInternetGateway', 'InternetGatewayId': gateway.id, 'VpcId': network.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_list_route_tables(self, route_table_ids=None, filters=None): """ Describes one or more of a VPC's route tables. These are used to determine where network traffic is directed. :param route_table_ids: Returns only route tables matching the provided route table IDs. If not specified, a list of all the route tables in the corresponding region is returned. :type route_table_ids: ``list`` :param filters: The filters so that the list returned includes information for certain route tables only. :type filters: ``dict`` :rtype: ``list`` of :class:`.EC2RouteTable` """ params = {'Action': 'DescribeRouteTables'} if route_table_ids: params.update(self._pathlist('RouteTableId', route_table_ids)) if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params) return self._to_route_tables(response.object)
[docs] def ex_create_route_table(self, network, name=None): """ Creates a route table within a VPC. :param vpc_id: The VPC that the subnet should be created in. :type vpc_id: :class:`.EC2Network` :rtype: :class: `.EC2RouteTable` """ params = {'Action': 'CreateRouteTable', 'VpcId': network.id} response = self.connection.request(self.path, params=params).object element = response.findall(fixxpath(xpath='routeTable', namespace=NAMESPACE))[0] route_table = self._to_route_table(element, name=name) if name and self.ex_create_tags(route_table, {'Name': name}): route_table.extra['tags']['Name'] = name return route_table
[docs] def ex_delete_route_table(self, route_table): """ Deletes a VPC route table. :param route_table: The route table to delete. :type route_table: :class:`.EC2RouteTable` :rtype: ``bool`` """ params = {'Action': 'DeleteRouteTable', 'RouteTableId': route_table.id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_associate_route_table(self, route_table, subnet): """ Associates a route table with a subnet within a VPC. Note: A route table can be associated with multiple subnets. :param route_table: The route table to associate. :type route_table: :class:`.EC2RouteTable` :param subnet: The subnet to associate with. :type subnet: :class:`.EC2Subnet` :return: Route table association ID. :rtype: ``str`` """ params = {'Action': 'AssociateRouteTable', 'RouteTableId': route_table.id, 'SubnetId': subnet.id} result = self.connection.request(self.path, params=params).object association_id = findtext(element=result, xpath='associationId', namespace=NAMESPACE) return association_id
[docs] def ex_dissociate_route_table(self, subnet_association): """ Dissociates a subnet from a route table. :param subnet_association: The subnet association object or subnet association ID. :type subnet_association: :class:`.EC2SubnetAssociation` or ``str`` :rtype: ``bool`` """ if isinstance(subnet_association, EC2SubnetAssociation): subnet_association_id = subnet_association.id else: subnet_association_id = subnet_association params = {'Action': 'DisassociateRouteTable', 'AssociationId': subnet_association_id} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_replace_route_table_association(self, subnet_association, route_table): """ Changes the route table associated with a given subnet in a VPC. Note: This method can be used to change which table is the main route table in the VPC (Specify the main route table's association ID and the route table to be the new main route table). :param subnet_association: The subnet association object or subnet association ID. :type subnet_association: :class:`.EC2SubnetAssociation` or ``str`` :param route_table: The new route table to associate. :type route_table: :class:`.EC2RouteTable` :return: A new route table association ID. :rtype: ``str`` """ if isinstance(subnet_association, EC2SubnetAssociation): subnet_association_id = subnet_association.id else: subnet_association_id = subnet_association params = {'Action': 'ReplaceRouteTableAssociation', 'AssociationId': subnet_association_id, 'RouteTableId': route_table.id} result = self.connection.request(self.path, params=params).object new_association_id = findtext(element=result, xpath='newAssociationId', namespace=NAMESPACE) return new_association_id
[docs] def ex_create_route(self, route_table, cidr, internet_gateway=None, node=None, network_interface=None, vpc_peering_connection=None): """ Creates a route entry in the route table. :param route_table: The route table to create the route in. :type route_table: :class:`.EC2RouteTable` :param cidr: The CIDR block used for the destination match. :type cidr: ``str`` :param internet_gateway: The Internet gateway to route traffic through. :type internet_gateway: :class:`.VPCInternetGateway` :param node: The NAT instance to route traffic through. :type node: :class:`Node` :param network_interface: The network interface of the node to route traffic through. :type network_interface: :class:`.EC2NetworkInterface` :param vpc_peering_connection: The VPC peering connection. :type vpc_peering_connection: :class:`.VPCPeeringConnection` :rtype: ``bool`` Note: You must specify one of the following: internet_gateway, node, network_interface, vpc_peering_connection. """ params = {'Action': 'CreateRoute', 'RouteTableId': route_table.id, 'DestinationCidrBlock': cidr} if internet_gateway: params['GatewayId'] = internet_gateway.id if node: params['InstanceId'] = node.id if network_interface: params['NetworkInterfaceId'] = network_interface.id if vpc_peering_connection: params['VpcPeeringConnectionId'] = vpc_peering_connection.id res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_delete_route(self, route_table, cidr): """ Deletes a route entry from the route table. :param route_table: The route table to delete the route from. :type route_table: :class:`.EC2RouteTable` :param cidr: The CIDR block used for the destination match. :type cidr: ``str`` :rtype: ``bool`` """ params = {'Action': 'DeleteRoute', 'RouteTableId': route_table.id, 'DestinationCidrBlock': cidr} res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_replace_route(self, route_table, cidr, internet_gateway=None, node=None, network_interface=None, vpc_peering_connection=None): """ Replaces an existing route entry within a route table in a VPC. :param route_table: The route table to replace the route in. :type route_table: :class:`.EC2RouteTable` :param cidr: The CIDR block used for the destination match. :type cidr: ``str`` :param internet_gateway: The new internet gateway to route traffic through. :type internet_gateway: :class:`.VPCInternetGateway` :param node: The new NAT instance to route traffic through. :type node: :class:`Node` :param network_interface: The new network interface of the node to route traffic through. :type network_interface: :class:`.EC2NetworkInterface` :param vpc_peering_connection: The new VPC peering connection. :type vpc_peering_connection: :class:`.VPCPeeringConnection` :rtype: ``bool`` Note: You must specify one of the following: internet_gateway, node, network_interface, vpc_peering_connection. """ params = {'Action': 'ReplaceRoute', 'RouteTableId': route_table.id, 'DestinationCidrBlock': cidr} if internet_gateway: params['GatewayId'] = internet_gateway.id if node: params['InstanceId'] = node.id if network_interface: params['NetworkInterfaceId'] = network_interface.id if vpc_peering_connection: params['VpcPeeringConnectionId'] = vpc_peering_connection.id res = self.connection.request(self.path, params=params).object return self._get_boolean(res)
[docs] def ex_modify_volume(self, volume, parameters): """ Modify volume parameters. A list of valid parameters can be found at https://goo.gl/N0rPEQ :param volume: Volume instance :type volume: :class:`Volume` :param parameters: Dictionary with updated volume parameters :type parameters: ``dict`` :return: Volume modification status object :rtype: :class:`VolumeModification """ parameters = parameters or {} volume_type = parameters.get('VolumeType') if volume_type and volume_type not in VALID_VOLUME_TYPES: raise ValueError('Invalid volume type specified: %s' % volume_type) parameters.update({'Action': 'ModifyVolume', 'VolumeId': volume.id}) response = self.connection.request(self.path, params=parameters.copy()).object return self._to_volume_modification(response.findall( fixxpath(xpath='volumeModification', namespace=NAMESPACE))[0])
[docs] def ex_describe_volumes_modifications(self, dry_run=False, volume_ids=None, filters=None): """ Describes one or more of your volume modifications. :param dry_run: dry_run :type dry_run: ``bool`` :param volume_ids: The volume_ids so that the response includes information for only said volumes :type volume_ids: ``dict`` :param filters: The filters so that the response includes information for only certain volumes :type filters: ``dict`` :return: List of volume modification status objects :rtype: ``list`` of :class:`VolumeModification """ params = {'Action': 'DescribeVolumesModifications'} if dry_run: params.update({'DryRun': dry_run}) if volume_ids: params.update(self._pathlist('VolumeId', volume_ids)) if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params).object return self._to_volume_modifications(response)
def _ex_connection_class_kwargs(self): kwargs = super(BaseEC2NodeDriver, self)._ex_connection_class_kwargs() # pylint: disable=no-member if hasattr(self, 'token') and self.token is not None: kwargs['token'] = self.token # Force signature_version 4 for tokens or auth breaks kwargs['signature_version'] = '4' else: kwargs['signature_version'] = self.signature_version return kwargs def _to_nodes(self, object, xpath): return [self._to_node(el) for el in object.findall(fixxpath(xpath=xpath, namespace=NAMESPACE))] def _to_node(self, element): try: state = self.NODE_STATE_MAP[findattr(element=element, xpath="instanceState/name", namespace=NAMESPACE) ] except KeyError: state = NodeState.UNKNOWN created = parse_date(findtext(element=element, xpath='launchTime', namespace=NAMESPACE)) instance_id = findtext(element=element, xpath='instanceId', namespace=NAMESPACE) public_ip = findtext(element=element, xpath='ipAddress', namespace=NAMESPACE) public_ips = [public_ip] if public_ip else [] private_ip = findtext(element=element, xpath='privateIpAddress', namespace=NAMESPACE) private_ips = [private_ip] if private_ip else [] product_codes = [] for p in findall(element=element, xpath="productCodesSet/item/productCode", namespace=NAMESPACE): product_codes.append(p) # Get our tags tags = self._get_resource_tags(element) name = tags.get('Name', instance_id) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['node']) # Add additional properties to our extra dictionary extra['block_device_mapping'] = self._to_instance_device_mappings( element) extra['groups'] = self._get_security_groups(element) extra['network_interfaces'] = self._to_interfaces(element) extra['product_codes'] = product_codes extra['tags'] = tags return Node(id=instance_id, name=name, state=state, public_ips=public_ips, private_ips=private_ips, driver=self.connection.driver, created_at=created, extra=extra) def _to_images(self, object): return [self._to_image(el) for el in object.findall( fixxpath(xpath='imagesSet/item', namespace=NAMESPACE)) ] def _to_image(self, element): id = findtext(element=element, xpath='imageId', namespace=NAMESPACE) name = findtext(element=element, xpath='name', namespace=NAMESPACE) # Build block device mapping block_device_mapping = self._to_device_mappings(element) billing_products = [] for p in findall(element=element, xpath="billingProducts/item/billingProduct", namespace=NAMESPACE): billing_products.append(p.text) # Get our tags tags = self._get_resource_tags(element) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['image']) # Add our tags and block device mapping extra['tags'] = tags extra['block_device_mapping'] = block_device_mapping extra['billing_products'] = billing_products return NodeImage(id=id, name=name, driver=self, extra=extra) def _to_volume(self, element, name=None): """ Parse the XML element and return a StorageVolume object. :param name: An optional name for the volume. If not provided then either tag with a key "Name" or volume ID will be used (which ever is available first in that order). :type name: ``str`` :rtype: :class:`StorageVolume` """ volId = findtext(element=element, xpath='volumeId', namespace=NAMESPACE) size = findtext(element=element, xpath='size', namespace=NAMESPACE) raw_state = findtext(element=element, xpath='status', namespace=NAMESPACE) state = self.VOLUME_STATE_MAP.get(raw_state, StorageVolumeState.UNKNOWN) # Get our tags tags = self._get_resource_tags(element) # If name was not passed into the method then # fall back then use the volume id name = name if name else tags.get('Name', volId) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['volume']) extra['tags'] = tags return StorageVolume(id=volId, name=name, size=int(size), driver=self, state=state, extra=extra) def _to_volume_modifications(self, object): return [self._to_volume_modification(el) for el in object.findall( fixxpath(xpath='volumeModificationSet/item', namespace=NAMESPACE)) ] def _to_volume_modification(self, element): """ Parse the XML element and return a StorageVolume object. :rtype: :class:`EC2VolumeModification` """ params = self._get_extra_dict(element, VOLUME_MODIFICATION_ATTRIBUTE_MAP) return EC2VolumeModification(**params) def _to_snapshots(self, response): return [self._to_snapshot(el) for el in response.findall( fixxpath(xpath='snapshotSet/item', namespace=NAMESPACE)) ] def _to_snapshot(self, element, name=None): snapId = findtext(element=element, xpath='snapshotId', namespace=NAMESPACE) size = findtext(element=element, xpath='volumeSize', namespace=NAMESPACE) created = parse_date(findtext(element=element, xpath='startTime', namespace=NAMESPACE)) # Get our tags tags = self._get_resource_tags(element) # If name was not passed into the method then # fall back then use the snapshot id name = name if name else tags.get('Name', snapId) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['snapshot']) # Add tags and name to the extra dict extra['tags'] = tags extra['name'] = name # state state = self.SNAPSHOT_STATE_MAP.get( extra["state"], VolumeSnapshotState.UNKNOWN ) return VolumeSnapshot(snapId, size=int(size), driver=self, extra=extra, created=created, state=state, name=name) def _to_import_snapshot_task(self, element): status = findtext(element=element, xpath='importSnapshotTaskSet/item/' 'snapshotTaskDetail/status', namespace=NAMESPACE) if status != 'completed': snapshotId = None else: xpath = 'importSnapshotTaskSet/item/snapshotTaskDetail/snapshotId' snapshotId = findtext(element=element, xpath=xpath, namespace=NAMESPACE) return EC2ImportSnapshotTask(status, snapshotId=snapshotId) def _to_key_pairs(self, elems): key_pairs = [self._to_key_pair(elem=elem) for elem in elems] return key_pairs def _to_key_pair(self, elem): name = findtext(element=elem, xpath='keyName', namespace=NAMESPACE) fingerprint = findtext(element=elem, xpath='keyFingerprint', namespace=NAMESPACE).strip() private_key = findtext(element=elem, xpath='keyMaterial', namespace=NAMESPACE) key_pair = KeyPair(name=name, public_key=None, fingerprint=fingerprint, private_key=private_key, driver=self) return key_pair def _to_security_groups(self, response): return [self._to_security_group(el) for el in response.findall( fixxpath(xpath='securityGroupInfo/item', namespace=NAMESPACE)) ] def _to_security_group(self, element): # security group id sg_id = findtext(element=element, xpath='groupId', namespace=NAMESPACE) # security group name name = findtext(element=element, xpath='groupName', namespace=NAMESPACE) # Get our tags tags = self._get_resource_tags(element) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['security_group']) # Add tags to the extra dict extra['tags'] = tags # Get ingress rules ingress_rules = self._to_security_group_rules( element, 'ipPermissions/item' ) # Get egress rules egress_rules = self._to_security_group_rules( element, 'ipPermissionsEgress/item' ) return EC2SecurityGroup(sg_id, name, ingress_rules, egress_rules, extra=extra) def _to_security_group_rules(self, element, xpath): return [self._to_security_group_rule(el) for el in element.findall( fixxpath(xpath=xpath, namespace=NAMESPACE)) ] def _to_security_group_rule(self, element): """ Parse the XML element and return a SecurityGroup object. :rtype: :class:`EC2SecurityGroup` """ rule = {} rule['protocol'] = findtext(element=element, xpath='ipProtocol', namespace=NAMESPACE) rule['from_port'] = findtext(element=element, xpath='fromPort', namespace=NAMESPACE) rule['to_port'] = findtext(element=element, xpath='toPort', namespace=NAMESPACE) # get security groups elements = element.findall(fixxpath( xpath='groups/item', namespace=NAMESPACE )) rule['group_pairs'] = [] for element in elements: item = { 'user_id': findtext( element=element, xpath='userId', namespace=NAMESPACE), 'group_id': findtext( element=element, xpath='groupId', namespace=NAMESPACE), 'group_name': findtext( element=element, xpath='groupName', namespace=NAMESPACE) } rule['group_pairs'].append(item) # get ip ranges elements = element.findall(fixxpath( xpath='ipRanges/item', namespace=NAMESPACE )) rule['cidr_ips'] = [ findtext( element=element, xpath='cidrIp', namespace=NAMESPACE ) for element in elements] return rule def _to_networks(self, response): return [self._to_network(el) for el in response.findall( fixxpath(xpath='vpcSet/item', namespace=NAMESPACE)) ] def _to_network(self, element, name=None): # Get the network id vpc_id = findtext(element=element, xpath='vpcId', namespace=NAMESPACE) # Get our tags tags = self._get_resource_tags(element) # Set our name if the Name key/value if available # If we don't get anything back then use the vpc_id name = name if name else tags.get('Name', vpc_id) cidr_block = findtext(element=element, xpath='cidrBlock', namespace=NAMESPACE) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['network']) # Add tags to the extra dict extra['tags'] = tags return EC2Network(vpc_id, name, cidr_block, extra=extra) def _to_addresses(self, response, only_associated): """ Builds a list of dictionaries containing elastic IP properties. :param only_associated: If true, return only those addresses that are associated with an instance. If false, return all addresses. :type only_associated: ``bool`` :rtype: ``list`` of :class:`ElasticIP` """ addresses = [] for el in response.findall(fixxpath(xpath='addressesSet/item', namespace=NAMESPACE)): addr = self._to_address(el, only_associated) if addr is not None: addresses.append(addr) return addresses def _to_address(self, element, only_associated): instance_id = findtext(element=element, xpath='instanceId', namespace=NAMESPACE) public_ip = findtext(element=element, xpath='publicIp', namespace=NAMESPACE) domain = findtext(element=element, xpath='domain', namespace=NAMESPACE) # Build our extra dict extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['elastic_ip']) # Return NoneType if only associated IPs are requested if only_associated and not instance_id: return None return ElasticIP(public_ip, domain, instance_id, extra=extra) def _to_placement_groups(self, response): return [self._to_placement_group(el) for el in response.findall( fixxpath(xpath='placementGroupSet/item', namespace=NAMESPACE))] def _to_placement_group(self, element): name = findtext(element=element, xpath='groupName', namespace=NAMESPACE) state = findtext(element=element, xpath='state', namespace=NAMESPACE) strategy = findtext(element=element, xpath='strategy', namespace=NAMESPACE) return EC2PlacementGroup(name, state, strategy) def _to_subnets(self, response): return [self._to_subnet(el) for el in response.findall( fixxpath(xpath='subnetSet/item', namespace=NAMESPACE)) ] def _to_subnet(self, element, name=None): # Get the subnet ID subnet_id = findtext(element=element, xpath='subnetId', namespace=NAMESPACE) # Get our tags tags = self._get_resource_tags(element) # If we don't get anything back then use the subnet_id name = name if name else tags.get('Name', subnet_id) state = findtext(element=element, xpath='state', namespace=NAMESPACE) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['subnet']) # Also include our tags extra['tags'] = tags return EC2NetworkSubnet(subnet_id, name, state, extra=extra) def _to_interfaces(self, response): return [self._to_interface(el) for el in response.findall( fixxpath(xpath='networkInterfaceSet/item', namespace=NAMESPACE)) ] def _to_interface(self, element, name=None): """ Parse the XML element and return an EC2NetworkInterface object. :param name: An optional name for the interface. If not provided then either tag with a key "Name" or the interface ID will be used (whichever is available first in that order). :type name: ``str`` :rtype: :class: `EC2NetworkInterface` """ interface_id = findtext(element=element, xpath='networkInterfaceId', namespace=NAMESPACE) state = findtext(element=element, xpath='status', namespace=NAMESPACE) # Get tags tags = self._get_resource_tags(element) name = name if name else tags.get('Name', interface_id) # Build security groups groups = self._get_security_groups(element) # Build private IPs priv_ips = [] for item in findall(element=element, xpath='privateIpAddressesSet/item', namespace=NAMESPACE): priv_ips.append({'private_ip': findtext(element=item, xpath='privateIpAddress', namespace=NAMESPACE), 'private_dns': findtext(element=item, xpath='privateDnsName', namespace=NAMESPACE), 'primary': findtext(element=item, xpath='primary', namespace=NAMESPACE)}) # Build our attachment dictionary which we will add into extra later attributes_map = \ RESOURCE_EXTRA_ATTRIBUTES_MAP['network_interface_attachment'] attachment = self._get_extra_dict(element, attributes_map) # Build our extra dict attributes_map = RESOURCE_EXTRA_ATTRIBUTES_MAP['network_interface'] extra = self._get_extra_dict(element, attributes_map) # Include our previously built items as well extra['tags'] = tags extra['attachment'] = attachment extra['private_ips'] = priv_ips extra['groups'] = groups return EC2NetworkInterface(interface_id, name, state, extra=extra) def _to_reserved_nodes(self, object, xpath): return [self._to_reserved_node(el) for el in object.findall(fixxpath(xpath=xpath, namespace=NAMESPACE))] def _to_reserved_node(self, element): """ Build an EC2ReservedNode object using the reserved instance properties. Information on these properties can be found at http://goo.gl/ulXCC7. """ # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['reserved_node']) try: size = [size for size in self.list_sizes() if size.id == extra['instance_type']][0] except IndexError: size = None return EC2ReservedNode(id=findtext(element=element, xpath='reservedInstancesId', namespace=NAMESPACE), state=findattr(element=element, xpath='state', namespace=NAMESPACE), driver=self, size=size, extra=extra) def _to_device_mappings(self, object): return [self._to_device_mapping(el) for el in object.findall( fixxpath(xpath='blockDeviceMapping/item', namespace=NAMESPACE)) ] def _to_device_mapping(self, element): """ Parse the XML element and return a dictionary of device properties. Additional information can be found at http://goo.gl/GjWYBf. @note: EBS volumes do not have a virtual name. Only ephemeral disks use this property. :rtype: ``dict`` """ mapping = {} mapping['device_name'] = findattr(element=element, xpath='deviceName', namespace=NAMESPACE) mapping['virtual_name'] = findattr(element=element, xpath='virtualName', namespace=NAMESPACE) # If virtual name does not exist then this is an EBS volume. # Build the EBS dictionary leveraging the _get_extra_dict method. if mapping['virtual_name'] is None: mapping['ebs'] = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['ebs_volume']) return mapping def _to_instance_device_mappings(self, object): return [self._to_instance_device_mapping(el) for el in object.findall( fixxpath(xpath='blockDeviceMapping/item', namespace=NAMESPACE)) ] def _to_instance_device_mapping(self, element): """ Parse the XML element and return a dictionary of device properties. Additional information can be found at https://goo.gl/OGK88a. :rtype: ``dict`` """ mapping = {} mapping['device_name'] = findattr(element=element, xpath='deviceName', namespace=NAMESPACE) mapping['ebs'] = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['ebs_instance_block_device']) return mapping def _to_internet_gateways(self, object, xpath): return [self._to_internet_gateway(el) for el in object.findall(fixxpath(xpath=xpath, namespace=NAMESPACE))] def _to_internet_gateway(self, element, name=None): id = findtext(element=element, xpath='internetGatewayId', namespace=NAMESPACE) vpc_id = findtext(element=element, xpath='attachmentSet/item/vpcId', namespace=NAMESPACE) state = findtext(element=element, xpath='attachmentSet/item/state', namespace=NAMESPACE) # If there's no attachment state, let's # set it to available if not state: state = 'available' # Get our tags tags = self._get_resource_tags(element) # If name was not passed into the method then # fall back then use the gateway id name = name if name else tags.get('Name', id) return VPCInternetGateway(id=id, name=name, vpc_id=vpc_id, state=state, driver=self.connection.driver, extra={'tags': tags}) def _to_route_tables(self, response): return [self._to_route_table(el) for el in response.findall( fixxpath(xpath='routeTableSet/item', namespace=NAMESPACE)) ] def _to_route_table(self, element, name=None): # route table id route_table_id = findtext(element=element, xpath='routeTableId', namespace=NAMESPACE) # Get our tags tags = self._get_resource_tags(element) # Get our extra dictionary extra = self._get_extra_dict( element, RESOURCE_EXTRA_ATTRIBUTES_MAP['route_table']) # Add tags to the extra dict extra['tags'] = tags # Get routes routes = self._to_routes(element, 'routeSet/item') # Get subnet associations subnet_associations = self._to_subnet_associations( element, 'associationSet/item') # Get propagating routes virtual private gateways (VGW) IDs propagating_gateway_ids = [] for el in element.findall(fixxpath(xpath='propagatingVgwSet/item', namespace=NAMESPACE)): propagating_gateway_ids.append(findtext(element=el, xpath='gatewayId', namespace=NAMESPACE)) name = name if name else tags.get('Name', id) return EC2RouteTable(route_table_id, name, routes, subnet_associations, propagating_gateway_ids, extra=extra) def _to_routes(self, element, xpath): return [self._to_route(el) for el in element.findall( fixxpath(xpath=xpath, namespace=NAMESPACE)) ] def _to_route(self, element): """ Parse the XML element and return a route object :rtype: :class: `EC2Route` """ destination_cidr = findtext(element=element, xpath='destinationCidrBlock', namespace=NAMESPACE) gateway_id = findtext(element=element, xpath='gatewayId', namespace=NAMESPACE) instance_id = findtext(element=element, xpath='instanceId', namespace=NAMESPACE) owner_id = findtext(element=element, xpath='instanceOwnerId', namespace=NAMESPACE) interface_id = findtext(element=element, xpath='networkInterfaceId', namespace=NAMESPACE) state = findtext(element=element, xpath='state', namespace=NAMESPACE) origin = findtext(element=element, xpath='origin', namespace=NAMESPACE) vpc_peering_connection_id = findtext(element=element, xpath='vpcPeeringConnectionId', namespace=NAMESPACE) return EC2Route(destination_cidr, gateway_id, instance_id, owner_id, interface_id, state, origin, vpc_peering_connection_id) def _to_subnet_associations(self, element, xpath): return [self._to_subnet_association(el) for el in element.findall( fixxpath(xpath=xpath, namespace=NAMESPACE)) ] def _to_subnet_association(self, element): """ Parse the XML element and return a route table association object :rtype: :class: `EC2SubnetAssociation` """ association_id = findtext(element=element, xpath='routeTableAssociationId', namespace=NAMESPACE) route_table_id = findtext(element=element, xpath='routeTableId', namespace=NAMESPACE) subnet_id = findtext(element=element, xpath='subnetId', namespace=NAMESPACE) main = findtext(element=element, xpath='main', namespace=NAMESPACE) main = True if main else False return EC2SubnetAssociation(association_id, route_table_id, subnet_id, main) def _pathlist(self, key, arr): """ Converts a key and an array of values into AWS query param format. """ params = {} i = 0 for value in arr: i += 1 params['%s.%s' % (key, i)] = value return params def _get_boolean(self, element): tag = '{%s}%s' % (NAMESPACE, 'return') return element.findtext(tag) == 'true' def _get_terminate_boolean(self, element): status = element.findtext(".//{%s}%s" % (NAMESPACE, 'name')) return any([term_status == status for term_status in ('shutting-down', 'terminated')]) def _add_instance_filter(self, params, node): """ Add instance filter to the provided params dictionary. """ filters = {'instance-id': node.id} params.update(self._build_filters(filters)) return params def _get_state_boolean(self, element): """ Checks for the instances's state """ state = findall(element=element, xpath='instancesSet/item/currentState/name', namespace=NAMESPACE)[0].text return state in ('stopping', 'pending', 'starting') def _get_extra_dict(self, element, mapping): """ Extract attributes from the element based on rules provided in the mapping dictionary. :param element: Element to parse the values from. :type element: xml.etree.ElementTree.Element. :param mapping: Dictionary with the extra layout :type node: :class:`Node` :rtype: ``dict`` """ extra = {} for attribute, values in mapping.items(): transform_func = values['transform_func'] value = findattr(element=element, xpath=values['xpath'], namespace=NAMESPACE) if value is not None: extra[attribute] = transform_func(value) else: extra[attribute] = None return extra def _get_resource_tags(self, element): """ Parse tags from the provided element and return a dictionary with key/value pairs. :rtype: ``dict`` """ tags = {} # Get our tag set by parsing the element tag_set = findall(element=element, xpath='tagSet/item', namespace=NAMESPACE) for tag in tag_set: key = findtext(element=tag, xpath='key', namespace=NAMESPACE) value = findtext(element=tag, xpath='value', namespace=NAMESPACE) tags[key] = value return tags def _get_block_device_mapping_params(self, block_device_mapping): """ Return a list of dictionaries with query parameters for a valid block device mapping. :param mapping: List of dictionaries with the drive layout :type mapping: ``list`` or ``dict`` :return: Dictionary representation of the drive mapping :rtype: ``dict`` """ if not isinstance(block_device_mapping, (list, tuple)): raise AttributeError( 'block_device_mapping not list or tuple') params = {} for idx, mapping in enumerate(block_device_mapping): idx += 1 # We want 1-based indexes if not isinstance(mapping, dict): raise AttributeError( 'mapping %s in block_device_mapping ' 'not a dict' % mapping) for k, v in mapping.items(): if not isinstance(v, dict): params['BlockDeviceMapping.%d.%s' % (idx, k)] = str(v) else: for key, value in v.items(): params['BlockDeviceMapping.%d.%s.%s' % (idx, k, key)] = str(value) return params def _get_billing_product_params(self, billing_products): """ Return a list of dictionaries with valid param for billing product. :param billing_product: List of billing code values(str) :type billing product: ``list`` :return: Dictionary representation of the billing product codes :rtype: ``dict`` """ if not isinstance(billing_products, (list, tuple)): raise AttributeError( 'billing_products not list or tuple') params = {} for idx, v in enumerate(billing_products): idx += 1 # We want 1-based indexes params['BillingProduct.%d' % (idx)] = str(v) return params def _get_disk_container_params(self, disk_container): """ Return a list of dictionaries with query parameters for a valid disk container. :param disk_container: List of dictionaries with disk_container details :type disk_container: ``list`` or ``dict`` :return: Dictionary representation of the disk_container :rtype: ``dict`` """ if not isinstance(disk_container, (list, tuple)): raise AttributeError('disk_container not list or tuple') params = {} for idx, content in enumerate(disk_container): idx += 1 # We want 1-based indexes if not isinstance(content, dict): raise AttributeError( 'content %s in disk_container not a dict' % content) for k, v in content.items(): if not isinstance(v, dict): params['DiskContainer.%s' % (k)] = str(v) else: for key, value in v.items(): params['DiskContainer.%s.%s' % (k, key)] = str(value) return params def _get_client_data_params(self, client_data): """ Return a dictionary with query parameters for a valid client data. :param client_data: List of dictionaries with the disk upload details :type client_data: ``dict`` :return: Dictionary representation of the client data :rtype: ``dict`` """ if not isinstance(client_data, (list, tuple)): raise AttributeError('client_data not list or tuple') params = {} for idx, content in enumerate(client_data): idx += 1 # We want 1-based indexes if not isinstance(content, dict): raise AttributeError( 'content %s in client_data' 'not a dict' % content) for k, v in content.items(): params['ClientData.%s' % (k)] = str(v) return params def _get_common_security_group_params(self, group_id, protocol, from_port, to_port, cidr_ips, group_pairs, description=None): """ Return a dictionary with common query parameters which are used when operating on security groups. :rtype: ``dict`` """ params = {'GroupId': group_id, 'IpPermissions.1.IpProtocol': protocol, 'IpPermissions.1.FromPort': from_port, 'IpPermissions.1.ToPort': to_port} if cidr_ips is not None: ip_ranges = {} for index, cidr_ip in enumerate(cidr_ips): index += 1 ip_ranges['IpPermissions.1.IpRanges.%s.CidrIp' % (index)] = cidr_ip if description is not None: ip_ranges['IpPermissions.1.IpRanges.%s.Description' % (index)] = description params.update(ip_ranges) if group_pairs is not None: user_groups = {} for index, group_pair in enumerate(group_pairs): index += 1 if 'group_id' in group_pair.keys(): user_groups['IpPermissions.1.Groups.%s.GroupId' % (index)] = group_pair['group_id'] if 'group_name' in group_pair.keys(): user_groups['IpPermissions.1.Groups.%s.GroupName' % (index)] = group_pair['group_name'] if 'user_id' in group_pair.keys(): user_groups['IpPermissions.1.Groups.%s.UserId' % (index)] = group_pair['user_id'] params.update(user_groups) return params def _get_security_groups(self, element): """ Parse security groups from the provided element and return a list of security groups with the id ane name key/value pairs. :rtype: ``list`` of ``dict`` """ groups = [] for item in findall(element=element, xpath='groupSet/item', namespace=NAMESPACE): groups.append({ 'group_id': findtext(element=item, xpath='groupId', namespace=NAMESPACE), 'group_name': findtext(element=item, xpath='groupName', namespace=NAMESPACE) }) return groups def _build_filters(self, filters): """ Return a dictionary with filter query parameters which are used when listing networks, security groups, etc. :param filters: Dict of filter names and filter values :type filters: ``dict`` :rtype: ``dict`` """ filter_entries = {} for filter_idx, filter_data in enumerate(filters.items()): filter_idx += 1 # We want 1-based indexes filter_name, filter_values = filter_data filter_key = 'Filter.%s.Name' % (filter_idx) filter_entries[filter_key] = filter_name if isinstance(filter_values, list): for value_idx, value in enumerate(filter_values): value_idx += 1 # We want 1-based indexes value_key = 'Filter.%s.Value.%s' % (filter_idx, value_idx) filter_entries[value_key] = value else: value_key = 'Filter.%s.Value.1' % (filter_idx) filter_entries[value_key] = filter_values return filter_entries
[docs]class EC2NodeDriver(BaseEC2NodeDriver): """ Amazon EC2 node driver. """ connectionCls = EC2Connection type = Provider.EC2 name = 'Amazon EC2' website = 'http://aws.amazon.com/ec2/' path = '/' NODE_STATE_MAP = { 'pending': NodeState.PENDING, 'running': NodeState.RUNNING, 'shutting-down': NodeState.UNKNOWN, 'terminated': NodeState.TERMINATED, 'stopped': NodeState.STOPPED } def __init__(self, key, secret=None, secure=True, host=None, port=None, region='us-east-1', token=None, **kwargs): if hasattr(self, '_region'): region = self._region # pylint: disable=no-member valid_regions = self.list_regions() if region not in valid_regions: raise ValueError('Invalid region: %s' % (region)) details = REGION_DETAILS[region] self.region_name = region self.token = token self.api_name = details['api_name'] self.country = details['country'] self.signature_version = details.get('signature_version', DEFAULT_SIGNATURE_VERSION) host = host or details['endpoint'] super(EC2NodeDriver, self).__init__(key=key, secret=secret, secure=secure, host=host, port=port, **kwargs)
[docs] @classmethod def list_regions(cls): return VALID_EC2_REGIONS
[docs]class IdempotentParamError(LibcloudError): """ Request used the same client token as a previous, but non-identical request. """ def __str__(self): return repr(self.value)
class EucConnection(EC2Connection): """ Connection class for Eucalyptus """ host = None
[docs]class EucNodeDriver(BaseEC2NodeDriver): """ Driver class for Eucalyptus """ name = 'Eucalyptus' website = 'http://www.eucalyptus.com/' api_name = 'ec2_us_east' region_name = 'us-east-1' connectionCls = EucConnection signature_version = '2' def __init__(self, key, secret=None, secure=True, host=None, path=None, port=None, api_version=DEFAULT_EUCA_API_VERSION): """ @inherits: :class:`EC2NodeDriver.__init__` :param path: The host where the API can be reached. :type path: ``str`` :param api_version: The API version to extend support for Eucalyptus proprietary API calls :type api_version: ``str`` """ super(EucNodeDriver, self).__init__(key, secret, secure, host, port) if path is None: path = '/services/Eucalyptus' self.path = path self.EUCA_NAMESPACE = 'http://msgs.eucalyptus.com/%s' % (api_version)
[docs] def list_locations(self): raise NotImplementedError( 'list_locations not implemented for this driver')
def _to_sizes(self, response): return [self._to_size(el) for el in response.findall( fixxpath(xpath='instanceTypeDetails/item', namespace=self.EUCA_NAMESPACE))] def _to_size(self, el): name = findtext(element=el, xpath='name', namespace=self.EUCA_NAMESPACE) cpu = findtext(element=el, xpath='cpu', namespace=self.EUCA_NAMESPACE) disk = findtext(element=el, xpath='disk', namespace=self.EUCA_NAMESPACE) memory = findtext(element=el, xpath='memory', namespace=self.EUCA_NAMESPACE) return NodeSize(id=name, name=name, ram=int(memory), disk=int(disk), bandwidth=None, price=None, driver=EucNodeDriver, extra={ 'cpu': int(cpu) })
[docs] def list_sizes(self): """ Lists available nodes sizes. :rtype: ``list`` of :class:`NodeSize` """ params = {'Action': 'DescribeInstanceTypes'} response = self.connection.request(self.path, params=params).object return self._to_sizes(response)
def _add_instance_filter(self, params, node): """ Eucalyptus driver doesn't support filtering on instance id so this is a no-op. """ pass
class NimbusConnection(EC2Connection): """ Connection class for Nimbus """ host = None
[docs]class NimbusNodeDriver(BaseEC2NodeDriver): """ Driver class for Nimbus """ type = Provider.NIMBUS name = 'Nimbus' website = 'http://www.nimbusproject.org/' country = 'Private' api_name = 'nimbus' region_name = 'nimbus' friendly_name = 'Nimbus Private Cloud' connectionCls = NimbusConnection signature_version = '2'
[docs] def ex_describe_addresses(self, nodes): """ Nimbus doesn't support elastic IPs, so this is a pass-through. @inherits: :class:`EC2NodeDriver.ex_describe_addresses` """ nodes_elastic_ip_mappings = {} for node in nodes: # empty list per node nodes_elastic_ip_mappings[node.id] = [] return nodes_elastic_ip_mappings
[docs] def ex_create_tags(self, resource, tags): """ Nimbus doesn't support creating tags, so this is a pass-through. @inherits: :class:`EC2NodeDriver.ex_create_tags` """ pass
class OutscaleConnection(EC2Connection): """ Connection class for Outscale """ version = DEFAULT_OUTSCALE_API_VERSION host = None class OutscaleNodeDriver(BaseEC2NodeDriver): """ Base Outscale FCU node driver. Outscale per provider driver classes inherit from it. """ connectionCls = OutscaleConnection name = 'Outscale' website = 'http://www.outscale.com' path = '/' signature_version = '2' NODE_STATE_MAP = { 'pending': NodeState.PENDING, 'running': NodeState.RUNNING, 'shutting-down': NodeState.UNKNOWN, 'terminated': NodeState.TERMINATED, 'stopped': NodeState.STOPPED } def __init__(self, key, secret=None, secure=True, host=None, port=None, region='us-east-1', region_details=None, **kwargs): if hasattr(self, '_region'): region = getattr(self, '_region', None) if region_details is None: raise ValueError('Invalid region_details argument') if region not in region_details.keys(): raise ValueError('Invalid region: %s' % (region)) self.region_name = region self.region_details = region_details details = self.region_details[region] self.api_name = details['api_name'] self.country = details['country'] self.connectionCls.host = details['endpoint'] self._not_implemented_msg =\ 'This method is not supported in the Outscale driver' super(OutscaleNodeDriver, self).__init__(key=key, secret=secret, secure=secure, host=host, port=port, **kwargs) def create_node(self, **kwargs): """ Creates a new Outscale node. The ex_iamprofile keyword is not supported. @inherits: :class:`BaseEC2NodeDriver.create_node` :keyword ex_keyname: The name of the key pair :type ex_keyname: ``str`` :keyword ex_userdata: The user data :type ex_userdata: ``str`` :keyword ex_security_groups: A list of names of security groups to assign to the node. :type ex_security_groups: ``list`` :keyword ex_metadata: The Key/Value metadata to associate with a node. :type ex_metadata: ``dict`` :keyword ex_mincount: The minimum number of nodes to launch :type ex_mincount: ``int`` :keyword ex_maxcount: The maximum number of nodes to launch :type ex_maxcount: ``int`` :keyword ex_clienttoken: A unique identifier to ensure idempotency :type ex_clienttoken: ``str`` :keyword ex_blockdevicemappings: ``list`` of ``dict`` block device mappings. :type ex_blockdevicemappings: ``list`` of ``dict`` :keyword ex_ebs_optimized: EBS-Optimized if True :type ex_ebs_optimized: ``bool`` """ if 'ex_iamprofile' in kwargs: raise NotImplementedError("ex_iamprofile not implemented") return super(OutscaleNodeDriver, self).create_node(**kwargs) def ex_create_network(self, cidr_block, name=None): """ Creates a network/VPC. Outscale does not support instance_tenancy. :param cidr_block: The CIDR block assigned to the network :type cidr_block: ``str`` :param name: An optional name for the network :type name: ``str`` :return: Dictionary of network properties :rtype: ``dict`` """ return super(OutscaleNodeDriver, self).ex_create_network(cidr_block, name=name) def ex_modify_instance_attribute(self, node, disable_api_termination=None, ebs_optimized=None, group_id=None, source_dest_check=None, user_data=None, instance_type=None, attributes=None): """ Modifies node attributes. Ouscale supports the following attributes: 'DisableApiTermination.Value', 'EbsOptimized', 'GroupId.n', 'SourceDestCheck.Value', 'UserData.Value', 'InstanceType.Value' :param node: Node instance :type node: :class:`Node` :param attributes: A dictionary with node attributes :type attributes: ``dict`` :return: True on success, False otherwise. :rtype: ``bool`` """ attributes = attributes or {} if disable_api_termination is not None: attributes['DisableApiTermination.Value'] = disable_api_termination if ebs_optimized is not None: attributes['EbsOptimized'] = ebs_optimized if group_id is not None: attributes['GroupId.n'] = group_id if source_dest_check is not None: attributes['SourceDestCheck.Value'] = source_dest_check if user_data is not None: attributes['UserData.Value'] = user_data if instance_type is not None: attributes['InstanceType.Value'] = instance_type return super(OutscaleNodeDriver, self).ex_modify_instance_attribute( node, attributes) def ex_register_image(self, name, description=None, architecture=None, root_device_name=None, block_device_mapping=None): """ Registers a Machine Image based off of an EBS-backed instance. Can also be used to create images from snapshots. Outscale does not support image_location, kernel_id and ramdisk_id. :param name: The name for the AMI being registered :type name: ``str`` :param description: The description of the AMI (optional) :type description: ``str`` :param architecture: The architecture of the AMI (i386/x86_64) (optional) :type architecture: ``str`` :param root_device_name: The device name for the root device Required if registering an EBS-backed AMI :type root_device_name: ``str`` :param block_device_mapping: A dictionary of the disk layout (optional) :type block_device_mapping: ``dict`` :rtype: :class:`NodeImage` """ return super(OutscaleNodeDriver, self).ex_register_image( name, description=description, architecture=architecture, root_device_name=root_device_name, block_device_mapping=block_device_mapping) def ex_copy_image(self, source_region, image, name=None, description=None): """ Outscale does not support copying images. @inherits: :class:`EC2NodeDriver.ex_copy_image` """ raise NotImplementedError(self._not_implemented_msg) def ex_get_limits(self): """ Outscale does not support getting limits. @inherits: :class:`EC2NodeDriver.ex_get_limits` """ raise NotImplementedError(self._not_implemented_msg) def ex_create_network_interface(self, subnet, name=None, description=None, private_ip_address=None): """ Outscale does not support creating a network interface within a VPC. @inherits: :class:`EC2NodeDriver.ex_create_network_interface` """ raise NotImplementedError(self._not_implemented_msg) def ex_delete_network_interface(self, network_interface): """ Outscale does not support deleting a network interface within a VPC. @inherits: :class:`EC2NodeDriver.ex_delete_network_interface` """ raise NotImplementedError(self._not_implemented_msg) def ex_attach_network_interface_to_node(self, network_interface, node, device_index): """ Outscale does not support attaching a network interface. @inherits: :class:`EC2NodeDriver.ex_attach_network_interface_to_node` """ raise NotImplementedError(self._not_implemented_msg) def ex_detach_network_interface(self, attachment_id, force=False): """ Outscale does not support detaching a network interface @inherits: :class:`EC2NodeDriver.ex_detach_network_interface` """ raise NotImplementedError(self._not_implemented_msg) def list_sizes(self, location=None): """ Lists available nodes sizes. This overrides the EC2 default method in order to use Outscale information or data. :rtype: ``list`` of :class:`NodeSize` """ available_types =\ self.region_details[self.region_name]['instance_types'] sizes = [] for instance_type in available_types: attributes = OUTSCALE_INSTANCE_TYPES[instance_type] attributes = copy.deepcopy(attributes) price = self._get_size_price(size_id=instance_type) attributes.update({'price': price}) sizes.append(NodeSize(driver=self, **attributes)) return sizes def ex_modify_instance_keypair(self, instance_id, key_name=None): """ Modifies the keypair associated with a specified instance. Once the modification is done, you must restart the instance. :param instance_id: The ID of the instance :type instance_id: ``string`` :param key_name: The name of the keypair :type key_name: ``string`` """ params = {'Action': 'ModifyInstanceKeypair'} params.update({'instanceId': instance_id}) if key_name is not None: params.update({'keyName': key_name}) response = self.connection.request(self.path, params=params, method='GET').object return (findtext(element=response, xpath='return', namespace=OUTSCALE_NAMESPACE) == 'true') def _to_quota(self, elem): """ To Quota """ quota = {} for reference_quota_item in findall(element=elem, xpath='referenceQuotaSet/item', namespace=OUTSCALE_NAMESPACE): reference = findtext(element=reference_quota_item, xpath='reference', namespace=OUTSCALE_NAMESPACE) quota_set = [] for quota_item in findall(element=reference_quota_item, xpath='quotaSet/item', namespace=OUTSCALE_NAMESPACE): ownerId = findtext(element=quota_item, xpath='ownerId', namespace=OUTSCALE_NAMESPACE) name = findtext(element=quota_item, xpath='name', namespace=OUTSCALE_NAMESPACE) displayName = findtext(element=quota_item, xpath='displayName', namespace=OUTSCALE_NAMESPACE) description = findtext(element=quota_item, xpath='description', namespace=OUTSCALE_NAMESPACE) groupName = findtext(element=quota_item, xpath='groupName', namespace=OUTSCALE_NAMESPACE) maxQuotaValue = findtext(element=quota_item, xpath='maxQuotaValue', namespace=OUTSCALE_NAMESPACE) usedQuotaValue = findtext(element=quota_item, xpath='usedQuotaValue', namespace=OUTSCALE_NAMESPACE) quota_set.append({'ownerId': ownerId, 'name': name, 'displayName': displayName, 'description': description, 'groupName': groupName, 'maxQuotaValue': maxQuotaValue, 'usedQuotaValue': usedQuotaValue}) quota[reference] = quota_set return quota def ex_describe_quotas(self, dry_run=False, filters=None, max_results=None, marker=None): """ Describes one or more of your quotas. :param dry_run: dry_run :type dry_run: ``bool`` :param filters: The filters so that the response returned includes information for certain quotas only. :type filters: ``dict`` :param max_results: The maximum number of items that can be returned in a single page (by default, 100) :type max_results: ``int`` :param marker: Set quota marker :type marker: ``string`` :return: (is_truncated, quota) tuple :rtype: ``(bool, dict)`` """ if filters: raise NotImplementedError( 'quota filters are not implemented') if marker: raise NotImplementedError( 'quota marker is not implemented') params = {'Action': 'DescribeQuotas'} if dry_run: params.update({'DryRun': dry_run}) if max_results: params.update({'MaxResults': max_results}) response = self.connection.request(self.path, params=params, method='GET').object quota = self._to_quota(response) is_truncated = findtext(element=response, xpath='isTruncated', namespace=OUTSCALE_NAMESPACE) return is_truncated, quota def _to_product_type(self, elem): productTypeId = findtext(element=elem, xpath='productTypeId', namespace=OUTSCALE_NAMESPACE) description = findtext(element=elem, xpath='description', namespace=OUTSCALE_NAMESPACE) return {'productTypeId': productTypeId, 'description': description} def ex_get_product_type(self, image_id, snapshot_id=None): """ Gets the product type of a specified OMI or snapshot. :param image_id: The ID of the OMI :type image_id: ``string`` :param snapshot_id: The ID of the snapshot :type snapshot_id: ``string`` :return: A product type :rtype: ``dict`` """ params = {'Action': 'GetProductType'} params.update({'ImageId': image_id}) if snapshot_id is not None: params.update({'SnapshotId': snapshot_id}) response = self.connection.request(self.path, params=params, method='GET').object product_type = self._to_product_type(response) return product_type def _to_product_types(self, elem): product_types = [] for product_types_item in findall(element=elem, xpath='productTypeSet/item', namespace=OUTSCALE_NAMESPACE): productTypeId = findtext(element=product_types_item, xpath='productTypeId', namespace=OUTSCALE_NAMESPACE) description = findtext(element=product_types_item, xpath='description', namespace=OUTSCALE_NAMESPACE) product_types.append({'productTypeId': productTypeId, 'description': description}) return product_types def ex_describe_product_types(self, filters=None): """ Describes product types. :param filters: The filters so that the list returned includes information for certain quotas only. :type filters: ``dict`` :return: A product types list :rtype: ``list`` """ params = {'Action': 'DescribeProductTypes'} if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params, method='GET').object product_types = self._to_product_types(response) return product_types def _to_instance_types(self, elem): instance_types = [] for instance_types_item in findall(element=elem, xpath='instanceTypeSet/item', namespace=OUTSCALE_NAMESPACE): name = findtext(element=instance_types_item, xpath='name', namespace=OUTSCALE_NAMESPACE) vcpu = findtext(element=instance_types_item, xpath='vcpu', namespace=OUTSCALE_NAMESPACE) memory = findtext(element=instance_types_item, xpath='memory', namespace=OUTSCALE_NAMESPACE) storageSize = findtext(element=instance_types_item, xpath='storageSize', namespace=OUTSCALE_NAMESPACE) storageCount = findtext(element=instance_types_item, xpath='storageCount', namespace=OUTSCALE_NAMESPACE) maxIpAddresses = findtext(element=instance_types_item, xpath='maxIpAddresses', namespace=OUTSCALE_NAMESPACE) ebsOptimizedAvailable = findtext(element=instance_types_item, xpath='ebsOptimizedAvailable', namespace=OUTSCALE_NAMESPACE) d = {'name': name, 'vcpu': vcpu, 'memory': memory, 'storageSize': storageSize, 'storageCount': storageCount, 'maxIpAddresses': maxIpAddresses, 'ebsOptimizedAvailable': ebsOptimizedAvailable} instance_types.append(d) return instance_types def ex_describe_instance_types(self, filters=None): """ Describes instance types. :param filters: The filters so that the list returned includes information for instance types only :type filters: ``dict`` :return: A instance types list :rtype: ``list`` """ params = {'Action': 'DescribeInstanceTypes'} if filters: params.update(self._build_filters(filters)) response = self.connection.request(self.path, params=params, method='GET').object instance_types = self._to_instance_types(response) return instance_types
[docs]class OutscaleSASNodeDriver(OutscaleNodeDriver): """ Outscale SAS node driver """ name = 'Outscale SAS' type = Provider.OUTSCALE_SAS def __init__(self, key, secret=None, secure=True, host=None, port=None, region='us-east-1', region_details=None, **kwargs): super(OutscaleSASNodeDriver, self).__init__( key=key, secret=secret, secure=secure, host=host, port=port, region=region, region_details=OUTSCALE_SAS_REGION_DETAILS, **kwargs)
[docs]class OutscaleINCNodeDriver(OutscaleNodeDriver): """ Outscale INC node driver """ name = 'Outscale INC' type = Provider.OUTSCALE_INC def __init__(self, key, secret=None, secure=True, host=None, port=None, region='us-east-1', region_details=None, **kwargs): super(OutscaleINCNodeDriver, self).__init__( key=key, secret=secret, secure=secure, host=host, port=port, region=region, region_details=OUTSCALE_INC_REGION_DETAILS, **kwargs)