~launchpad-pqm/launchpad/devel

9389.6.6 by Michael Hudson
move main to its own file, add standard header to all new files
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
9389.6.9 by Michael Hudson
docstrings, __all__s and a little whitespace
4
"""Code to represent a single machine instance in EC2."""
9389.6.6 by Michael Hudson
move main to its own file, add standard header to all new files
5
6
__metaclass__ = type
9389.6.9 by Michael Hudson
docstrings, __all__s and a little whitespace
7
__all__ = [
8
    'EC2Instance',
9
    ]
9389.6.6 by Michael Hudson
move main to its own file, add standard header to all new files
10
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
11
import code
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
12
import glob
9397.1.8 by Michael Hudson
no plan survives contact etc
13
import os
9389.6.3 by Michael Hudson
give EC2Instance to its own file
14
import select
15
import socket
16
import subprocess
17
import sys
18
import time
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
19
import traceback
9389.6.3 by Michael Hudson
give EC2Instance to its own file
20
9453.2.22 by Michael Hudson
kill error_and_quit
21
from bzrlib.errors import BzrCommandError
11962.1.2 by Gavin Panella
Format imports.
22
from devscripts.ec2test.session import EC2SessionName
9389.6.3 by Michael Hudson
give EC2Instance to its own file
23
import paramiko
24
9453.2.30 by Michael Hudson
start responding to review
25
9397.2.25 by Michael Hudson
act on comments from jml
26
DEFAULT_INSTANCE_TYPE = 'c1.xlarge'
27
AVAILABLE_INSTANCE_TYPES = ('m1.large', 'm1.xlarge', 'c1.xlarge')
9389.6.3 by Michael Hudson
give EC2Instance to its own file
28
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
29
9389.6.3 by Michael Hudson
give EC2Instance to its own file
30
class AcceptAllPolicy:
31
    """We accept all unknown host key."""
32
33
    def missing_host_key(self, client, hostname, key):
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
34
        # Normally the console output is supposed to contain the Host key but
35
        # it doesn't seem to be the case here, so we trust that the host we
36
        # are connecting to is the correct one.
9389.6.3 by Michael Hudson
give EC2Instance to its own file
37
        pass
38
39
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
40
def get_user_key():
10835.1.3 by Maris Fogels
Small tweak to the module variables and docstring.
41
    """Get a SSH key from the agent.  Raise an error if no keys were found.
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
42
43
    This key will be used to let the user log in (as $USER) to the instance.
44
    """
45
    agent = paramiko.Agent()
46
    keys = agent.get_keys()
47
    if len(keys) == 0:
9453.2.22 by Michael Hudson
kill error_and_quit
48
        raise BzrCommandError(
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
49
            'You must have an ssh agent running with keys installed that '
10835.1.4 by Maris Fogels
Updated an old error message.
50
            'will allow the script to access Launchpad and get your '
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
51
            'branch.\n')
10835.1.1 by Maris Fogels
Added an XXX for bug 577118 in ec2test.
52
10835.1.3 by Maris Fogels
Small tweak to the module variables and docstring.
53
    # XXX mars 2010-05-07 bug=577118
10835.1.1 by Maris Fogels
Added an XXX for bug 577118 in ec2test.
54
    # Popping the first key off of the stack can create problems if the person
55
    # has more than one key in their ssh-agent, but alas, we have no good way
10835.1.2 by Maris Fogels
Added a bit more to the comment.
56
    # to detect the right key to use.  See bug 577118 for a workaround.
10835.1.3 by Maris Fogels
Small tweak to the module variables and docstring.
57
    return keys[0]
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
58
59
9550.13.40 by Michael Hudson
self-review-ish comments
60
# Commands to run to turn a blank image into one usable for the rest of the
61
# ec2 functionality.  They come in two parts, one set that need to be run as
62
# root and another that should be run as the 'ec2test' user.
11699.1.1 by Gary Poster
make update-image work without hiccups on lucid.
63
# Note that the sources from http://us.ec2.archive.ubuntu.com/ubuntu/ are per
64
# instructions described in http://is.gd/g1MIT .  When we switch to
65
# Eucalyptus, we can dump this.
9550.13.40 by Michael Hudson
self-review-ish comments
66
67
from_scratch_root = """
9550.13.44 by Michael Hudson
review inspired comments
68
# From 'help set':
69
# -x  Print commands and their arguments as they are executed.
70
# -e  Exit immediately if a command exits with a non-zero status.
9550.13.30 by Michael Hudson
this might possibly work now
71
set -xe
72
73
sed -ie 's/main universe/main universe multiverse/' /etc/apt/sources.list
74
75
. /etc/lsb-release
76
77
cat >> /etc/apt/sources.list << EOF
78
deb http://ppa.launchpad.net/launchpad/ubuntu $DISTRIB_CODENAME main
79
deb http://ppa.launchpad.net/bzr/ubuntu $DISTRIB_CODENAME main
80
deb http://ppa.launchpad.net/bzr-beta-ppa/ubuntu $DISTRIB_CODENAME main
11699.1.1 by Gary Poster
make update-image work without hiccups on lucid.
81
deb http://us.ec2.archive.ubuntu.com/ubuntu/ $DISTRIB_CODENAME multiverse
82
deb-src http://us.ec2.archive.ubuntu.com/ubuntu/ $DISTRIB_CODENAME main
9550.13.30 by Michael Hudson
this might possibly work now
83
EOF
84
9550.13.40 by Michael Hudson
self-review-ish comments
85
# This next part is cribbed from rocketfuel-setup
9550.13.30 by Michael Hudson
this might possibly work now
86
dev_host() {
9550.13.43 by Michael Hudson
oops!
87
  sed -i \"s/^127.0.0.88.*$/&\ ${hostname}/\" /etc/hosts
9550.13.30 by Michael Hudson
this might possibly work now
88
}
89
90
echo 'Adding development hosts on local machine'
91
echo '
92
# Launchpad virtual domains. This should be on one line.
93
127.0.0.88      launchpad.dev
94
' >> /etc/hosts
95
96
declare -a hostnames
97
hostnames=$(cat <<EOF
98
    answers.launchpad.dev
99
    api.launchpad.dev
100
    bazaar-internal.launchpad.dev
101
    beta.launchpad.dev
102
    blueprints.launchpad.dev
103
    bugs.launchpad.dev
104
    code.launchpad.dev
105
    feeds.launchpad.dev
106
    id.launchpad.dev
107
    keyserver.launchpad.dev
108
    lists.launchpad.dev
109
    openid.launchpad.dev
110
    ppa.launchpad.dev
111
    private-ppa.launchpad.dev
112
    shipit.edubuntu.dev
113
    shipit.kubuntu.dev
114
    shipit.ubuntu.dev
10212.7.16 by Guilherme Salgado
Rename testopenid.launchpad.dev to testopenid.dev on lib/devscripts/ec2test/instance.py
115
    testopenid.dev
9550.13.30 by Michael Hudson
this might possibly work now
116
    translations.launchpad.dev
117
    xmlrpc-private.launchpad.dev
118
    xmlrpc.launchpad.dev
119
EOF
120
    )
121
122
for hostname in $hostnames; do
123
  dev_host;
124
done
125
126
echo '
127
127.0.0.99      bazaar.launchpad.dev
128
' >> /etc/hosts
129
9550.13.40 by Michael Hudson
self-review-ish comments
130
# Add the keys for the three PPAs added to sources.list above.
9550.13.30 by Michael Hudson
this might possibly work now
131
apt-key adv --recv-keys --keyserver pool.sks-keyservers.net 2af499cb24ac5f65461405572d1ffb6c0a5174af
132
apt-key adv --recv-keys --keyserver pool.sks-keyservers.net ece2800bacf028b31ee3657cd702bf6b8c6c1efd
133
apt-key adv --recv-keys --keyserver pool.sks-keyservers.net cbede690576d1e4e813f6bb3ebaf723d37b19b80
134
135
aptitude update
136
aptitude -y full-upgrade
137
10271.3.2 by Michael Hudson
fixes
138
DEBIAN_FRONTEND=noninteractive apt-get -y install launchpad-developer-dependencies apache2 apache2-mpm-worker
9550.13.30 by Michael Hudson
this might possibly work now
139
10287.1.1 by Guilherme Salgado
Make sure ec2 instances are shut down 8h after they start running the test suite
140
# Create the ec2test user, give them passwordless sudo.
9550.13.30 by Michael Hudson
this might possibly work now
141
adduser --gecos "" --disabled-password ec2test
142
echo 'ec2test\tALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
143
9550.13.32 by Michael Hudson
create ~ec2test/.ssh before putting stuff in it
144
mkdir /home/ec2test/.ssh
9550.13.30 by Michael Hudson
this might possibly work now
145
cat > /home/ec2test/.ssh/config << EOF
146
CheckHostIP no
147
StrictHostKeyChecking no
148
EOF
149
9550.13.32 by Michael Hudson
create ~ec2test/.ssh before putting stuff in it
150
mkdir /var/launchpad
151
chown -R ec2test:ec2test /var/www /var/launchpad /home/ec2test/
9550.13.35 by Michael Hudson
really run as ec2test user
152
"""
153
154
9550.13.40 by Michael Hudson
self-review-ish comments
155
from_scratch_ec2test = """
9550.13.44 by Michael Hudson
review inspired comments
156
# From 'help set':
157
# -x  Print commands and their arguments as they are executed.
158
# -e  Exit immediately if a command exits with a non-zero status.
9550.13.36 by Michael Hudson
set -xe for these commands too
159
set -xe
160
9550.13.35 by Michael Hudson
really run as ec2test user
161
bzr launchpad-login %(launchpad-login)s
162
bzr init-repo --2a /var/launchpad
9550.13.50 by Michael Hudson
pre-landing update to refer to proper trunk branches
163
bzr branch lp:~launchpad-pqm/launchpad/devel /var/launchpad/test
9550.13.35 by Michael Hudson
really run as ec2test user
164
bzr branch --standalone lp:lp-source-dependencies /var/launchpad/download-cache
165
mkdir /var/launchpad/sourcecode
166
/var/launchpad/test/utilities/update-sourcecode /var/launchpad/sourcecode
9550.13.30 by Michael Hudson
this might possibly work now
167
"""
168
169
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
170
postmortem_banner = """\
171
Postmortem Console. EC2 instance is not yet dead.
172
It will shut down when you exit this prompt (CTRL-D)
173
174
Tab-completion is enabled.
175
EC2Instance is available as `instance`.
176
Also try these:
177
  http://%(dns)s/current_test.log
9760.5.1 by Guilherme Salgado
Fix the bug
178
  ssh -A ec2test@%(dns)s
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
179
"""
180
181
9389.6.3 by Michael Hudson
give EC2Instance to its own file
182
class EC2Instance:
183
    """A single EC2 instance."""
184
9397.2.25 by Michael Hudson
act on comments from jml
185
    @classmethod
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
186
    def make(cls, name, instance_type, machine_id, demo_networks=None,
187
             credentials=None):
9397.2.26 by Michael Hudson
docstrings!
188
        """Construct an `EC2Instance`.
189
190
        :param name: The name to use for the key pair and security group for
191
            the instance.
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
192
        :type name: `EC2SessionName`
9397.2.26 by Michael Hudson
docstrings!
193
        :param instance_type: One of the AVAILABLE_INSTANCE_TYPES.
9453.2.20 by Michael Hudson
kill make_instance helper
194
        :param machine_id: The AMI to use, or None to do the usual regexp
9550.13.40 by Michael Hudson
self-review-ish comments
195
            matching.  If you put 'based-on:' before the AMI id, it is assumed
196
            that the id specifies a blank image that should be made into one
197
            suitable for the other ec2 functions (see `from_scratch_root` and
198
            `from_scratch_ec2test` above).
9453.2.30 by Michael Hudson
start responding to review
199
        :param demo_networks: A list of networks to add to the security group
200
            to allow access to the instance.
9453.2.20 by Michael Hudson
kill make_instance helper
201
        :param credentials: An `EC2Credentials` object.
9397.2.26 by Michael Hudson
docstrings!
202
        """
9952.1.4 by Jeroen Vermeulen
Review changes.
203
        # This import breaks in the test environment.  Do it here so
204
        # that unit tests (which don't use this factory) can still
205
        # import EC2Instance.
9942.2.1 by Jeroen Vermeulen
Added test, reproduced problem.
206
        from bzrlib.plugins.launchpad.account import get_lp_login
9952.1.4 by Jeroen Vermeulen
Review changes.
207
208
        # XXX JeroenVermeulen 2009-11-27 bug=489073: EC2Credentials
209
        # imports boto, which isn't necessarily installed in our test
210
        # environment.  Doing the import here so that unit tests (which
211
        # don't use this factory) can still import EC2Instance.
9952.1.1 by Jeroen Vermeulen
Work around import issues.
212
        from devscripts.ec2test.credentials import EC2Credentials
9942.2.1 by Jeroen Vermeulen
Added test, reproduced problem.
213
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
214
        assert isinstance(name, EC2SessionName)
9397.2.25 by Michael Hudson
act on comments from jml
215
        if instance_type not in AVAILABLE_INSTANCE_TYPES:
216
            raise ValueError('unknown instance_type %s' % (instance_type,))
217
9550.13.40 by Michael Hudson
self-review-ish comments
218
        # We call this here so that it has a chance to complain before the
219
        # instance is started (which can take some time).
9550.13.44 by Michael Hudson
review inspired comments
220
        user_key = get_user_key()
9550.13.40 by Michael Hudson
self-review-ish comments
221
9453.2.20 by Michael Hudson
kill make_instance helper
222
        if credentials is None:
223
            credentials = EC2Credentials.load_from_file()
224
9397.2.25 by Michael Hudson
act on comments from jml
225
        # Make the EC2 connection.
226
        account = credentials.connect(name)
227
228
        # We do this here because it (1) cleans things up and (2) verifies
229
        # that the account is correctly set up. Both of these are appropriate
230
        # for initialization.
231
        #
232
        # We always recreate the keypairs because there is no way to
9594.2.16 by Gavin Panella
Undo unnecessary changes.
233
        # programmatically retrieve the private key component, unless we
234
        # generate it.
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
235
        account.collect_garbage()
9397.2.25 by Michael Hudson
act on comments from jml
236
9550.13.39 by Michael Hudson
trivial fix
237
        if machine_id and machine_id.startswith('based-on:'):
9550.13.30 by Michael Hudson
this might possibly work now
238
            from_scratch = True
239
            machine_id = machine_id[len('based-on:'):]
240
        else:
241
            from_scratch = False
242
9397.2.25 by Michael Hudson
act on comments from jml
243
        # get the image
244
        image = account.acquire_image(machine_id)
245
246
        login = get_lp_login()
247
        if not login:
9453.2.22 by Michael Hudson
kill error_and_quit
248
            raise BzrCommandError(
9397.2.25 by Michael Hudson
act on comments from jml
249
                'you must have set your launchpad login in bzr.')
250
251
        return EC2Instance(
9636.1.15 by Jonathan Lange
Remove the vals dict.
252
            name, image, instance_type, demo_networks, account,
9636.1.14 by Jonathan Lange
Drop launchpad-login from the vals dictionary.
253
            from_scratch, user_key, login)
9389.6.3 by Michael Hudson
give EC2Instance to its own file
254
9397.2.1 by Michael Hudson
rename controller variables to account
255
    def __init__(self, name, image, instance_type, demo_networks, account,
9636.1.15 by Jonathan Lange
Remove the vals dict.
256
                 from_scratch, user_key, launchpad_login):
9389.6.3 by Michael Hudson
give EC2Instance to its own file
257
        self._name = name
258
        self._image = image
9397.2.1 by Michael Hudson
rename controller variables to account
259
        self._account = account
9389.6.3 by Michael Hudson
give EC2Instance to its own file
260
        self._instance_type = instance_type
261
        self._demo_networks = demo_networks
262
        self._boto_instance = None
9550.13.30 by Michael Hudson
this might possibly work now
263
        self._from_scratch = from_scratch
9550.13.44 by Michael Hudson
review inspired comments
264
        self._user_key = user_key
9636.1.14 by Jonathan Lange
Drop launchpad-login from the vals dictionary.
265
        self._launchpad_login = launchpad_login
9389.6.3 by Michael Hudson
give EC2Instance to its own file
266
267
    def log(self, msg):
268
        """Log a message on stdout, flushing afterwards."""
269
        # XXX: JonathanLange 2009-05-31 bug=383076: Should delete this and use
270
        # Python logging module instead.
271
        sys.stdout.write(msg)
272
        sys.stdout.flush()
273
274
    def start(self):
275
        """Start the instance."""
276
        if self._boto_instance is not None:
277
            self.log('Instance %s already started' % self._boto_instance.id)
278
            return
279
        start = time.time()
9397.2.1 by Michael Hudson
rename controller variables to account
280
        self.private_key = self._account.acquire_private_key()
9453.2.7 by Michael Hudson
flesh out demo command
281
        self.security_group = self._account.acquire_security_group(
9389.6.3 by Michael Hudson
give EC2Instance to its own file
282
            demo_networks=self._demo_networks)
283
        reservation = self._image.run(
9594.2.16 by Gavin Panella
Undo unnecessary changes.
284
            key_name=self._name, security_groups=[self._name],
9389.6.3 by Michael Hudson
give EC2Instance to its own file
285
            instance_type=self._instance_type)
286
        self._boto_instance = reservation.instances[0]
287
        self.log('Instance %s starting..' % self._boto_instance.id)
288
        while self._boto_instance.state == 'pending':
289
            self.log('.')
290
            time.sleep(5)
291
            self._boto_instance.update()
292
        if self._boto_instance.state == 'running':
293
            self.log(' started on %s\n' % self.hostname)
294
            elapsed = time.time() - start
295
            self.log('Started in %d minutes %d seconds\n' %
296
                     (elapsed // 60, elapsed % 60))
297
            self._output = self._boto_instance.get_console_output()
298
            self.log(self._output.output)
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
299
            self._ec2test_user_has_keys = False
9389.6.3 by Michael Hudson
give EC2Instance to its own file
300
        else:
9453.2.22 by Michael Hudson
kill error_and_quit
301
            raise BzrCommandError(
9389.6.3 by Michael Hudson
give EC2Instance to its own file
302
                'failed to start: %s\n' % self._boto_instance.state)
303
304
    def shutdown(self):
305
        """Shut down the instance."""
306
        if self._boto_instance is None:
307
            self.log('no instance created\n')
308
            return
309
        self._boto_instance.update()
310
        if self._boto_instance.state not in ('shutting-down', 'terminated'):
311
            # terminate instance
312
            self._boto_instance.stop()
313
            self._boto_instance.update()
314
        self.log('instance %s\n' % (self._boto_instance.state,))
315
316
    @property
317
    def hostname(self):
318
        if self._boto_instance is None:
319
            return None
320
        return self._boto_instance.public_dns_name
321
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
322
    def _connect(self, username):
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
323
        """Connect to the instance as `user`. """
324
        ssh = paramiko.SSHClient()
325
        ssh.set_missing_host_key_policy(AcceptAllPolicy())
9550.13.2 by Michael Hudson
move towards using a pre-setup account
326
        connect_args = {
9550.13.6 by Michael Hudson
some complexity to cope with how the base instance does root login
327
            'username': username,
9550.13.2 by Michael Hudson
move towards using a pre-setup account
328
            'pkey': self.private_key,
329
            'allow_agent': False,
330
            'look_for_keys': False,
331
            }
9389.6.3 by Michael Hudson
give EC2Instance to its own file
332
        for count in range(10):
333
            try:
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
334
                ssh.connect(self.hostname, **connect_args)
9389.6.3 by Michael Hudson
give EC2Instance to its own file
335
            except (socket.error, paramiko.AuthenticationException), e:
9550.13.2 by Michael Hudson
move towards using a pre-setup account
336
                self.log('_connect: %r\n' % (e,))
9389.6.3 by Michael Hudson
give EC2Instance to its own file
337
                if count < 9:
338
                    time.sleep(5)
339
                    self.log('retrying...')
340
                else:
341
                    raise
342
            else:
343
                break
9550.13.6 by Michael Hudson
some complexity to cope with how the base instance does root login
344
        return EC2InstanceConnection(self, username, ssh)
9397.1.4 by Michael Hudson
move the unix user creation method to its own method on EC2Instance
345
9550.13.40 by Michael Hudson
self-review-ish comments
346
    def _upload_local_key(self, conn, remote_filename):
9550.13.44 by Michael Hudson
review inspired comments
347
        """Upload a key from the local user's agent to `remote_filename`.
348
349
        The key will be uploaded in a format suitable for
350
        ~/.ssh/authorized_keys.
351
        """
9550.13.40 by Michael Hudson
self-review-ish comments
352
        authorized_keys_file = conn.sftp.open(remote_filename, 'w')
9550.13.31 by Michael Hudson
enable logging in as root with the local key before trying to use run_with_ssh_agent as root
353
        authorized_keys_file.write(
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
354
            "%s %s\n" % (
355
                self._user_key.get_name(), self._user_key.get_base64()))
9550.13.31 by Michael Hudson
enable logging in as root with the local key before trying to use run_with_ssh_agent as root
356
        authorized_keys_file.close()
357
9550.13.44 by Michael Hudson
review inspired comments
358
    def _ensure_ec2test_user_has_keys(self, connection=None):
359
        """Make sure that we can connect over ssh as the 'ec2test' user.
360
361
        We add both the key that was used to start the instance (so
362
        _connect('ec2test') works and a key from the locally running ssh agent
363
        (so EC2InstanceConnection.run_with_ssh_agent works).
364
        """
365
        if not self._ec2test_user_has_keys:
366
            if connection is None:
10271.3.1 by Michael Hudson
maybe this is all?
367
                connection = self._connect('ubuntu')
9550.13.44 by Michael Hudson
review inspired comments
368
                our_connection = True
369
            else:
370
                our_connection = False
371
            self._upload_local_key(connection, 'local_key')
372
            connection.perform(
10271.3.1 by Michael Hudson
maybe this is all?
373
                'cat /home/ubuntu/.ssh/authorized_keys local_key '
10271.3.2 by Michael Hudson
fixes
374
                '| sudo tee /home/ec2test/.ssh/authorized_keys > /dev/null'
10271.3.1 by Michael Hudson
maybe this is all?
375
                '&& rm local_key')
10271.3.2 by Michael Hudson
fixes
376
            connection.perform('sudo chown -R ec2test:ec2test /home/ec2test/')
377
            connection.perform('sudo chmod 644 /home/ec2test/.ssh/*')
9550.13.44 by Michael Hudson
review inspired comments
378
            if our_connection:
379
                connection.close()
380
            self.log(
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
381
                'You can now use ssh -A ec2test@%s to '
382
                'log in the instance.\n' % self.hostname)
9550.13.44 by Michael Hudson
review inspired comments
383
            self._ec2test_user_has_keys = True
384
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
385
    def connect(self):
386
        """Connect to the instance as a user with passwordless sudo.
387
388
        This may involve first connecting as root and adding SSH keys to the
9550.13.30 by Michael Hudson
this might possibly work now
389
        user's account, and in the case of a from scratch image, it will do a
390
        lot of set up.
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
391
        """
9550.13.30 by Michael Hudson
this might possibly work now
392
        if self._from_scratch:
10271.3.1 by Michael Hudson
maybe this is all?
393
            ubuntu_connection = self._connect('ubuntu')
394
            self._upload_local_key(ubuntu_connection, 'local_key')
395
            ubuntu_connection.perform(
9550.13.37 by Michael Hudson
more streamlining, possibly with more correctness too
396
                'cat local_key >> ~/.ssh/authorized_keys && rm local_key')
10271.3.1 by Michael Hudson
maybe this is all?
397
            ubuntu_connection.run_script(from_scratch_root, sudo=True)
398
            self._ensure_ec2test_user_has_keys(ubuntu_connection)
399
            ubuntu_connection.close()
9550.13.44 by Michael Hudson
review inspired comments
400
            conn = self._connect('ec2test')
9636.1.14 by Jonathan Lange
Drop launchpad-login from the vals dictionary.
401
            conn.run_script(
402
                from_scratch_ec2test
403
                % {'launchpad-login': self._launchpad_login})
9550.13.35 by Michael Hudson
really run as ec2test user
404
            self._from_scratch = False
9550.13.44 by Michael Hudson
review inspired comments
405
            return conn
406
        self._ensure_ec2test_user_has_keys()
407
        return self._connect('ec2test')
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
408
9942.2.4 by Jeroen Vermeulen
Cleaned up the test: generalized the stubbing.
409
    def _report_traceback(self):
410
        """Print traceback."""
411
        traceback.print_exc()
412
9550.13.41 by Michael Hudson
revert a change that turned out not to be much use
413
    def set_up_and_run(self, postmortem, shutdown, func, *args, **kw):
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
414
        """Start, run `func` and then maybe shut down.
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
415
9550.13.6 by Michael Hudson
some complexity to cope with how the base instance does root login
416
        :param config: A dictionary specifying details of how the instance
417
            should be run:
9550.13.41 by Michael Hudson
revert a change that turned out not to be much use
418
        :param postmortem: If true, any exceptions will be caught and an
419
            interactive session run to allow debugging the problem.
420
        :param shutdown: If true, shut down the instance after `func` and
421
            postmortem (if any) are completed.
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
422
        :param func: A callable that will be called when the instance is
423
            running and a user account has been set up on it.
424
        :param args: Passed to `func`.
425
        :param kw: Passed to `func`.
426
        """
9926.2.1 by Michael Hudson
in set_up_and_run, only pay attention to the shutdown if the supplied function exits normally
427
        # We ignore the value of the 'shutdown' argument and always shut down
9926.2.2 by Michael Hudson
typo
428
        # unless `func` returns normally.
9926.2.1 by Michael Hudson
in set_up_and_run, only pay attention to the shutdown if the supplied function exits normally
429
        really_shutdown = True
9949.1.1 by Michael Hudson
don't always shutdown for headless builds :(
430
        retval = None
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
431
        try:
9550.13.41 by Michael Hudson
revert a change that turned out not to be much use
432
            self.start()
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
433
            try:
9949.1.1 by Michael Hudson
don't always shutdown for headless builds :(
434
                retval = func(*args, **kw)
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
435
            except Exception:
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
436
                # When running in postmortem mode, it is really helpful to see
437
                # if there are any exceptions before it waits in the console
438
                # (in the finally block), and you can't figure out why it's
439
                # broken.
9942.2.4 by Jeroen Vermeulen
Cleaned up the test: generalized the stubbing.
440
                self._report_traceback()
9926.2.1 by Michael Hudson
in set_up_and_run, only pay attention to the shutdown if the supplied function exits normally
441
            else:
442
                really_shutdown = shutdown
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
443
        finally:
444
            try:
9550.13.41 by Michael Hudson
revert a change that turned out not to be much use
445
                if postmortem:
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
446
                    console = code.InteractiveConsole(locals())
9639.1.1 by Gavin Panella
Revert revision 9636, which itself was a reversion of an earlier revision.
447
                    console.interact(
448
                        postmortem_banner % {'dns': self.hostname})
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
449
                    print 'Postmortem console closed.'
450
            finally:
9926.2.1 by Michael Hudson
in set_up_and_run, only pay attention to the shutdown if the supplied function exits normally
451
                if really_shutdown:
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
452
                    self.shutdown()
9949.1.1 by Michael Hudson
don't always shutdown for headless builds :(
453
        return retval
9453.2.17 by Michael Hudson
move run_with_instance to be EC2Instance.set_up_and_run
454
9397.2.15 by Michael Hudson
a bit more LBYL
455
    def _copy_single_file(self, sftp, local_path, remote_dir):
9397.2.26 by Michael Hudson
docstrings!
456
        """Copy `local_path` to `remote_dir` on this instance.
457
458
        The name in the remote directory will be that of the local file.
459
460
        :param sftp: A paramiko SFTP object.
461
        :param local_path: The local path.
462
        :param remote_dir: The directory on the instance to copy into.
463
        """
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
464
        name = os.path.basename(local_path)
465
        remote_path = os.path.join(remote_dir, name)
466
        remote_file = sftp.open(remote_path, 'w')
467
        remote_file.write(open(local_path).read())
468
        remote_file.close()
9397.2.15 by Michael Hudson
a bit more LBYL
469
        return remote_path
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
470
471
    def copy_key_and_certificate_to_image(self, sftp):
9397.2.26 by Michael Hudson
docstrings!
472
        """Copy the AWS private key and certificate to the image.
473
474
        :param sftp: A paramiko SFTP object.
475
        """
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
476
        remote_ec2_dir = '/mnt/ec2'
9397.2.15 by Michael Hudson
a bit more LBYL
477
        remote_pk = self._copy_single_file(
478
            sftp, self.local_pk, remote_ec2_dir)
9397.2.21 by Michael Hudson
even even even more small fixes
479
        remote_cert = self._copy_single_file(
9397.2.15 by Michael Hudson
a bit more LBYL
480
            sftp, self.local_cert, remote_ec2_dir)
481
        return (remote_pk, remote_cert)
482
483
    def _check_single_glob_match(self, local_dir, pattern, file_kind):
9397.2.26 by Michael Hudson
docstrings!
484
        """Check that `pattern` matches one file in `local_dir` and return it.
485
486
        :param local_dir: The local directory to look in.
487
        :param pattern: The glob patten to match.
488
        :param file_kind: The sort of file we're looking for, to be used in
489
            error messages.
490
        """
9397.2.15 by Michael Hudson
a bit more LBYL
491
        pattern = os.path.join(local_dir, pattern)
492
        matches = glob.glob(pattern)
493
        if len(matches) != 1:
9453.2.22 by Michael Hudson
kill error_and_quit
494
            raise BzrCommandError(
9397.2.15 by Michael Hudson
a bit more LBYL
495
                '%r must match a single %s file' % (pattern, file_kind))
496
        return matches[0]
497
11962.1.1 by Gavin Panella
Create an S3 bucket for the given AMI name in check_bundling_prerequisites().
498
    def check_bundling_prerequisites(self, name, credentials):
9397.2.15 by Michael Hudson
a bit more LBYL
499
        """Check, as best we can, that all the files we need to bundle exist.
500
        """
11699.1.1 by Gary Poster
make update-image work without hiccups on lucid.
501
        if subprocess.call(['which', 'ec2-register']):
502
            raise BzrCommandError(
503
                '`ec2-register` command not found.  '
504
                'Try `sudo apt-get install ec2-api-tools`.')
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
505
        local_ec2_dir = os.path.expanduser('~/.ec2')
9397.2.15 by Michael Hudson
a bit more LBYL
506
        if not os.path.exists(local_ec2_dir):
9453.2.22 by Michael Hudson
kill error_and_quit
507
            raise BzrCommandError(
9397.2.15 by Michael Hudson
a bit more LBYL
508
                "~/.ec2 must exist and contain aws_user, aws_id, a private "
509
                "key file and a certificate.")
510
        aws_user_file = os.path.expanduser('~/.ec2/aws_user')
511
        if not os.path.exists(aws_user_file):
9453.2.22 by Michael Hudson
kill error_and_quit
512
            raise BzrCommandError(
9397.2.15 by Michael Hudson
a bit more LBYL
513
                "~/.ec2/aws_user must exist and contain your numeric AWS id.")
514
        self.aws_user = open(aws_user_file).read().strip()
515
        self.local_cert = self._check_single_glob_match(
516
            local_ec2_dir, 'cert-*.pem', 'certificate')
517
        self.local_pk = self._check_single_glob_match(
518
            local_ec2_dir, 'pk-*.pem', 'private key')
11962.1.1 by Gavin Panella
Create an S3 bucket for the given AMI name in check_bundling_prerequisites().
519
        # The bucket `name` needs to exist and be accessible. We create it
520
        # here to reserve the name. If the bucket already exists and conforms
521
        # to the above requirements, this is a no-op.
522
        credentials.connect_s3().create_bucket(name)
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
523
9397.2.25 by Michael Hudson
act on comments from jml
524
    def bundle(self, name, credentials):
9397.2.26 by Michael Hudson
docstrings!
525
        """Bundle, upload and register the instance as a new AMI.
526
527
        :param name: The name-to-be of the new AMI.
528
        :param credentials: An `EC2Credentials` object.
529
        """
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
530
        connection = self.connect()
11699.1.1 by Gary Poster
make update-image work without hiccups on lucid.
531
        # See http://is.gd/g1MIT .  When we switch to Eucalyptus, we can dump
532
        # this installation of the ec2-ami-tools.
533
        connection.perform(
534
            'sudo env DEBIAN_FRONTEND=noninteractive '
535
            'apt-get -y  install ec2-ami-tools')
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
536
        connection.perform('rm -f .ssh/authorized_keys')
537
        connection.perform('sudo mkdir /mnt/ec2')
538
        connection.perform('sudo chown $USER:$USER /mnt/ec2')
9550.13.34 by Michael Hudson
make using sftp more convenient
539
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
540
        remote_pk, remote_cert = self.copy_key_and_certificate_to_image(
9550.13.34 by Michael Hudson
make using sftp more convenient
541
            connection.sftp)
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
542
543
        bundle_dir = os.path.join('/mnt', name)
544
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
545
        connection.perform('sudo mkdir ' + bundle_dir)
546
        connection.perform(' '.join([
9550.13.2 by Michael Hudson
move towards using a pre-setup account
547
            'sudo ec2-bundle-vol',
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
548
            '-d %s' % bundle_dir,
9550.13.4 by Michael Hudson
fix
549
            '--batch',   # Set batch-mode, which doesn't use prompts.
9397.2.4 by Michael Hudson
this might work, probably not though...
550
            '-k %s' % remote_pk,
551
            '-c %s' % remote_cert,
9397.2.15 by Michael Hudson
a bit more LBYL
552
            '-u %s' % self.aws_user,
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
553
            ]))
554
555
        # Assume that the manifest is 'image.manifest.xml', since "image" is
556
        # the default prefix.
557
        manifest = os.path.join(bundle_dir, 'image.manifest.xml')
558
559
        # Best check that the manifest actually exists though.
560
        test = 'test -f %s' % manifest
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
561
        connection.perform(test)
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
562
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
563
        connection.perform(' '.join([
9550.13.2 by Michael Hudson
move towards using a pre-setup account
564
            'sudo ec2-upload-bundle',
9397.2.10 by Michael Hudson
grr v2
565
            '-b %s' % name,
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
566
            '-m %s' % manifest,
9397.2.25 by Michael Hudson
act on comments from jml
567
            '-a %s' % credentials.identifier,
568
            '-s %s' % credentials.secret,
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
569
            ]))
570
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
571
        connection.close()
9397.2.2 by Michael Hudson
copy paste stuff from ec2-generate-windmill-image.py
572
9397.2.4 by Michael Hudson
this might work, probably not though...
573
        # This is invoked locally.
574
        mfilename = os.path.basename(manifest)
9397.2.10 by Michael Hudson
grr v2
575
        manifest_path = os.path.join(name, mfilename)
9397.2.4 by Michael Hudson
this might work, probably not though...
576
577
        env = os.environ.copy()
578
        if 'JAVA_HOME' not in os.environ:
579
            env['JAVA_HOME'] = '/usr/lib/jvm/default-java'
580
        cmd = [
581
            'ec2-register',
9397.2.15 by Michael Hudson
a bit more LBYL
582
            '--private-key=%s' % self.local_pk,
583
            '--cert=%s' % self.local_cert,
10554.1.2 by Jonathan Lange
Add the --name option, which seems to be required
584
            '--name=%s' % (name,),
9636.1.1 by Jonathan Lange
Clean up PEP 8 violations.
585
            manifest_path,
9397.2.4 by Michael Hudson
this might work, probably not though...
586
            ]
587
        self.log("Executing command: %s" % ' '.join(cmd))
588
        subprocess.check_call(cmd, env=env)
589
590
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
591
class EC2InstanceConnection:
592
    """An ssh connection to an `EC2Instance`."""
593
594
    def __init__(self, instance, username, ssh):
9550.13.34 by Michael Hudson
make using sftp more convenient
595
        self._instance = instance
596
        self._username = username
597
        self._ssh = ssh
598
        self._sftp = None
599
600
    @property
601
    def sftp(self):
602
        if self._sftp is None:
603
            self._sftp = self._ssh.open_sftp()
604
        return self._sftp
9389.6.3 by Michael Hudson
give EC2Instance to its own file
605
9636.1.4 by Jonathan Lange
Slightly clearer stdout / stderr behaviour.
606
    def perform(self, cmd, ignore_failure=False, out=None, err=None):
9389.6.3 by Michael Hudson
give EC2Instance to its own file
607
        """Perform 'cmd' on server.
608
609
        :param ignore_failure: If False, raise an error on non-zero exit
610
            statuses.
611
        :param out: A stream to write the output of the remote command to.
9636.1.4 by Jonathan Lange
Slightly clearer stdout / stderr behaviour.
612
        :param err: A stream to write the error of the remote command to.
9389.6.3 by Michael Hudson
give EC2Instance to its own file
613
        """
9636.1.4 by Jonathan Lange
Slightly clearer stdout / stderr behaviour.
614
        if out is None:
615
            out = sys.stdout
616
        if err is None:
617
            err = sys.stderr
9550.13.34 by Michael Hudson
make using sftp more convenient
618
        self._instance.log(
619
            '%s@%s$ %s\n'
620
            % (self._username, self._instance._boto_instance.id, cmd))
621
        session = self._ssh.get_transport().open_session()
9389.6.3 by Michael Hudson
give EC2Instance to its own file
622
        session.exec_command(cmd)
623
        session.shutdown_write()
624
        while 1:
625
            select.select([session], [], [], 0.5)
626
            if session.recv_ready():
627
                data = session.recv(4096)
628
                if data:
9636.1.4 by Jonathan Lange
Slightly clearer stdout / stderr behaviour.
629
                    out.write(data)
630
                    out.flush()
9389.6.3 by Michael Hudson
give EC2Instance to its own file
631
            if session.recv_stderr_ready():
632
                data = session.recv_stderr(4096)
633
                if data:
9636.1.4 by Jonathan Lange
Slightly clearer stdout / stderr behaviour.
634
                    err.write(data)
635
                    err.flush()
9389.6.3 by Michael Hudson
give EC2Instance to its own file
636
            if session.exit_status_ready():
637
                break
638
        session.close()
639
        # XXX: JonathanLange 2009-05-31: If the command is killed by a signal
640
        # on the remote server, the SSH protocol does not send an exit_status,
641
        # it instead sends a different message with the number of the signal
642
        # that killed the process. AIUI, this code will fail confusingly if
643
        # that happens.
644
        res = session.recv_exit_status()
645
        if res and not ignore_failure:
646
            raise RuntimeError('Command failed: %s' % (cmd,))
647
        return res
648
649
    def run_with_ssh_agent(self, cmd, ignore_failure=False):
650
        """Run 'cmd' in a subprocess.
651
652
        Use this to run commands that require local SSH credentials. For
653
        example, getting private branches from Launchpad.
654
        """
9550.13.34 by Michael Hudson
make using sftp more convenient
655
        self._instance.log(
9550.13.29 by Michael Hudson
some more, some less genericity, make the remote user called ec2test
656
            '%s@%s$ %s\n'
9550.13.34 by Michael Hudson
make using sftp more convenient
657
            % (self._username, self._instance._boto_instance.id, cmd))
658
        call = ['ssh', '-A', self._username + '@' + self._instance.hostname,
9389.6.3 by Michael Hudson
give EC2Instance to its own file
659
               '-o', 'CheckHostIP no',
660
               '-o', 'StrictHostKeyChecking no',
661
               '-o', 'UserKnownHostsFile ~/.ec2/known_hosts',
662
               cmd]
663
        res = subprocess.call(call)
664
        if res and not ignore_failure:
665
            raise RuntimeError('Command failed: %s' % (cmd,))
666
        return res
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
667
10271.3.1 by Michael Hudson
maybe this is all?
668
    def run_script(self, script_text, sudo=False):
9550.13.45 by Michael Hudson
bit more
669
        """Upload `script_text` to the instance and run it with bash."""
9550.13.37 by Michael Hudson
more streamlining, possibly with more correctness too
670
        script = self.sftp.open('script.sh', 'w')
671
        script.write(script_text)
672
        script.close()
10271.3.1 by Michael Hudson
maybe this is all?
673
        cmd = '/bin/bash script.sh'
674
        if sudo:
675
            cmd = 'sudo ' + cmd
676
        self.run_with_ssh_agent(cmd)
9550.13.37 by Michael Hudson
more streamlining, possibly with more correctness too
677
        # At least for mwhudson, the paramiko connection often drops while the
678
        # script is running.  Reconnect just in case.
9550.13.45 by Michael Hudson
bit more
679
        self.reconnect()
680
        self.perform('rm script.sh')
681
682
    def reconnect(self):
683
        """Close the connection and reopen it."""
9550.13.37 by Michael Hudson
more streamlining, possibly with more correctness too
684
        self.close()
685
        self._ssh = self._instance._connect(self._username)._ssh
686
9397.1.3 by Michael Hudson
resolve one of Jono's XXXs: make EC2Instance less stateful
687
    def close(self):
9550.13.34 by Michael Hudson
make using sftp more convenient
688
        if self._sftp is not None:
689
            self._sftp.close()
690
            self._sftp = None
691
        self._ssh.close()
692
        self._ssh = None