Merge lp:~johnsca/charm-helpers/provide-data-fix into lp:charm-helpers

Proposed by Cory Johns on 2015-05-20
Status: Merged
Merged at revision: 376
Proposed branch: lp:~johnsca/charm-helpers/provide-data-fix
Merge into: lp:charm-helpers
Diff against target: 191 lines (+83/-55)
4 files modified
VERSION (+1/-1)
charmhelpers/core/services/base.py (+30/-9)
scripts/update-revno (+2/-2)
tests/core/test_services.py (+50/-43)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/provide-data-fix
Reviewer Review Type Date Requested Status
Tim Van Steenburgh Approve on 2015-05-20
Nick Moffitt 2015-05-20 Approve on 2015-05-20
Review via email: mp+259630@code.launchpad.net

Description of the Change

Fix issues with provide_data in services framework:

  * Providing data is gated on data being complete, with no option for sending partial data (necessary for two-way communication)

  * Providing data is gated on current hook (no way to, e.g., send new data out in response to a config-changed hook)

  * Providing data happens before data_ready with no way of telling if data_ready was triggered

I also fixed an issue with update-revno where the generated version.py was failing lint.

To post a comment you must log in.
377. By Cory Johns on 2015-05-20

Removed class type check

review: Approve
Tim Van Steenburgh (tvansteenburgh) wrote :

+1 LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'VERSION'
2--- VERSION 2015-04-27 11:27:24 +0000
3+++ VERSION 2015-05-20 14:52:28 +0000
4@@ -1,1 +1,1 @@
5-0.3.1
6+0.3.2
7
8=== modified file 'charmhelpers/core/services/base.py'
9--- charmhelpers/core/services/base.py 2015-01-22 13:56:24 +0000
10+++ charmhelpers/core/services/base.py 2015-05-20 14:52:28 +0000
11@@ -15,8 +15,8 @@
12 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
13
14 import os
15-import re
16 import json
17+from inspect import getargspec
18 from collections import Iterable, OrderedDict
19
20 from charmhelpers.core import host
21@@ -132,8 +132,8 @@
22 if hook_name == 'stop':
23 self.stop_services()
24 else:
25+ self.reconfigure_services()
26 self.provide_data()
27- self.reconfigure_services()
28 cfg = hookenv.config()
29 if cfg.implicit_save:
30 cfg.save()
31@@ -145,15 +145,36 @@
32 A provider must have a `name` attribute, which indicates which relation
33 to set data on, and a `provide_data()` method, which returns a dict of
34 data to set.
35+
36+ The `provide_data()` method can optionally accept two parameters:
37+
38+ * ``remote_service`` The name of the remote service that the data will
39+ be provided to. The `provide_data()` method will be called once
40+ for each connected service (not unit). This allows the method to
41+ tailor its data to the given service.
42+ * ``service_ready`` Whether or not the service definition had all of
43+ its requirements met, and thus the ``data_ready`` callbacks run.
44+
45+ Note that the ``provided_data`` methods are now called **after** the
46+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
47+ a chance to generate any data necessary for the providing to the remote
48+ services.
49 """
50- hook_name = hookenv.hook_name()
51- for service in self.services.values():
52+ for service_name, service in self.services.items():
53+ service_ready = self.is_ready(service_name)
54 for provider in service.get('provided_data', []):
55- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
56- data = provider.provide_data()
57- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
58- if _ready:
59- hookenv.relation_set(None, data)
60+ for relid in hookenv.relation_ids(provider.name):
61+ units = hookenv.related_units(relid)
62+ if not units:
63+ continue
64+ remote_service = units[0].split('/')[0]
65+ argspec = getargspec(provider.provide_data)
66+ if len(argspec.args) > 1:
67+ data = provider.provide_data(remote_service, service_ready)
68+ else:
69+ data = provider.provide_data()
70+ if data:
71+ hookenv.relation_set(relid, data)
72
73 def reconfigure_services(self, *service_names):
74 """
75
76=== modified file 'scripts/update-revno'
77--- scripts/update-revno 2013-05-11 20:24:29 +0000
78+++ scripts/update-revno 2015-05-20 14:52:28 +0000
79@@ -6,6 +6,6 @@
80 REVNO="${REVNO}+"
81 fi
82 cat << EOF > charmhelpers/version.py
83-CHARMHELPERS_VERSION='${VERSION}'
84-CHARMHELPERS_BZRREVNO='${REVNO}'
85+CHARMHELPERS_VERSION = '${VERSION}'
86+CHARMHELPERS_BZRREVNO = '${REVNO}'
87 EOF
88
89=== modified file 'tests/core/test_services.py'
90--- tests/core/test_services.py 2015-04-03 14:53:10 +0000
91+++ tests/core/test_services.py 2015-05-20 14:52:28 +0000
92@@ -378,49 +378,56 @@
93 assert not manager.was_ready('bar')
94
95 @mock.patch.object(services.base.hookenv, 'relation_set')
96- @mock.patch.object(services.base.hookenv, 'hook_name')
97- def test_provide_data_no_match(self, hook_name, relation_set):
98- provider = mock.Mock()
99- provider.name = 'provided'
100- manager = services.ServiceManager([
101- {'service': 'service', 'provided_data': [provider]}
102- ])
103- hook_name.return_value = 'not-provided-relation-joined'
104- manager.provide_data()
105- assert not provider.provide_data.called
106-
107- hook_name.return_value = 'provided-relation-broken'
108- manager.provide_data()
109- assert not provider.provide_data.called
110-
111- @mock.patch.object(services.base.hookenv, 'relation_set')
112- @mock.patch.object(services.base.hookenv, 'hook_name')
113- def test_provide_data_not_ready(self, hook_name, relation_set):
114- provider = mock.Mock()
115- provider.name = 'provided'
116- data = provider.provide_data.return_value = {'data': True}
117- provider._is_ready.return_value = False
118- manager = services.ServiceManager([
119- {'service': 'service', 'provided_data': [provider]}
120- ])
121- hook_name.return_value = 'provided-relation-joined'
122- manager.provide_data()
123- assert not relation_set.called
124- provider._is_ready.assert_called_once_with(data)
125-
126- @mock.patch.object(services.base.hookenv, 'relation_set')
127- @mock.patch.object(services.base.hookenv, 'hook_name')
128- def test_provide_data_ready(self, hook_name, relation_set):
129- provider = mock.Mock()
130- provider.name = 'provided'
131- data = provider.provide_data.return_value = {'data': True}
132- provider._is_ready.return_value = True
133- manager = services.ServiceManager([
134- {'service': 'service', 'provided_data': [provider]}
135- ])
136- hook_name.return_value = 'provided-relation-changed'
137- manager.provide_data()
138- relation_set.assert_called_once_with(None, data)
139+ @mock.patch.object(services.base.hookenv, 'related_units')
140+ @mock.patch.object(services.base.hookenv, 'relation_ids')
141+ def test_provide_data_no_match(self, relation_ids, related_units, relation_set):
142+ provider = mock.Mock()
143+ provider.name = 'provided'
144+ manager = services.ServiceManager([
145+ {'service': 'service', 'provided_data': [provider]}
146+ ])
147+ relation_ids.return_value = []
148+ manager.provide_data()
149+ assert not provider.provide_data.called
150+ relation_ids.assert_called_once_with('provided')
151+
152+ @mock.patch.object(services.base.hookenv, 'relation_set')
153+ @mock.patch.object(services.base.hookenv, 'related_units')
154+ @mock.patch.object(services.base.hookenv, 'relation_ids')
155+ def test_provide_data_not_ready(self, relation_ids, related_units, relation_set):
156+ provider = mock.Mock()
157+ provider.name = 'provided'
158+ pd = mock.Mock()
159+ data = pd.return_value = {'data': True}
160+ provider.provide_data = lambda remote_service, service_ready: pd(remote_service, service_ready)
161+ manager = services.ServiceManager([
162+ {'service': 'service', 'provided_data': [provider]}
163+ ])
164+ manager.is_ready = mock.Mock(return_value=False)
165+ relation_ids.return_value = ['relid']
166+ related_units.return_value = ['service/0']
167+ manager.provide_data()
168+ relation_set.assert_called_once_with('relid', data)
169+ pd.assert_called_once_with('service', False)
170+
171+ @mock.patch.object(services.base.hookenv, 'relation_set')
172+ @mock.patch.object(services.base.hookenv, 'related_units')
173+ @mock.patch.object(services.base.hookenv, 'relation_ids')
174+ def test_provide_data_ready(self, relation_ids, related_units, relation_set):
175+ provider = mock.Mock()
176+ provider.name = 'provided'
177+ pd = mock.Mock()
178+ data = pd.return_value = {'data': True}
179+ provider.provide_data = lambda remote_service, service_ready: pd(remote_service, service_ready)
180+ manager = services.ServiceManager([
181+ {'service': 'service', 'provided_data': [provider]}
182+ ])
183+ manager.is_ready = mock.Mock(return_value=True)
184+ relation_ids.return_value = ['relid']
185+ related_units.return_value = ['service/0']
186+ manager.provide_data()
187+ relation_set.assert_called_once_with('relid', data)
188+ pd.assert_called_once_with('service', True)
189
190
191 class TestRelationContext(unittest.TestCase):

Subscribers

People subscribed via source and target branches