Merge lp:~johnsca/charm-helpers/multi-unit into lp:~cf-charmers/charm-helpers/cloud-foundry

Proposed by Cory Johns
Status: Merged
Merged at revision: 183
Proposed branch: lp:~johnsca/charm-helpers/multi-unit
Merge into: lp:~cf-charmers/charm-helpers/cloud-foundry
Diff against target: 312 lines (+104/-81)
4 files modified
charmhelpers/contrib/cloudfoundry/contexts.py (+17/-6)
charmhelpers/core/services.py (+72/-53)
tests/contrib/cloudfoundry/test_render_context.py (+8/-9)
tests/core/test_services.py (+7/-13)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/multi-unit
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+221801@code.launchpad.net

Description of the change

Refactored RelationContext multi-unit support

Added a little magic to the RelationContext to make it better able to support
multiple units and still DWIM in both cases. This might be too magical,
though.

https://codereview.appspot.com/96680043/

To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) wrote :

Reviewers: mp+221801_code.launchpad.net,

Message:
Please take a look.

Description:
Refactored RelationContext multi-unit support

Added a little magic to the RelationContext to make it better able to
support
multiple units and still DWIM in both cases. This might be too magical,
though.

https://code.launchpad.net/~johnsca/charm-helpers/multi-unit/+merge/221801

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/96680043/

Affected files (+105, -28 lines):
   A [revision details]
   M charmhelpers/contrib/cloudfoundry/contexts.py
   M charmhelpers/core/services.py
   M tests/contrib/cloudfoundry/test_render_context.py
   M tests/core/test_services.py

lp:~johnsca/charm-helpers/multi-unit updated
186. By Cory Johns

Fixed issue with default unit in services framework and improved docs

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for this. I include some thoughts below, I hope they are
agreeable.

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode239
charmhelpers/core/services.py:239:
I make suggestions below on altering this process. Let me know what you
think.

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
charmhelpers/core/services.py:298: over the units, not the items of the
default unit.
There is a rule I try to use when generating/process data these days, no
data should live in the key side of the mapping. In the context of a
template for iteration is simpler to iterate

context[interface][{_unit: 'unit/0', 'foo': 'bar'},
                    {_unit: 'unit/1', 'foo': bar}]

This rule makes additional sense when thinking that knowing the unit
names to request them would imply a broken charm and thus shouldn't be
an index in this context. I'd suggest transitioning to that. We could
also make the rule (and document here) that only elements with complete
data appear in the list.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

On 2014/06/04 20:57:39, benjamin.saller wrote:

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
> charmhelpers/core/services.py:298: over the units, not the items of
the default
> unit.
> There is a rule I try to use when generating/process data these days,
no data
> should live in the key side of the mapping. In the context of a
template for
> iteration is simpler to iterate

> context[interface][{_unit: 'unit/0', 'foo': 'bar'},
> {_unit: 'unit/1', 'foo': bar}]

> This rule makes additional sense when thinking that knowing the unit
names to
> request them would imply a broken charm and thus shouldn't be an index
in this
> context. I'd suggest transitioning to that. We could also make the
rule (and
> document here) that only elements with complete data appear in the
list.

Can you explain your reasoning behind making use of the key? I'm not
averse to changing it to being a list instead of a mapping, but using a
"special" field to hold the unit name ('_unit', in your example) seems
more dangerous, since it could potentially conflict with actual fields,
even if unlikely due to the underscore prefix.

With your comment that the charm shouldn't actually know / care about
the unit name, maybe we can just leave it out altogether, especially if
it only contains "complete" data sets.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

That should be: "behind *not* making use of the key"

https://codereview.appspot.com/96680043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

On 2014/06/04 21:09:21, johnsca wrote:
> On 2014/06/04 20:57:39, benjamin.saller wrote:
> >

https://codereview.appspot.com/96680043/diff/20001/charmhelpers/core/services.py#newcode298
> > charmhelpers/core/services.py:298: over the units, not the items of
the
> default
> > unit.
> > There is a rule I try to use when generating/process data these
days, no data
> > should live in the key side of the mapping. In the context of a
template for
> > iteration is simpler to iterate
> >
> > context[interface][{_unit: 'unit/0', 'foo': 'bar'},
> > {_unit: 'unit/1', 'foo': bar}]
> >
> > This rule makes additional sense when thinking that knowing the unit
names to
> > request them would imply a broken charm and thus shouldn't be an
index in this
> > context. I'd suggest transitioning to that. We could also make the
rule (and
> > document here) that only elements with complete data appear in the
list.

> Can you explain your reasoning behind making use of the key? I'm not
averse to
> changing it to being a list instead of a mapping, but using a
"special" field to
> hold the unit name ('_unit', in your example) seems more dangerous,
since it
> could potentially conflict with actual fields, even if unlikely due to
the
> underscore prefix.

> With your comment that the charm shouldn't actually know / care about
the unit
> name, maybe we can just leave it out altogether, especially if it only
contains
> "complete" data sets.

I think for our use case we don't actually need the unitname at all and
could omit it, in the more general case I expect that there are cases
where people will want to delta the relation data vs previous hook calls
(cached data) before taking some actions.

The list vs dict thing is a pattern that I picked up working in the
machine learning space. It tends to be easier to consume and pivot flat
tuple style data. I was looking for a nice right up justifying this
pattern, its even known as "<someones> law" but I can't remember that
name :-/

However if the value in the key side of the mapping can't be known/used
its a good indicator to me that we can flatten the structure. Maybe not
very persuasive.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
187. By Cory Johns

Refactored RelationContext to be a bit more intuitive

Revision history for this message
Cory Johns (johnsca) wrote :
lp:~johnsca/charm-helpers/multi-unit updated
188. By Cory Johns

Added mapping iteration methods to RelationContext helper

Revision history for this message
Cory Johns (johnsca) wrote :
lp:~johnsca/charm-helpers/multi-unit updated
189. By Cory Johns

Added ports for log relations and cleaned up other relation keys

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for this, very close. Mostly this comes down to me wanting
another pass over the examples/docs around the multi-unit data access.

Hopefully I pointed to what I am after clearly, if not please let me
know.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py
File charmhelpers/contrib/cloudfoundry/contexts.py (right):

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode36
charmhelpers/contrib/cloudfoundry/contexts.py:36: required_keys =
['address', 'port', 'user', 'password']
You have a follow on for the charms updating the templates as well?

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode69
charmhelpers/contrib/cloudfoundry/contexts.py:69: required_keys =
['hostname', 'port']
I am a little cautious about this one as this isn't really a CF
component, it would be interesting if down the road we had something
like

EtcdInterface = charmhelpers.interfaces.Interface('etcd')

To produce this class from a interface registry. We'll want that sort of
thing for the interface testing plans anyway.

just rambling, nothing to do now.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode210
charmhelpers/core/services.py:210: """
This feels a bit magical. I'm fine with letting it in but the 'why'
you'd use it both ways should be explained a little more below. See my
following comment about the example being a bit unclear.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode308
charmhelpers/core/services.py:308: from unit 'wordpress/0'.
I worry that this example isn't clear. self.interface and the 'foo' key
are assumed.

I'd like two examples here in the style of

'if you need to iterate all the units of a relation, for example in a
template, you can ...'

'if you want the keys and values of any matching unit containing all the
expected keys of an interface, you can ...'

sorry to ask you to rephrase this, and thank you.

https://codereview.appspot.com/96680043/

Revision history for this message
Cory Johns (johnsca) wrote :

On 2014/06/06 16:43:12, benjamin.saller wrote:

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode36
> charmhelpers/contrib/cloudfoundry/contexts.py:36: required_keys =
['address',
> 'port', 'user', 'password']
> You have a follow on for the charms updating the templates as well?

Yes, as part of the port conflict resolution. Just testing and hashing
out bugs currently.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/contrib/cloudfoundry/contexts.py#newcode69
> charmhelpers/contrib/cloudfoundry/contexts.py:69: required_keys =
['hostname',
> 'port']
> I am a little cautious about this one as this isn't really a CF
component, it
> would be interesting if down the road we had something like

> EtcdInterface = charmhelpers.interfaces.Interface('etcd')

> To produce this class from a interface registry. We'll want that sort
of thing
> for the interface testing plans anyway.

I had considered having RelationContext.__init__ accept the interface
name and required keys as parameters so that you don't have to create
stub subclasses, and only need to subclass if you want to add extra
behavior (e.g., the MysqlRelation). I think this is the right way to
go, and will do so.

https://codereview.appspot.com/96680043/diff/80001/charmhelpers/core/services.py#newcode210
> charmhelpers/core/services.py:210: """
> This feels a bit magical. I'm fine with letting it in but the 'why'
you'd use it
> both ways should be explained a little more below. See my following
comment
> about the example being a bit unclear.

I agree. I am very on the fence as to whether to stick with the
"magical" implementation vs creating separate classes, or even just
forcing it to always be a list to force charm authors to always consider
the possibility that there may be more units.

The reason I didn't go with separate classes is because I didn't want to
have to duplicate all the stub RelationContext subclasses, but if we
make it so the interface and keys can be passed in, we can avoid
creating 95% of the stub classes, so that'd be ok.

But now I'm thinking that forcing it to be a list might be better, since
charm authors really should keep in mind that the admin could throw more
units at the relation.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
190. By Cory Johns

Fixed Cloud Foundry MysqlRelation for new RelationContext data format

191. By Cory Johns

Removed magical default unit behavior from RelationContext

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks, notes follow.

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py#newcode259
charmhelpers/core/services.py:259: order: 'wordpress/0', 'wordpress/1',
'mediawiki/0'.
I think you do a fine job of explaining this now, but the use-case for
it is still unclear to me.

If there is more than one relation with the same interface I don't think
we can silently combine those lists (the data is coming from different
relation ids). They represent different services and should expect
different orchestration.

I'd go so far as to say this is a whole in our model.

In CF mysql has relations to UAA and CC but doesn't use the services
framework to manage this. If it did and iterated those units uniformly
havoc could follow, no?

The default case isn't this and we built for it, but unless my thinking
here is muddy, we'd need to do something different here. Currently the
relation id information would be lost. We'd at minimum have to put the
rid in the data, or break the units into collections by relation.

Breaking them up by relation is to my mind similar to always processing
this as a list (rather than the default unit notion of before), its the
more complex case, but its one that some services need to be aware of.

I apologize for not picking up on this sooner. If you'd like to talk
about this in the hangout let me know.

https://codereview.appspot.com/96680043/diff/100001/charmhelpers/core/services.py#newcode268
charmhelpers/core/services.py:268: {%- endfor %}
This is good, thanks, it might have to turn into

interface[0][0]['key']

for interface[first relation][first unit][key]

lets talk.

https://codereview.appspot.com/96680043/

lp:~johnsca/charm-helpers/multi-unit updated
192. By Cory Johns

Added doc note about not (currently) preserving relation-id and unit-id info in RelationContext

Revision history for this message
Cory Johns (johnsca) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

+1 LTGM, thanks for talking through that issue.

https://codereview.appspot.com/96680043/diff/120001/charmhelpers/core/services.py
File charmhelpers/core/services.py (right):

https://codereview.appspot.com/96680043/diff/120001/charmhelpers/core/services.py#newcode272
charmhelpers/core/services.py:272: set of data came from, you'll need to
extend this class to preserve
Great, thanks

https://codereview.appspot.com/96680043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/cloudfoundry/contexts.py'
2--- charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-26 18:49:24 +0000
3+++ charmhelpers/contrib/cloudfoundry/contexts.py 2014-06-06 19:43:53 +0000
4@@ -33,7 +33,7 @@
5
6 class NatsRelation(RelationContext):
7 interface = 'nats'
8- required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']
9+ required_keys = ['address', 'port', 'user', 'password']
10
11
12 class MysqlRelation(RelationContext):
13@@ -44,9 +44,10 @@
14 def get_data(self):
15 RelationContext.get_data(self)
16 if self.is_ready():
17- if 'port' not in self['db']:
18- self['db']['port'] = '3306'
19- self['db']['dsn'] = self.dsn_template.format(**self['db'])
20+ for unit in self['db']:
21+ if 'port' not in unit:
22+ unit['port'] = '3306'
23+ unit['dsn'] = self.dsn_template.format(**unit)
24
25
26 class RouterRelation(RelationContext):
27@@ -56,9 +57,19 @@
28
29 class LogRouterRelation(RelationContext):
30 interface = 'logrouter'
31- required_keys = ['shared-secret', 'logrouter-address']
32+ required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
33
34
35 class LoggregatorRelation(RelationContext):
36 interface = 'loggregator'
37- required_keys = ['shared_secret', 'loggregator_address']
38+ required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
39+
40+
41+class EtcdRelation(RelationContext):
42+ interface = 'etcd'
43+ required_keys = ['hostname', 'port']
44+
45+
46+class CloudControllerRelation(RelationContext):
47+ interface = 'cc'
48+ required_keys = ['hostname', 'port', 'user', 'password']
49
50=== modified file 'charmhelpers/core/services.py'
51--- charmhelpers/core/services.py 2014-05-29 17:23:48 +0000
52+++ charmhelpers/core/services.py 2014-06-06 19:43:53 +0000
53@@ -11,6 +11,14 @@
54 """
55 Register a list of services, given their definitions.
56
57+ Traditional charm authoring is focused on implementing hooks. That is,
58+ the charm author is thinking in terms of "What hook am I handling; what
59+ does this hook need to do?" However, in most cases, the real question
60+ should be "Do I have the information I need to configure and start this
61+ piece of software and, if so, what are the steps for doing so." The
62+ ServiceManager framework tries to bring the focus to the data and the
63+ setup tasks, in the most declarative way possible.
64+
65 Service definitions are dicts in the following formats (all keys except
66 'service' are optional):
67
68@@ -67,28 +75,28 @@
69 a mongodb relation and which runs a custom `db_migrate` function prior to
70 restarting the service, and a Runit serivce called spadesd.
71
72- >>> manager = services.ServiceManager([
73- ... {
74- ... 'service': 'bingod',
75- ... 'ports': [80, 443],
76- ... 'required_data': [MongoRelation(), config()],
77- ... 'data_ready': [
78- ... services.template(source='bingod.conf'),
79- ... services.template(source='bingod.ini',
80- ... target='/etc/bingod.ini',
81- ... owner='bingo', perms=0400),
82- ... ],
83- ... },
84- ... {
85- ... 'service': 'spadesd',
86- ... 'data_ready': services.template(source='spadesd_run.j2',
87- ... target='/etc/sv/spadesd/run',
88- ... perms=0555),
89- ... 'start': runit_start,
90- ... 'stop': runit_stop,
91- ... },
92- ... ])
93- ... manager.manage()
94+ manager = services.ServiceManager([
95+ {
96+ 'service': 'bingod',
97+ 'ports': [80, 443],
98+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
99+ 'data_ready': [
100+ services.template(source='bingod.conf'),
101+ services.template(source='bingod.ini',
102+ target='/etc/bingod.ini',
103+ owner='bingo', perms=0400),
104+ ],
105+ },
106+ {
107+ 'service': 'spadesd',
108+ 'data_ready': services.template(source='spadesd_run.j2',
109+ target='/etc/sv/spadesd/run',
110+ perms=0555),
111+ 'start': runit_start,
112+ 'stop': runit_stop,
113+ },
114+ ])
115+ manager.manage()
116 """
117 self.services = {}
118 for service in services or []:
119@@ -222,46 +230,57 @@
120
121 __nonzero__ = __bool__
122
123+ def __repr__(self):
124+ return super(RelationContext, self).__repr__()
125+
126 def is_ready(self):
127 """
128- Returns True if all of the required_keys are available.
129- """
130- return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys))
131+ Returns True if all of the `required_keys` are available from any units.
132+ """
133+ return len(self.get(self.interface, [])) > 0
134+
135+ def _is_ready(self, unit_data):
136+ """
137+ Helper method that tests a set of relation data and returns True if
138+ all of the `required_keys` are present.
139+ """
140+ return set(unit_data.keys()).issuperset(set(self.required_keys))
141
142 def get_data(self):
143 """
144- Retrieve the relation data and store it under `self[self.interface]`.
145-
146- If there are more than one units related on the desired interface,
147- then each unit will have its data stored under `self[self.interface][unit_id]`
148- and one of the units with complete information will chosen at random
149- to fill the values at `self[self.interface]`.
150-
151-
152- For example:
153-
154- {
155- 'foo': 'bar',
156- 'unit/0': {
157- 'foo': 'bar',
158- },
159- 'unit/1': {
160- 'foo': 'baz',
161- },
162- }
163+ Retrieve the relation data for each unit involved in a realtion and,
164+ if complete, store it in a list under `self[self.interface]`.
165+
166+ The units are sorted lexographically first by the service ID, then by
167+ the unit ID. Thus, if an interface has two other services, 'db:1'
168+ and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
169+ and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
170+ set of data, the relation data for the units will be stored in the
171+ order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
172+
173+ If you only care about a single unit on the relation, you can just
174+ access it as `{{ interface[0]['key'] }}`. However, if you can at all
175+ support multiple units on a relation, you should iterate over the list,
176+ like:
177+
178+ {% for unit in interface -%}
179+ {{ unit['key'] }}{% if not loop.last %},{% endif %}
180+ {%- endfor %}
181+
182+ Note that since all sets of relation data from all related services and
183+ units are in a single list, if you need to know which service or unit a
184+ set of data came from, you'll need to extend this class to preserve
185+ that information.
186 """
187 if not hookenv.relation_ids(self.interface):
188 return
189
190- ns = self.setdefault(self.interface, {})
191- required = set(self.required_keys)
192- for rid in hookenv.relation_ids(self.interface):
193- for unit in hookenv.related_units(rid):
194+ ns = self.setdefault(self.interface, [])
195+ for rid in sorted(hookenv.relation_ids(self.interface)):
196+ for unit in sorted(hookenv.related_units(rid)):
197 reldata = hookenv.relation_get(rid=rid, unit=unit)
198- unit_ns = ns.setdefault(unit, {})
199- unit_ns.update(reldata)
200- if set(reldata.keys()).issuperset(required):
201- ns.update(reldata)
202+ if self._is_ready(reldata):
203+ ns.append(reldata)
204
205
206 class ManagerCallback(object):
207@@ -316,6 +335,6 @@
208
209
210 # Convenience aliases
211-template = TemplateCallback
212+render_template = template = TemplateCallback
213 open_ports = PortManagerCallback()
214 close_ports = PortManagerCallback()
215
216=== modified file 'tests/contrib/cloudfoundry/test_render_context.py'
217--- tests/contrib/cloudfoundry/test_render_context.py 2014-05-26 18:49:24 +0000
218+++ tests/contrib/cloudfoundry/test_render_context.py 2014-06-06 19:43:53 +0000
219@@ -18,23 +18,22 @@
220 @mock.patch('charmhelpers.core.hookenv.relation_get')
221 def test_nats_relation_populated(self, mrel, mid, mrelated):
222 mid.return_value = ['nats']
223- mrel.return_value = {'nats_port': 1234, 'nats_address': 'host',
224- 'nats_user': 'user', 'nats_password': 'password'}
225+ mrel.return_value = {'port': 1234, 'address': 'host',
226+ 'user': 'user', 'password': 'password'}
227 mrelated.return_value = ['router/0']
228 n = contexts.NatsRelation()
229- expected = {'nats': {'nats_port': 1234, 'nats_address': 'host',
230- 'nats_user': 'user', 'nats_password': 'password',
231- 'router/0': {'nats_port': 1234, 'nats_address': 'host',
232- 'nats_user': 'user', 'nats_password': 'password'}}}
233+ expected = {'nats': [{'port': 1234, 'address': 'host',
234+ 'user': 'user', 'password': 'password'}]}
235 self.assertTrue(bool(n))
236 self.assertEqual(n, expected)
237+ self.assertEqual(n['nats'][0]['port'], 1234)
238
239 @mock.patch('charmhelpers.core.hookenv.related_units')
240 @mock.patch('charmhelpers.core.hookenv.relation_ids')
241 @mock.patch('charmhelpers.core.hookenv.relation_get')
242 def test_nats_relation_partial(self, mrel, mid, mrelated):
243 mid.return_value = ['nats']
244- mrel.return_value = {'nats_address': 'host'}
245+ mrel.return_value = {'address': 'host'}
246 mrelated.return_value = ['router/0']
247 n = contexts.NatsRelation()
248 self.assertEqual(n, {})
249@@ -56,10 +55,10 @@
250 mrel.return_value = {'domain': 'example.com'}
251 mrelated.return_value = ['router/0']
252 n = contexts.RouterRelation()
253- expected = {'router': {'domain': 'example.com',
254- 'router/0': {'domain': 'example.com'}}}
255+ expected = {'router': [{'domain': 'example.com'}]}
256 self.assertTrue(bool(n))
257 self.assertEqual(n, expected)
258+ self.assertEqual(n['router'][0]['domain'], 'example.com')
259
260
261 class TestStoredContext(unittest.TestCase):
262
263=== modified file 'tests/core/test_services.py'
264--- tests/core/test_services.py 2014-05-29 17:23:48 +0000
265+++ tests/core/test_services.py 2014-06-06 19:43:53 +0000
266@@ -310,7 +310,7 @@
267 mhookenv.related_units.return_value = []
268 self.context.get_data()
269 self.assertFalse(self.context.is_ready())
270- self.assertEqual(self.context, {'http': {}})
271+ self.assertEqual(self.context, {'http': []})
272
273 @mock.patch.object(services, 'hookenv')
274 def test_incomplete(self, mhookenv):
275@@ -319,8 +319,8 @@
276 mhookenv.relation_get.side_effect = [{}, {'foo': '1'}]
277 self.assertFalse(bool(self.context))
278 self.assertEqual(mhookenv.relation_get.call_args_list, [
279+ mock.call(rid='apache', unit='apache/0'),
280 mock.call(rid='nginx', unit='nginx/0'),
281- mock.call(rid='apache', unit='apache/0'),
282 ])
283
284 @mock.patch.object(services, 'hookenv')
285@@ -329,23 +329,17 @@
286 mhookenv.related_units.side_effect = lambda i: [i+'/0']
287 mhookenv.relation_get.side_effect = [{'foo': '1'}, {'foo': '2', 'bar': '3'}, {}]
288 self.context.get_data()
289- self.assertEqual(self.context, {'http': {
290- 'foo': '2',
291- 'bar': '3',
292- 'nginx/0': {
293- 'foo': '1',
294- },
295- 'apache/0': {
296+ self.assertTrue(self.context.is_ready())
297+ self.assertEqual(self.context, {'http': [
298+ {
299 'foo': '2',
300 'bar': '3',
301 },
302- 'tomcat/0': {
303- },
304- }})
305+ ]})
306 mhookenv.relation_ids.assert_called_with('http')
307 self.assertEqual(mhookenv.relation_get.call_args_list, [
308+ mock.call(rid='apache', unit='apache/0'),
309 mock.call(rid='nginx', unit='nginx/0'),
310- mock.call(rid='apache', unit='apache/0'),
311 mock.call(rid='tomcat', unit='tomcat/0'),
312 ])
313

Subscribers

People subscribed via source and target branches