~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to utilities/ec2-generate-windmill-image.py

  • Committer: Curtis Hovey
  • Date: 2011-07-15 15:46:51 UTC
  • mto: This revision was merged to the branch mainline in revision 13449.
  • Revision ID: curtis.hovey@canonical.com-20110715154651-eahw01tqq6z60mnk
Removed code and tools that think windmill is still a test layer.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
#
3
 
# Copyright 2009 Canonical Ltd.  This software is licensed under the
4
 
# GNU Affero General Public License version 3 (see the file LICENSE).
5
 
 
6
 
"""
7
 
Generate an EC2 image that is capable of running the Windmill browser UI
8
 
testing tool.
9
 
 
10
 
You must provide a base image that will be augmented with the necessary
11
 
packages and configuration.
12
 
 
13
 
The script requires certain options to be specified in order to function
14
 
properly.  These options may be supplied using command-line switches, or
15
 
via a config file, with the --config command-line switch.  The default
16
 
config file location is ~/.ec2/ec2bundle.cfg
17
 
 
18
 
The config file format simply replicates the required command-line options
19
 
as configuration keys.
20
 
 
21
 
---- ec2bundle.cfg ---
22
 
 
23
 
[DEFAULT]
24
 
key = gsg-keypair
25
 
identity-file = ~/.ec2/foo-keypair-id_rsa
26
 
private-key = ~/.ec2/pk-HKZYKTAIG2ECMXYIBH3HXV4ZBZQ55CLO.pem
27
 
cert =~/.ec2/cert-HKZYKTAIG2ECMXYIBH3HXV4ZBZQ55CLO.pem
28
 
user-id = AIDADH4IGTRXXKCD
29
 
access-key = SOMEBIGSTRINGOFDIGITS
30
 
secret-key = s0m3funKyStr1Ng0fD1gitZ
31
 
#bucket = foo  # Required, but you probably want to customize it each time.
32
 
 
33
 
---- fin ---
34
 
 
35
 
"""
36
 
 
37
 
__metatype__ = type
38
 
 
39
 
 
40
 
# Reuse a whole bunch of code from ec2test.py.
41
 
import ConfigParser
42
 
import ec2test
43
 
import logging
44
 
import optparse
45
 
import os
46
 
import paramiko
47
 
import select
48
 
import socket
49
 
import subprocess
50
 
import sys
51
 
import time
52
 
 
53
 
 
54
 
log   = logging.getLogger(__name__)
55
 
info  = log.info
56
 
debug = log.debug
57
 
 
58
 
 
59
 
usage = """
60
 
Generate an EC2 image for Windmill testing in Firefox.
61
 
 
62
 
usage: %prog [options] AMI-ID
63
 
"""
64
 
 
65
 
class Instance:
66
 
    """An EC2 instance controller."""
67
 
 
68
 
    def __init__(self, instance):
69
 
        self._instance = instance
70
 
 
71
 
    @property
72
 
    def id(self):
73
 
        return self._instance.id
74
 
 
75
 
    @property
76
 
    def hostname(self):
77
 
        return self._instance.public_dns_name
78
 
 
79
 
    def stop(self):
80
 
        instance = self._instance
81
 
 
82
 
        instance.update()
83
 
        if instance.state not in ('shutting-down', 'terminated'):
84
 
            # terminate instance
85
 
            instance.stop()
86
 
            instance.update()
87
 
 
88
 
        info('instance %s\n' % (instance.state,))
89
 
 
90
 
    def wait_for_instance_to_start(self):
91
 
        """Wait for the instance to transition to the "running" state."""
92
 
        instance = self._instance
93
 
        info('Instance %s starting..' % instance.id)
94
 
 
95
 
        start = time.time()
96
 
        while instance.state == 'pending':
97
 
            sys.stdout.write('.')
98
 
            sys.stdout.flush()
99
 
            time.sleep(5)
100
 
            instance.update()
101
 
        if instance.state == 'running':
102
 
            info('\ninstance now running at %s\n' % instance.public_dns_name)
103
 
            elapsed = time.time() - start
104
 
            info('Started in %d minutes %d seconds\n' %
105
 
                     (elapsed // 60, elapsed % 60))
106
 
            cout = instance.get_console_output()
107
 
            info(cout.output)
108
 
        else:
109
 
            raise RuntimeError('failed to start: %s\n' % instance.state)
110
 
 
111
 
    @classmethod
112
 
    def from_image(cls, account, ami_id, instance_type):
113
 
        """Return a new instance using the given startup parameters."""
114
 
        info("Starting instance")
115
 
 
116
 
        # Set up a security group that opens up ports 22, 80, and 443.  Also
117
 
        # opens up access for our IP.
118
 
        account.acquire_security_group()
119
 
 
120
 
        image = account.acquire_image(ami_id)
121
 
        key = account.name
122
 
        debug("Image: %s, Type: %s, Key: %s" % (
123
 
              ami_id, instance_type, key))
124
 
 
125
 
        reservation = image.run(
126
 
            key_name=key,
127
 
            security_groups=[key],
128
 
            instance_type=instance_type)
129
 
 
130
 
        instance = cls(reservation.instances[0])
131
 
        instance.wait_for_instance_to_start()
132
 
        return instance
133
 
 
134
 
    @classmethod
135
 
    def from_running_instance(cls, account, instance_id):
136
 
        """Create an object from an already running EC2 instance."""
137
 
        instance = account.get_instance(instance_id)
138
 
        if not instance:
139
 
            raise RuntimeError(
140
 
                "Unable to connect to instance %s" % instance_id)
141
 
 
142
 
        info("Connected to instance %s" % instance_id)
143
 
        proxy = cls(instance)
144
 
        # Just to be extra safe.
145
 
        proxy.wait_for_instance_to_start()
146
 
        return proxy
147
 
 
148
 
 
149
 
class SSHConnector:
150
 
    """Handle the various aspects of using an SSH connection."""
151
 
 
152
 
    def __init__(self, hostname, user, identity_file):
153
 
        self.hostname = hostname
154
 
        self.user = user
155
 
        self.identity_file = os.path.expanduser(identity_file)
156
 
        self._client = None
157
 
 
158
 
    def get_private_key(self):
159
 
        """Generate a private key object for our keyfile"""
160
 
        fp = os.path.expanduser(self.identity_file)
161
 
        return paramiko.RSAKey.from_private_key(open(fp))
162
 
 
163
 
    def connect(self):
164
 
        info('Waiting for SSH to come available: %s@%s\n' % (
165
 
             self.user, self.hostname))
166
 
        debug("Using private key file: %s" % self.identity_file)
167
 
 
168
 
        private_key = self.get_private_key()
169
 
 
170
 
        for count in range(10):
171
 
            self._client = paramiko.SSHClient()
172
 
            self._client.set_missing_host_key_policy(ec2test.AcceptAllPolicy())
173
 
 
174
 
            try:
175
 
 
176
 
                self._client.connect(
177
 
                    self.hostname,
178
 
                    username=self.user,
179
 
                    pkey=private_key,
180
 
                    allow_agent=False,
181
 
                    look_for_keys=False)
182
 
 
183
 
            except (socket.error, paramiko.AuthenticationException), e:
184
 
                log.warning('wait_for_connection: %r' % (e,))
185
 
                if count < 9:
186
 
                    time.sleep(5)
187
 
                    info('retrying...')
188
 
                else:
189
 
                    raise
190
 
            else:
191
 
                break
192
 
 
193
 
    def exec_command(self, remote_command, check_return=True):
194
 
        """Execute a command on the remote server.
195
 
 
196
 
        Raises an error if the command returns an exit status that is not
197
 
        zero, unless the option `check_return=False' has been given.
198
 
        """
199
 
        info('Executing command: %s@%s %s\n' % (self.user, self.hostname, remote_command))
200
 
 
201
 
        session = self._client.get_transport().open_session()
202
 
        session.exec_command(remote_command)
203
 
        session.shutdown_write()
204
 
 
205
 
        # TODO: change this to use the logging module
206
 
        while True:
207
 
            select.select([session], [], [], 0.5)
208
 
            if session.recv_ready():
209
 
                data = session.recv(4096)
210
 
                if data:
211
 
                    sys.stdout.write(data)
212
 
                    sys.stdout.flush()
213
 
            if session.recv_stderr_ready():
214
 
                data = session.recv_stderr(4096)
215
 
                if data:
216
 
                    sys.stderr.write(data)
217
 
                    sys.stderr.flush()
218
 
            if session.exit_status_ready():
219
 
                break
220
 
        session.close()
221
 
 
222
 
        # XXX: JonathanLange 2009-05-31: If the command is killed by a signal
223
 
        # on the remote server, the SSH protocol does not send an exit_status,
224
 
        # it instead sends a different message with the number of the signal
225
 
        # that killed the process. AIUI, this code will fail confusingly if
226
 
        # that happens.
227
 
        exit_status = session.recv_exit_status()
228
 
        if exit_status and check_return:
229
 
            raise RuntimeError('Command failed: %s' % (remote_command,))
230
 
        return exit_status
231
 
 
232
 
    def copy_to_remote(self, local_filename, remote_filename):
233
 
        cmd = [
234
 
            'scp',
235
 
            '-i', self.identity_file,
236
 
            local_filename,
237
 
            '%s@%s:%s' % (self.user, self.hostname, remote_filename)
238
 
            ]
239
 
        info("Executing command: %s" % ' '.join(cmd))
240
 
        subprocess.check_call(cmd)
241
 
 
242
 
    def user_command(self):
243
 
        """Return a user-friendly ssh command-line string."""
244
 
        return "ssh -i %s %s@%s" % (
245
 
            self.identity_file,
246
 
            self.user,
247
 
            self.hostname)
248
 
 
249
 
 
250
 
class ImageBundler:
251
 
    """Bundle an EC2 image on a remote system."""
252
 
 
253
 
    def __init__(self, private_key, cert, account_id, target_bucket,
254
 
                 access_key, secret_key, ssh):
255
 
        self.private_key = os.path.expanduser(private_key)
256
 
        self.cert = os.path.expanduser(cert)
257
 
        self.account_id = account_id
258
 
        self.target_bucket = target_bucket
259
 
        self.access_key = access_key
260
 
        self.secret_key = secret_key
261
 
        self.ssh = ssh
262
 
 
263
 
        # Use the instance /mnt directory by default, because it has a few
264
 
        # hundred GB of free space to work with.
265
 
        self._bundle_dir = os.path.join('/mnt', target_bucket)
266
 
 
267
 
    def bundle_image(self):
268
 
        self.configure_bundling_environment()
269
 
        manifest = self._bundle_image()
270
 
        self._upload_bundle(manifest)
271
 
        self._register_image(manifest)
272
 
 
273
 
    def remote_private_keypath(self):
274
 
        # ALWAYS have these files in /mnt on the remote system.  Otherwise
275
 
        # they will get bundled along with the image.
276
 
        return os.path.join('/mnt', os.path.basename(self.private_key))
277
 
 
278
 
    def remote_certpath(self):
279
 
        # ALWAYS have these files in /mnt on the remote system.  Otherwise
280
 
        # they will get bundled along with the image.
281
 
        return os.path.join('/mnt', os.path.basename(self.cert))
282
 
 
283
 
    def configure_bundling_environment(self):
284
 
        """Configure what we need on the instance for bundling the image."""
285
 
        # Send our keypair to the remote environment so that it can be used
286
 
        # to bundle the image.
287
 
        local_cert = os.path.abspath(self.cert)
288
 
        local_pkey = os.path.abspath(self.private_key)
289
 
 
290
 
        # ALWAYS copy these files into /mnt on the remote system.  Otherwise
291
 
        # they will get bundled along with the image.
292
 
        remote_cert = self.remote_certpath()
293
 
        remote_pkey = self.remote_private_keypath()
294
 
 
295
 
        # See if the files are present, and copy them over if they are not.
296
 
        self._ensure_remote_file(remote_cert, local_cert)
297
 
        self._ensure_remote_file(remote_pkey, local_pkey)
298
 
 
299
 
    def _ensure_remote_file(self, remote_file, desired_file):
300
 
        info("Checking for '%s' on the remote system" % remote_file)
301
 
        test = 'test -f %s' % remote_file
302
 
        exit_status = self.ssh.exec_command(test, check_return=False)
303
 
        if bool(exit_status):
304
 
            self.ssh.copy_to_remote(desired_file, remote_file)
305
 
 
306
 
    def _bundle_image(self):
307
 
        # Create the bundle in a subdirectory, to avoid spamming up /mnt.
308
 
        self.ssh.exec_command(
309
 
            'mkdir %s' % self._bundle_dir, check_return=False)
310
 
 
311
 
        cmd = [
312
 
            'ec2-bundle-vol',
313
 
            '-d %s' % self._bundle_dir,
314
 
            '-b',   # Set batch-mode, which doesn't use prompts.
315
 
            '-k %s' % self.remote_private_keypath(),
316
 
            '-c %s' % self.remote_certpath(),
317
 
            '-u %s'  % self.account_id
318
 
            ]
319
 
 
320
 
        self.ssh.exec_command(' '.join(cmd))
321
 
        # Assume that the manifest is 'image.manifest.xml', since "image" is
322
 
        # the default prefix.
323
 
        manifest = os.path.join(self._bundle_dir, 'image.manifest.xml')
324
 
 
325
 
        # Best check that the manifest actually exists though.
326
 
        test = 'test -f %s' % manifest
327
 
        exit_status = self.ssh.exec_command(test, check_return=False)
328
 
 
329
 
        if bool(exit_status):
330
 
            raise RuntimeError(
331
 
                "Failed to write the image manifest file: %s" % manifest)
332
 
 
333
 
        return manifest
334
 
 
335
 
    def _upload_bundle(self, manifest):
336
 
        cmd = [
337
 
            'ec2-upload-bundle',
338
 
            '-b %s' % self.target_bucket,
339
 
            '-m %s' % manifest,
340
 
            '-a %s' % self.access_key,
341
 
            '-s %s' % self.secret_key
342
 
            ]
343
 
        self.ssh.exec_command(' '.join(cmd))
344
 
 
345
 
    def _register_image(self, manifest):
346
 
        # This is invoked locally.
347
 
        mfilename = os.path.basename(manifest)
348
 
        manifest_path = os.path.join(self.target_bucket, mfilename)
349
 
 
350
 
        env = os.environ.copy()
351
 
        env['JAVA_HOME'] = '/usr/lib/jvm/default-java'
352
 
        cmd = [
353
 
            'ec2-register',
354
 
            '--private-key=%s' % self.private_key,
355
 
            '--cert=%s' % self.cert,
356
 
            manifest_path
357
 
            ]
358
 
        info("Executing command: %s" % ' '.join(cmd))
359
 
        subprocess.check_call(cmd, env=env)
360
 
 
361
 
 
362
 
class XvfbSystemConfigurator:
363
 
    """Configure a remote operating system over SSH to use the xvfb server."""
364
 
 
365
 
    def __init__(self, ssh):
366
 
        self.ssh = ssh
367
 
 
368
 
    def configure_system(self):
369
 
        """Configure the operating system with the needed packages, etc."""
370
 
        do = self.ssh.exec_command
371
 
 
372
 
        # Make sure we know about all the packages, and where they may be
373
 
        # found.
374
 
        do("apt-get -y update")
375
 
        # Install the necessary packages
376
 
        do("apt-get -y install xvfb firefox xfonts-base")
377
 
 
378
 
 
379
 
class CombinedConfigParser:
380
 
    """Store and reconcile options for both optparse and ConfigParser."""
381
 
 
382
 
    def __init__(self, optparser, cfgparser):
383
 
        self._optparser = optparser
384
 
        self._cfgparser = cfgparser
385
 
 
386
 
        # A list of required optparse options.
387
 
        self.required_options = []
388
 
        self.known_options = []
389
 
 
390
 
        # Our parsed positional command-line arguments, as returned by
391
 
        # optparse.OptionParser.parse_args()
392
 
        self.args = None
393
 
 
394
 
        # An optparse option.dest to 'cfg-key' mapping.
395
 
        self._option_to_cfgkey = {}
396
 
 
397
 
        self._parsed_cli_options = None
398
 
        self._parsed_cfg_options = None
399
 
 
400
 
    def __getattr__(self, name):
401
 
        return self.get(name)
402
 
 
403
 
    def add_option(self, *args, **kwds):
404
 
        """Wrap the OptionParser.add_option() method, and add our options."""
405
 
        try:
406
 
            # We can't pass unknown kwds to make_option, or it will barf.
407
 
            is_required = kwds.pop('required')
408
 
        except KeyError:
409
 
            is_required = False
410
 
 
411
 
        option = optparse.make_option(*args, **kwds)
412
 
        self._optparser.add_option(option)
413
 
 
414
 
        if is_required:
415
 
            self.add_required_option(option)
416
 
 
417
 
        self._add_option_to_cfg_mapping(option, args)
418
 
 
419
 
    def add_required_option(self, option):
420
 
        """Add a required option.
421
 
 
422
 
        Takes an optparse.Option object.
423
 
        """
424
 
        self.required_options.append(option)
425
 
 
426
 
    def _add_option_to_cfg_mapping(self, option, option_constructor_args):
427
 
        # Convert the long options into .ini keys.  Use the last long option
428
 
        # given.
429
 
        for switch in reversed(option_constructor_args):
430
 
            if switch.startswith('--'):
431
 
                # We found a '--foo' switch, so use it.  Drop the '--',
432
 
                # because the config file doesn't use the prefixes.
433
 
                self._option_to_cfgkey[option.dest] = switch[2:]
434
 
 
435
 
    def error(self, message):
436
 
        """Wrap optparse.OptionParser.error()."""
437
 
        self._optparser.error(message)
438
 
 
439
 
    def parse_config_file(self, filepath):
440
 
        fp = os.path.expanduser(filepath)
441
 
 
442
 
        if not os.path.exists(fp):
443
 
            self.error("The config file '%s' does not exist!" % fp)
444
 
 
445
 
        self._cfgparser.read(fp)
446
 
        self._parsed_cfg_options = self._cfgparser.defaults()
447
 
 
448
 
        num_opts = len(self._parsed_cfg_options)
449
 
        debug("Loaded %d options from %s" % (num_opts, fp))
450
 
 
451
 
    def parse_cli_args(self, argv):
452
 
        """Wrap optparse.OptionParser.parse_args()."""
453
 
        options, args = self._optparser.parse_args(argv)
454
 
        self._parsed_cli_options = options
455
 
        self.args = args
456
 
        return (options, args)
457
 
 
458
 
    def verify_options(self):
459
 
        """Verify that all required options are there.
460
 
 
461
 
        Raise an optparse.OptionParser.error() if something is missing.
462
 
 
463
 
        Make sure you parsed the config file with parse_config_file() before
464
 
        doing this.
465
 
        """
466
 
        debug("Verifying options")
467
 
        if not self._parsed_cfg_options:
468
 
            debug("No config file options found")
469
 
 
470
 
        for option in self.required_options:
471
 
            # Check for a command-line option.
472
 
 
473
 
            option_name = option.dest
474
 
 
475
 
            if self.get(option_name) is None:
476
 
                self._required_option_error(option)
477
 
            else:
478
 
                debug("Found required option: %s" % option_name)
479
 
 
480
 
    def _required_option_error(self, option):
481
 
        msg = "Required option '%s' was not given (-h for help)" % str(option)
482
 
        self.error(msg)
483
 
 
484
 
    def get(self, name, default=None):
485
 
        """Return the appropriate option, CLI first, CFG second."""
486
 
        cli_name = name
487
 
        cfg_name = self._option_to_cfgkey.get(name)
488
 
 
489
 
        value = self._getoption(cli_name)
490
 
 
491
 
        if value is None and cfg_name is not None:
492
 
            # No command-line option was supplied, but we do have a config
493
 
            # file entry with that name.
494
 
            value = self._getcfg(cfg_name)
495
 
 
496
 
            if value is None:
497
 
                # No config file option was supplied either, so return the
498
 
                # default.
499
 
                return default
500
 
 
501
 
        return value
502
 
 
503
 
    def _getoption(self, key, default=None):
504
 
        return getattr(self._parsed_cli_options, key, default)
505
 
 
506
 
    def _getcfg(self, key, default=None):
507
 
        return self._parsed_cfg_options.get(key, default)
508
 
 
509
 
 
510
 
def get_credentials():
511
 
    """Return an EC2Credentials object for accessing the webservice."""
512
 
    # Get the AWS identifier and secret identifier.
513
 
    return ec2test.EC2Credentials.load_from_file()
514
 
 
515
 
 
516
 
def parse_config_file(filepath):
517
 
    config = ConfigParser.ConfigParser()
518
 
    config.read(filepath)
519
 
    return config
520
 
 
521
 
 
522
 
def parse_options(argv):
523
 
    oparser = optparse.OptionParser(usage)
524
 
    cparser = ConfigParser.SafeConfigParser()
525
 
    parser = CombinedConfigParser(oparser, cparser)
526
 
 
527
 
    # What follows are "Required options" - these must be supplied from either
528
 
    # the command-line, or from a config file.
529
 
    parser.add_option(
530
 
        '-k', '--key',
531
 
        dest="keypair_name",
532
 
        required=True,
533
 
        help="The name of the AWS key pair to use for launching instances.")
534
 
 
535
 
    parser.add_option(
536
 
        '-K', '--private-key',
537
 
        dest="private_key",
538
 
        required=True,
539
 
        help="The X.509 private keyfile that will be used to sign the new "
540
 
             "image.")
541
 
 
542
 
    parser.add_option(
543
 
        '-C', '--cert',
544
 
        dest="cert",
545
 
        required=True,
546
 
        help="The X.509 certificate that will be used to bundle the new "
547
 
             "image.")
548
 
 
549
 
    parser.add_option(
550
 
        '-i', '--identity-file',
551
 
        dest='identity_file',
552
 
        required=True,
553
 
        help="The location of the RSA private key that SSH will use to "
554
 
             "connect to the instance.")
555
 
 
556
 
    parser.add_option(
557
 
        '-b', '--bucket',
558
 
        dest="bucket",
559
 
        required=True,
560
 
        help="The bucket that the image will be placed into.")
561
 
 
562
 
    parser.add_option(
563
 
        '-u', '--user-id',
564
 
        dest="account_id",
565
 
        required=True,
566
 
        help="Your 12 digit AWS account ID")
567
 
 
568
 
    parser.add_option(
569
 
        '-a', '--access-key',
570
 
        dest="access_key",
571
 
        required=True,
572
 
        help="Your AWS access key.")
573
 
 
574
 
    parser.add_option(
575
 
        '-s', '--secret-key',
576
 
        dest="secret_key",
577
 
        required=True,
578
 
        help="Your AWS secret key.")
579
 
 
580
 
 
581
 
    # Start our "Optional options."
582
 
    parser.add_option(
583
 
        '-v', '--verbose',
584
 
        action='store_true',
585
 
        dest='verbose',
586
 
        default=False,
587
 
        help="Turn on debug output.")
588
 
 
589
 
    parser.add_option(
590
 
        '-t', '--instance-type',
591
 
        dest="instance_type",
592
 
        default="m1.large",
593
 
        help="The type of instance to be launched.  Should be the same as "
594
 
             "the base image's required type. [default: %default]")
595
 
 
596
 
    parser.add_option(
597
 
        '-c', '--config',
598
 
        dest='config',
599
 
        default="~/.ec2/ec2bundle.cfg",
600
 
        help="Load script options from the supplied config file. (.ini "
601
 
             "format, see the module docstring for details.) "
602
 
             "[default: %default]")
603
 
 
604
 
    parser.add_option(
605
 
        '--keepalive',
606
 
        action='store_true',
607
 
        dest="keepalive",
608
 
        default=False,
609
 
        help="Don't shut down the instance when we are done building (or "
610
 
             "erroring out).")
611
 
 
612
 
    parser.add_option(
613
 
        '--no-bundle',
614
 
        action='store_true',
615
 
        dest='no_bundle',
616
 
        default=False,
617
 
        help="Don't create a bundle, just start the server and configure the "
618
 
             "environment.")
619
 
 
620
 
    parser.add_option(
621
 
        '--use-instance',
622
 
        dest='running_instance',
623
 
        help="Use the supplied EC2 instance ID, instead of starting our own "
624
 
             "server.  The instance will be left running.")
625
 
 
626
 
 
627
 
    options, args = parser.parse_cli_args(argv)
628
 
 
629
 
    # Do this ASAP
630
 
    if options.verbose:
631
 
        log.setLevel(logging.DEBUG)
632
 
 
633
 
    if options.config:
634
 
        parser.parse_config_file(options.config)
635
 
 
636
 
    # Make sure all the required args are present.  Will error-out if
637
 
    # something is missing.
638
 
    parser.verify_options()
639
 
 
640
 
    if len(args) != 2:
641
 
        parser.error("You must provide an AMI ID that can serve as the new "
642
 
                     "image's base.")
643
 
 
644
 
    return parser
645
 
 
646
 
 
647
 
def main(argv):
648
 
    config = parse_options(argv)
649
 
 
650
 
    credentials = get_credentials()
651
 
    account = credentials.connect(config.keypair_name)
652
 
 
653
 
    # Save the flag so we can change it.  This is how we enforce shutdown
654
 
    # policies.
655
 
    keepalive = config.keepalive
656
 
 
657
 
    if config.running_instance:
658
 
        # We want to keep the server alive if the user supplied their own
659
 
        # instance.  Killing it without their consent would be cruel.
660
 
        keepalive = True
661
 
 
662
 
    ssh_user_command = None
663
 
    try:
664
 
        try:
665
 
            instance = None
666
 
            if config.running_instance:
667
 
                # Connect to an already running instance.
668
 
                instance = Instance.from_running_instance(
669
 
                    account, config.running_instance)
670
 
            else:
671
 
                # Run an instance for our base image.
672
 
                instance = Instance.from_image(
673
 
                    account, config.args[1], config.instance_type)
674
 
 
675
 
            ssh = SSHConnector(
676
 
                instance.hostname, 'root', config.identity_file)
677
 
            ssh.connect()
678
 
            ssh_user_command = ssh.user_command()
679
 
 
680
 
            system_configurator = XvfbSystemConfigurator(ssh)
681
 
            system_configurator.configure_system()
682
 
 
683
 
            if not config.no_bundle:
684
 
                bundler = ImageBundler(
685
 
                    config.private_key,
686
 
                    config.cert,
687
 
                    config.account_id,
688
 
                    config.bucket,
689
 
                    config.access_key,
690
 
                    config.secret_key,
691
 
                    ssh)
692
 
                bundler.bundle_image()
693
 
 
694
 
        except:
695
 
            # Log the exception now so it doesn't interfere with or get eaten
696
 
            # by the instance shutdown.
697
 
            log.exception("Oops!")
698
 
    finally:
699
 
        if keepalive:
700
 
            log.warning("instance %s is now running on its own" % instance.id)
701
 
            if ssh_user_command:
702
 
                info("You may now ssh into the instance using the following command:")
703
 
                info("  $ %s" % ssh_user_command)
704
 
 
705
 
            log.warning("Remember to shut the instance down when you are done!")
706
 
        else:
707
 
            instance.stop()
708
 
 
709
 
 
710
 
if __name__ == '__main__':
711
 
    logging.basicConfig()
712
 
    main(sys.argv)