Merge lp:~jjo/juju-deployer/diff-usability-fixes_and_diff-tests into lp:juju-deployer

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
Reviewer Review Type Date Requested Status
Kapil Thangavelu Approve
Review via email: mp+197271@code.launchpad.net

Commit message

implement test_diff (using augmented env/mem.py).

To post a comment you must log in.
Revision history for this message
JuanJo Ciarlante (jjo) wrote :

To ease review, I stacked this over lp:~jjo/juju-deployer/diff-usability-fixes

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

lgtm

review: Approve

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'])

Subscribers

People subscribed via source and target branches