Merge lp:~pfalcon/linaro-aws-tools/linaro-ami-ver-1.5 into lp:linaro-aws-tools

Proposed by Paul Sokolovsky
Status: Merged
Merged at revision: 99
Proposed branch: lp:~pfalcon/linaro-aws-tools/linaro-ami-ver-1.5
Merge into: lp:linaro-aws-tools
Diff against target: 480 lines (+217/-67)
11 files modified
linaro-ami/README (+36/-12)
linaro-ami/linaro-ami (+15/-2)
linaro-ami/linaro-ami.conf (+7/-1)
linaro-ami/linaro_ami.py (+14/-5)
linaro-ami/remote_executor.py (+18/-4)
linaro-ami/setup.cfg (+3/-0)
linaro-ami/tests/integration/__init__.py (+5/-0)
linaro-ami/tests/integration/integration_test_support.py (+42/-0)
linaro-ami/tests/integration/test_linaro_ami.py (+9/-3)
linaro-ami/tests/integration/test_remote_executor_int.py (+58/-0)
linaro-ami/tests/unit/test_remote_executor.py (+10/-40)
To merge this branch: bzr merge lp:~pfalcon/linaro-aws-tools/linaro-ami-ver-1.5
Reviewer Review Type Date Requested Status
Stevan Radaković code Approve
Данило Шеган Pending
Review via email: mp+115088@code.launchpad.net

Description of the change

These changes fix/elaborated number of issues as tracked by linked bugs, specifically:

1. Recovers support for --key/--private-key switch (as present in many utils from ec2-api-tools) to specify user's EC2-regestered SSH key, to make sure that object ownership is properly recorded/given user has access to them for diagnostics.

2. Testsuite was elaborated and restructured to clearly separate unit and integration tests. Only quick-running, side-effects free unit tests were set up to run by default. Docs for running tests were updated.

3. Issues faced during working on lp:1019257 were fixed, with regression tests added, as well as fixes and tests for similar conditions.

To post a comment you must log in.
Revision history for this message
Loïc Minier (lool) wrote :

This looks good implementation wise, but do we really need to bother
with the key passing args? I personally rely on boto's env handling to
use different keys for other scripts; see the "run-boto" script in
~linaro/bin on people.linaro.org for an example. This limits the
complexity of the code and keeps the key handling external.

--
Loïc Minier

Revision history for this message
Stevan Radaković (stevanr) wrote :

Looks good to me. I still agree with Loic though, as we had a lot of discussions on the same topic internally.

Revision history for this message
Stevan Radaković (stevanr) wrote :

Hey Paul, great work.

README file doesn't contain anything about the 2 new (old) config options nor the instructions for the new form of testing remote executor (EC2_KEY instead of old EC2_PRIVATE_KEY).
I also couldn't run remote executor integration tests with any of my previous keys.

Otherwise, changes look good.

review: Needs Fixing
Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> do we really need to bother with the key passing args?

The problem is that there're many those "keys" in AWS, each serving different purpose (and it's not easy to grasp the whole picture). Let's look at some part of the whole picture:

There're AWS_ACCESS_KEY_ID which is kinda login and AWS_SECRET_ACCESS_KEY which is kinda password for that login - for accessing *AWS* API as the whole. But that's not only credentials AWS uses, specific subservices use additional stuff. For example, EC2 requires SSH public key to be registered (assigned id in the form of "key name"), that key name should be specified when creating an instance, and of course, then user needs corresponding private key to access the instance. Let's do ec2-run-instances --help and read:

[]
     -K, --private-key KEY
          Specify KEY as the private key to use. Defaults to the value of the
          EC2_PRIVATE_KEY environment variable (if set). Overrides the default.
[]
     -k, --key KEYPAIR
          Specifies the key pair to use when launching the instance(s).

So, answering the original questions, if we should bother with all those keys, the good answer is that we should, inasmuch as AWS/EC2 itself does (in the form of its standard utilities in particular). Trying to circumvent AWS/EC2 credential process is bad idea because 1) it may lead to unexpected results (including in data integrity/security domain); 2) for people who know AWS/EC2 credentials semantics, it again will lead to unexpected results.

117. By Paul Sokolovsky

Update decsription of commands and options.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> README file doesn't contain anything about the 2 new (old) config options

Ok, updated.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> nor the instructions for the new form of testing remote executor

Weird, I'm sure I was updating that section of README! Now I see That I did that in wrong branch, let me port that over.

118. By Paul Sokolovsky

Mention that boto 2.0+ is required.

Earlier versions had different credentials setting method and may miss
some API calls.

(Initially committed to wrong branch).

119. By Paul Sokolovsky

Update Tests section for elaborated unit/integration testsuite.

(Initially committed to wrong branch).

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> Weird, I'm sure I was updating that section of README! Now I see That I did that in wrong branch, let me port that over.

Here they go. That should make it clear how to run integration part of testsuite. In particular, EC2_KEY doesn't replaces EC2_PRIVATE_KEY, they're used both, essentially, EC2_PRIVATE_KEY is the analog of command-line --private-key option, and in the same vein, EC2_KEY is the analog of --key option.

I run complete testsuite as follows (EC2_PRIVATE_KEY is set in env to point to my priv key of keypair regiseterd as pfalcon@home):

EC2_KEY=pfalcon@home nosetests .

Running just unit tests is as easy as

nosetests

Revision history for this message
Stevan Radaković (stevanr) wrote :

> > README file doesn't contain anything about the 2 new (old) config options
>
> Ok, updated.

Ok thanks.
Approve +1.

review: Approve (code)
Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Merged, thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'linaro-ami/README'
2--- linaro-ami/README 2012-07-13 10:05:48 +0000
3+++ linaro-ami/README 2012-07-17 18:43:18 +0000
4@@ -22,18 +22,42 @@
5 $ export AWS_ACCESS_KEY_ID=EXAMPLEKEY
6 $ export AWS_SECRET_ACCESS_KEY=EXAMPLESECRETKEY
7
8-Script Options
9---------------
10-
11-Currently, only one command is recognizable, 'create', which takes one
12-additional argument which is the AMI name which should be used from the
13-config file (see below).
14-
15-Script also has one configurable option:
16-
17---config Path to the optional configuration file. Default value is
18- linaro-ami.conf
19-
20+Script Commands And Options
21+---------------------------
22+
23+linaro-ami accepts following commands:
24+
25+ * create
26+
27+ Create a custom AMI. This command takes one additional argument which
28+ is the AMI section name as defined in the config file (see below).
29+
30+ * list
31+
32+ List existing custom AMIs. For each AMI, name, AMI id, description,
33+ and some other properties are printed.
34+
35+
36+Script also accepts following options:
37+
38+--config
39+
40+ Path to the configuration file. Default value is linaro-ami.conf
41+ in the current directory.
42+
43+--key=KEYPAIR
44+
45+ Name of the EC2-registered SSH keypair name to use when creating
46+ work instances. If not supplied, a temporary key will be created
47+ and removed once processing finishes. This option is useful for
48+ debugging AMI creation process and to ensure that proper ownership
49+ information is recorded in EC2 at all times.
50+
51+--private-key=FILE
52+
53+ This option should be supplied if --key was (or, alternatively,
54+ EC2_PRIVATE_KEY environment variable should be set) and point to
55+ private SSH key from the pair as named by --key.
56
57 Configuration File Syntax
58 -------------------------
59
60=== modified file 'linaro-ami/linaro-ami'
61--- linaro-ami/linaro-ami 2012-06-29 07:58:28 +0000
62+++ linaro-ami/linaro-ami 2012-07-17 18:43:18 +0000
63@@ -22,24 +22,37 @@
64 optparser.add_option(
65 "--type", default=DEFAULT_AMI_TYPE,
66 help="List images of type (%default, use empty for any)")
67+ optparser.add_option(
68+ "-K", "--private-key", metavar="FILE",
69+ help="Private key file (env:EC2_PRIVATE_KEY)")
70+ optparser.add_option(
71+ "-k", "--key", metavar="KEY",
72+ help="Name of EC2-registered key provided by --private-key")
73+
74 options, args = optparser.parse_args(sys.argv[1:])
75 if len(args) < 1:
76 optparser.error("Wrong number of arguments")
77
78+ if not options.private_key:
79+ options.private_key = os.environ.get("EC2_PRIVATE_KEY")
80+
81 configuration = ConfigParser()
82 configuration.read(options.config)
83 aws_controller = AwsController()
84 remote_executor = RemoteExecutor()
85
86 controller = linaro_ami.LinaroAMI(configuration, aws_controller,
87- remote_executor, "ubuntu", options)
88+ remote_executor,
89+ "ubuntu", options.private_key,
90+ options)
91 if args[0] == "create":
92 if len(args) != 2:
93 optparser.error("Usage: %s create <AMI template name from config>"
94 % sys.argv[0])
95 if not controller.has_ami(args[1]):
96 optparser.error("AMI template '%s' not found" % args[1])
97- controller.command_create(args[1])
98+ ami_id = controller.command_create(args[1])
99+ print "Created AMI: %s" % ami_id
100 elif args[0] == "delete":
101 # TODO
102 controller.command_delete()
103
104=== modified file 'linaro-ami/linaro-ami.conf'
105--- linaro-ami/linaro-ami.conf 2012-07-04 12:01:22 +0000
106+++ linaro-ami/linaro-ami.conf 2012-07-17 18:43:18 +0000
107@@ -4,7 +4,7 @@
108 instance_type = t1.micro
109 log_file = linaro-ami.log
110 bzr_install = sudo apt-get --assume-yes install bzr
111-key_name = ami-test-key
112+key_name = linaro-ami-key
113
114 [ab-natty-64bit]
115 base_ami = ami-87c31aee
116@@ -12,6 +12,12 @@
117 init_script = node/setup-build-android
118 description = Build Slave AMI for android-build.linaro.org based on Natty 64bit
119
120+[ci-natty-64bit]
121+base_ami = ami-87c31aee
122+init_script_repo = lp:~linaro-infrastructure/linaro-ci/lci-build-tools
123+init_script = node/setup-natty-node
124+description = Build Slave AMI for ci.linaro.org based on Natty 64bit
125+
126 [ci-oneiric-64bit]
127 base_ami = ami-baba68d3
128 init_script_repo = lp:~linaro-infrastructure/linaro-ci/lci-build-tools
129
130=== modified file 'linaro-ami/linaro_ami.py'
131--- linaro-ami/linaro_ami.py 2012-07-02 15:59:56 +0000
132+++ linaro-ami/linaro_ami.py 2012-07-17 18:43:18 +0000
133@@ -26,10 +26,12 @@
134 """
135
136 def __init__(self, config, aws_controller, remote_executor, username,
137+ key_filename=None,
138 options={}, aws_access_key_id=None,
139 aws_secret_access_key=None):
140 self.log = logging.getLogger("linaro-ami")
141 self.username = username
142+ self.key_filename = key_filename
143 self.options = options
144 self.aws_access_key_id = aws_access_key_id
145 self.aws_secret_access_key = aws_secret_access_key
146@@ -48,21 +50,27 @@
147 based on the parameters provided.
148 """
149
150- self.log.info("Command create start.")
151+ self.log.info("Starting instance to create AMI...")
152 self.aws.connect()
153
154 instance = None
155 base_ami = self.config.get(ami_name, "base_ami")
156 instance_type = self.config.get(ami_name, "instance_type")
157 script_repo = self.config.get(ami_name, "init_script_repo")
158- key_name = self.config.get(ami_name, "key_name")
159+ if self.options.key:
160+ key_autocreate = False
161+ key_name = self.options.key
162+ else:
163+ key_autocreate = True
164+ key_name = self.config.get(ami_name, "key_name")
165
166 try:
167- self.aws.import_key_pair(key_name, self.executor.get_public_key())
168+ if key_autocreate:
169+ self.aws.import_key_pair(key_name, self.executor.get_public_key())
170 instance = self.aws.run_instance_and_wait(base_ami,
171 key_name, instance_type)
172 self.log.info("Started instance: %s", instance.public_dns_name)
173- self.executor.connect(instance.public_dns_name, self.username)
174+ self.executor.connect(instance.public_dns_name, self.username, self.key_filename)
175 self.log.debug("Connected to instance")
176
177 self.log.info("Installing bzr.")
178@@ -100,7 +108,8 @@
179 except Exception, e:
180 logging.error(e.__class__.__name__ + ":" + str(e))
181 finally:
182- self.aws.delete_key_pair(key_name)
183+ if key_autocreate:
184+ self.aws.delete_key_pair(key_name)
185 self.executor.close()
186 if instance:
187 self.aws.terminate_instance(instance.id)
188
189=== modified file 'linaro-ami/remote_executor.py'
190--- linaro-ami/remote_executor.py 2012-07-03 15:24:06 +0000
191+++ linaro-ami/remote_executor.py 2012-07-17 18:43:18 +0000
192@@ -18,9 +18,12 @@
193 # Key used for authentication on the server
194 self.key = paramiko.RSAKey.generate(1024)
195
196- def connect(self, address, username):
197+ def connect(self, address, username, key_filename=None):
198 """Connect to address on port 22 with username and provided RSA key."""
199- self.client.connect(address, username=username, pkey=self.key)
200+ if key_filename:
201+ self.client.connect(address, username=username, key_filename=key_filename)
202+ else:
203+ self.client.connect(address, username=username, pkey=self.key)
204
205 def execute(self, command):
206 """Execute a command remotely, returns output from that command."""
207@@ -29,10 +32,21 @@
208 # library to execute script remotely and return output.
209 transport = self.client.get_transport()
210 channel = transport.open_session()
211- output_stream = channel.makefile()
212+ channel.set_combine_stderr(True)
213 channel.exec_command(command)
214+ # Closed remote stdin, so we don't get deadlock
215+ # on something trying to read it there.
216+ channel.shutdown_write()
217+ output = ""
218+ while True:
219+ # 512 below comes from a minimal buffer a canaonical unix pipe
220+ # may have. Of course, that might be not related to paramiko's
221+ # channel buffer, but good enoug anyway.
222+ data = channel.recv(512)
223+ if not data:
224+ break
225+ output += data
226 status = channel.recv_exit_status()
227- output = output_stream.read()
228 channel.close()
229 # Only need status to know if command has failed actually
230 # We don't want continuation if user script failed
231
232=== added file 'linaro-ami/setup.cfg'
233--- linaro-ami/setup.cfg 1970-01-01 00:00:00 +0000
234+++ linaro-ami/setup.cfg 2012-07-17 18:43:18 +0000
235@@ -0,0 +1,3 @@
236+[nosetests]
237+verbosity=2
238+tests=tests/unit/
239
240=== added directory 'linaro-ami/tests/integration'
241=== added file 'linaro-ami/tests/integration/__init__.py'
242--- linaro-ami/tests/integration/__init__.py 1970-01-01 00:00:00 +0000
243+++ linaro-ami/tests/integration/__init__.py 2012-07-17 18:43:18 +0000
244@@ -0,0 +1,5 @@
245+from . import integration_test_support
246+
247+
248+def teardown():
249+ integration_test_support.remove_remote_hosts()
250
251=== added file 'linaro-ami/tests/integration/integration_test_support.py'
252--- linaro-ami/tests/integration/integration_test_support.py 1970-01-01 00:00:00 +0000
253+++ linaro-ami/tests/integration/integration_test_support.py 2012-07-17 18:43:18 +0000
254@@ -0,0 +1,42 @@
255+import logging
256+import os
257+import time
258+
259+import boto
260+
261+
262+TEST_INSTANCE_NAME = "linaro-ami integration tests"
263+
264+def get_remote_host():
265+ connection = boto.connect_ec2()
266+ for reservation in connection.get_all_instances():
267+ for i in reservation.instances:
268+ if i.tags.get("Name") == TEST_INSTANCE_NAME and i.state == "running":
269+ print "Found existing inst"
270+ return i.dns_name
271+ print "Didn't find existing inst - creating"
272+ reservation = connection.run_instances("ami-87c31aee", key_name=os.environ["EC2_KEY"],
273+ instance_type="t1.micro")
274+ instance = reservation.instances[0]
275+ connection.create_tags([instance.id], {"Name": TEST_INSTANCE_NAME})
276+ while instance.state != "running":
277+ instance.update()
278+ time.sleep(5)
279+ # Allow OS to boot and sshd to start
280+ time.sleep(20)
281+ return instance.dns_name
282+
283+
284+def remove_remote_hosts():
285+ connection = boto.connect_ec2()
286+ for reservation in connection.get_all_instances():
287+ for i in reservation.instances:
288+ if i.state == "running":
289+ if i.tags.get("Name") == TEST_INSTANCE_NAME \
290+ or i.key_name in ("ami-test-key", "infra-test-key"):
291+ print "Terminating test instance:", i.id
292+ connection.terminate_instances([i.id])
293+
294+
295+if __name__ == "__main__":
296+ print get_remote_host()
297
298=== renamed file 'linaro-ami/tests/test_aws_controller.py' => 'linaro-ami/tests/integration/test_aws_controller.py'
299=== renamed file 'linaro-ami/tests/test_linaro_ami.py' => 'linaro-ami/tests/integration/test_linaro_ami.py'
300--- linaro-ami/tests/test_linaro_ami.py 2012-07-02 15:59:56 +0000
301+++ linaro-ami/tests/integration/test_linaro_ami.py 2012-07-17 18:43:18 +0000
302@@ -2,12 +2,18 @@
303
304 import linaro_ami
305
306-from test_support import TestSupport
307+from ..test_support import TestSupport
308 from ConfigParser import ConfigParser
309 from aws_controller import AwsController
310 from remote_executor import RemoteExecutor
311
312
313+class MockOptions:
314+
315+ def __getattr__(self, name):
316+ return None
317+
318+
319 class TestLinaroAMI:
320
321 def __init__(self):
322@@ -21,13 +27,13 @@
323 remote_executor = RemoteExecutor()
324
325 controller = linaro_ami.LinaroAMI(configuration, aws_controller,
326- remote_executor, "ubuntu")
327+ remote_executor, "ubuntu", options=MockOptions())
328 ami_id = controller.command_create("dummy-test")
329 print ami_id
330 all_images = controller.aws.list_images()
331 print all_images
332 result = filter(lambda i: i.id == ami_id, all_images)
333- assert len(result) == 1
334+ assert len(result) == 1, "Actual result: %s" % result
335
336 def _test_command_list(self):
337 controller = linaro_ami.LinaroAMI("tests/data/linaro-ami.conf",
338
339=== added file 'linaro-ami/tests/integration/test_remote_executor_int.py'
340--- linaro-ami/tests/integration/test_remote_executor_int.py 1970-01-01 00:00:00 +0000
341+++ linaro-ami/tests/integration/test_remote_executor_int.py 2012-07-17 18:43:18 +0000
342@@ -0,0 +1,58 @@
343+import os
344+import time
345+import signal
346+import random
347+
348+import remote_executor
349+import paramiko
350+from . import integration_test_support
351+from remote_executor import RemoteCommandFailedError
352+from ..test_support import TestSupport
353+
354+
355+class TestRemoteExecutorIntegration:
356+
357+ def __init__(self):
358+ TestSupport.setup_logging()
359+ self.instance_address = integration_test_support.get_remote_host()
360+ self.executor = remote_executor.RemoteExecutor()
361+
362+ def test_stdout_capture(self):
363+ self.executor.connect(self.instance_address, "ubuntu", os.environ["EC2_PRIVATE_KEY"])
364+ s = "test string " + str(random.random())
365+ output = self.executor.execute("echo -n '%s'" % s)
366+ assert output == s
367+
368+ def test_stderr_capture(self):
369+ self.executor.connect(self.instance_address, "ubuntu", os.environ["EC2_PRIVATE_KEY"])
370+ s = "test string " + str(random.random())
371+ output = self.executor.execute("echo -n '%s' 1>&2" % s)
372+ assert output == s
373+
374+ def test_long_stdout(self):
375+ self.executor.connect(self.instance_address, "ubuntu", os.environ["EC2_PRIVATE_KEY"])
376+ size = 500000
377+ signal.alarm(10)
378+ output = self.executor.execute("cat /dev/urandom | head -c%d" % size)
379+ signal.alarm(0)
380+ assert len(output) == size
381+
382+ def test_long_stderr(self):
383+ self.executor.connect(self.instance_address, "ubuntu", os.environ["EC2_PRIVATE_KEY"])
384+ size = 50000
385+ signal.alarm(10)
386+ output = self.executor.execute("cat /dev/urandom | head -c%d 1>&2" % size)
387+ signal.alarm(0)
388+ assert len(output) == size
389+
390+ def test_blocked_stdin(self):
391+ # Test that we don't block indefinitely on remote command trying to read stdin
392+ self.executor.connect(self.instance_address, "ubuntu", os.environ["EC2_PRIVATE_KEY"])
393+ # We may hang here, provide (abrupt) way out in such case
394+ # TODO: Unfortunately, using custom signal handler doesn't
395+ # work here (it's not being called, or maybe calling it
396+ # block the py interpreter or something)
397+ # signal.signal(signal.SIGALRM, sig_handler)
398+ signal.alarm(5)
399+ self.executor.execute("cat")
400+ signal.alarm(0)
401
402=== added directory 'linaro-ami/tests/unit'
403=== added file 'linaro-ami/tests/unit/__init__.py'
404=== renamed file 'linaro-ami/tests/test_remote_executor.py' => 'linaro-ami/tests/unit/test_remote_executor.py'
405--- linaro-ami/tests/test_remote_executor.py 2012-07-02 15:59:56 +0000
406+++ linaro-ami/tests/unit/test_remote_executor.py 2012-07-17 18:43:18 +0000
407@@ -1,9 +1,11 @@
408 import time
409 import signal
410
411+from mock import Mock
412+
413 import remote_executor
414 import paramiko
415-from . import test_support
416+from .. import test_support
417 from remote_executor import RemoteCommandFailedError
418
419 class MockClient:
420@@ -19,29 +21,12 @@
421 class MockTransport:
422
423 def open_session(self):
424- return MockChannel()
425-
426-class MockOutputStream:
427-
428- def read(self):
429- return "test"
430-
431-class MockChannel:
432-
433- def makefile(self):
434- return MockOutputStream()
435-
436- def exec_command(self, command):
437- return
438-
439- def get_pty(self):
440- return
441-
442- def recv_exit_status(self):
443- return 0
444-
445- def close(self):
446- return
447+ channel = Mock()
448+ # With mock 0.8, the below would be just = ["test", ""]
449+ channel.recv.side_effect = lambda sz, buf=["test", ""]: buf.pop(0)
450+ channel.recv_exit_status.return_value = 0
451+ return channel
452+
453
454 class TestRemoteExecutor:
455
456@@ -57,23 +42,8 @@
457 def test_execute(self):
458 self.executor.client = MockClient()
459 output = self.executor.execute("command")
460- assert output == "test"
461+ assert output == "test", "Unexpected result: " + output
462
463 def test_get_public_key(self):
464 public_key = self.executor.get_public_key()
465 assert ' ' in public_key
466-
467- def _test_execute_stdin(self):
468- def sig_handler(signum, stack_frame):
469- print "foo"
470- assert False, "Remote execution hanged on stdin-reading command"
471- self.connect()
472- # We may hang here, provide (abrupt) way out in such case
473- # TODO: Unfortunately, using custom signal handler doesn't
474- # work here (it's not being called, or maybe calling it
475- # block the py interpreter or something)
476- # signal.signal(signal.SIGALRM, sig_handler)
477- signal.alarm(5)
478- stdin, stdout, stderr, status = self.executor.execute("cat")
479- signal.alarm(0)
480- assert status == -1, "Received unexpected status: %d" % status

Subscribers

People subscribed via source and target branches