~awstrial-dev/awstrial/trunk

106.1.3 by Dave Walker (Daviey)
Added LICENCE, AUTHORS and boilterplate to .py files
1
#    AWSTrial, A mechanism and service for offering a cloud image trial
2
#
3
#    Copyright (C) 2010  Scott Moser <smoser@ubuntu.com>
4
#    Copyright (C) 2010  Dave Walker (Daviey) <DaveWalker@ubuntu.com>
5
#    Copyright (C) 2010  Michael Hall <mhall119@gmail.com>
6
#    Copyright (C) 2010  Dustin Kirkland <kirkland@ubuntu.com>
7
#    Copyright (C) 2010  Andreas Hasenack <andreas@canonical.com>
8
#
9
#    This program is free software: you can redistribute it and/or modify
10
#    it under the terms of the GNU Affero General Public License as
11
#    published by the Free Software Foundation, either version 3 of the
12
#    License, or (at your option) any later version.
13
#
14
#    This program is distributed in the hope that it will be useful,
15
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
16
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
#    GNU Affero General Public License for more details.
18
#
19
#    You should have received a copy of the GNU Affero General Public License
20
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
289.1.11 by Hampton Paulk
remove dead imports
22
from trial.models import Campaign, Instances
1 by Dave Walker (Daviey)
Initial commit
23
from django.contrib.auth.models import User
263.1.1 by Michael Hall
Use the Site.name for the django.contrib.sites defined in settings.SITE_ID as the base url for callbacks
24
from django.contrib.sites.models import Site
36 by Scott Moser
make 'runit' use multipart input to cloud-init
25
from django.template.loader import render_to_string
1 by Dave Walker (Daviey)
Initial commit
26
229.4.2 by Matthew Nuzum
Using from django.conf import settings instead of import settings
27
from django.conf import settings
1 by Dave Walker (Daviey)
Initial commit
28
29
from boto.ec2.connection import EC2Connection, RegionInfo
30
from boto import connect_ec2
9 by Scott Moser
add 'region' info most of the way through. add ami id to Instance
31
import boto
1 by Dave Walker (Daviey)
Initial commit
32
import time
229.4.1 by Matthew Nuzum
Adding awstrial, fixing paths to modules so that they're not prefixed with awstrial
33
from trial import util
1 by Dave Walker (Daviey)
Initial commit
34
74 by Scott Moser
[UGLY] persist "config" information in the database.
35
import yaml
1 by Dave Walker (Daviey)
Initial commit
36
37
9 by Scott Moser
add 'region' info most of the way through. add ami id to Instance
38
def ec2_connection(region):
235.2.1 by Matthew Nuzum
Alternate cloud configuration.
39
    a = 1
40
    if settings.ALTERNATE_CLOUD:
41
        # For testing against euca
42
        region = RegionInfo(
43
            name=settings.ALTERNATE_CLOUD['region'],
44
            endpoint=settings.ALTERNATE_CLOUD['endpoint']
45
        )
46
47
        connection = boto.ec2.EC2Connection(
239.2.1 by Michael Hall
Adds test cases for making a connection to AWS or an alternate cloud. Fixes dateutils version dependency and adds dependency for mock
48
            aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
49
            aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
235.2.1 by Matthew Nuzum
Alternate cloud configuration.
50
            region=region,
51
            is_secure=settings.ALTERNATE_CLOUD['is_secure'],
52
            port=settings.ALTERNATE_CLOUD['port'],
53
            path=settings.ALTERNATE_CLOUD['endpoint_path']
54
        )
55
56
    else:
57
58
        reginfo = RegionInfo(name=region, endpoint='ec2.%s.amazonaws.com' % region)
59
        connection = boto.ec2.EC2Connection(
239.2.1 by Michael Hall
Adds test cases for making a connection to AWS or an alternate cloud. Fixes dateutils version dependency and adds dependency for mock
60
            aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
61
            aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
235.2.1 by Matthew Nuzum
Alternate cloud configuration.
62
            region = reginfo)
12 by Scott Moser
add ec2_helper routines
63
1 by Dave Walker (Daviey)
Initial commit
64
    return connection
65
264.2.1 by Michael Hall
Convert byobu-enable script to just installing the countdown widget, include it all the time
66
def runit(campaign,lpid=None,regions=None,config=None, password=None):
36 by Scott Moser
make 'runit' use multipart input to cloud-init
67
    sec_key = util.rand_str(32)
102 by Scott Moser
add "region roll over"
68
    if regions is None:
69
        regions = [r for r in settings.REGIONS_TRY_ORDER
70
                         if settings.REGION2AMI.has_key(r)]
71
        regions = [r for r in settings.REGIONS_TRY_ORDER
72
                         if settings.REGION2AMI.has_key(r)]
36 by Scott Moser
make 'runit' use multipart input to cloud-init
73
74 by Scott Moser
[UGLY] persist "config" information in the database.
74
    cust = { }
75
    cust['config'] = config
157.1.2 by Scott Moser
add setting of one time use password
76
    cust['password'] = password
74 by Scott Moser
[UGLY] persist "config" information in the database.
77
36 by Scott Moser
make 'runit' use multipart input to cloud-init
78
    parts=[]
79
    parts.append(
80
        util.partItem(
81
            render_to_string("import-launchpad-ssh-keys",
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
82
                { 'debug' : settings.DEBUG, 'launchpad_id' : lpid, 'password' : password } ),
144 by Scott Moser
provide number prefix on user-scripts to give ordering to run-parts
83
            part_type=util.CI_SCRIPT, filename="10-launchpad-ssh-keys"))
263.1.1 by Michael Hall
Use the Site.name for the django.contrib.sites defined in settings.SITE_ID as the base url for callbacks
84
85
    site = Site.objects.get(pk=settings.SITE_ID)
86
    callback_url = "%s/info_callback/%s" % (site.name, sec_key)
36 by Scott Moser
make 'runit' use multipart input to cloud-init
87
    parts.append(
88
        util.partItem(
130 by Scott Moser
rename phone-home to info-callback
89
            render_to_string("info-callback",
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
90
                { 'debug' : settings.DEBUG, 'callback_url' : "%s/initial/" % callback_url } ),
144 by Scott Moser
provide number prefix on user-scripts to give ordering to run-parts
91
            part_type=util.CI_SCRIPT, filename="99-info-callback"))
36 by Scott Moser
make 'runit' use multipart input to cloud-init
92
43 by Scott Moser
schedule at jobs for warning/shutdown. add first login message to console.
93
    parts.append(
94
        util.partItem(
95
            render_to_string("schedule-warnings",
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
96
                { 'debug' : settings.DEBUG, 'launch_time' : util.dtnow().strftime("%H:%M %Y-%m-%d UTC") } ),
144 by Scott Moser
provide number prefix on user-scripts to give ordering to run-parts
97
            part_type=util.CI_SCRIPT, filename="50-schedule-warnings"))
43 by Scott Moser
schedule at jobs for warning/shutdown. add first login message to console.
98
99
    parts.append(
100
        util.partItem(
218.1.1 by Ubuntu
add hook to callback with IP of ssh login
101
            render_to_string("log-login",
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
102
                { 'debug' : settings.DEBUG, 'callback_url' : "%s/login/" % callback_url }),
218.1.1 by Ubuntu
add hook to callback with IP of ssh login
103
            part_type=util.CI_BOOTHOOK, filename="50-log-login"))
43 by Scott Moser
schedule at jobs for warning/shutdown. add first login message to console.
104
105 by Andreas Hasenack
Added personal-hello to the parts so that it is actually used.
105
    parts.append(
106
        util.partItem(
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
107
            render_to_string("personal-hello", { 'debug' : settings.DEBUG, 'launchpad_id' : lpid }),
144 by Scott Moser
provide number prefix on user-scripts to give ordering to run-parts
108
            part_type=util.CI_SCRIPT, filename="50-personal-hello"))
105 by Andreas Hasenack
Added personal-hello to the parts so that it is actually used.
109
157.1.2 by Scott Moser
add setting of one time use password
110
    parts.append(
111
        util.partItem(
112
            render_to_string("password-enable",
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
113
                { 'debug' : settings.DEBUG, 'user': 'ubuntu', 'password' : password }),
157.1.2 by Scott Moser
add setting of one time use password
114
            part_type=util.CI_SCRIPT, filename="55-password-enable"))
115
264.2.1 by Michael Hall
Convert byobu-enable script to just installing the countdown widget, include it all the time
116
    parts.append(
117
        util.partItem(
118
            render_to_string("byobu-countdown", { 'debug' : settings.DEBUG, }),
119
            part_type=util.CI_BOOTHOOK, filename="byobu-countdown"))
120
    cust['byobu']=True
56 by Scott Moser
add byobu button
121
45 by Scott Moser
add select box with configs on launch page.
122
    instcfg = None
229.4.4 by Matthew Nuzum
Updating references to settings.configs to be settings.CONFIGS in order to work properly with imports of settings
123
    for c in settings.CONFIGS:
74 by Scott Moser
[UGLY] persist "config" information in the database.
124
        if c['name'] == config:
125
            instcfg = c
126
            break
127
            
45 by Scott Moser
add select box with configs on launch page.
128
    if not instcfg:
129
        raise Exception("Invalid config %s" % config)
130
131
    if instcfg['template']:
132
        parts.append(
133
            util.partItem(
259.1.1 by Michael Hall
Add framework for automating the collection of test data on launched instances
134
                render_to_string(instcfg['template'], { 'debug' : settings.DEBUG, }),
45 by Scott Moser
add select box with configs on launch page.
135
                part_type=util.CI_CLOUDCONFIG, filename=instcfg['template']))
136
246.1.2 by Michael Hall
Allow null values for Instances.ami_id and Instances.region so we can create the record before we have an actual reservation. Creating the record first provides a locking mechanism while we wait for the reservation in EC2, so that the user can not launch another one while we wait
137
    # Create the instance record in the database before creating it in ec2
138
    i = Instances.objects.create(instance_id='i-00000000',
139
                  campaign=Campaign.objects.get(name=campaign),
140
                  owner = User.objects.get(username=str(lpid)),
141
                  reservation_time=util.dtnow(),
142
                  secret=sec_key,
143
                  config_info=yaml.dump(cust))
144
3 by Dave Walker (Daviey)
Settings for AWS with hard coded keys :(
145
    if True: #settings.DEBUG == False:
9 by Scott Moser
add 'region' info most of the way through. add ami id to Instance
146
102 by Scott Moser
add "region roll over"
147
        region = None
148
        reservation = None
149
        ex = None
150
        
246.1.2 by Michael Hall
Allow null values for Instances.ami_id and Instances.region so we can create the record before we have an actual reservation. Creating the record first provides a locking mechanism while we wait for the reservation in EC2, so that the user can not launch another one while we wait
151
        try:
152
            for region in regions:
153
                if settings.ALTERNATE_CLOUD:
154
                    ami = settings.ALTERNATE_CLOUD['ami']
155
                else:
156
                    ami = settings.REGION2AMI[region]
157
158
                try:
159
                    ec2 = ec2_connection(region)
160
                    reservation = ec2.run_instances(ami,
161
                                   instance_type=settings.INSTANCE_TYPE,
162
                                   key_name=settings.INSTANCE_KEY_NAME,
163
                                   security_groups=settings.INSTANCE_SECURITY_GROUPS,
164
                                   user_data=util.parts2mime(parts)
165
                                   )
166
                                   
167
                    break
168
                except boto.exception.EC2ResponseError as ex:
169
                    if ex.error_code == 'InstanceLimitExceeded': continue
170
                    if ex.error_code == 'InsufficientInstanceCapacity': continue
171
                    raise
172
173
            if reservation is None:
174
                raise ex
175
176
            instance = reservation.instances[0]
177
            i.instance_id=instance.id
178
            i.ami_id=ami
179
            i.region=region
180
            i.reservation_time=util.dtnow()
181
            i.save()
182
            #while not instance.update() == 'running': 
183
            #  time.sleep(5)
184
        except boto.exception.EC2ResponseError as ex:
185
            i.delete()
102 by Scott Moser
add "region roll over"
186
            raise ex
1 by Dave Walker (Daviey)
Initial commit
187
    else:
246.1.2 by Michael Hall
Allow null values for Instances.ami_id and Instances.region so we can create the record before we have an actual reservation. Creating the record first provides a locking mechanism while we wait for the reservation in EC2, so that the user can not launch another one while we wait
188
        i.ami = "ami-BADCOFFEE"
189
        i.instance = "i-406D088D"
190
        i.save()
1 by Dave Walker (Daviey)
Initial commit
191
12 by Scott Moser
add ec2_helper routines
192
# take an 'Instances' object, and a of a boto Instance
193
# and updates Instances object iff it has changed
194
def update_instance_from_aws(instance,aws_data):
195
    aws2inst = {
196
        'id' : 'instance_id',
197
        'launch_time' : 'reservation_time',
24 by Dave Walker (Daviey)
add population of hostname and ip in aws_poll
198
        'image_id' : 'ami_id',
199
        'ip_address' : 'ip',
200
        'public_dns_name' : 'hostname',
12 by Scott Moser
add ec2_helper routines
201
    }
202
    update = False
203
    for aattr,iattr in aws2inst.items():
18 by Scott Moser
committing code for killing instances. (broken).
204
        if aattr == "launch_time":
205
            aval = util.aws2datetime(getattr(aws_data,aattr,None))
206
        else:
207
            aval = getattr(aws_data,aattr,None)
15 by Scott Moser
ec2_helper.py: 2 fixes for tracebacks on bad variable names
208
        ival = getattr(instance,iattr,None)
27 by Dave Walker (Daviey)
do not set null/empty values to instance
209
        if aval and aval != ival:
12 by Scott Moser
add ec2_helper routines
210
            update = True
18 by Scott Moser
committing code for killing instances. (broken).
211
            setattr(instance,iattr,aval)
12 by Scott Moser
add ec2_helper routines
212
    state2datemap = {
17 by Scott Moser
database changes: add fields, remove one
213
        "pending" : "reservation_time",
12 by Scott Moser
add ec2_helper routines
214
        "running" : "running_time",
38 by Scott Moser
address 'stopped' state in aws_poll (I think). remove some printfs there
215
        "stopped" : "running_time",
12 by Scott Moser
add ec2_helper routines
216
        "shutting-down" : "shutdown_time",
28 by Dave Walker (Daviey)
squash a couple bugs resulting in terminated time not getting set
217
        "terminated" : "terminated_time"
12 by Scott Moser
add ec2_helper routines
218
    }
200 by Dave Walker (Daviey)
Made aws_poll.py silent, needs -v argument support
219
    #print "state=%s" % aws_data.state
12 by Scott Moser
add ec2_helper routines
220
    for state,iattr in state2datemap.items():
221
        if ( aws_data.state == state and
222
             not getattr(instance,iattr,None) ):
18 by Scott Moser
committing code for killing instances. (broken).
223
            setattr(instance,iattr,util.dtnow())
12 by Scott Moser
add ec2_helper routines
224
            update = True
225
    return update
226
227
def update_instances(instances,region,conn=None):
228
    resultset = query_instances(instances, region, conn)
229
    aws_ihash = { }
230
    for res in resultset:
231
        for instance in res.instances:
232
            aws_ihash[instance.id]=instance
233
200 by Dave Walker (Daviey)
Made aws_poll.py silent, needs -v argument support
234
    #import sys; sys.stderr.write("%-15s: aws had %s, inst list had %s\n"
235
    #    % (region,len(aws_ihash), len(instances)))
12 by Scott Moser
add ec2_helper routines
236
237
    for inst in instances:
202 by Scott Moser
significantly reduce window for race condition on aws_poll (LP: #658362)
238
        # re-fetch the instance, in case it changed between
239
        # the original 'query_instances' and now.  It might have
240
        # changed if console_poll, or info_callback updated it (LP: #658362)
241
        inst = Instances.objects.get(instance_id=inst.instance_id,
242
                                     region=region)
243
12 by Scott Moser
add ec2_helper routines
244
        if inst.instance_id not in aws_ihash:
101 by Scott Moser
update terminated_time database for instances missing form aws query results
245
            # update the instance here also as it could be we lauched
246
            # then never updated until it was completely gone from the
247
            # aws side (hours later)
248
            inst.terminated_time = util.dtnow()
157.1.1 by Scott Moser
fix 2 errors in ec2_helper.py. one in simple syntax one in logic.
249
            import sys; sys.stderr.write("instance %s not seen in results, marking as terminated\n" % inst.instance_id)
101 by Scott Moser
update terminated_time database for instances missing form aws query results
250
            inst.save()
157.1.1 by Scott Moser
fix 2 errors in ec2_helper.py. one in simple syntax one in logic.
251
        elif update_instance_from_aws(inst,aws_ihash[inst.instance_id]):
200 by Dave Walker (Daviey)
Made aws_poll.py silent, needs -v argument support
252
            #import sys; sys.stderr.write("saving state of %s\n" % inst.instance_id)
15 by Scott Moser
ec2_helper.py: 2 fixes for tracebacks on bad variable names
253
            inst.save()
12 by Scott Moser
add ec2_helper routines
254
255
# return boto response for DescribeInstances of all instances
256
def query_instances(instances,region,conn=None):
257
    instanceList = []
258
    if not conn:
259
        conn = ec2_connection(region)
260
    for i in instances:
261
        instanceList.append(i.instance_id)
18 by Scott Moser
committing code for killing instances. (broken).
262
    return (conn.get_all_instances(instance_ids=instanceList))
263
264
265
def terminate_instances(instances,region,conn=None):
12 by Scott Moser
add ec2_helper routines
266
    """ Should reuse query_interface """
18 by Scott Moser
committing code for killing instances. (broken).
267
    if not len(instances):
268
        emp=()
269
        return emp
270
271
    if not conn:
272
        conn = ec2_connection(region)
273
274
    idlist = [ ]
275
    for i in instances: idlist.append(i.instance_id)
276
277
    return(conn.terminate_instances(idlist))
9 by Scott Moser
add 'region' info most of the way through. add ami id to Instance
278
26 by Dave Walker (Daviey)
add console_poll utility to pull console logs
279
def get_console_output(instance,region,conn=None):
280
    if not conn:
281
        conn = ec2_connection(region)
282
    response=conn.get_console_output(instance.instance_id)
228 by Scott Moser
pull back console_poll improvements from natty to trunk
283
    if not response.output: return("")
215 by Scott Moser
fix trace when some characters are in console output:
284
    output = response.output.decode('utf-8','replace')
285
    return "%s\n%s\n" % (response.timestamp, output)
26 by Dave Walker (Daviey)
add console_poll utility to pull console logs
286
9 by Scott Moser
add 'region' info most of the way through. add ami id to Instance
287
# vi: ts=4 expandtab