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