Merge lp:~jml/udd/deployable into lp:udd

Proposed by Jonathan Lange
Status: Merged
Approved by: James Westby
Approved revision: 569
Merged at revision: 565
Proposed branch: lp:~jml/udd/deployable
Merge into: lp:udd
Diff against target: 351 lines (+331/-0)
4 files modified
fabfile.py (+1/-0)
fabtasks/__init__.py (+1/-0)
fabtasks/deploy.py (+323/-0)
fabtasks/lp_creds.txt (+6/-0)
To merge this branch: bzr merge lp:~jml/udd/deployable
Reviewer Review Type Date Requested Status
James Westby Approve
Review via email: mp+97224@code.launchpad.net

Commit message

A fabtask to deploy udd to a new EC2 instance

Description of the change

I wanted to test out the behaviour of lp:udd with certain config changes set, but I didn't particularly want to go to the hassle of getting it running on my own machine. So, I made a script that deploys an instance to EC2.

It uses fabric to do so, and is heavily derived from the script that james_w made for lp:pkgme-service.

Some things to note:
 * it uses fixed credentials to access Launchpad. I created a user, ~jml+libdep-bot, and have stored read-only credentials for accessing Launchpad in fabtasks/lp_creds.txt
 * it doesn't install bzr-buildeb. Not sure if this is a problem.
 * it doesn't do the Apache / cricket / website configuration, mostly because I'm not interested in that stuff

Still, I think it's useful. It also might be the only existing documentation about how to deploy this thing from scratch, so that makes it handy in its own right.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Hi,

This looks good.

The credentials being in the branch aren't great, but making them read-only is
a good compromise.

It would be good to extract out the generic ec2 stuff to somewhere else, but
we should probably just live with it and expect to use juju for that before
too long.

Thanks,

James

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'fabfile.py'
2--- fabfile.py 1970-01-01 00:00:00 +0000
3+++ fabfile.py 2012-03-15 22:31:18 +0000
4@@ -0,0 +1,1 @@
5+from fabtasks import *
6
7=== added directory 'fabtasks'
8=== added file 'fabtasks/__init__.py'
9--- fabtasks/__init__.py 1970-01-01 00:00:00 +0000
10+++ fabtasks/__init__.py 2012-03-15 22:31:18 +0000
11@@ -0,0 +1,1 @@
12+from .deploy import *
13
14=== added file 'fabtasks/deploy.py'
15--- fabtasks/deploy.py 1970-01-01 00:00:00 +0000
16+++ fabtasks/deploy.py 2012-03-15 22:31:18 +0000
17@@ -0,0 +1,323 @@
18+from datetime import datetime
19+import os
20+import subprocess
21+import time
22+
23+from fabric.api import env, run, put
24+from fabric.utils import abort
25+
26+from boto import ec2
27+
28+
29+RELEASE = 'lucid'
30+ARCH = 'amd64'
31+FS_TYPE = 'ebs'
32+INSTANCE_TYPE = 't1.micro'
33+USERNAME = 'ubuntu'
34+REGION_NAME = 'us-east-1'
35+
36+NAME_KEY = "name"
37+NAME_PREFIX = "fab/udd"
38+
39+ALL_HOSTS = "0.0.0.0/0"
40+
41+
42+def _get_aws_credentials():
43+ """Get the AWS credentials for the user from ~/.ec2/aws_id.
44+
45+ :returns: a tuple of access key id, secret access key
46+ """
47+ key_id = None
48+ secret_access_key = None
49+ with open(os.path.expanduser("~/.ec2/aws_id")) as f:
50+ for i, line in enumerate(f.readlines()):
51+ if i == 0:
52+ key_id = line.strip()
53+ if i == 1:
54+ secret_access_key = line.strip()
55+ if key_id is None:
56+ raise AssertionError("Missing key id in ~/.ec2/aws_id")
57+ if secret_access_key is None:
58+ raise AssertionError("Missing secret access key in ~/.ec2/aws_id")
59+ return key_id, secret_access_key
60+
61+
62+def _get_ami_id(region_name):
63+ """Get the AMI to use for a particular region.
64+
65+ This consults the ubuntu-cloudimg-query tool to find out the best
66+ AMI to use for a particular region.
67+
68+ :returns: the ami id (as a string)
69+ """
70+ proc = subprocess.Popen(
71+ ['ubuntu-cloudimg-query', RELEASE, ARCH, FS_TYPE, region_name],
72+ stdout=subprocess.PIPE)
73+ stdout, _ = proc.communicate()
74+ if proc.returncode != 0:
75+ raise AssertionError("calling ubuntu-cloudimg-query failed")
76+ return stdout.strip()
77+
78+
79+def _get_security_group(conn, name):
80+ security_group = conn.create_security_group(name,
81+ "Access to the fab pkgme-service ec2 deployment %s" % name)
82+ security_group.authorize('tcp', 22, 22, ALL_HOSTS)
83+ security_group.authorize('tcp', 80, 80, ALL_HOSTS)
84+ security_group.authorize('tcp', 443, 443, ALL_HOSTS)
85+ return security_group
86+
87+
88+def _new_ec2_instance(keypair):
89+ """Starts a new ec2 instance, giving the specified keypair access.
90+
91+ This will use the AWS credentials from ~/.ec2/aws_id, and the
92+ AMI recommended by ubuntu-cloudimg-query.
93+
94+ :returns: the boto Instance object of the launched instance. It
95+ will be in a state where it can be accessed over ssh.
96+ """
97+ key_id, secret_access_key = _get_aws_credentials()
98+ ami_id = _get_ami_id(REGION_NAME)
99+ conn = ec2.connect_to_region(REGION_NAME, aws_access_key_id=key_id,
100+ aws_secret_access_key=secret_access_key)
101+ _remove_old_security_groups(conn)
102+ image = conn.get_image(ami_id)
103+ now = datetime.utcnow()
104+ name = "%s/%s" % (NAME_PREFIX, now.isoformat())
105+ security_group = _get_security_group(conn, name)
106+ reservation = image.run(instance_type=INSTANCE_TYPE, key_name=keypair,
107+ security_groups=[security_group.name])
108+ instance = reservation.instances[0]
109+ print("Waiting for instance %s to start..." % instance.id)
110+ while True:
111+ time.sleep(10)
112+ instance.update()
113+ if instance.state != 'pending':
114+ break
115+ if instance.state != 'running':
116+ raise AssertionError("Instance failed to start")
117+ print("Instance started as %s" % instance.dns_name)
118+ print("Waiting for ssh to come up")
119+ # FIXME: use something better than a sleep to determine this.
120+ time.sleep(30)
121+ instance.add_tag(NAME_KEY, name)
122+ return instance
123+
124+
125+def deploy_to_ec2(branch="lp:udd", use_staging_deps=False, keypair='ec2-keypair'):
126+ """Deploy to a new ec2 instance.
127+
128+ This command will spin up an ec2 instance, and deploy to it.
129+
130+ To run this command you must first set up an ec2 account.
131+
132+ http://aws.amazon.com/ec2/
133+
134+ Once you have that create access keys for this to use. Go to
135+
136+ https://aws-portal.amazon.com/gp/aws/developer/account?ie=UTF8&action=access-key
137+
138+ and create an access key. Then create a file at ~/.ec2/aws_id and
139+ put the "Access Key ID" on the first line, and the "Secret Access Key" on
140+ the second line (with nothing else on either line.)
141+
142+ Next you will need an ec2 keypair. Go to the EC2 console at
143+
144+ https://console.aws.amazon.com/ec2/home?region=us-east-1
145+
146+ click on "Key Pairs" and then "Create Key Pair", name the keypair "ec2-keypair".
147+ Save the resulting file as
148+
149+ ~/.ec2/ec2-keypair.pem
150+
151+ Now you are ready to deploy, so run
152+
153+ fab deploy_to_ec2:keypair=ec2-keypair -i ~/.ec2/ec2-keypair.pem
154+
155+ and wait.
156+
157+ Note that you will be responsible for terminating the instance after
158+ use (see the destroy_ec2_instances command.)
159+
160+ You can also specify the following arguments:
161+
162+ * branch: the branch to deploy, defaults to lp:pkgme-service. To
163+ test some in-progress work push to lp:~you/pkgme-service/something
164+ and then specify that as the branch.
165+
166+ * use_staging_deps: whether to use the staging PPA for
167+ dependencies, as well as the production one, defaults to False.
168+
169+ Arguments are all specified by attaching them to the command name, e.g.
170+
171+ fab deploy_to_ec2:keypair=ec2-keypair,branch=lp:~me/pkgme-service/something
172+ """
173+ instance = _new_ec2_instance(keypair)
174+ _set_instance_as_host(instance)
175+ deploy_to_existing(branch=branch, use_staging_deps=use_staging_deps)
176+
177+
178+def _set_instance_as_host(instance):
179+ """Set the host to be acted on by fab to this instance."""
180+ env.host_string = "%s@%s:22" % (USERNAME, instance.dns_name)
181+
182+
183+def deploy_to_existing(branch="lp:udd", use_staging_deps=False):
184+ """Deploy to an existing instance.
185+
186+ This command will deploy to an existing instance. Don't use it on an
187+ instance you care about as it may overwrite anything.
188+
189+ To specify the host to deploy to use the -H option of fab:
190+
191+ fab -H ubuntu@<host> deploy_to_existing
192+
193+ You can also specify the following arguments:
194+
195+ * branch: the branch to deploy, defaults to lp:pkgme-service. To
196+ test some in-progress work push to lp:~you/pkgme-service/something
197+ and then specify that as the branch.
198+
199+ * pkgme_branch: the branch of pkgme to deploy, defaults to
200+ lp:pkgme.
201+
202+ * pkgme_binary_branch: the branch of pkgme-binary to deploy,
203+ defaults to lp:pkgme-binary.
204+
205+ * use_staging_deps: whether to use the staging PPA for
206+ dependencies, as well as the production one, defaults to False.
207+
208+ Arguments are all specified by attaching them to the command name, e.g.
209+
210+ fab deploy_to_ec2:keypair=ec2-keypair,branch=lp:~me/pkgme-service/something
211+ """
212+ # Don't install recommends, as IS don't either
213+ run(r'echo APT::Install-Recommends "false"\; | sudo tee /etc/apt/apt.conf.d/30-no-install-recommends')
214+
215+ run('sudo apt-get update -q')
216+ # # Upgrade the base system in case we are shipping any updates in
217+ # # our PPAs
218+ run('sudo apt-get dist-upgrade -q -y --force-yes')
219+
220+ # We don't care about postfix being configured, and don't want the
221+ # debconf note on install.
222+ run('echo "postfix postfix/main_mailer_type select No configuration" | sudo debconf-set-selections')
223+
224+ # Install the dependencies needed to get puppet going
225+ # TODO: move the rest of the dependencies to puppet
226+ run('sudo apt-get install -q -y --force-yes bzr apache2 python-debian')
227+
228+ # Create the installation area.
229+ # TODO: move this to a puppet configuration
230+ run('sudo useradd pkg_import')
231+
232+ BASE_DIR = '/srv/package-import.canonical.com/new'
233+ run('sudo mkdir -p %s' % (BASE_DIR,))
234+
235+ # Temporarily owned by 'ubuntu' so we can do the rest of the things
236+ # without root.
237+ run('sudo chown -R ubuntu %s' % (BASE_DIR,))
238+
239+ # Grab the branches
240+ # TODO: investigate re-using IS' config-manager config
241+ run('bzr branch -q %s %s/scripts' % (branch, BASE_DIR))
242+
243+ # Exporting in an effort to save time and to thus avoid timeouts.
244+ run('bzr export -q -r 6468 %s/bzr lp:bzr' % (BASE_DIR,))
245+ run('bzr branch -q lp:debian/distro-info %s/distro-info' % (BASE_DIR,))
246+
247+ lp_creds = os.path.join(
248+ os.path.dirname(os.path.abspath(__file__)), 'lp_creds.txt')
249+ put(lp_creds, '%s/lp_creds.txt' % (BASE_DIR,))
250+
251+ run('sudo chown -R pkg_import %s' % (BASE_DIR,))
252+
253+ # TODO: Do much of this with puppet instead.
254+ run('sudo cp %s/scripts/etc-init.d-mass-import /etc/init.d/mass-import' % (BASE_DIR,))
255+ run('sudo cp %s/scripts/etc-apache2-sites-available-package-import.ubuntu.com /etc/apache2/sites-available/package-import.ubuntu.com' % (BASE_DIR,))
256+ run('sudo a2ensite package-import.ubuntu.com')
257+ run('sudo crontab -u pkg_import %s/scripts/importer.crontab' % (BASE_DIR,))
258+ run('sudo service apache2 reload')
259+ run('sudo service mass-import start')
260+
261+
262+def _connect_to_ec2():
263+ """Get a connection to ec2, using the credentials on disk."""
264+ key_id, secret_access_key = _get_aws_credentials()
265+ conn = ec2.connect_to_region(REGION_NAME, aws_access_key_id=key_id,
266+ aws_secret_access_key=secret_access_key)
267+ return conn
268+
269+
270+def _get_ec2_instances_in_states(conn, states):
271+ for reservation in conn.get_all_instances():
272+ for instance in reservation.instances:
273+ if instance.state in states:
274+ tags = getattr(instance, "tags", {})
275+ name = tags.get(NAME_KEY, None)
276+ if name and name.startswith(NAME_PREFIX):
277+ yield instance
278+
279+
280+def _get_started_ec2_instances(conn):
281+ """Get the ec2 instances started from here."""
282+ return _get_ec2_instances_in_states(conn, ['running'])
283+
284+
285+def _remove_old_security_groups(conn):
286+ instances = list(_get_ec2_instances_in_states(conn,
287+ ['pending', 'running', 'shutting-down', 'stopping', 'stopped']))
288+ instance_names = [i.tags[NAME_KEY] for i in instances]
289+ groups = conn.get_all_security_groups()
290+ for group in groups:
291+ if not group.name.startswith(NAME_PREFIX):
292+ # Not one of ours
293+ continue
294+ if group.name in instance_names:
295+ # Still in use
296+ continue
297+ print("Removing old security group: %s" % group.name)
298+ group.delete()
299+
300+
301+def destroy_ec2_instances():
302+ """Destroy any ec2 instances created by this deployment."""
303+ conn = _connect_to_ec2()
304+ for instance in _get_started_ec2_instances(conn):
305+ print("Stopping %s" % instance.id)
306+ instance.terminate()
307+ _remove_old_security_groups(conn)
308+
309+
310+def last_ec2_launched():
311+ """Cause other commands to act on the last ec2 instance launched from here.
312+
313+ Use this in a list of commands to have the rest of the commands act on the
314+ last ec2 instance launched with deploy_to_ec2, e.g.
315+
316+ fab -i ~/.ec2/ec2-keypair.pem last_ec2_launched -- ls
317+
318+ This will then run ls on that instance.
319+ """
320+ def get_date(instance):
321+ return instance.tags[NAME_KEY][len(NAME_PREFIX):]
322+ last_instance = None
323+ conn = _connect_to_ec2()
324+ _remove_old_security_groups(conn)
325+ for instance in _get_started_ec2_instances(conn):
326+ if last_instance is None:
327+ last_instance = instance
328+ else:
329+ if get_date(instance) > get_date(last_instance):
330+ last_instance = instance
331+ if last_instance is None:
332+ abort("No instances found.")
333+ _set_instance_as_host(last_instance)
334+
335+
336+def list_running_ec2_instances():
337+ """List any ec2 instances that were started from here and are still running."""
338+ conn = _connect_to_ec2()
339+ for instance in _get_started_ec2_instances(conn):
340+ print("%s: %s" % (instance.id, instance.dns_name))
341
342=== added file 'fabtasks/lp_creds.txt'
343--- fabtasks/lp_creds.txt 1970-01-01 00:00:00 +0000
344+++ fabtasks/lp_creds.txt 2012-03-15 22:31:18 +0000
345@@ -0,0 +1,6 @@
346+[1]
347+consumer_key = package-import
348+consumer_secret =
349+access_token = ncTGp3RRfz4RtCLNXNXl
350+access_secret = BnCSnWsKCDX4lB11bCg4drtLjQmnxdmWGg1NCljZ16XqprTntQVPCcCsPPPCDSXtk1Zljss8pltB47x2
351+

Subscribers

People subscribed via source and target branches