Merge lp:~mwhudson/launchpad/ami-from-scratch into lp:launchpad
- ami-from-scratch
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | not available | ||||
Proposed branch: | lp:~mwhudson/launchpad/ami-from-scratch | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
878 lines 5 files modified
lib/devscripts/ec2test/builtins.py (+23/-17) lib/devscripts/ec2test/instance.py (+252/-121) lib/devscripts/ec2test/sshconfig.py (+0/-95) lib/devscripts/ec2test/testrunner.py (+15/-23) utilities/update-sourcecode (+4/-2) |
||||
To merge this branch: | bzr merge lp:~mwhudson/launchpad/ami-from-scratch | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Lange (community) | Approve | ||
Review via email: mp+12331@code.launchpad.net |
Commit message
Description of the change
Michael Hudson-Doyle (mwhudson) wrote : | # |
Jonathan Lange (jml) wrote : | # |
On Thu, Sep 24, 2009 at 8:30 AM, Michael Hudson
<email address hidden> wrote:
> Michael Hudson has proposed merging lp:~mwhudson/launchpad/ami-from-scratch into lp:launchpad/devel.
>
> Requested reviews:
> Jonathan Lange (jml)
>
>
> Hi jml,
>
> I hit back with an unsolicited ec2 branch :-)
>
Wuu.
> This one started with thinking about preparing an image for public consumption. I don't really know what files are lurking in unexpected places on the ec2test instances we currently use, so I started to think about building images from a blank hardy image such as those on alestic.com. Eventually, I came up with this approach, which hides all the setup of an instance in the EC2Instance object, and gives a way of saying that an image will come up 'blank' and needs completely setting up. It also changes to assuming a sudo account exists on a pre set-up image rather than creating a user on each run.
>
By "pre set-up image", you mean these "blank" images, right?
> As a side effect, you could say 'ec2 test -m based-on:<id of some blank karmic ami>' to run all the tests in a karmic image, which is nice.
>
That *is* nice.
> I made a couple of images with --public and got wgrant to test them (successfully). I shouldn't make the image public until this branch is about ready to land as while the trunk ec2 script works with the new image, it's rather slow (lots and lots of stuff gets rsynced off devpad).
>
Why will the public image be slow while trunk ec2 works with the new
image. I feel like I'm missing a couple of steps.
> Because I spent so long going back and forth on ways to do this, a fair bit of drive by clean up happened en route. I hope it doesn't muddle things too much.
>
I'm sure I'll appreciate the drive-by cleanup.
...
OK, I'm done reviewing the branch. I've got a lot of questions,
because there's a lot I don't know. Overall the branch looks good, the
code seems much more solid, despite the fact that you're adding
functionality.
I would, however, like to see your answers before the branch lands.
> Cheers,
> mwh
> --
> https:/
> You are requested to review the proposed merge of lp:~mwhudson/launchpad/ami-from-scratch into lp:launchpad/devel.
>
> === modified file 'lib/devscripts
> --- lib/devscripts/
> +++ lib/devscripts/
> @@ -95,6 +95,7 @@
> 'changes in your download cache, you must explicitly choose to '
> 'include or ignore the changes.'))
>
> +
> postmortem_option = Option(
> 'postmortem', short_name='p',
> help=('Drop to interactive prompt after the test and before shutting '
> @@ -308,7 +309,7 @@
> postmortem = True
> shutdown = True
> instance.
> - postmortem, shutdown, self.run_server, runner,
> + postmortem, shutdown, self.run_server, runner, instance,
> demo_network_
>
"instance" is a parameter of set_up_and_run? That's a bit weird. I wonder...
So I still don't know the motivation for this change, but I wonder if
set_u...
Jonathan Lange (jml) wrote : | # |
As per previous comment.
Michael Hudson-Doyle (mwhudson) wrote : | # |
Jonathan Lange wrote:
> On Thu, Sep 24, 2009 at 8:30 AM, Michael Hudson
> <email address hidden> wrote:
>> Michael Hudson has proposed merging lp:~mwhudson/launchpad/ami-from-scratch into lp:launchpad/devel.
>>
>> Requested reviews:
>> Jonathan Lange (jml)
>>
>>
>> Hi jml,
>>
>> I hit back with an unsolicited ec2 branch :-)
>>
>
> Wuu.
>
>> This one started with thinking about preparing an image for public consumption. I don't really know what files are lurking in unexpected places on the ec2test instances we currently use, so I started to think about building images from a blank hardy image such as those on alestic.com. Eventually, I came up with this approach, which hides all the setup of an instance in the EC2Instance object, and gives a way of saying that an image will come up 'blank' and needs completely setting up. It also changes to assuming a sudo account exists on a pre set-up image rather than creating a user on each run.
>>
>
> By "pre set-up image", you mean these "blank" images, right?
No, I mean an image that has been "prepared", as in, "not from scratch".
>> As a side effect, you could say 'ec2 test -m based-on:<id of some blank karmic ami>' to run all the tests in a karmic image, which is nice.
>>
>
> That *is* nice.
>
>> I made a couple of images with --public and got wgrant to test them (successfully). I shouldn't make the image public until this branch is about ready to land as while the trunk ec2 script works with the new image, it's rather slow (lots and lots of stuff gets rsynced off devpad).
>>
>
> Why will the public image be slow while trunk ec2 works with the new
> image. I feel like I'm missing a couple of steps.
Mostly because there is a lot of crap in
/code/rocketfue
sourcedeps.conf and so the rsync that the trunk ec2 test takes ages.
>> Because I spent so long going back and forth on ways to do this, a fair bit of drive by clean up happened en route. I hope it doesn't muddle things too much.
>>
>
> I'm sure I'll appreciate the drive-by cleanup.
>
> ...
>
> OK, I'm done reviewing the branch. I've got a lot of questions,
> because there's a lot I don't know. Overall the branch looks good, the
> code seems much more solid, despite the fact that you're adding
> functionality.
Yeah, I'm happy with the changes overall.
> I would, however, like to see your answers before the branch lands.
OK.
>> Cheers,
>> mwh
>> --
>> https:/
>> You are requested to review the proposed merge of lp:~mwhudson/launchpad/ami-from-scratch into lp:launchpad/devel.
>>
>> === modified file 'lib/devscripts
>> --- lib/devscripts/
>> +++ lib/devscripts/
>> @@ -95,6 +95,7 @@
>> 'changes in your download cache, you must explicitly choose to '
>> 'include or ignore the changes.'))
>>
>> +
>> postmortem_option = Option(
>> 'postmortem', short_name='p',
>> help=('Drop to interactive prompt after the test and before shutting '
>> @@ -308,7 +309,7 @@
>> po...
Jonathan Lange (jml) wrote : | # |
Thanks. Can you send me the interdiff of your changes when they're done?
One day, Launchpad will do this for us...
jml
Michael Hudson-Doyle (mwhudson) wrote : | # |
Michael Hudson wrote:
>>> user_connection
>>> 'bzr pull -d /var/launchpad/
>>> + user_connection
>>> + "/var/launchpad
>>> + "/var/launchpad
>>> + if public:
>>> + user_connection
>>> + 'rm -rf /var/launchpad/
>>> + '/var/launchpad
>>> + user_connection
>>> + 'rm -rf .ssh/known_hosts .bazaar .bzr.log')
>> This duplicates knowledge from update-sourcecode. Could we perhaps
>> instead change update-sourcecode to have an option to not even try to
>> get the private branches, and then remove this removal logic?
>>
>> Actually, this would be a worthwhile change even for day-to-day use of
>> update-sourcecode.
>
> Hm, yeah, that would be more reliable. I'll change that.
I did this in a separate branch, and got Tim to review it. So this
change has happened, but mostly isn't in the interdiff I'm about to
attach to this mail.
Cheers,
mwh
1 | === modified file 'lib/devscripts/ec2test/builtins.py' |
2 | --- lib/devscripts/ec2test/builtins.py 2009-09-24 07:13:28 +0000 |
3 | +++ lib/devscripts/ec2test/builtins.py 2009-09-25 03:14:09 +0000 |
4 | @@ -402,16 +402,16 @@ |
5 | # trunk contains a update-sourcecode script that works in a non-built |
6 | # tree, do this. This should be changed before landing. |
7 | user_connection.run_with_ssh_agent( |
8 | - 'bzr pull -d /var/launchpad/test lp:~mwhudson/launchpad/no-more-devpad-ssh') |
9 | + 'bzr pull -d /var/launchpad/test lp:~mwhudson/launchpad/ami-from-scratch') |
10 | user_connection.run_with_ssh_agent( |
11 | 'bzr pull -d /var/launchpad/download-cache lp:lp-source-dependencies') |
12 | + if public: |
13 | + update_sourcecode_options = '--public-only' |
14 | + else: |
15 | + update_sourcecode_options = '' |
16 | user_connection.run_with_ssh_agent( |
17 | "/var/launchpad/test/utilities/update-sourcecode " |
18 | - "/var/launchpad/sourcecode") |
19 | - if public: |
20 | - user_connection.perform( |
21 | - 'rm -rf /var/launchpad/sourcecode/shipit ' |
22 | - '/var/launchpad/sourcecode/canonical-identity-provider') |
23 | + "/var/launchpad/sourcecode" + update_sourcecode_options) |
24 | user_connection.perform( |
25 | 'rm -rf .ssh/known_hosts .bazaar .bzr.log') |
26 | user_connection.close() |
27 | |
28 | === modified file 'lib/devscripts/ec2test/instance.py' |
29 | --- lib/devscripts/ec2test/instance.py 2009-09-24 07:12:14 +0000 |
30 | +++ lib/devscripts/ec2test/instance.py 2009-09-25 03:09:22 +0000 |
31 | @@ -60,6 +60,9 @@ |
32 | # root and another that should be run as the 'ec2test' user. |
33 | |
34 | from_scratch_root = """ |
35 | +# From 'help set': |
36 | +# -x Print commands and their arguments as they are executed. |
37 | +# -e Exit immediately if a command exits with a non-zero status. |
38 | set -xe |
39 | |
40 | sed -ie 's/main universe/main universe multiverse/' /etc/apt/sources.list |
41 | @@ -74,7 +77,7 @@ |
42 | |
43 | # This next part is cribbed from rocketfuel-setup |
44 | dev_host() { |
45 | - sed -i 's/^127.0.0.88.*$/&\ ${hostname}/' /etc/hosts |
46 | + sed -i \"s/^127.0.0.88.*$/&\ ${hostname}/\" /etc/hosts |
47 | } |
48 | |
49 | echo 'Adding development hosts on local machine' |
50 | @@ -142,6 +145,9 @@ |
51 | |
52 | |
53 | from_scratch_ec2test = """ |
54 | +# From 'help set': |
55 | +# -x Print commands and their arguments as they are executed. |
56 | +# -e Exit immediately if a command exits with a non-zero status. |
57 | set -xe |
58 | |
59 | bzr launchpad-login %(launchpad-login)s |
60 | @@ -178,7 +184,7 @@ |
61 | |
62 | # We call this here so that it has a chance to complain before the |
63 | # instance is started (which can take some time). |
64 | - get_user_key() |
65 | + user_key = get_user_key() |
66 | |
67 | if credentials is None: |
68 | credentials = EC2Credentials.load_from_file() |
69 | @@ -213,10 +219,10 @@ |
70 | |
71 | return EC2Instance( |
72 | name, image, instance_type, demo_networks, account, vals, |
73 | - from_scratch) |
74 | + from_scratch, user_key) |
75 | |
76 | def __init__(self, name, image, instance_type, demo_networks, account, |
77 | - vals, from_scratch): |
78 | + vals, from_scratch, user_key): |
79 | self._name = name |
80 | self._image = image |
81 | self._account = account |
82 | @@ -225,6 +231,7 @@ |
83 | self._boto_instance = None |
84 | self._vals = vals |
85 | self._from_scratch = from_scratch |
86 | + self._user_key = user_key |
87 | |
88 | def log(self, msg): |
89 | """Log a message on stdout, flushing afterwards.""" |
90 | @@ -306,13 +313,42 @@ |
91 | return EC2InstanceConnection(self, username, ssh) |
92 | |
93 | def _upload_local_key(self, conn, remote_filename): |
94 | - """ """ |
95 | - user_key = get_user_key() |
96 | + """Upload a key from the local user's agent to `remote_filename`. |
97 | + |
98 | + The key will be uploaded in a format suitable for |
99 | + ~/.ssh/authorized_keys. |
100 | + """ |
101 | authorized_keys_file = conn.sftp.open(remote_filename, 'w') |
102 | authorized_keys_file.write( |
103 | - "%s %s\n" % (user_key.get_name(), user_key.get_base64())) |
104 | + "%s %s\n" % (self._user_key.get_name(), self._user_key.get_base64())) |
105 | authorized_keys_file.close() |
106 | |
107 | + def _ensure_ec2test_user_has_keys(self, connection=None): |
108 | + """Make sure that we can connect over ssh as the 'ec2test' user. |
109 | + |
110 | + We add both the key that was used to start the instance (so |
111 | + _connect('ec2test') works and a key from the locally running ssh agent |
112 | + (so EC2InstanceConnection.run_with_ssh_agent works). |
113 | + """ |
114 | + if not self._ec2test_user_has_keys: |
115 | + if connection is None: |
116 | + connection = self._connect('root') |
117 | + our_connection = True |
118 | + else: |
119 | + our_connection = False |
120 | + self._upload_local_key(connection, 'local_key') |
121 | + connection.perform( |
122 | + 'cat /root/.ssh/authorized_keys local_key ' |
123 | + '> /home/ec2test/.ssh/authorized_keys && rm local_key') |
124 | + connection.perform('chown -R ec2test:ec2test /home/ec2test/') |
125 | + connection.perform('chmod 644 /home/ec2test/.ssh/*') |
126 | + if our_connection: |
127 | + connection.close() |
128 | + self.log( |
129 | + 'You can now use ssh -A ec2test@%s to log in the instance.\n' % |
130 | + self.hostname) |
131 | + self._ec2test_user_has_keys = True |
132 | + |
133 | def connect(self): |
134 | """Connect to the instance as a user with passwordless sudo. |
135 | |
136 | @@ -326,27 +362,14 @@ |
137 | root_connection.perform( |
138 | 'cat local_key >> ~/.ssh/authorized_keys && rm local_key') |
139 | root_connection.run_script(from_scratch_root % self._vals) |
140 | - root_connection.close() |
141 | - # Don't set self._from_scratch to True yet, we haven't run |
142 | - # from_scratch_ec2test yet! |
143 | - if not self._ec2test_user_has_keys: |
144 | - root_connection = self._connect('root') |
145 | - self._upload_local_key(root_connection, 'local_key') |
146 | - root_connection.perform( |
147 | - 'cat /root/.ssh/authorized_keys local_key ' |
148 | - '> /home/ec2test/.ssh/authorized_keys && rm local_key') |
149 | - root_connection.perform('chown -R ec2test:ec2test /home/ec2test/') |
150 | - root_connection.perform('chmod 644 /home/ec2test/.ssh/*') |
151 | - root_connection.close() |
152 | - self.log( |
153 | - 'You can now use ssh -A ec2test@%s to log in the instance.\n' % |
154 | - self.hostname) |
155 | - self._ec2test_user_has_keys = True |
156 | - conn = self._connect('ec2test') |
157 | - if self._from_scratch: |
158 | + self._ensure_ec2test_user_has_keys(root_connection) |
159 | + root_connection.close() |
160 | + conn = self._connect('ec2test') |
161 | conn.run_script(from_scratch_ec2test % self._vals) |
162 | self._from_scratch = False |
163 | - return conn |
164 | + return conn |
165 | + self._ensure_ec2test_user_has_keys() |
166 | + return self._connect('ec2test') |
167 | |
168 | def set_up_and_run(self, postmortem, shutdown, func, *args, **kw): |
169 | """Start, run `func` and then maybe shut down. |
170 | @@ -590,15 +613,20 @@ |
171 | return res |
172 | |
173 | def run_script(self, script_text): |
174 | + """Upload `script_text` to the instance and run it with bash.""" |
175 | script = self.sftp.open('script.sh', 'w') |
176 | script.write(script_text) |
177 | script.close() |
178 | self.run_with_ssh_agent('/bin/bash script.sh') |
179 | # At least for mwhudson, the paramiko connection often drops while the |
180 | # script is running. Reconnect just in case. |
181 | + self.reconnect() |
182 | + self.perform('rm script.sh') |
183 | + |
184 | + def reconnect(self): |
185 | + """Close the connection and reopen it.""" |
186 | self.close() |
187 | self._ssh = self._instance._connect(self._username)._ssh |
188 | - self.perform('rm script.sh') |
189 | |
190 | def close(self): |
191 | if self._sftp is not None: |
Preview Diff
1 | === modified file 'lib/devscripts/ec2test/builtins.py' |
2 | --- lib/devscripts/ec2test/builtins.py 2009-09-27 00:27:17 +0000 |
3 | +++ lib/devscripts/ec2test/builtins.py 2009-09-27 20:39:13 +0000 |
4 | @@ -95,6 +95,7 @@ |
5 | 'changes in your download cache, you must explicitly choose to ' |
6 | 'include or ignore the changes.')) |
7 | |
8 | + |
9 | postmortem_option = Option( |
10 | 'postmortem', short_name='p', |
11 | help=('Drop to interactive prompt after the test and before shutting ' |
12 | @@ -308,7 +309,7 @@ |
13 | postmortem = True |
14 | shutdown = True |
15 | instance.set_up_and_run( |
16 | - postmortem, shutdown, self.run_server, runner, |
17 | + postmortem, shutdown, self.run_server, runner, instance, |
18 | demo_network_string) |
19 | |
20 | def run_server(self, runner, instance, demo_network_string): |
21 | @@ -335,7 +336,6 @@ |
22 | "\n\n") |
23 | |
24 | |
25 | - |
26 | class cmd_update_image(EC2Command): |
27 | """Make a new AMI.""" |
28 | |
29 | @@ -349,12 +349,17 @@ |
30 | help=('Run this command (with an ssh agent) on the image before ' |
31 | 'running the default update steps. Can be passed more than ' |
32 | 'once, the commands will be run in the order specified.')), |
33 | + Option( |
34 | + 'public', |
35 | + help=('Remove proprietary code from the sourcecode directory ' |
36 | + 'before bundling.')), |
37 | ] |
38 | |
39 | takes_args = ['ami_name'] |
40 | |
41 | def run(self, ami_name, machine=None, instance_type='m1.large', |
42 | - debug=False, postmortem=False, extra_update_image_command=[]): |
43 | + debug=False, postmortem=False, extra_update_image_command=[], |
44 | + public=False): |
45 | if debug: |
46 | pdb.set_trace() |
47 | |
48 | @@ -367,19 +372,18 @@ |
49 | |
50 | instance.set_up_and_run( |
51 | postmortem, True, self.update_image, instance, |
52 | - extra_update_image_command, ami_name, credentials) |
53 | + extra_update_image_command, ami_name, credentials, public) |
54 | |
55 | def update_image(self, instance, extra_update_image_command, ami_name, |
56 | - credentials): |
57 | + credentials, public): |
58 | """Bring the image up to date. |
59 | |
60 | The steps we take are: |
61 | |
62 | * run any commands specified with --extra-update-image-command |
63 | - * update sourcecode via rsync. |
64 | + * update sourcecode |
65 | * update the launchpad branch to the tip of the trunk branch. |
66 | * update the copy of the download-cache. |
67 | - * remove the user account. |
68 | * bundle the image |
69 | |
70 | :param instance: `EC2Instance` to operate on. |
71 | @@ -387,25 +391,27 @@ |
72 | instance in addition to the usual ones. |
73 | :param ami_name: The name to give the created AMI. |
74 | :param credentials: An `EC2Credentials` object. |
75 | + :param public: If true, remove proprietary code from the sourcecode |
76 | + directory before bundling. |
77 | """ |
78 | - user_connection = instance.connect_as_user() |
79 | + user_connection = instance.connect() |
80 | user_connection.perform('bzr launchpad-login %(launchpad-login)s') |
81 | for cmd in extra_update_image_command: |
82 | user_connection.run_with_ssh_agent(cmd) |
83 | user_connection.run_with_ssh_agent( |
84 | - "rsync -avp --partial --delete " |
85 | - "--filter='P *.o' --filter='P *.pyc' --filter='P *.so' " |
86 | - "devpad.canonical.com:/code/rocketfuel-built/launchpad/sourcecode/* " |
87 | - "/var/launchpad/sourcecode/") |
88 | - user_connection.run_with_ssh_agent( |
89 | 'bzr pull -d /var/launchpad/test ' + TRUNK_BRANCH) |
90 | user_connection.run_with_ssh_agent( |
91 | 'bzr pull -d /var/launchpad/download-cache lp:lp-source-dependencies') |
92 | + if public: |
93 | + update_sourcecode_options = '--public-only' |
94 | + else: |
95 | + update_sourcecode_options = '' |
96 | + user_connection.run_with_ssh_agent( |
97 | + "/var/launchpad/test/utilities/update-sourcecode " |
98 | + "/var/launchpad/sourcecode" + update_sourcecode_options) |
99 | + user_connection.perform( |
100 | + 'rm -rf .ssh/known_hosts .bazaar .bzr.log') |
101 | user_connection.close() |
102 | - root_connection = instance.connect_as_root() |
103 | - root_connection.perform( |
104 | - 'deluser --remove-home %(USER)s', ignore_failure=True) |
105 | - root_connection.close() |
106 | instance.bundle(ami_name, credentials) |
107 | |
108 | |
109 | |
110 | === modified file 'lib/devscripts/ec2test/instance.py' |
111 | --- lib/devscripts/ec2test/instance.py 2009-09-27 00:27:17 +0000 |
112 | +++ lib/devscripts/ec2test/instance.py 2009-09-27 20:39:13 +0000 |
113 | @@ -23,7 +23,6 @@ |
114 | |
115 | import paramiko |
116 | |
117 | -from devscripts.ec2test.sshconfig import SSHConfig |
118 | from devscripts.ec2test.credentials import EC2Credentials |
119 | |
120 | |
121 | @@ -56,19 +55,126 @@ |
122 | return user_key |
123 | |
124 | |
125 | +# Commands to run to turn a blank image into one usable for the rest of the |
126 | +# ec2 functionality. They come in two parts, one set that need to be run as |
127 | +# root and another that should be run as the 'ec2test' user. |
128 | + |
129 | +from_scratch_root = """ |
130 | +# From 'help set': |
131 | +# -x Print commands and their arguments as they are executed. |
132 | +# -e Exit immediately if a command exits with a non-zero status. |
133 | +set -xe |
134 | + |
135 | +sed -ie 's/main universe/main universe multiverse/' /etc/apt/sources.list |
136 | + |
137 | +. /etc/lsb-release |
138 | + |
139 | +cat >> /etc/apt/sources.list << EOF |
140 | +deb http://ppa.launchpad.net/launchpad/ubuntu $DISTRIB_CODENAME main |
141 | +deb http://ppa.launchpad.net/bzr/ubuntu $DISTRIB_CODENAME main |
142 | +deb http://ppa.launchpad.net/bzr-beta-ppa/ubuntu $DISTRIB_CODENAME main |
143 | +EOF |
144 | + |
145 | +# This next part is cribbed from rocketfuel-setup |
146 | +dev_host() { |
147 | + sed -i \"s/^127.0.0.88.*$/&\ ${hostname}/\" /etc/hosts |
148 | +} |
149 | + |
150 | +echo 'Adding development hosts on local machine' |
151 | +echo ' |
152 | +# Launchpad virtual domains. This should be on one line. |
153 | +127.0.0.88 launchpad.dev |
154 | +' >> /etc/hosts |
155 | + |
156 | +declare -a hostnames |
157 | +hostnames=$(cat <<EOF |
158 | + answers.launchpad.dev |
159 | + api.launchpad.dev |
160 | + bazaar-internal.launchpad.dev |
161 | + beta.launchpad.dev |
162 | + blueprints.launchpad.dev |
163 | + bugs.launchpad.dev |
164 | + code.launchpad.dev |
165 | + feeds.launchpad.dev |
166 | + id.launchpad.dev |
167 | + keyserver.launchpad.dev |
168 | + lists.launchpad.dev |
169 | + openid.launchpad.dev |
170 | + ppa.launchpad.dev |
171 | + private-ppa.launchpad.dev |
172 | + shipit.edubuntu.dev |
173 | + shipit.kubuntu.dev |
174 | + shipit.ubuntu.dev |
175 | + translations.launchpad.dev |
176 | + xmlrpc-private.launchpad.dev |
177 | + xmlrpc.launchpad.dev |
178 | +EOF |
179 | + ) |
180 | + |
181 | +for hostname in $hostnames; do |
182 | + dev_host; |
183 | +done |
184 | + |
185 | +echo ' |
186 | +127.0.0.99 bazaar.launchpad.dev |
187 | +' >> /etc/hosts |
188 | + |
189 | +# Add the keys for the three PPAs added to sources.list above. |
190 | +apt-key adv --recv-keys --keyserver pool.sks-keyservers.net 2af499cb24ac5f65461405572d1ffb6c0a5174af |
191 | +apt-key adv --recv-keys --keyserver pool.sks-keyservers.net ece2800bacf028b31ee3657cd702bf6b8c6c1efd |
192 | +apt-key adv --recv-keys --keyserver pool.sks-keyservers.net cbede690576d1e4e813f6bb3ebaf723d37b19b80 |
193 | + |
194 | +aptitude update |
195 | +aptitude -y full-upgrade |
196 | + |
197 | +apt-get -y install launchpad-developer-dependencies apache2 apache2-mpm-worker |
198 | + |
199 | +# Creat the ec2test user, give them passwordless sudo. |
200 | +adduser --gecos "" --disabled-password ec2test |
201 | +echo 'ec2test\tALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers |
202 | + |
203 | +mkdir /home/ec2test/.ssh |
204 | +cat > /home/ec2test/.ssh/config << EOF |
205 | +CheckHostIP no |
206 | +StrictHostKeyChecking no |
207 | +EOF |
208 | + |
209 | +mkdir /var/launchpad |
210 | +chown -R ec2test:ec2test /var/www /var/launchpad /home/ec2test/ |
211 | +""" |
212 | + |
213 | + |
214 | +from_scratch_ec2test = """ |
215 | +# From 'help set': |
216 | +# -x Print commands and their arguments as they are executed. |
217 | +# -e Exit immediately if a command exits with a non-zero status. |
218 | +set -xe |
219 | + |
220 | +bzr launchpad-login %(launchpad-login)s |
221 | +bzr init-repo --2a /var/launchpad |
222 | +bzr branch lp:~launchpad-pqm/launchpad/devel /var/launchpad/test |
223 | +bzr branch --standalone lp:lp-source-dependencies /var/launchpad/download-cache |
224 | +mkdir /var/launchpad/sourcecode |
225 | +/var/launchpad/test/utilities/update-sourcecode /var/launchpad/sourcecode |
226 | +""" |
227 | + |
228 | + |
229 | class EC2Instance: |
230 | """A single EC2 instance.""" |
231 | |
232 | @classmethod |
233 | - def make(cls, name, instance_type, machine_id, |
234 | - demo_networks=None, credentials=None): |
235 | + def make(cls, name, instance_type, machine_id, demo_networks=None, |
236 | + credentials=None): |
237 | """Construct an `EC2Instance`. |
238 | |
239 | :param name: The name to use for the key pair and security group for |
240 | the instance. |
241 | :param instance_type: One of the AVAILABLE_INSTANCE_TYPES. |
242 | :param machine_id: The AMI to use, or None to do the usual regexp |
243 | - matching. |
244 | + matching. If you put 'based-on:' before the AMI id, it is assumed |
245 | + that the id specifies a blank image that should be made into one |
246 | + suitable for the other ec2 functions (see `from_scratch_root` and |
247 | + `from_scratch_ec2test` above). |
248 | :param demo_networks: A list of networks to add to the security group |
249 | to allow access to the instance. |
250 | :param credentials: An `EC2Credentials` object. |
251 | @@ -76,6 +182,10 @@ |
252 | if instance_type not in AVAILABLE_INSTANCE_TYPES: |
253 | raise ValueError('unknown instance_type %s' % (instance_type,)) |
254 | |
255 | + # We call this here so that it has a chance to complain before the |
256 | + # instance is started (which can take some time). |
257 | + user_key = get_user_key() |
258 | + |
259 | if credentials is None: |
260 | credentials = EC2Credentials.load_from_file() |
261 | |
262 | @@ -91,6 +201,12 @@ |
263 | # generate it. |
264 | account.delete_previous_key_pair() |
265 | |
266 | + if machine_id and machine_id.startswith('based-on:'): |
267 | + from_scratch = True |
268 | + machine_id = machine_id[len('based-on:'):] |
269 | + else: |
270 | + from_scratch = False |
271 | + |
272 | # get the image |
273 | image = account.acquire_image(machine_id) |
274 | |
275 | @@ -102,12 +218,11 @@ |
276 | vals['launchpad-login'] = login |
277 | |
278 | return EC2Instance( |
279 | - name, image, instance_type, demo_networks, account, vals) |
280 | - |
281 | - # XXX: JonathanLange 2009-05-31: Separate out demo server |
282 | + name, image, instance_type, demo_networks, account, vals, |
283 | + from_scratch, user_key) |
284 | |
285 | def __init__(self, name, image, instance_type, demo_networks, account, |
286 | - vals): |
287 | + vals, from_scratch, user_key): |
288 | self._name = name |
289 | self._image = image |
290 | self._account = account |
291 | @@ -115,6 +230,8 @@ |
292 | self._demo_networks = demo_networks |
293 | self._boto_instance = None |
294 | self._vals = vals |
295 | + self._from_scratch = from_scratch |
296 | + self._user_key = user_key |
297 | |
298 | def log(self, msg): |
299 | """Log a message on stdout, flushing afterwards.""" |
300 | @@ -148,6 +265,7 @@ |
301 | (elapsed // 60, elapsed % 60)) |
302 | self._output = self._boto_instance.get_console_output() |
303 | self.log(self._output.output) |
304 | + self._ec2test_user_has_keys = False |
305 | else: |
306 | raise BzrCommandError( |
307 | 'failed to start: %s\n' % self._boto_instance.state) |
308 | @@ -170,22 +288,21 @@ |
309 | return None |
310 | return self._boto_instance.public_dns_name |
311 | |
312 | - def _connect(self, user, use_agent): |
313 | + def _connect(self, username): |
314 | """Connect to the instance as `user`. """ |
315 | ssh = paramiko.SSHClient() |
316 | ssh.set_missing_host_key_policy(AcceptAllPolicy()) |
317 | - connect_args = {'username': user} |
318 | - if not use_agent: |
319 | - connect_args.update({ |
320 | - 'pkey': self.private_key, |
321 | - 'allow_agent': False, |
322 | - 'look_for_keys': False, |
323 | - }) |
324 | + connect_args = { |
325 | + 'username': username, |
326 | + 'pkey': self.private_key, |
327 | + 'allow_agent': False, |
328 | + 'look_for_keys': False, |
329 | + } |
330 | for count in range(10): |
331 | try: |
332 | ssh.connect(self.hostname, **connect_args) |
333 | except (socket.error, paramiko.AuthenticationException), e: |
334 | - self.log('_connect: %r' % (e,)) |
335 | + self.log('_connect: %r\n' % (e,)) |
336 | if count < 9: |
337 | time.sleep(5) |
338 | self.log('retrying...') |
339 | @@ -193,83 +310,72 @@ |
340 | raise |
341 | else: |
342 | break |
343 | - return EC2InstanceConnection(self, user, ssh) |
344 | - |
345 | - def connect_as_root(self): |
346 | - return self._connect('root', False) |
347 | - |
348 | - def connect_as_user(self): |
349 | - return self._connect(self._vals['USER'], True) |
350 | - |
351 | - def set_up_user(self, user_key): |
352 | - """Set up an account named after the local user.""" |
353 | - root_connection = self.connect_as_root() |
354 | - as_root = root_connection.perform |
355 | - if self._vals['USER'] == 'gary': |
356 | - # This helps gary debug problems others are having by removing |
357 | - # much of the initial setup used to work on the original image. |
358 | - as_root('deluser --remove-home gary', ignore_failure=True) |
359 | - # Let root perform sudo without a password. |
360 | - as_root('echo "root\tALL=NOPASSWD: ALL" >> /etc/sudoers') |
361 | - # Add the user. |
362 | - as_root('adduser --gecos "" --disabled-password %(USER)s') |
363 | - # Give user sudo without password. |
364 | - as_root('echo "%(USER)s\tALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers') |
365 | - # Update the system. |
366 | - as_root('aptitude update') |
367 | - as_root('aptitude -y full-upgrade') |
368 | - # Set up ssh for user |
369 | - # Make user's .ssh directory |
370 | - as_root('sudo -u %(USER)s mkdir /home/%(USER)s/.ssh') |
371 | - root_sftp = root_connection.ssh.open_sftp() |
372 | - remote_ssh_dir = '/home/%(USER)s/.ssh' % self._vals |
373 | - # Create config file |
374 | - self.log('Creating %s/config\n' % (remote_ssh_dir,)) |
375 | - ssh_config_file_name = os.path.join( |
376 | - self._vals['HOME'], '.ssh', 'config') |
377 | - ssh_config_source = open(ssh_config_file_name) |
378 | - config = SSHConfig() |
379 | - config.parse(ssh_config_source) |
380 | - ssh_config_source.close() |
381 | - ssh_config_dest = root_sftp.open("%s/config" % remote_ssh_dir, 'w') |
382 | - ssh_config_dest.write('CheckHostIP no\n') |
383 | - ssh_config_dest.write('StrictHostKeyChecking no\n') |
384 | - for hostname in ('devpad.canonical.com', 'chinstrap.canonical.com'): |
385 | - ssh_config_dest.write('Host %s\n' % (hostname,)) |
386 | - data = config.lookup(hostname) |
387 | - for key in ('hostname', 'gssapiauthentication', 'proxycommand', |
388 | - 'user', 'forwardagent'): |
389 | - value = data.get(key) |
390 | - if value is not None: |
391 | - ssh_config_dest.write(' %s %s\n' % (key, value)) |
392 | - ssh_config_dest.close() |
393 | - # create authorized_keys |
394 | - self.log('Setting up %s/authorized_keys\n' % remote_ssh_dir) |
395 | - authorized_keys_file = root_sftp.open( |
396 | - "%s/authorized_keys" % remote_ssh_dir, 'w') |
397 | + return EC2InstanceConnection(self, username, ssh) |
398 | + |
399 | + def _upload_local_key(self, conn, remote_filename): |
400 | + """Upload a key from the local user's agent to `remote_filename`. |
401 | + |
402 | + The key will be uploaded in a format suitable for |
403 | + ~/.ssh/authorized_keys. |
404 | + """ |
405 | + authorized_keys_file = conn.sftp.open(remote_filename, 'w') |
406 | authorized_keys_file.write( |
407 | - "%s %s\n" % (user_key.get_name(), user_key.get_base64())) |
408 | + "%s %s\n" % (self._user_key.get_name(), self._user_key.get_base64())) |
409 | authorized_keys_file.close() |
410 | - root_sftp.close() |
411 | - # Chown and chmod the .ssh directory and contents that we just |
412 | - # created. |
413 | - as_root('chown -R %(USER)s:%(USER)s /home/%(USER)s/') |
414 | - as_root('chmod 644 /home/%(USER)s/.ssh/*') |
415 | - self.log( |
416 | - 'You can now use ssh -A %s to log in the instance.\n' % |
417 | - self.hostname) |
418 | - # What follows is somewhat ec2test specfic. |
419 | - # give the user permission to do whatever in /var/www |
420 | - as_root('chown -R %(USER)s:%(USER)s /var/www') |
421 | - # Make /var/launchpad owned by user. |
422 | - as_root('chown -R %(USER)s:%(USER)s /var/launchpad') |
423 | - # Clean out left-overs from the instance image. |
424 | - as_root('rm -fr /var/tmp/*') |
425 | - root_connection.close() |
426 | + |
427 | + def _ensure_ec2test_user_has_keys(self, connection=None): |
428 | + """Make sure that we can connect over ssh as the 'ec2test' user. |
429 | + |
430 | + We add both the key that was used to start the instance (so |
431 | + _connect('ec2test') works and a key from the locally running ssh agent |
432 | + (so EC2InstanceConnection.run_with_ssh_agent works). |
433 | + """ |
434 | + if not self._ec2test_user_has_keys: |
435 | + if connection is None: |
436 | + connection = self._connect('root') |
437 | + our_connection = True |
438 | + else: |
439 | + our_connection = False |
440 | + self._upload_local_key(connection, 'local_key') |
441 | + connection.perform( |
442 | + 'cat /root/.ssh/authorized_keys local_key ' |
443 | + '> /home/ec2test/.ssh/authorized_keys && rm local_key') |
444 | + connection.perform('chown -R ec2test:ec2test /home/ec2test/') |
445 | + connection.perform('chmod 644 /home/ec2test/.ssh/*') |
446 | + if our_connection: |
447 | + connection.close() |
448 | + self.log( |
449 | + 'You can now use ssh -A ec2test@%s to log in the instance.\n' % |
450 | + self.hostname) |
451 | + self._ec2test_user_has_keys = True |
452 | + |
453 | + def connect(self): |
454 | + """Connect to the instance as a user with passwordless sudo. |
455 | + |
456 | + This may involve first connecting as root and adding SSH keys to the |
457 | + user's account, and in the case of a from scratch image, it will do a |
458 | + lot of set up. |
459 | + """ |
460 | + if self._from_scratch: |
461 | + root_connection = self._connect('root') |
462 | + self._upload_local_key(root_connection, 'local_key') |
463 | + root_connection.perform( |
464 | + 'cat local_key >> ~/.ssh/authorized_keys && rm local_key') |
465 | + root_connection.run_script(from_scratch_root % self._vals) |
466 | + self._ensure_ec2test_user_has_keys(root_connection) |
467 | + root_connection.close() |
468 | + conn = self._connect('ec2test') |
469 | + conn.run_script(from_scratch_ec2test % self._vals) |
470 | + self._from_scratch = False |
471 | + return conn |
472 | + self._ensure_ec2test_user_has_keys() |
473 | + return self._connect('ec2test') |
474 | |
475 | def set_up_and_run(self, postmortem, shutdown, func, *args, **kw): |
476 | - """Start, set up a user account, run `func` and then maybe shut down. |
477 | + """Start, run `func` and then maybe shut down. |
478 | |
479 | + :param config: A dictionary specifying details of how the instance |
480 | + should be run: |
481 | :param postmortem: If true, any exceptions will be caught and an |
482 | interactive session run to allow debugging the problem. |
483 | :param shutdown: If true, shut down the instance after `func` and |
484 | @@ -279,10 +385,8 @@ |
485 | :param args: Passed to `func`. |
486 | :param kw: Passed to `func`. |
487 | """ |
488 | - user_key = get_user_key() |
489 | - self.start() |
490 | try: |
491 | - self.set_up_user(user_key) |
492 | + self.start() |
493 | try: |
494 | return func(*args, **kw) |
495 | except Exception: |
496 | @@ -309,7 +413,6 @@ |
497 | finally: |
498 | if shutdown: |
499 | self.shutdown() |
500 | - |
501 | |
502 | def _copy_single_file(self, sftp, local_path, remote_dir): |
503 | """Copy `local_path` to `remote_dir` on this instance. |
504 | @@ -333,7 +436,6 @@ |
505 | :param sftp: A paramiko SFTP object. |
506 | """ |
507 | remote_ec2_dir = '/mnt/ec2' |
508 | - sftp.mkdir(remote_ec2_dir) |
509 | remote_pk = self._copy_single_file( |
510 | sftp, self.local_pk, remote_ec2_dir) |
511 | remote_cert = self._copy_single_file( |
512 | @@ -379,20 +481,21 @@ |
513 | :param name: The name-to-be of the new AMI. |
514 | :param credentials: An `EC2Credentials` object. |
515 | """ |
516 | - root_connection = self.connect_as_root() |
517 | - sftp = root_connection.ssh.open_sftp() |
518 | - |
519 | - remote_pk, remote_cert = self.copy_key_and_certificate_to_image(sftp) |
520 | - |
521 | - sftp.close() |
522 | + connection = self.connect() |
523 | + connection.perform('rm -f .ssh/authorized_keys') |
524 | + connection.perform('sudo mkdir /mnt/ec2') |
525 | + connection.perform('sudo chown $USER:$USER /mnt/ec2') |
526 | + |
527 | + remote_pk, remote_cert = self.copy_key_and_certificate_to_image( |
528 | + connection.sftp) |
529 | |
530 | bundle_dir = os.path.join('/mnt', name) |
531 | |
532 | - root_connection.perform('mkdir ' + bundle_dir) |
533 | - root_connection.perform(' '.join([ |
534 | - 'ec2-bundle-vol', |
535 | + connection.perform('sudo mkdir ' + bundle_dir) |
536 | + connection.perform(' '.join([ |
537 | + 'sudo ec2-bundle-vol', |
538 | '-d %s' % bundle_dir, |
539 | - '-b', # Set batch-mode, which doesn't use prompts. |
540 | + '--batch', # Set batch-mode, which doesn't use prompts. |
541 | '-k %s' % remote_pk, |
542 | '-c %s' % remote_cert, |
543 | '-u %s' % self.aws_user, |
544 | @@ -404,18 +507,17 @@ |
545 | |
546 | # Best check that the manifest actually exists though. |
547 | test = 'test -f %s' % manifest |
548 | - root_connection.perform(test) |
549 | + connection.perform(test) |
550 | |
551 | - root_connection.perform(' '.join([ |
552 | - 'ec2-upload-bundle', |
553 | + connection.perform(' '.join([ |
554 | + 'sudo ec2-upload-bundle', |
555 | '-b %s' % name, |
556 | '-m %s' % manifest, |
557 | '-a %s' % credentials.identifier, |
558 | '-s %s' % credentials.secret, |
559 | ])) |
560 | |
561 | - sftp.close() |
562 | - root_connection.close() |
563 | + connection.close() |
564 | |
565 | # This is invoked locally. |
566 | mfilename = os.path.basename(manifest) |
567 | @@ -438,9 +540,16 @@ |
568 | """An ssh connection to an `EC2Instance`.""" |
569 | |
570 | def __init__(self, instance, username, ssh): |
571 | - self.instance = instance |
572 | - self.username = username |
573 | - self.ssh = ssh |
574 | + self._instance = instance |
575 | + self._username = username |
576 | + self._ssh = ssh |
577 | + self._sftp = None |
578 | + |
579 | + @property |
580 | + def sftp(self): |
581 | + if self._sftp is None: |
582 | + self._sftp = self._ssh.open_sftp() |
583 | + return self._sftp |
584 | |
585 | def perform(self, cmd, ignore_failure=False, out=None): |
586 | """Perform 'cmd' on server. |
587 | @@ -449,10 +558,11 @@ |
588 | statuses. |
589 | :param out: A stream to write the output of the remote command to. |
590 | """ |
591 | - cmd = cmd % self.instance._vals |
592 | - self.instance.log( |
593 | - '%s@%s$ %s\n' % (self.username, self.instance._boto_instance.id, cmd)) |
594 | - session = self.ssh.get_transport().open_session() |
595 | + cmd = cmd % self._instance._vals |
596 | + self._instance.log( |
597 | + '%s@%s$ %s\n' |
598 | + % (self._username, self._instance._boto_instance.id, cmd)) |
599 | + session = self._ssh.get_transport().open_session() |
600 | session.exec_command(cmd) |
601 | session.shutdown_write() |
602 | while 1: |
603 | @@ -488,9 +598,11 @@ |
604 | Use this to run commands that require local SSH credentials. For |
605 | example, getting private branches from Launchpad. |
606 | """ |
607 | - cmd = cmd % self.instance._vals |
608 | - self.instance.log('%s@%s$ %s\n' % (self.username, self.instance._boto_instance.id, cmd)) |
609 | - call = ['ssh', '-A', self.instance.hostname, |
610 | + cmd = cmd % self._instance._vals |
611 | + self._instance.log( |
612 | + '%s@%s$ %s\n' |
613 | + % (self._username, self._instance._boto_instance.id, cmd)) |
614 | + call = ['ssh', '-A', self._username + '@' + self._instance.hostname, |
615 | '-o', 'CheckHostIP no', |
616 | '-o', 'StrictHostKeyChecking no', |
617 | '-o', 'UserKnownHostsFile ~/.ec2/known_hosts', |
618 | @@ -500,6 +612,25 @@ |
619 | raise RuntimeError('Command failed: %s' % (cmd,)) |
620 | return res |
621 | |
622 | + def run_script(self, script_text): |
623 | + """Upload `script_text` to the instance and run it with bash.""" |
624 | + script = self.sftp.open('script.sh', 'w') |
625 | + script.write(script_text) |
626 | + script.close() |
627 | + self.run_with_ssh_agent('/bin/bash script.sh') |
628 | + # At least for mwhudson, the paramiko connection often drops while the |
629 | + # script is running. Reconnect just in case. |
630 | + self.reconnect() |
631 | + self.perform('rm script.sh') |
632 | + |
633 | + def reconnect(self): |
634 | + """Close the connection and reopen it.""" |
635 | + self.close() |
636 | + self._ssh = self._instance._connect(self._username)._ssh |
637 | + |
638 | def close(self): |
639 | - self.ssh.close() |
640 | - self.ssh = None |
641 | + if self._sftp is not None: |
642 | + self._sftp.close() |
643 | + self._sftp = None |
644 | + self._ssh.close() |
645 | + self._ssh = None |
646 | |
647 | === removed file 'lib/devscripts/ec2test/sshconfig.py' |
648 | --- lib/devscripts/ec2test/sshconfig.py 2009-09-27 01:51:50 +0000 |
649 | +++ lib/devscripts/ec2test/sshconfig.py 1970-01-01 00:00:00 +0000 |
650 | @@ -1,95 +0,0 @@ |
651 | -############################################################################# |
652 | -# Modified from paramiko.config. The change should be pushed upstream. |
653 | -# Our fork supports Host lines with more than one host. |
654 | - |
655 | -import fnmatch |
656 | - |
657 | -class SSHConfig (object): |
658 | - """ |
659 | - Representation of config information as stored in the format used by |
660 | - OpenSSH. Queries can be made via L{lookup}. The format is described in |
661 | - OpenSSH's C{ssh_config} man page. This class is provided primarily as a |
662 | - convenience to posix users (since the OpenSSH format is a de-facto |
663 | - standard on posix) but should work fine on Windows too. |
664 | - |
665 | - @since: 1.6 |
666 | - """ |
667 | - |
668 | - def __init__(self): |
669 | - """ |
670 | - Create a new OpenSSH config object. |
671 | - """ |
672 | - self._config = [ { 'host': '*' } ] |
673 | - |
674 | - def parse(self, file_obj): |
675 | - """ |
676 | - Read an OpenSSH config from the given file object. |
677 | - |
678 | - @param file_obj: a file-like object to read the config file from |
679 | - @type file_obj: file |
680 | - """ |
681 | - configs = [self._config[0]] |
682 | - for line in file_obj: |
683 | - line = line.rstrip('\n').lstrip() |
684 | - if (line == '') or (line[0] == '#'): |
685 | - continue |
686 | - if '=' in line: |
687 | - key, value = line.split('=', 1) |
688 | - key = key.strip().lower() |
689 | - else: |
690 | - # find first whitespace, and split there |
691 | - i = 0 |
692 | - while (i < len(line)) and not line[i].isspace(): |
693 | - i += 1 |
694 | - if i == len(line): |
695 | - raise Exception('Unparsable line: %r' % line) |
696 | - key = line[:i].lower() |
697 | - value = line[i:].lstrip() |
698 | - |
699 | - if key == 'host': |
700 | - del configs[:] |
701 | - # the value may be multiple hosts, space-delimited |
702 | - for host in value.split(): |
703 | - # do we have a pre-existing host config to append to? |
704 | - matches = [c for c in self._config if c['host'] == host] |
705 | - if len(matches) > 0: |
706 | - configs.append(matches[0]) |
707 | - else: |
708 | - config = { 'host': host } |
709 | - self._config.append(config) |
710 | - configs.append(config) |
711 | - else: |
712 | - for config in configs: |
713 | - config[key] = value |
714 | - |
715 | - def lookup(self, hostname): |
716 | - """ |
717 | - Return a dict of config options for a given hostname. |
718 | - |
719 | - The host-matching rules of OpenSSH's C{ssh_config} man page are used, |
720 | - which means that all configuration options from matching host |
721 | - specifications are merged, with more specific hostmasks taking |
722 | - precedence. In other words, if C{"Port"} is set under C{"Host *"} |
723 | - and also C{"Host *.example.com"}, and the lookup is for |
724 | - C{"ssh.example.com"}, then the port entry for C{"Host *.example.com"} |
725 | - will win out. |
726 | - |
727 | - The keys in the returned dict are all normalized to lowercase (look for |
728 | - C{"port"}, not C{"Port"}. No other processing is done to the keys or |
729 | - values. |
730 | - |
731 | - @param hostname: the hostname to lookup |
732 | - @type hostname: str |
733 | - """ |
734 | - matches = [ |
735 | - x for x in self._config if fnmatch.fnmatch(hostname, x['host'])] |
736 | - # sort in order of shortest match (usually '*') to longest |
737 | - matches.sort(lambda x,y: cmp(len(x['host']), len(y['host']))) |
738 | - ret = {} |
739 | - for m in matches: |
740 | - ret.update(m) |
741 | - del ret['host'] |
742 | - return ret |
743 | - |
744 | -# END paramiko config fork |
745 | -############################################################################# |
746 | |
747 | === modified file 'lib/devscripts/ec2test/testrunner.py' |
748 | --- lib/devscripts/ec2test/testrunner.py 2009-09-27 00:27:17 +0000 |
749 | +++ lib/devscripts/ec2test/testrunner.py 2009-09-27 20:39:13 +0000 |
750 | @@ -368,14 +368,13 @@ |
751 | |
752 | |
753 | def configure_system(self): |
754 | - user_connection = self._instance.connect_as_user() |
755 | + user_connection = self._instance.connect() |
756 | as_user = user_connection.perform |
757 | - user_sftp = user_connection.ssh.open_sftp() |
758 | # Set up bazaar.conf with smtp information if necessary |
759 | if self.email or self.message: |
760 | - as_user('mkdir /home/%(USER)s/.bazaar') |
761 | - bazaar_conf_file = user_sftp.open( |
762 | - "/home/%(USER)s/.bazaar/bazaar.conf" % self.vals, 'w') |
763 | + as_user('mkdir .bazaar') |
764 | + bazaar_conf_file = user_connection.sftp.open( |
765 | + ".bazaar/bazaar.conf", 'w') |
766 | bazaar_conf_file.write( |
767 | 'smtp_server = %(smtp_server)s\n' % self.vals) |
768 | if self.vals['smtp_username']: |
769 | @@ -387,25 +386,18 @@ |
770 | bazaar_conf_file.close() |
771 | # Copy remote ec2-remote over |
772 | self.log('Copying ec2test-remote.py to remote machine.\n') |
773 | - user_sftp.put( |
774 | + user_connection.sftp.put( |
775 | os.path.join(os.path.dirname(os.path.realpath(__file__)), |
776 | 'ec2test-remote.py'), |
777 | '/var/launchpad/ec2test-remote.py') |
778 | - user_sftp.close() |
779 | # Set up launchpad login and email |
780 | as_user('bzr launchpad-login %(launchpad-login)s') |
781 | user_connection.close() |
782 | |
783 | def prepare_tests(self): |
784 | - user_connection = self._instance.connect_as_user() |
785 | + user_connection = self._instance.connect() |
786 | # Clean up the test branch left in the instance image. |
787 | user_connection.perform('rm -rf /var/launchpad/test') |
788 | - # get newest sources |
789 | - user_connection.run_with_ssh_agent( |
790 | - "rsync -avp --partial --delete " |
791 | - "--filter='P *.o' --filter='P *.pyc' --filter='P *.so' " |
792 | - "devpad.canonical.com:/code/rocketfuel-built/launchpad/sourcecode/* " |
793 | - "/var/launchpad/sourcecode/") |
794 | # Get trunk. |
795 | user_connection.run_with_ssh_agent( |
796 | 'bzr branch %(trunk_branch)s /var/launchpad/test') |
797 | @@ -415,6 +407,10 @@ |
798 | 'cd /var/launchpad/test; bzr merge %(branch)s') |
799 | else: |
800 | self.log('(Testing trunk, so no branch merge.)') |
801 | + # get newest sources |
802 | + user_connection.run_with_ssh_agent( |
803 | + "/var/launchpad/test/utilities/update-sourcecode " |
804 | + "/var/launchpad/sourcecode") |
805 | # Get any new sourcecode branches as requested |
806 | for dest, src in self.branches: |
807 | fulldest = os.path.join('/var/launchpad/test/sourcecode', dest) |
808 | @@ -455,20 +451,18 @@ |
809 | p('mv /var/launchpad/download-cache /var/launchpad/tmp/download-cache') |
810 | if (self.include_download_cache_changes and |
811 | self.download_cache_additions): |
812 | - sftp = user_connection.ssh.open_sftp() |
813 | root = os.path.realpath( |
814 | os.path.join(self.original_branch, 'download-cache')) |
815 | for info in self.download_cache_additions: |
816 | src = os.path.join(root, info[0]) |
817 | self.log('Copying %s to remote machine.\n' % (src,)) |
818 | - sftp.put( |
819 | + user_connection.sftp.put( |
820 | src, |
821 | os.path.join('/var/launchpad/tmp/download-cache', info[0])) |
822 | - sftp.close() |
823 | p('/var/launchpad/test/utilities/link-external-sourcecode ' |
824 | '-p/var/launchpad/tmp -t/var/launchpad/test'), |
825 | # set up database |
826 | - p('/var/launchpad/test/utilities/launchpad-database-setup %(USER)s') |
827 | + p('/var/launchpad/test/utilities/launchpad-database-setup $USER') |
828 | # close ssh connection |
829 | user_connection.close() |
830 | |
831 | @@ -476,7 +470,7 @@ |
832 | """Turn ec2 instance into a demo server.""" |
833 | self.configure_system() |
834 | self.prepare_tests() |
835 | - user_connection = self._instance.connect_as_user() |
836 | + user_connection = self._instance.connect() |
837 | p = user_connection.perform |
838 | p('make -C /var/launchpad/test schema') |
839 | p('mkdir -p /var/tmp/bazaar.launchpad.dev/static') |
840 | @@ -510,7 +504,7 @@ |
841 | def run_tests(self): |
842 | self.configure_system() |
843 | self.prepare_tests() |
844 | - user_connection = self._instance.connect_as_user() |
845 | + user_connection = self._instance.connect() |
846 | |
847 | # Make sure we activate the failsafe --shutdown feature. This will |
848 | # make the server shut itself down after the test run completes, or |
849 | @@ -580,11 +574,9 @@ |
850 | |
851 | # deliver results as requested |
852 | if self.file: |
853 | - sftp = user_connection.ssh.open_sftp() |
854 | self.log( |
855 | 'Writing abridged test results to %s.\n' % self.file) |
856 | - sftp.get('/var/www/summary.log', self.file) |
857 | - sftp.close() |
858 | + user_connection.sftp.get('/var/www/summary.log', self.file) |
859 | user_connection.close() |
860 | |
861 | def get_remote_test_options(self): |
862 | |
863 | === modified file 'utilities/update-sourcecode' |
864 | --- utilities/update-sourcecode 2009-09-27 00:27:17 +0000 |
865 | +++ utilities/update-sourcecode 2009-09-27 20:39:13 +0000 |
866 | @@ -5,10 +5,12 @@ |
867 | |
868 | """Update the sourcecode managed in sourcecode/.""" |
869 | |
870 | -import _pythonpath |
871 | - |
872 | +import os |
873 | import sys |
874 | |
875 | +sys.path.append( |
876 | + os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib')) |
877 | + |
878 | from devscripts import sourcecode |
879 | |
880 |
Hi jml,
I hit back with an unsolicited ec2 branch :-)
This one started with thinking about preparing an image for public consumption. I don't really know what files are lurking in unexpected places on the ec2test instances we currently use, so I started to think about building images from a blank hardy image such as those on alestic.com. Eventually, I came up with this approach, which hides all the setup of an instance in the EC2Instance object, and gives a way of saying that an image will come up 'blank' and needs completely setting up. It also changes to assuming a sudo account exists on a pre set-up image rather than creating a user on each run.
As a side effect, you could say 'ec2 test -m based-on:<id of some blank karmic ami>' to run all the tests in a karmic image, which is nice.
I made a couple of images with --public and got wgrant to test them (successfully). I shouldn't make the image public until this branch is about ready to land as while the trunk ec2 script works with the new image, it's rather slow (lots and lots of stuff gets rsynced off devpad).
Because I spent so long going back and forth on ways to do this, a fair bit of drive by clean up happened en route. I hope it doesn't muddle things too much.
Cheers,
mwh