~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Jonathan Lange
  • Date: 2009-08-31 05:15:30 UTC
  • mto: This revision was merged to the branch mainline in revision 9271.
  • Revision ID: jml@canonical.com-20090831051530-pex7jge5d54ixay2
Add lp-dev-tools that lack license confusion, changing the license to match
the rest of the Launchpad source tree.

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)