Merge lp:~jjo/juju-deployer/diff-usability-fixes_and_diff-tests into lp:juju-deployer
- diff-usability-fixes_and_diff-tests
- Merge into trunk
Proposed by
JuanJo Ciarlante
Status: | Merged |
---|---|
Merged at revision: | 90 |
Proposed branch: | lp:~jjo/juju-deployer/diff-usability-fixes_and_diff-tests |
Merge into: | lp:juju-deployer |
Diff against target: |
429 lines (+286/-35) 7 files modified
deployer/action/diff.py (+14/-9) deployer/env/go.py (+1/-1) deployer/env/mem.py (+136/-15) deployer/tests/test_data/blog-haproxy-services.yaml (+14/-0) deployer/tests/test_data/blog.yaml (+9/-9) deployer/tests/test_deployment.py (+1/-1) deployer/tests/test_diff.py (+111/-0) |
To merge this branch: | bzr merge lp:~jjo/juju-deployer/diff-usability-fixes_and_diff-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu | Approve | ||
Review via email:
|
Commit message
implement test_diff (using augmented env/mem.py).
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JuanJo Ciarlante (jjo) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'deployer/action/diff.py' |
2 | --- deployer/action/diff.py 2013-11-21 03:19:56 +0000 |
3 | +++ deployer/action/diff.py 2013-11-29 22:24:49 +0000 |
4 | @@ -122,10 +122,10 @@ |
5 | |
6 | def _diff_service(self, e_s, d_s, charm): |
7 | mod = {} |
8 | - if 'constraints' in d_s: |
9 | - d_sc = parse_constraints(d_s['constraints']) |
10 | - if d_sc != e_s['constraints']: |
11 | - mod['constraints'] = e_s['constraints'] |
12 | + d_sc = parse_constraints(d_s.get('constraints','')) |
13 | + if d_sc != e_s['constraints']: |
14 | + mod['env-constraints'] = e_s['constraints'] |
15 | + mod['cfg-constraints'] = d_sc |
16 | for k, v in d_s.get('options', {}).items(): |
17 | # Deploy options not known to the env may originate |
18 | # from charm version delta or be an invalid config. |
19 | @@ -133,17 +133,22 @@ |
20 | continue |
21 | e_v = e_s['options'].get(k, {}).get('value') |
22 | if e_v != v: |
23 | - mod['config'] = {k: e_v} |
24 | + mod.setdefault('env-config', {}).update({k: e_v}) |
25 | + mod.setdefault('cfg-config', {}).update({k: v}) |
26 | if not charm or not charm.is_subordinate(): |
27 | if e_s['unit_count'] != d_s.get('num_units', 1): |
28 | mod['num_units'] = e_s['unit_count'] - d_s.get('num_units', 1) |
29 | return mod |
30 | |
31 | - def run(self): |
32 | + def do_diff(self): |
33 | self.start_time = time.time() |
34 | + self.deployment.resolve() |
35 | self.env.connect() |
36 | self.env_status = self.env.status() |
37 | self.load_env() |
38 | - delta = self.get_delta() |
39 | - if delta: |
40 | - print yaml_dump(delta) |
41 | + return self.get_delta() |
42 | + |
43 | + def run(self): |
44 | + diff = self.do_diff() |
45 | + if diff: |
46 | + print yaml_dump(diff) |
47 | |
48 | === modified file 'deployer/env/go.py' |
49 | --- deployer/env/go.py 2013-11-15 15:13:01 +0000 |
50 | +++ deployer/env/go.py 2013-11-29 22:24:49 +0000 |
51 | @@ -62,7 +62,7 @@ |
52 | return self.client.get_constraints(svc_name) |
53 | except EnvError, e: |
54 | if 'constraints do not apply to subordinate services' in str(e): |
55 | - return None |
56 | + return {} |
57 | else: |
58 | raise e |
59 | |
60 | |
61 | === modified file 'deployer/env/mem.py' |
62 | --- deployer/env/mem.py 2013-07-22 15:29:31 +0000 |
63 | +++ deployer/env/mem.py 2013-11-29 22:24:49 +0000 |
64 | @@ -1,26 +1,64 @@ |
65 | +from deployer.utils import parse_constraints |
66 | +from jujuclient import (UnitErrors, |
67 | + EnvError) |
68 | |
69 | |
70 | class MemoryEnvironment(object): |
71 | - |
72 | - def __init__(self): |
73 | - self.data = {} |
74 | - |
75 | - def add_units(self, service_name, num_units): |
76 | + """ In memory deployment: not all features implemented |
77 | + (notably subordinates and their relations). |
78 | + """ |
79 | + |
80 | + def __init__(self, name, deployment): |
81 | + """ Two main dicts: _services (return-able as part of status(), |
82 | + and _services_data (to hold e.g. config, constraints) |
83 | + """ |
84 | + super(MemoryEnvironment, self).__init__() |
85 | + self.name = name |
86 | + self._deployment = deployment |
87 | + self._services = {} |
88 | + self._services_data = {} |
89 | + self._relations = {} |
90 | + self._do_deploy() |
91 | + |
92 | + def add_units(self, svc_name, num): |
93 | """Add units |
94 | """ |
95 | - for n in range(num_units): |
96 | - self.data['services'][service_name] |
97 | - |
98 | - def _get_service(self, service_name): |
99 | - if not service_name in self.data['services']: |
100 | + next_num = self._services_data[svc_name]['next_unit_num'] |
101 | + for idx in xrange(next_num, next_num + num): |
102 | + self._services[svc_name]['units'].append( |
103 | + '{}/{}'.format(svc_name, idx)) |
104 | + self._services_data[svc_name]['next_unit_num'] = \ |
105 | + next_num + num |
106 | + |
107 | + def remove_unit(self, unit_name): |
108 | + """ Remove a unit by name """ |
109 | + svc_name = unit_name.split('/')[0] |
110 | + units_idx = {unit: idx for idx, unit in |
111 | + enumerate(self._services[svc_name]['units'])} |
112 | + try: |
113 | + self._services[svc_name]['units'].pop( |
114 | + units_idx[unit_name]) |
115 | + except KeyError: |
116 | + raise UnitErrors("Invalid unit name") |
117 | + |
118 | + def _get_service(self, svc_name): |
119 | + """ Get service by name (as returned by status()) |
120 | + """ |
121 | + if not svc_name in self._services: |
122 | raise EnvError("Invalid service name") |
123 | - idx = self.data['services'][service_name]['unit_sequence'] |
124 | - return |
125 | + return self._services[svc_name] |
126 | |
127 | def add_relation(self, endpoint_a, endpoint_b): |
128 | """Add relations |
129 | """ |
130 | |
131 | + def destroy_service(self, svc_name): |
132 | + """ Destroy a service |
133 | + """ |
134 | + if not svc_name in self._services: |
135 | + raise EnvError("Invalid service name") |
136 | + del self._services[svc_name] |
137 | + |
138 | def close(self): |
139 | """ |
140 | """ |
141 | @@ -29,11 +67,33 @@ |
142 | """ |
143 | """ |
144 | |
145 | + def set_config(self, svc_name, cfg_dict): |
146 | + """ Set service config from passed dict, keeping |
147 | + the structure as needed for status() return |
148 | + """ |
149 | + config = self.get_config(svc_name) |
150 | + if cfg_dict: |
151 | + for cfg_k, cfg_v in cfg_dict.items(): |
152 | + config_entry = config.setdefault(cfg_k, {}) |
153 | + config_entry['value'] = cfg_v |
154 | + |
155 | + def set_constraints(self, svc_name, constr_str): |
156 | + """ Set service constraints from "key=value ..." |
157 | + passed string |
158 | + """ |
159 | + constraints = parse_constraints(constr_str) if constr_str else {} |
160 | + self._services_data[svc_name]['constraints'] = constraints |
161 | + |
162 | def get_config(self, svc_name): |
163 | - pass |
164 | + """ Return service configs - note its structure: |
165 | + config{thename: {'value': thevalue}, ...} |
166 | + """ |
167 | + return self._services_data[svc_name]['config'] |
168 | |
169 | def get_constraints(self, svc_name): |
170 | - pass |
171 | + """ Return service constraints dict |
172 | + """ |
173 | + return self._services_data[svc_name]['constraints'] |
174 | |
175 | def get_cli_status(self): |
176 | pass |
177 | @@ -44,8 +104,69 @@ |
178 | def resolve_errors(self, retry_count=0, timeout=600, watch=False, delay=5): |
179 | pass |
180 | |
181 | + def _do_deploy(self): |
182 | + """ Fake deploy: build in-memory representation of the deployed set |
183 | + of services from deployment |
184 | + """ |
185 | + self._compile_relations() |
186 | + for service in self._deployment.get_services(): |
187 | + svc_name = service.name |
188 | + charm = self._deployment.get_charm_for(svc_name) |
189 | + relations = self._relations.setdefault(svc_name, {}) |
190 | + self._services[svc_name] = { |
191 | + 'units': [], |
192 | + 'charm': charm.name, |
193 | + 'relations': relations, |
194 | + } |
195 | + self._services_data[svc_name] = { |
196 | + 'next_unit_num': 0, |
197 | + 'config': {}, |
198 | + 'constraints': {}, |
199 | + } |
200 | + # XXX: Incomplete relations support: only add units for non-subords |
201 | + num_units = 0 if charm.is_subordinate() else service.num_units |
202 | + self.add_units(svc_name, num_units) |
203 | + self.set_config(svc_name, service.config) |
204 | + self.set_constraints(svc_name, service.constraints) |
205 | + |
206 | + def _compile_relations(self): |
207 | + """ Compile a relation dictionary by svc_name, with |
208 | + their values structured for status() return |
209 | + """ |
210 | + for rel in self._deployment.get_relations(): |
211 | + for src, dst in (rel[0], rel[1]), (rel[1], rel[0]): |
212 | + try: |
213 | + src_requires = self._deployment.get_charm_for( |
214 | + src).get_requires() |
215 | + dst_provides = self._deployment.get_charm_for( |
216 | + dst).get_provides() |
217 | + except KeyError: |
218 | + continue |
219 | + # Create dicts key-ed by: |
220 | + # { interface_type: (interface_name, svc_name)} |
221 | + src_dict = {} |
222 | + dst_dict = {} |
223 | + # {rel_name: [{interface: name }...]} |
224 | + for _, interfaces in src_requires.items(): |
225 | + for interface in interfaces: |
226 | + src_dict[interface.get('interface')] = ( |
227 | + interface.get('name'), src) |
228 | + for _, interfaces in dst_provides.items(): |
229 | + for interface in interfaces: |
230 | + dst_dict[interface.get('interface')] = ( |
231 | + interface.get('name'), dst) |
232 | + # Create juju env relation entries as: |
233 | + # {svc_name: { interface_name: [ svc_name2, ...] }, ...} |
234 | + for src_rel, (if_name, src_svc_name) in src_dict.items(): |
235 | + if src_rel in dst_dict: |
236 | + src_rels = self._relations.setdefault(src_svc_name, {}) |
237 | + src_rels_name = src_rels.setdefault(if_name, []) |
238 | + dst_svc_name = dst_dict[src_rel][1] |
239 | + src_rels[if_name].append(dst_svc_name) |
240 | + |
241 | def status(self): |
242 | - pass |
243 | + """ Return all services status """ |
244 | + return {'services': self._services} |
245 | |
246 | def wait_for_units( |
247 | self, timeout, goal_state="started", watch=False, no_exit=False): |
248 | |
249 | === added file 'deployer/tests/test_data/blog-haproxy-services.yaml' |
250 | --- deployer/tests/test_data/blog-haproxy-services.yaml 1970-01-01 00:00:00 +0000 |
251 | +++ deployer/tests/test_data/blog-haproxy-services.yaml 2013-11-29 22:24:49 +0000 |
252 | @@ -0,0 +1,14 @@ |
253 | +- service_name: blog |
254 | + service_host: 0.0.0.0 |
255 | + service_port: 80 |
256 | + service_options: |
257 | + - mode http |
258 | + - option httplog |
259 | + - balance uri |
260 | + - hash-type consistent |
261 | + - timeout client 60000 |
262 | + - timeout server 60000 |
263 | + server_options: |
264 | + - check inter 2000 |
265 | + - rise 2 |
266 | + - fall 5 |
267 | |
268 | === modified file 'deployer/tests/test_data/blog.yaml' |
269 | --- deployer/tests/test_data/blog.yaml 2013-07-22 15:29:31 +0000 |
270 | +++ deployer/tests/test_data/blog.yaml 2013-11-29 22:24:49 +0000 |
271 | @@ -28,14 +28,14 @@ |
272 | tuning: optimized |
273 | engine: apache |
274 | wp-content: include-base64://blog.snippet |
275 | - memcached: |
276 | - branch: lp:charms/precise/memcached |
277 | - options: |
278 | - size: 100 |
279 | - haproxy: |
280 | - charm_url: cs:precise/haproxy |
281 | - options: |
282 | - services: include-file://blog-include.yaml |
283 | + cache: |
284 | + branch: lp:charms/precise/memcached |
285 | + options: |
286 | + size: 100 |
287 | + haproxy: |
288 | + charm_url: cs:precise/haproxy |
289 | + options: |
290 | + services: include-file://blog-haproxy-services.yaml |
291 | relations: |
292 | - [blog, db] |
293 | - - blog |
294 | @@ -56,4 +56,4 @@ |
295 | db: |
296 | constraints: instance-type=m1.large |
297 | options: |
298 | - tuning-level: safest |
299 | \ No newline at end of file |
300 | + tuning-level: safest |
301 | |
302 | === modified file 'deployer/tests/test_deployment.py' |
303 | --- deployer/tests/test_deployment.py 2013-10-31 19:19:36 +0000 |
304 | +++ deployer/tests/test_deployment.py 2013-11-29 22:24:49 +0000 |
305 | @@ -41,7 +41,7 @@ |
306 | |
307 | # Load up overrides and resolves |
308 | d.load_overrides(["key=abc"]) |
309 | - d.resolve_config() |
310 | + d.resolve() |
311 | |
312 | # Verify include-base64 |
313 | self.assertEqual(d.get_service('newrelic').config, {'key': 'abc'}) |
314 | |
315 | === added file 'deployer/tests/test_diff.py' |
316 | --- deployer/tests/test_diff.py 1970-01-01 00:00:00 +0000 |
317 | +++ deployer/tests/test_diff.py 2013-11-29 22:24:49 +0000 |
318 | @@ -0,0 +1,111 @@ |
319 | +""" Unittest for juju-deployer diff action (--diff) """ |
320 | +# pylint: disable=C0103 |
321 | +import StringIO |
322 | +import os |
323 | +import unittest |
324 | +import shutil |
325 | +import tempfile |
326 | +from deployer.env.mem import MemoryEnvironment |
327 | +from deployer.config import ConfigStack |
328 | +from deployer.utils import setup_logging |
329 | + |
330 | +from .base import Base |
331 | +from ..action.diff import Diff |
332 | + |
333 | + |
334 | +# pylint: disable=C0111, R0904 |
335 | +@unittest.skipIf("DEB_BUILD_ARCH" in os.environ, |
336 | + "Requires configured bzr launchpad id and network access") |
337 | +class DiffTest(Base): |
338 | + |
339 | + def setUp(self): |
340 | + self.output = setup_logging( |
341 | + debug=True, verbose=True, stream=StringIO.StringIO()) |
342 | + |
343 | + # Because fetch_charms is expensive, do it once for all tests |
344 | + @classmethod |
345 | + def setUpClass(cls): |
346 | + deployment = ConfigStack( |
347 | + [os.path.join( |
348 | + cls.test_data_dir, "blog.yaml")]).get('wordpress-prod') |
349 | + cls._dir = tempfile.mkdtemp() |
350 | + os.mkdir(os.path.join(cls._dir, "precise")) |
351 | + deployment.repo_path = cls._dir |
352 | + print 'Fetching charms into "{}" ...'.format(cls._dir) |
353 | + deployment.fetch_charms() |
354 | + deployment.resolve() |
355 | + cls._deployment = deployment |
356 | + |
357 | + @classmethod |
358 | + def tearDownClass(cls): |
359 | + print 'Removing "{}"'.format(cls._dir) |
360 | + shutil.rmtree(cls._dir) |
361 | + |
362 | + @classmethod |
363 | + def get_deployment(cls): |
364 | + """ Return saved deployment at class initialization |
365 | + """ |
366 | + return cls._deployment |
367 | + |
368 | + def test_diff_nil(self): |
369 | + dpl = self.get_deployment() |
370 | + # No changes, assert nil diff |
371 | + env = MemoryEnvironment(dpl.name, dpl) |
372 | + diff = Diff(env, dpl, {}).do_diff() |
373 | + self.assertEqual(diff, {}) |
374 | + |
375 | + def test_diff_num_units(self): |
376 | + # Removing 1 unit must show -1 'num_units' |
377 | + dpl = self.get_deployment() |
378 | + env = MemoryEnvironment(dpl.name, dpl) |
379 | + env.remove_unit(env.status()['services']['haproxy']['units'][0]) |
380 | + diff = Diff(env, dpl, {}).do_diff() |
381 | + self.assertEqual( |
382 | + diff['services']['modified']['haproxy']['num_units'], -1) |
383 | + # re-adding a unit -> nil diff |
384 | + env.add_units('haproxy', 1) |
385 | + diff = Diff(env, dpl, {}).do_diff() |
386 | + self.assertEqual(diff, {}) |
387 | + |
388 | + def test_diff_config(self): |
389 | + dpl = self.get_deployment() |
390 | + env = MemoryEnvironment(dpl.name, dpl) |
391 | + env.set_config('blog', {'tuning': 'bare'}) |
392 | + diff = Diff(env, dpl, {}).do_diff() |
393 | + mod_blog = diff['services']['modified']['blog'] |
394 | + self.assertTrue(mod_blog['env-config']['tuning'] != |
395 | + mod_blog['cfg-config']['tuning']) |
396 | + self.assertEquals(mod_blog['env-config']['tuning'], 'bare') |
397 | + |
398 | + def test_diff_config_many(self): |
399 | + dpl = self.get_deployment() |
400 | + env = MemoryEnvironment(dpl.name, dpl) |
401 | + env.set_config('blog', {'tuning': 'bare', 'engine': 'duck'}) |
402 | + diff = Diff(env, dpl, {}).do_diff() |
403 | + mod_blog = diff['services']['modified']['blog'] |
404 | + self.assertEqual( |
405 | + set(mod_blog['env-config'].keys()), |
406 | + set(['tuning', 'engine'])) |
407 | + self.assertTrue(mod_blog['env-config']['tuning'] != |
408 | + mod_blog['cfg-config']['tuning']) |
409 | + self.assertTrue(mod_blog['env-config']['engine'] != |
410 | + mod_blog['cfg-config']['engine']) |
411 | + |
412 | + def test_diff_constraints(self): |
413 | + dpl = self.get_deployment() |
414 | + env = MemoryEnvironment(dpl.name, dpl) |
415 | + env.set_constraints('haproxy', 'foo=bar') |
416 | + diff = Diff(env, dpl, {}).do_diff() |
417 | + mod_haproxy = diff['services']['modified']['haproxy'] |
418 | + self.assertTrue( |
419 | + mod_haproxy['env-constraints'] != mod_haproxy['cfg-constraints']) |
420 | + self.assertEqual(mod_haproxy['env-constraints'], {'foo': 'bar'}) |
421 | + |
422 | + def test_diff_service_destroy(self): |
423 | + dpl = self.get_deployment() |
424 | + env = MemoryEnvironment(dpl.name, dpl) |
425 | + env.destroy_service('haproxy') |
426 | + diff = Diff(env, dpl, {}).do_diff() |
427 | + self.assertTrue(str(diff['relations']['missing'][0]).find('haproxy') |
428 | + != -1) |
429 | + self.assertTrue(diff['services']['missing'].keys() == ['haproxy']) |
To ease review, I stacked this over lp:~jjo/juju-deployer/diff-usability-fixes