1# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/ 2# Copyright (c) 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved 3# 4# Permission is hereby granted, free of charge, to any person obtaining a 5# copy of this software and associated documentation files (the 6# "Software"), to deal in the Software without restriction, including 7# without limitation the rights to use, copy, modify, merge, publish, dis- 8# tribute, sublicense, and/or sell copies of the Software, and to permit 9# persons to whom the Software is furnished to do so, subject to the fol- 10# lowing conditions: 11# 12# The above copyright notice and this permission notice shall be included 13# in all copies or substantial portions of the Software. 14# 15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 17# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 18# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21# IN THE SOFTWARE. 22 23""" 24Represents an EC2 Elastic Network Interface 25""" 26from boto.exception import BotoClientError 27from boto.ec2.ec2object import TaggedEC2Object 28from boto.resultset import ResultSet 29from boto.ec2.group import Group 30 31 32class Attachment(object): 33 """ 34 :ivar id: The ID of the attachment. 35 :ivar instance_id: The ID of the instance. 36 :ivar device_index: The index of this device. 37 :ivar status: The status of the device. 38 :ivar attach_time: The time the device was attached. 39 :ivar delete_on_termination: Whether the device will be deleted 40 when the instance is terminated. 41 """ 42 43 def __init__(self): 44 self.id = None 45 self.instance_id = None 46 self.instance_owner_id = None 47 self.device_index = 0 48 self.status = None 49 self.attach_time = None 50 self.delete_on_termination = False 51 52 def __repr__(self): 53 return 'Attachment:%s' % self.id 54 55 def startElement(self, name, attrs, connection): 56 return None 57 58 def endElement(self, name, value, connection): 59 if name == 'attachmentId': 60 self.id = value 61 elif name == 'instanceId': 62 self.instance_id = value 63 elif name == 'deviceIndex': 64 self.device_index = int(value) 65 elif name == 'instanceOwnerId': 66 self.instance_owner_id = value 67 elif name == 'status': 68 self.status = value 69 elif name == 'attachTime': 70 self.attach_time = value 71 elif name == 'deleteOnTermination': 72 if value.lower() == 'true': 73 self.delete_on_termination = True 74 else: 75 self.delete_on_termination = False 76 else: 77 setattr(self, name, value) 78 79 80class NetworkInterface(TaggedEC2Object): 81 """ 82 An Elastic Network Interface. 83 84 :ivar id: The ID of the ENI. 85 :ivar subnet_id: The ID of the VPC subnet. 86 :ivar vpc_id: The ID of the VPC. 87 :ivar description: The description. 88 :ivar owner_id: The ID of the owner of the ENI. 89 :ivar requester_managed: 90 :ivar status: The interface's status (available|in-use). 91 :ivar mac_address: The MAC address of the interface. 92 :ivar private_ip_address: The IP address of the interface within 93 the subnet. 94 :ivar source_dest_check: Flag to indicate whether to validate 95 network traffic to or from this network interface. 96 :ivar groups: List of security groups associated with the interface. 97 :ivar attachment: The attachment object. 98 :ivar private_ip_addresses: A list of PrivateIPAddress objects. 99 """ 100 101 def __init__(self, connection=None): 102 super(NetworkInterface, self).__init__(connection) 103 self.id = None 104 self.subnet_id = None 105 self.vpc_id = None 106 self.availability_zone = None 107 self.description = None 108 self.owner_id = None 109 self.requester_managed = False 110 self.status = None 111 self.mac_address = None 112 self.private_ip_address = None 113 self.source_dest_check = None 114 self.groups = [] 115 self.attachment = None 116 self.private_ip_addresses = [] 117 118 def __repr__(self): 119 return 'NetworkInterface:%s' % self.id 120 121 def startElement(self, name, attrs, connection): 122 retval = super(NetworkInterface, self).startElement(name, attrs, connection) 123 if retval is not None: 124 return retval 125 if name == 'groupSet': 126 self.groups = ResultSet([('item', Group)]) 127 return self.groups 128 elif name == 'attachment': 129 self.attachment = Attachment() 130 return self.attachment 131 elif name == 'privateIpAddressesSet': 132 self.private_ip_addresses = ResultSet([('item', PrivateIPAddress)]) 133 return self.private_ip_addresses 134 else: 135 return None 136 137 def endElement(self, name, value, connection): 138 if name == 'networkInterfaceId': 139 self.id = value 140 elif name == 'subnetId': 141 self.subnet_id = value 142 elif name == 'vpcId': 143 self.vpc_id = value 144 elif name == 'availabilityZone': 145 self.availability_zone = value 146 elif name == 'description': 147 self.description = value 148 elif name == 'ownerId': 149 self.owner_id = value 150 elif name == 'requesterManaged': 151 if value.lower() == 'true': 152 self.requester_managed = True 153 else: 154 self.requester_managed = False 155 elif name == 'status': 156 self.status = value 157 elif name == 'macAddress': 158 self.mac_address = value 159 elif name == 'privateIpAddress': 160 self.private_ip_address = value 161 elif name == 'sourceDestCheck': 162 if value.lower() == 'true': 163 self.source_dest_check = True 164 else: 165 self.source_dest_check = False 166 else: 167 setattr(self, name, value) 168 169 def _update(self, updated): 170 self.__dict__.update(updated.__dict__) 171 172 def update(self, validate=False, dry_run=False): 173 """ 174 Update the data associated with this ENI by querying EC2. 175 176 :type validate: bool 177 :param validate: By default, if EC2 returns no data about the 178 ENI the update method returns quietly. If 179 the validate param is True, however, it will 180 raise a ValueError exception if no data is 181 returned from EC2. 182 """ 183 rs = self.connection.get_all_network_interfaces( 184 [self.id], 185 dry_run=dry_run 186 ) 187 if len(rs) > 0: 188 self._update(rs[0]) 189 elif validate: 190 raise ValueError('%s is not a valid ENI ID' % self.id) 191 return self.status 192 193 def attach(self, instance_id, device_index, dry_run=False): 194 """ 195 Attach this ENI to an EC2 instance. 196 197 :type instance_id: str 198 :param instance_id: The ID of the EC2 instance to which it will 199 be attached. 200 201 :type device_index: int 202 :param device_index: The interface nunber, N, on the instance (eg. ethN) 203 204 :rtype: bool 205 :return: True if successful 206 """ 207 return self.connection.attach_network_interface( 208 self.id, 209 instance_id, 210 device_index, 211 dry_run=dry_run 212 ) 213 214 def detach(self, force=False, dry_run=False): 215 """ 216 Detach this ENI from an EC2 instance. 217 218 :type force: bool 219 :param force: Forces detachment if the previous detachment 220 attempt did not occur cleanly. 221 222 :rtype: bool 223 :return: True if successful 224 """ 225 attachment_id = getattr(self.attachment, 'id', None) 226 227 return self.connection.detach_network_interface( 228 attachment_id, 229 force, 230 dry_run=dry_run 231 ) 232 233 def delete(self, dry_run=False): 234 return self.connection.delete_network_interface( 235 self.id, 236 dry_run=dry_run 237 ) 238 239 240class PrivateIPAddress(object): 241 def __init__(self, connection=None, private_ip_address=None, 242 primary=None): 243 self.connection = connection 244 self.private_ip_address = private_ip_address 245 self.primary = primary 246 247 def startElement(self, name, attrs, connection): 248 pass 249 250 def endElement(self, name, value, connection): 251 if name == 'privateIpAddress': 252 self.private_ip_address = value 253 elif name == 'primary': 254 self.primary = True if value.lower() == 'true' else False 255 256 def __repr__(self): 257 return "PrivateIPAddress(%s, primary=%s)" % (self.private_ip_address, 258 self.primary) 259 260 261class NetworkInterfaceCollection(list): 262 def __init__(self, *interfaces): 263 self.extend(interfaces) 264 265 def build_list_params(self, params, prefix=''): 266 for i, spec in enumerate(self): 267 full_prefix = '%sNetworkInterface.%s.' % (prefix, i) 268 if spec.network_interface_id is not None: 269 params[full_prefix + 'NetworkInterfaceId'] = \ 270 str(spec.network_interface_id) 271 if spec.device_index is not None: 272 params[full_prefix + 'DeviceIndex'] = \ 273 str(spec.device_index) 274 else: 275 params[full_prefix + 'DeviceIndex'] = 0 276 if spec.subnet_id is not None: 277 params[full_prefix + 'SubnetId'] = str(spec.subnet_id) 278 if spec.description is not None: 279 params[full_prefix + 'Description'] = str(spec.description) 280 if spec.delete_on_termination is not None: 281 params[full_prefix + 'DeleteOnTermination'] = \ 282 'true' if spec.delete_on_termination else 'false' 283 if spec.secondary_private_ip_address_count is not None: 284 params[full_prefix + 'SecondaryPrivateIpAddressCount'] = \ 285 str(spec.secondary_private_ip_address_count) 286 if spec.private_ip_address is not None: 287 params[full_prefix + 'PrivateIpAddress'] = \ 288 str(spec.private_ip_address) 289 if spec.groups is not None: 290 for j, group_id in enumerate(spec.groups): 291 query_param_key = '%sSecurityGroupId.%s' % (full_prefix, j) 292 params[query_param_key] = str(group_id) 293 if spec.private_ip_addresses is not None: 294 for k, ip_addr in enumerate(spec.private_ip_addresses): 295 query_param_key_prefix = ( 296 '%sPrivateIpAddresses.%s' % (full_prefix, k)) 297 params[query_param_key_prefix + '.PrivateIpAddress'] = \ 298 str(ip_addr.private_ip_address) 299 if ip_addr.primary is not None: 300 params[query_param_key_prefix + '.Primary'] = \ 301 'true' if ip_addr.primary else 'false' 302 303 # Associating Public IPs have special logic around them: 304 # 305 # * Only assignable on an device_index of ``0`` 306 # * Only on one interface 307 # * Only if there are no other interfaces being created 308 # * Only if it's a new interface (which we can't really guard 309 # against) 310 # 311 # More details on http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-RunInstances.html 312 if spec.associate_public_ip_address is not None: 313 if not params[full_prefix + 'DeviceIndex'] in (0, '0'): 314 raise BotoClientError( 315 "Only the interface with device index of 0 can " + \ 316 "be provided when using " + \ 317 "'associate_public_ip_address'." 318 ) 319 320 if len(self) > 1: 321 raise BotoClientError( 322 "Only one interface can be provided when using " + \ 323 "'associate_public_ip_address'." 324 ) 325 326 key = full_prefix + 'AssociatePublicIpAddress' 327 328 if spec.associate_public_ip_address: 329 params[key] = 'true' 330 else: 331 params[key] = 'false' 332 333 334class NetworkInterfaceSpecification(object): 335 def __init__(self, network_interface_id=None, device_index=None, 336 subnet_id=None, description=None, private_ip_address=None, 337 groups=None, delete_on_termination=None, 338 private_ip_addresses=None, 339 secondary_private_ip_address_count=None, 340 associate_public_ip_address=None): 341 self.network_interface_id = network_interface_id 342 self.device_index = device_index 343 self.subnet_id = subnet_id 344 self.description = description 345 self.private_ip_address = private_ip_address 346 self.groups = groups 347 self.delete_on_termination = delete_on_termination 348 self.private_ip_addresses = private_ip_addresses 349 self.secondary_private_ip_address_count = \ 350 secondary_private_ip_address_count 351 self.associate_public_ip_address = associate_public_ip_address 352