1# Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2010, Eucalyptus Systems, Inc.
3# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.  All Rights Reserved
4#
5# Permission is hereby granted, free of charge, to any person obtaining a
6# copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish, dis-
9# tribute, sublicense, and/or sell copies of the Software, and to permit
10# persons to whom the Software is furnished to do so, subject to the fol-
11# lowing conditions:
12#
13# The above copyright notice and this permission notice shall be included
14# in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22# IN THE SOFTWARE.
23
24"""
25Represents an EC2 Instance
26"""
27import boto
28from boto.ec2.ec2object import EC2Object, TaggedEC2Object
29from boto.resultset import ResultSet
30from boto.ec2.address import Address
31from boto.ec2.blockdevicemapping import BlockDeviceMapping
32from boto.ec2.image import ProductCodes
33from boto.ec2.networkinterface import NetworkInterface
34from boto.ec2.group import Group
35import base64
36
37
38class InstanceState(object):
39    """
40    The state of the instance.
41
42    :ivar code: The low byte represents the state. The high byte is an
43        opaque internal value and should be ignored.  Valid values:
44
45        * 0 (pending)
46        * 16 (running)
47        * 32 (shutting-down)
48        * 48 (terminated)
49        * 64 (stopping)
50        * 80 (stopped)
51
52    :ivar name: The name of the state of the instance.  Valid values:
53
54        * "pending"
55        * "running"
56        * "shutting-down"
57        * "terminated"
58        * "stopping"
59        * "stopped"
60    """
61    def __init__(self, code=0, name=None):
62        self.code = code
63        self.name = name
64
65    def __repr__(self):
66        return '%s(%d)' % (self.name, self.code)
67
68    def startElement(self, name, attrs, connection):
69        pass
70
71    def endElement(self, name, value, connection):
72        if name == 'code':
73            self.code = int(value)
74        elif name == 'name':
75            self.name = value
76        else:
77            setattr(self, name, value)
78
79
80class InstancePlacement(object):
81    """
82    The location where the instance launched.
83
84    :ivar zone: The Availability Zone of the instance.
85    :ivar group_name: The name of the placement group the instance is
86        in (for cluster compute instances).
87    :ivar tenancy: The tenancy of the instance (if the instance is
88        running within a VPC). An instance with a tenancy of dedicated
89        runs on single-tenant hardware.
90    """
91    def __init__(self, zone=None, group_name=None, tenancy=None):
92        self.zone = zone
93        self.group_name = group_name
94        self.tenancy = tenancy
95
96    def __repr__(self):
97        return self.zone
98
99    def startElement(self, name, attrs, connection):
100        pass
101
102    def endElement(self, name, value, connection):
103        if name == 'availabilityZone':
104            self.zone = value
105        elif name == 'groupName':
106            self.group_name = value
107        elif name == 'tenancy':
108            self.tenancy = value
109        else:
110            setattr(self, name, value)
111
112
113class Reservation(EC2Object):
114    """
115    Represents a Reservation response object.
116
117    :ivar id: The unique ID of the Reservation.
118    :ivar owner_id: The unique ID of the owner of the Reservation.
119    :ivar groups: A list of Group objects representing the security
120                  groups associated with launched instances.
121    :ivar instances: A list of Instance objects launched in this
122                     Reservation.
123    """
124    def __init__(self, connection=None):
125        super(Reservation, self).__init__(connection)
126        self.id = None
127        self.owner_id = None
128        self.groups = []
129        self.instances = []
130
131    def __repr__(self):
132        return 'Reservation:%s' % self.id
133
134    def startElement(self, name, attrs, connection):
135        if name == 'instancesSet':
136            self.instances = ResultSet([('item', Instance)])
137            return self.instances
138        elif name == 'groupSet':
139            self.groups = ResultSet([('item', Group)])
140            return self.groups
141        else:
142            return None
143
144    def endElement(self, name, value, connection):
145        if name == 'reservationId':
146            self.id = value
147        elif name == 'ownerId':
148            self.owner_id = value
149        else:
150            setattr(self, name, value)
151
152    def stop_all(self, dry_run=False):
153        for instance in self.instances:
154            instance.stop(dry_run=dry_run)
155
156
157class Instance(TaggedEC2Object):
158    """
159    Represents an instance.
160
161    :ivar id: The unique ID of the Instance.
162    :ivar groups: A list of Group objects representing the security
163                  groups associated with the instance.
164    :ivar public_dns_name: The public dns name of the instance.
165    :ivar private_dns_name: The private dns name of the instance.
166    :ivar state: The string representation of the instance's current state.
167    :ivar state_code: An integer representation of the instance's
168        current state.
169    :ivar previous_state: The string representation of the instance's
170        previous state.
171    :ivar previous_state_code: An integer representation of the
172        instance's current state.
173    :ivar key_name: The name of the SSH key associated with the instance.
174    :ivar instance_type: The type of instance (e.g. m1.small).
175    :ivar launch_time: The time the instance was launched.
176    :ivar image_id: The ID of the AMI used to launch this instance.
177    :ivar placement: The availability zone in which the instance is running.
178    :ivar placement_group: The name of the placement group the instance
179        is in (for cluster compute instances).
180    :ivar placement_tenancy: The tenancy of the instance, if the instance
181        is running within a VPC.  An instance with a tenancy of dedicated
182        runs on a single-tenant hardware.
183    :ivar kernel: The kernel associated with the instance.
184    :ivar ramdisk: The ramdisk associated with the instance.
185    :ivar architecture: The architecture of the image (i386|x86_64).
186    :ivar hypervisor: The hypervisor used.
187    :ivar virtualization_type: The type of virtualization used.
188    :ivar product_codes: A list of product codes associated with this instance.
189    :ivar ami_launch_index: This instances position within it's launch group.
190    :ivar monitored: A boolean indicating whether monitoring is enabled or not.
191    :ivar monitoring_state: A string value that contains the actual value
192        of the monitoring element returned by EC2.
193    :ivar spot_instance_request_id: The ID of the spot instance request
194        if this is a spot instance.
195    :ivar subnet_id: The VPC Subnet ID, if running in VPC.
196    :ivar vpc_id: The VPC ID, if running in VPC.
197    :ivar private_ip_address: The private IP address of the instance.
198    :ivar ip_address: The public IP address of the instance.
199    :ivar platform: Platform of the instance (e.g. Windows)
200    :ivar root_device_name: The name of the root device.
201    :ivar root_device_type: The root device type (ebs|instance-store).
202    :ivar block_device_mapping: The Block Device Mapping for the instance.
203    :ivar state_reason: The reason for the most recent state transition.
204    :ivar groups: List of security Groups associated with the instance.
205    :ivar interfaces: List of Elastic Network Interfaces associated with
206        this instance.
207    :ivar ebs_optimized: Whether instance is using optimized EBS volumes
208        or not.
209    :ivar instance_profile: A Python dict containing the instance
210        profile id and arn associated with this instance.
211    """
212
213    def __init__(self, connection=None):
214        super(Instance, self).__init__(connection)
215        self.id = None
216        self.dns_name = None
217        self.public_dns_name = None
218        self.private_dns_name = None
219        self.key_name = None
220        self.instance_type = None
221        self.launch_time = None
222        self.image_id = None
223        self.kernel = None
224        self.ramdisk = None
225        self.product_codes = ProductCodes()
226        self.ami_launch_index = None
227        self.monitored = False
228        self.monitoring_state = None
229        self.spot_instance_request_id = None
230        self.subnet_id = None
231        self.vpc_id = None
232        self.private_ip_address = None
233        self.ip_address = None
234        self.requester_id = None
235        self._in_monitoring_element = False
236        self.persistent = False
237        self.root_device_name = None
238        self.root_device_type = None
239        self.block_device_mapping = None
240        self.state_reason = None
241        self.group_name = None
242        self.client_token = None
243        self.eventsSet = None
244        self.groups = []
245        self.platform = None
246        self.interfaces = []
247        self.hypervisor = None
248        self.virtualization_type = None
249        self.architecture = None
250        self.instance_profile = None
251        self._previous_state = None
252        self._state = InstanceState()
253        self._placement = InstancePlacement()
254
255    def __repr__(self):
256        return 'Instance:%s' % self.id
257
258    @property
259    def state(self):
260        return self._state.name
261
262    @property
263    def state_code(self):
264        return self._state.code
265
266    @property
267    def previous_state(self):
268        if self._previous_state:
269            return self._previous_state.name
270        return None
271
272    @property
273    def previous_state_code(self):
274        if self._previous_state:
275            return self._previous_state.code
276        return 0
277
278    @property
279    def placement(self):
280        return self._placement.zone
281
282    @property
283    def placement_group(self):
284        return self._placement.group_name
285
286    @property
287    def placement_tenancy(self):
288        return self._placement.tenancy
289
290    def startElement(self, name, attrs, connection):
291        retval = super(Instance, self).startElement(name, attrs, connection)
292        if retval is not None:
293            return retval
294        if name == 'monitoring':
295            self._in_monitoring_element = True
296        elif name == 'blockDeviceMapping':
297            self.block_device_mapping = BlockDeviceMapping()
298            return self.block_device_mapping
299        elif name == 'productCodes':
300            return self.product_codes
301        elif name == 'stateReason':
302            self.state_reason = SubParse('stateReason')
303            return self.state_reason
304        elif name == 'groupSet':
305            self.groups = ResultSet([('item', Group)])
306            return self.groups
307        elif name == "eventsSet":
308            self.eventsSet = SubParse('eventsSet')
309            return self.eventsSet
310        elif name == 'networkInterfaceSet':
311            self.interfaces = ResultSet([('item', NetworkInterface)])
312            return self.interfaces
313        elif name == 'iamInstanceProfile':
314            self.instance_profile = SubParse('iamInstanceProfile')
315            return self.instance_profile
316        elif name == 'currentState':
317            return self._state
318        elif name == 'previousState':
319            self._previous_state = InstanceState()
320            return self._previous_state
321        elif name == 'instanceState':
322            return self._state
323        elif name == 'placement':
324            return self._placement
325        return None
326
327    def endElement(self, name, value, connection):
328        if name == 'instanceId':
329            self.id = value
330        elif name == 'imageId':
331            self.image_id = value
332        elif name == 'dnsName' or name == 'publicDnsName':
333            self.dns_name = value           # backwards compatibility
334            self.public_dns_name = value
335        elif name == 'privateDnsName':
336            self.private_dns_name = value
337        elif name == 'keyName':
338            self.key_name = value
339        elif name == 'amiLaunchIndex':
340            self.ami_launch_index = value
341        elif name == 'previousState':
342            self.previous_state = value
343        elif name == 'instanceType':
344            self.instance_type = value
345        elif name == 'rootDeviceName':
346            self.root_device_name = value
347        elif name == 'rootDeviceType':
348            self.root_device_type = value
349        elif name == 'launchTime':
350            self.launch_time = value
351        elif name == 'platform':
352            self.platform = value
353        elif name == 'kernelId':
354            self.kernel = value
355        elif name == 'ramdiskId':
356            self.ramdisk = value
357        elif name == 'state':
358            if self._in_monitoring_element:
359                self.monitoring_state = value
360                if value == 'enabled':
361                    self.monitored = True
362                self._in_monitoring_element = False
363        elif name == 'spotInstanceRequestId':
364            self.spot_instance_request_id = value
365        elif name == 'subnetId':
366            self.subnet_id = value
367        elif name == 'vpcId':
368            self.vpc_id = value
369        elif name == 'privateIpAddress':
370            self.private_ip_address = value
371        elif name == 'ipAddress':
372            self.ip_address = value
373        elif name == 'requesterId':
374            self.requester_id = value
375        elif name == 'persistent':
376            if value == 'true':
377                self.persistent = True
378            else:
379                self.persistent = False
380        elif name == 'groupName':
381            if self._in_monitoring_element:
382                self.group_name = value
383        elif name == 'clientToken':
384            self.client_token = value
385        elif name == "eventsSet":
386            self.events = value
387        elif name == 'hypervisor':
388            self.hypervisor = value
389        elif name == 'virtualizationType':
390            self.virtualization_type = value
391        elif name == 'architecture':
392            self.architecture = value
393        elif name == 'ebsOptimized':
394            self.ebs_optimized = (value == 'true')
395        else:
396            setattr(self, name, value)
397
398    def _update(self, updated):
399        self.__dict__.update(updated.__dict__)
400
401    def update(self, validate=False, dry_run=False):
402        """
403        Update the instance's state information by making a call to fetch
404        the current instance attributes from the service.
405
406        :type validate: bool
407        :param validate: By default, if EC2 returns no data about the
408                         instance the update method returns quietly.  If
409                         the validate param is True, however, it will
410                         raise a ValueError exception if no data is
411                         returned from EC2.
412        """
413        rs = self.connection.get_all_reservations([self.id], dry_run=dry_run)
414        if len(rs) > 0:
415            r = rs[0]
416            for i in r.instances:
417                if i.id == self.id:
418                    self._update(i)
419        elif validate:
420            raise ValueError('%s is not a valid Instance ID' % self.id)
421        return self.state
422
423    def terminate(self, dry_run=False):
424        """
425        Terminate the instance
426        """
427        rs = self.connection.terminate_instances([self.id], dry_run=dry_run)
428        if len(rs) > 0:
429            self._update(rs[0])
430
431    def stop(self, force=False, dry_run=False):
432        """
433        Stop the instance
434
435        :type force: bool
436        :param force: Forces the instance to stop
437
438        :rtype: list
439        :return: A list of the instances stopped
440        """
441        rs = self.connection.stop_instances([self.id], force, dry_run=dry_run)
442        if len(rs) > 0:
443            self._update(rs[0])
444
445    def start(self, dry_run=False):
446        """
447        Start the instance.
448        """
449        rs = self.connection.start_instances([self.id], dry_run=dry_run)
450        if len(rs) > 0:
451            self._update(rs[0])
452
453    def reboot(self, dry_run=False):
454        return self.connection.reboot_instances([self.id], dry_run=dry_run)
455
456    def get_console_output(self, dry_run=False):
457        """
458        Retrieves the console output for the instance.
459
460        :rtype: :class:`boto.ec2.instance.ConsoleOutput`
461        :return: The console output as a ConsoleOutput object
462        """
463        return self.connection.get_console_output(self.id, dry_run=dry_run)
464
465    def confirm_product(self, product_code, dry_run=False):
466        return self.connection.confirm_product_instance(
467            self.id,
468            product_code,
469            dry_run=dry_run
470        )
471
472    def use_ip(self, ip_address, dry_run=False):
473        """
474        Associates an Elastic IP to the instance.
475
476        :type ip_address: Either an instance of
477            :class:`boto.ec2.address.Address` or a string.
478        :param ip_address: The IP address to associate
479            with the instance.
480
481        :rtype: bool
482        :return: True if successful
483        """
484
485        if isinstance(ip_address, Address):
486            ip_address = ip_address.public_ip
487        return self.connection.associate_address(
488            self.id,
489            ip_address,
490            dry_run=dry_run
491        )
492
493    def monitor(self, dry_run=False):
494        return self.connection.monitor_instance(self.id, dry_run=dry_run)
495
496    def unmonitor(self, dry_run=False):
497        return self.connection.unmonitor_instance(self.id, dry_run=dry_run)
498
499    def get_attribute(self, attribute, dry_run=False):
500        """
501        Gets an attribute from this instance.
502
503        :type attribute: string
504        :param attribute: The attribute you need information about
505            Valid choices are:
506
507            * instanceType
508            * kernel
509            * ramdisk
510            * userData
511            * disableApiTermination
512            * instanceInitiatedShutdownBehavior
513            * rootDeviceName
514            * blockDeviceMapping
515            * productCodes
516            * sourceDestCheck
517            * groupSet
518            * ebsOptimized
519
520        :rtype: :class:`boto.ec2.image.InstanceAttribute`
521        :return: An InstanceAttribute object representing the value of the
522                 attribute requested
523        """
524        return self.connection.get_instance_attribute(
525            self.id,
526            attribute,
527            dry_run=dry_run
528        )
529
530    def modify_attribute(self, attribute, value, dry_run=False):
531        """
532        Changes an attribute of this instance
533
534        :type attribute: string
535        :param attribute: The attribute you wish to change.
536
537            * instanceType - A valid instance type (m1.small)
538            * kernel - Kernel ID (None)
539            * ramdisk - Ramdisk ID (None)
540            * userData - Base64 encoded String (None)
541            * disableApiTermination - Boolean (true)
542            * instanceInitiatedShutdownBehavior - stop|terminate
543            * sourceDestCheck - Boolean (true)
544            * groupSet - Set of Security Groups or IDs
545            * ebsOptimized - Boolean (false)
546
547        :type value: string
548        :param value: The new value for the attribute
549
550        :rtype: bool
551        :return: Whether the operation succeeded or not
552        """
553        return self.connection.modify_instance_attribute(
554            self.id,
555            attribute,
556            value,
557            dry_run=dry_run
558        )
559
560    def reset_attribute(self, attribute, dry_run=False):
561        """
562        Resets an attribute of this instance to its default value.
563
564        :type attribute: string
565        :param attribute: The attribute to reset. Valid values are:
566                          kernel|ramdisk
567
568        :rtype: bool
569        :return: Whether the operation succeeded or not
570        """
571        return self.connection.reset_instance_attribute(
572            self.id,
573            attribute,
574            dry_run=dry_run
575        )
576
577    def create_image(self, name, description=None, no_reboot=False,
578                     dry_run=False):
579        """
580        Will create an AMI from the instance in the running or stopped
581        state.
582
583        :type name: string
584        :param name: The name of the new image
585
586        :type description: string
587        :param description: An optional human-readable string describing
588                            the contents and purpose of the AMI.
589
590        :type no_reboot: bool
591        :param no_reboot: An optional flag indicating that the bundling process
592                          should not attempt to shutdown the instance before
593                          bundling.  If this flag is True, the responsibility
594                          of maintaining file system integrity is left to the
595                          owner of the instance.
596
597        :rtype: string
598        :return: The new image id
599        """
600        return self.connection.create_image(
601            self.id,
602            name,
603            description,
604            no_reboot,
605            dry_run=dry_run
606        )
607
608
609class ConsoleOutput(object):
610    def __init__(self, parent=None):
611        self.parent = parent
612        self.instance_id = None
613        self.timestamp = None
614        self.output = None
615
616    def startElement(self, name, attrs, connection):
617        return None
618
619    def endElement(self, name, value, connection):
620        if name == 'instanceId':
621            self.instance_id = value
622        elif name == 'timestamp':
623            self.timestamp = value
624        elif name == 'output':
625            self.output = base64.b64decode(value)
626        else:
627            setattr(self, name, value)
628
629
630class InstanceAttribute(dict):
631    ValidValues = ['instanceType', 'kernel', 'ramdisk', 'userData',
632                   'disableApiTermination',
633                   'instanceInitiatedShutdownBehavior',
634                   'rootDeviceName', 'blockDeviceMapping', 'sourceDestCheck',
635                   'groupSet']
636
637    def __init__(self, parent=None):
638        dict.__init__(self)
639        self.instance_id = None
640        self.request_id = None
641        self._current_value = None
642
643    def startElement(self, name, attrs, connection):
644        if name == 'blockDeviceMapping':
645            self[name] = BlockDeviceMapping()
646            return self[name]
647        elif name == 'groupSet':
648            self[name] = ResultSet([('item', Group)])
649            return self[name]
650        else:
651            return None
652
653    def endElement(self, name, value, connection):
654        if name == 'instanceId':
655            self.instance_id = value
656        elif name == 'requestId':
657            self.request_id = value
658        elif name == 'value':
659            if value == 'true':
660                value = True
661            elif value == 'false':
662                value = False
663            self._current_value = value
664        elif name in self.ValidValues:
665            self[name] = self._current_value
666
667
668class SubParse(dict):
669    def __init__(self, section, parent=None):
670        dict.__init__(self)
671        self.section = section
672
673    def startElement(self, name, attrs, connection):
674        return None
675
676    def endElement(self, name, value, connection):
677        if name != self.section:
678            self[name] = value
679