Merge lp:~sseman/juju-ci-tools/resource-attach-2 into lp:juju-ci-tools

Proposed by Seman
Status: Merged
Merged at revision: 1421
Proposed branch: lp:~sseman/juju-ci-tools/resource-attach-2
Merge into: lp:juju-ci-tools
Diff against target: 457 lines (+261/-54)
5 files modified
assess_resources.py (+70/-12)
jujupy.py (+16/-0)
tests/test_assess_resources.py (+142/-42)
tests/test_jujupy.py (+29/-0)
utility.py (+4/-0)
To merge this branch: bzr merge lp:~sseman/juju-ci-tools/resource-attach-2
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+295532@code.launchpad.net

Description of the change

This is the second Resources branch.

- Added wait_for_resource function to wait until a resource has been downloaded to a unit.
- Added a CI test: update a resource file, attach and make sure the update is propagated to the unit.
- Added large tests by creating dummy large file and use it as a resource. An average, it takes about an hour to download 200 MB file from the controller to the unit. To me, this is slow. Users may have to wait for about half an hour to download Java runtime.

This completes adding CI tests for Resources. The next branch will add test for Resource Charmstore.

First branch:
https://code.launchpad.net/~sseman/juju-ci-tools/resource-attach/+merge/295141

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi seman. I like this branch's feature, but am not excited by the passing of args to so many methods. It isn't clear which args are used. I had to search the diff to learn that a deep function needs agent_timeout and resource_timeout. The function's required are ambiguous. There are a lot of args to out scripts and I don't want to lookup/make everything to setup timeouts.

I prefer to agent_timeout and resource_timeout as args. If passing timeouts is a problem for many scripts, maybe we want to create a timeout object that has default timeouts for everything and we can override it in scripts and tests.

review: Needs Information (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

We agreed on IRC to replace args with agent_timeout and resource_timeout.

review: Approve (code)
1419. By Seman

Replaced args with agent_timeout and resource_timeout.

Revision history for this message
Seman (sseman) wrote :

Replaced args with agent_timeout and resource_timeout.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'assess_resources.py'
2--- assess_resources.py 2016-05-19 03:28:29 +0000
3+++ assess_resources.py 2016-05-24 22:43:31 +0000
4@@ -6,6 +6,7 @@
5 import logging
6 import os
7 import sys
8+from tempfile import NamedTemporaryFile
9
10 from deploy_stack import (
11 BootstrapManager,
12@@ -56,38 +57,95 @@
13 raise JujuAssertionError('Resource id not found.')
14
15
16-def push_resource(client, resource_name, finger_print, size, deploy=True):
17+def push_resource(client, resource_name, finger_print, size, agent_timeout,
18+ resource_timeout, deploy=True, resource_file=None):
19 charm_name = 'dummy-resource'
20 charm_path = local_charm_path(charm=charm_name, juju_ver=client.version)
21- resource_file = os.path.join(charm_path, '{}.txt'.format(resource_name))
22+ if resource_file is None:
23+ resource_file = os.path.join(
24+ charm_path, '{}.txt'.format(resource_name))
25+ else:
26+ resource_file = os.path.join(charm_path, resource_file)
27 resource_arg = '{}={}'.format(resource_name, resource_file)
28- log.info("Deploy charm with resource {}".format(resource_name))
29+ log.info("Deploy charm with resource {} Size: {} File: {}".format(
30+ resource_name, size, resource_file))
31 if deploy:
32 client.deploy(charm_path, resource=resource_arg)
33 else:
34 client.attach(charm_name, resource=resource_arg)
35- # TODO: maybe need to add a wait until resource get is executed.
36- client.wait_for_started()
37+ client.wait_for_started(timeout=agent_timeout)
38+ resource_id = '{}/{}'.format(charm_name, resource_name)
39+ client.wait_for_resource(
40+ resource_id, charm_name, timeout=resource_timeout)
41 status = client.list_resources(charm_name)
42- resource_id = '{}/{}'.format(charm_name, resource_name)
43 verify_status(status, resource_id, resource_name, finger_print, size)
44-
45-
46-def assess_resources(client):
47+ client.show_status()
48+
49+
50+def fill_dummy_file(file_path, size):
51+ with open(file_path, "wb") as f:
52+ f.seek(size - 1)
53+ f.write('\0')
54+
55+
56+def large_assess(client, agent_timeout, resource_timeout):
57+ tests = [
58+ {"size": 1024 * 1024 * 10,
59+ "finger_print": ('d7c014629d74ae132cc9f88e3ec2f31652f40a7a1fcc52c54b'
60+ '04d6c0d089169bcd55958d1277b4cdf6262f21c712d0a7')},
61+ {"size": 1024 * 1024 * 100,
62+ "finger_print": ('c11e93892b66de781e4d0883efe10482f8d1642f3b6574ba2e'
63+ 'e0da6f8db03f53c0eadfb5e5e0463574c113024ded369e')},
64+ {"size": 1024 * 1024 * 200,
65+ "finger_print": ('77db39eca74c6205e31a7701e488a1df4b9b38a527a6084bd'
66+ 'bb6843fd430a0b51047378ee0255e633b32c0dda3cf43ab')}]
67+ for test in tests:
68+ with NamedTemporaryFile(suffix=".txt") as temp_file:
69+ fill_dummy_file(temp_file.name, size=test['size'])
70+ push_resource(
71+ client, 'bar', test['finger_print'], test['size'],
72+ agent_timeout, resource_timeout, deploy=False,
73+ resource_file=temp_file.name)
74+
75+
76+def assess_resources(client, args):
77 finger_print = ('4ddc48627c6404e538bb0957632ef68618c0839649d9ad9e41ad94472'
78 'c1589f4b7f9d830df6c4b209d7eb1b4b5522c4d')
79 size = 27
80- push_resource(client, 'foo', finger_print, size)
81+ push_resource(client, 'foo', finger_print, size, args.agent_timeout,
82+ args.resource_timeout)
83 finger_print = ('ffbf43d68a6960de63908bb05c14a026abeda136119d3797431bdd7b'
84 '469c1f027e57a28aeec0df01a792e9e70aad2d6b')
85 size = 17
86- push_resource(client, 'bar', finger_print, size, deploy=False)
87+ push_resource(client, 'bar', finger_print, size, args.agent_timeout,
88+ args.resource_timeout, deploy=False)
89+ finger_print = ('2a3821585efcccff1562efea4514dd860cd536441954e182a764991'
90+ '0e21f6a179a015677a68a351a11d3d2f277e551e4')
91+ size = 27
92+ push_resource(client, 'bar', finger_print, size, args.agent_timeout,
93+ args.resource_timeout, deploy=False, resource_file='baz.txt')
94+ with NamedTemporaryFile(suffix=".txt") as temp_file:
95+ size = 1024 * 1024
96+ finger_print = ('3164673a8ac27576ab5fc06b9adc4ce0aca5bd3025384b1cf2128'
97+ 'a8795e747c431e882785a0bf8dc70b42995db388575')
98+ fill_dummy_file(temp_file.name, size=size)
99+ push_resource(client, 'bar', finger_print, size, args.agent_timeout,
100+ args.resource_timeout, deploy=False,
101+ resource_file=temp_file.name)
102+ if args.large_test_enabled:
103+ large_assess(client, args.agent_timeout, args.resource_timeout)
104
105
106 def parse_args(argv):
107 """Parse all arguments."""
108 parser = argparse.ArgumentParser(description="Assess resources")
109 add_basic_testing_arguments(parser)
110+ parser.add_argument('--large-test-enabled', action='store_true',
111+ help="Uses large file for testing.")
112+ parser.add_argument('--agent-timeout', type=int, default=1800,
113+ help='The time to wait for agents to start')
114+ parser.add_argument('--resource-timeout', type=int, default=1800,
115+ help='The time to wait for agents to start')
116 return parser.parse_args(argv)
117
118
119@@ -96,7 +154,7 @@
120 configure_logging(args.verbose)
121 bs_manager = BootstrapManager.from_args(args)
122 with bs_manager.booted_context(args.upload_tools):
123- assess_resources(bs_manager.client)
124+ assess_resources(bs_manager.client, args)
125 return 0
126
127
128
129=== modified file 'jujupy.py'
130--- jujupy.py 2016-05-23 01:34:59 +0000
131+++ jujupy.py 2016-05-24 22:43:31 +0000
132@@ -36,6 +36,7 @@
133 ensure_deleted,
134 ensure_dir,
135 is_ipv6_address,
136+ JujuResourceTimeout,
137 pause,
138 quote,
139 scoped_environ,
140@@ -910,6 +911,21 @@
141 args = args + ('--details',)
142 return yaml_loads(self.get_juju_output('list-resources', *args))
143
144+ def wait_for_resource(self, resource_id, service_or_unit, timeout=60):
145+ log.info('Waiting for resource. Resource id:{}'.format(resource_id))
146+ for _ in until_timeout(timeout):
147+ resources = self.list_resources(service_or_unit)['resources']
148+ for resource in resources:
149+ if resource['expected']['resourceid'] == resource_id:
150+ if (resource['expected']['fingerprint'] ==
151+ resource['unit']['fingerprint']):
152+ return
153+ time.sleep(.1)
154+ raise JujuResourceTimeout(
155+ 'Timeout waiting for a resource to be downloaded. '
156+ 'ResourceId: {} Service or Unit: {} Timeout: {}'.format(
157+ resource_id, service_or_unit, timeout))
158+
159 def upgrade_charm(self, service, charm_path=None):
160 args = (service,)
161 if charm_path is not None:
162
163=== modified file 'tests/test_assess_resources.py'
164--- tests/test_assess_resources.py 2016-05-19 03:28:29 +0000
165+++ tests/test_assess_resources.py 2016-05-24 22:43:31 +0000
166@@ -1,10 +1,11 @@
167-import copy
168 import logging
169+from argparse import Namespace
170 from mock import Mock, patch, call
171 import StringIO
172
173 from assess_resources import (
174 assess_resources,
175+ large_assess,
176 parse_args,
177 push_resource,
178 main,
179@@ -57,36 +58,24 @@
180 mock_e.assert_called_once_with("an-env")
181 mock_c.assert_called_once_with(env, "/bin/juju", debug=False)
182 self.assertEqual(mock_bc.call_count, 1)
183- mock_assess.assert_called_once_with(client)
184-
185-
186-class TestAssess(TestCase):
187-
188- status = {'resources': [{
189- 'expected': {
190- 'origin': 'upload', 'used': True, 'description': 'foo resource.',
191- 'username': 'admin@local', 'resourceid': 'dummy-resource/foo',
192- 'name': 'foo', 'serviceid': 'dummy-resource', 'path': 'foo.txt',
193- 'fingerprint': '1234', 'type': 'file', 'size': 27},
194- 'unit': {
195- 'origin': 'upload', 'username': 'admin@local', 'used': True,
196- 'name': 'foo', 'resourceid': 'dummy-resource/foo',
197- 'serviceid': 'dummy-resource', 'fingerprint': '1234',
198- 'path': 'foo.txt', 'size': 27, 'type': 'file',
199- 'description': 'foo resource.'}}]}
200+ mock_assess.assert_called_once_with(client, make_args())
201+
202+
203+class TestAssessResources(TestCase):
204
205 def test_verify_status(self):
206- verify_status(self.status, 'dummy-resource/foo', 'foo', '1234', 27)
207+ verify_status(
208+ make_resource_list(), 'dummy-resource/foo', 'foo', '1234', 27)
209
210 def test_verify_status_exception(self):
211- status = copy.deepcopy(self.status)
212+ status = make_resource_list()
213 status['resources'][0]['expected']['origin'] = 'charmstore'
214 with self.assertRaisesRegexp(
215 JujuAssertionError, 'Unexpected resource list values'):
216 verify_status(status, 'dummy-resource/foo', 'foo', '1234', 27)
217
218 def test_verify_status_unit_exception(self):
219- status = copy.deepcopy(self.status)
220+ status = make_resource_list()
221 status['resources'][0]['unit']['origin'] = 'charmstore'
222 with self.assertRaisesRegexp(
223 JujuAssertionError, 'Unexpected unit resource list values'):
224@@ -95,38 +84,149 @@
225 def test_verify_status_no_resoruce_id_exception(self):
226 with self.assertRaisesRegexp(
227 JujuAssertionError, 'Resource id not found.'):
228- verify_status(self.status, 'NO_ID', 'foo', '1234', 27)
229+ verify_status(make_resource_list(), 'NO_ID', 'foo', '1234', 27)
230
231 def test_push_resource(self):
232 mock_client = Mock(
233- spec=["deploy", "wait_for_started", "list_resources"])
234+ spec=["deploy", "wait_for_started", "list_resources",
235+ "wait_for_resource", "show_status"])
236 mock_client.version = '2.0.0'
237- mock_client.list_resources.return_value = self.status
238- push_resource(mock_client, 'foo', '1234', 27)
239+ mock_client.list_resources.return_value = make_resource_list()
240+ push_resource(mock_client, 'foo', '1234', 27, 1800, 1800)
241 mock_client.deploy.assert_called_once_with(
242 'dummy-resource', resource='foo=dummy-resource/foo.txt')
243- mock_client.wait_for_started.assert_called_once_with()
244+ mock_client.wait_for_started.assert_called_once_with(timeout=1800)
245+ mock_client.show_status.assert_called_once_with()
246
247 def test_push_resource_attach(self):
248 mock_client = Mock(
249- spec=["attach", "wait_for_started", "list_resources"])
250+ spec=["attach", "wait_for_started", "list_resources",
251+ "wait_for_resource", "show_status"])
252 mock_client.version = '2.0.0'
253- mock_client.list_resources.return_value = self.status
254- push_resource(mock_client, 'foo', '1234', 27, deploy=False)
255+ mock_client.list_resources.return_value = make_resource_list()
256+ push_resource(mock_client, 'foo', '1234', 27, 1800, 1800, deploy=False)
257 mock_client.attach.assert_called_once_with(
258 'dummy-resource', resource='foo=dummy-resource/foo.txt')
259- mock_client.wait_for_started.assert_called_once_with()
260+ mock_client.wait_for_started.assert_called_once_with(timeout=1800)
261
262 def test_assess_resources(self):
263- with patch("assess_resources.push_resource", autospec=True) as mock_p:
264- assess_resources(None)
265- calls = [
266- call(
267- None, 'foo',
268- '4ddc48627c6404e538bb0957632ef68618c0839649d9ad9e41ad94472c158'
269- '9f4b7f9d830df6c4b209d7eb1b4b5522c4d', 27),
270- call(
271- None, 'bar',
272- 'ffbf43d68a6960de63908bb05c14a026abeda136119d3797431bdd7b469c1'
273- 'f027e57a28aeec0df01a792e9e70aad2d6b', 17, deploy=False)]
274- self.assertEqual(mock_p.mock_calls, calls)
275+ fake_file = FakeFile()
276+ args = make_args()
277+ with patch("assess_resources.push_resource", autospec=True) as mock_p:
278+ with patch('assess_resources.NamedTemporaryFile',
279+ autospec=True) as mock_ntf:
280+ mock_ntf.return_value.__enter__.return_value = fake_file
281+ assess_resources(None, args)
282+ calls = [
283+ call(
284+ None, 'foo',
285+ '4ddc48627c6404e538bb0957632ef68618c0839649d9ad9e41ad94472c158'
286+ '9f4b7f9d830df6c4b209d7eb1b4b5522c4d', 27, 1800, 1800),
287+ call(
288+ None, 'bar',
289+ 'ffbf43d68a6960de63908bb05c14a026abeda136119d3797431bdd7b469c1'
290+ 'f027e57a28aeec0df01a792e9e70aad2d6b', 17, 1800, 1800,
291+ deploy=False),
292+ call(
293+ None, 'bar',
294+ '2a3821585efcccff1562efea4514dd860cd536441954e182a7649910e21f6'
295+ 'a179a015677a68a351a11d3d2f277e551e4', 27, 1800, 1800,
296+ deploy=False, resource_file='baz.txt'),
297+ call(
298+ None, 'bar',
299+ '3164673a8ac27576ab5fc06b9adc4ce0aca5bd3025384b1cf2128a8795e74'
300+ '7c431e882785a0bf8dc70b42995db388575', 1024 * 1024, 1800, 1800,
301+ deploy=False, resource_file='/tmp/fake'),
302+ ]
303+ self.assertEqual(mock_p.mock_calls, calls)
304+
305+ def test_assess_resources_large_test(self):
306+ fake_file = FakeFile()
307+ args = make_args()
308+ args.large_test_enabled = True
309+ with patch("assess_resources.push_resource", autospec=True) as mock_p:
310+ with patch('assess_resources.fill_dummy_file',
311+ autospec=True) as mock_fdf:
312+ with patch('assess_resources.NamedTemporaryFile',
313+ autospec=True) as mock_ntf:
314+ mock_ntf.return_value.__enter__.return_value = fake_file
315+ with patch('assess_resources.large_assess',
316+ autospec=True) as mock_lt:
317+ assess_resources(None, args)
318+ calls = [
319+ call(
320+ None, 'foo',
321+ '4ddc48627c6404e538bb0957632ef68618c0839649d9ad9e41ad94472c158'
322+ '9f4b7f9d830df6c4b209d7eb1b4b5522c4d', 27, 1800, 1800),
323+ call(
324+ None, 'bar',
325+ 'ffbf43d68a6960de63908bb05c14a026abeda136119d3797431bdd7b469c1'
326+ 'f027e57a28aeec0df01a792e9e70aad2d6b', 17, 1800, 1800,
327+ deploy=False),
328+ call(
329+ None, 'bar',
330+ '2a3821585efcccff1562efea4514dd860cd536441954e182a7649910e21f6'
331+ 'a179a015677a68a351a11d3d2f277e551e4', 27, 1800, 1800,
332+ deploy=False, resource_file='baz.txt'),
333+ call(None, 'bar',
334+ '3164673a8ac27576ab5fc06b9adc4ce0aca5bd3025384b1cf2128a8795e7'
335+ '47c431e882785a0bf8dc70b42995db388575', 1024 * 1024, 1800,
336+ 1800, deploy=False, resource_file='/tmp/fake')]
337+ self.assertEqual(mock_p.mock_calls, calls)
338+ mock_fdf.assert_called_once_with('/tmp/fake', 1024 * 1024)
339+ mock_lt.assert_called_once_with(None, 1800, 1800)
340+
341+ def test_large_tests(self):
342+ fake_file = FakeFile()
343+ with patch("assess_resources.push_resource", autospec=True) as mock_pr:
344+ with patch('assess_resources.NamedTemporaryFile',
345+ autospec=True) as mock_ntf:
346+ mock_ntf.return_value.__enter__.return_value = fake_file
347+ with patch('assess_resources.fill_dummy_file',
348+ autospec=True):
349+ large_assess(None, 1800, 1800)
350+ calls = [
351+ call(
352+ None, 'bar',
353+ 'd7c014629d74ae132cc9f88e3ec2f31652f40a7a1fcc52c54b04d6c0d0891'
354+ '69bcd55958d1277b4cdf6262f21c712d0a7', 1024 * 1024 * 10, 1800,
355+ 1800, deploy=False, resource_file='/tmp/fake'),
356+ call(
357+ None, 'bar',
358+ 'c11e93892b66de781e4d0883efe10482f8d1642f3b6574ba2ee0da6f8db03'
359+ 'f53c0eadfb5e5e0463574c113024ded369e', 1024 * 1024 * 100, 1800,
360+ 1800, deploy=False, resource_file='/tmp/fake'),
361+ call(
362+ None, 'bar',
363+ '77db39eca74c6205e31a7701e488a1df4b9b38a527a6084bdbb6843fd430a'
364+ '0b51047378ee0255e633b32c0dda3cf43ab', 1024 * 1024 * 200, 1800,
365+ 1800, deploy=False, resource_file='/tmp/fake')]
366+ self.assertEqual(mock_pr.mock_calls, calls)
367+
368+
369+def make_args():
370+ return Namespace(
371+ agent_stream=None, agent_timeout=1800, agent_url=None,
372+ bootstrap_host=None, debug=False, env='an-env', juju_bin='/bin/juju',
373+ keep_env=False, large_test_enabled=False, logs='/tmp/logs', machine=[],
374+ region=None, resource_timeout=1800, series=None,
375+ temp_env_name='an-env-mod', upload_tools=False, verbose=10)
376+
377+
378+def make_resource_list():
379+ return {'resources': [{
380+ 'expected': {
381+ 'origin': 'upload', 'used': True, 'description': 'foo resource.',
382+ 'username': 'admin@local', 'resourceid': 'dummy-resource/foo',
383+ 'name': 'foo', 'serviceid': 'dummy-resource', 'path': 'foo.txt',
384+ 'fingerprint': '1234', 'type': 'file', 'size': 27},
385+ 'unit': {
386+ 'origin': 'upload', 'username': 'admin@local', 'used': True,
387+ 'name': 'foo', 'resourceid': 'dummy-resource/foo',
388+ 'serviceid': 'dummy-resource', 'fingerprint': '1234',
389+ 'path': 'foo.txt', 'size': 27, 'type': 'file',
390+ 'description': 'foo resource.'}}]}
391+
392+
393+class FakeFile:
394+ name = '/tmp/fake'
395
396=== modified file 'tests/test_jujupy.py'
397--- tests/test_jujupy.py 2016-05-23 03:38:07 +0000
398+++ tests/test_jujupy.py 2016-05-24 22:43:31 +0000
399@@ -79,7 +79,9 @@
400 TestCase,
401 FakeHomeTestCase,
402 )
403+from tests.test_assess_resources import make_resource_list
404 from utility import (
405+ JujuResourceTimeout,
406 scoped_environ,
407 temp_dir,
408 )
409@@ -1609,6 +1611,33 @@
410 mock_gjo.assert_called_with(
411 'list-resources', '--format', 'yaml', 'foo', '--details')
412
413+ def test_wait_for_resource(self):
414+ client = EnvJujuClient(JujuData('local'), None, None)
415+ with patch.object(
416+ client, 'list_resources',
417+ return_value=make_resource_list()) as mock_lr:
418+ client.wait_for_resource('dummy-resource/foo', 'foo')
419+ mock_lr.assert_called_once_with('foo')
420+
421+ def test_wait_for_resource_timeout(self):
422+ client = EnvJujuClient(JujuData('local'), None, None)
423+ resource_list = make_resource_list()
424+ resource_list['resources'][0]['expected']['resourceid'] = 'bad_id'
425+ with patch.object(
426+ client, 'list_resources',
427+ return_value=resource_list) as mock_lr:
428+ with patch('jujupy.until_timeout', autospec=True,
429+ return_value=[0, 1]) as mock_ju:
430+ with patch('time.sleep', autospec=True) as mock_ts:
431+ with self.assertRaisesRegexp(
432+ JujuResourceTimeout,
433+ 'Timeout waiting for a resource to be downloaded'):
434+ client.wait_for_resource('dummy-resource/foo', 'foo')
435+ calls = [call('foo'), call('foo')]
436+ self.assertEqual(mock_lr.mock_calls, calls)
437+ self.assertEqual(mock_ts.mock_calls, [call(.1), call(.1)])
438+ self.assertEqual(mock_ju.mock_calls, [call(60)])
439+
440 def test_deploy_bundle_2x(self):
441 client = EnvJujuClient(JujuData('an_env', None),
442 '1.23-series-arch', None)
443
444=== modified file 'utility.py'
445--- utility.py 2016-05-19 03:28:29 +0000
446+++ utility.py 2016-05-24 22:43:31 +0000
447@@ -97,6 +97,10 @@
448 """Exception for juju assertion failures."""
449
450
451+class JujuResourceTimeout(Exception):
452+ """A timeout exception for a resource not being downloaded into a unit."""
453+
454+
455 class enforce_juju_path(argparse.Action):
456 """Enforces that a path ending with juju is given."""
457 def __call__(self, parser, namespace, values, option_string=None):

Subscribers

People subscribed via source and target branches