Merge lp:~johnsca/charms/trusty/cf-go-router/port-conflicts into lp:~cf-charmers/charms/trusty/cf-go-router/trunk

Proposed by Cory Johns
Status: Merged
Merged at revision: 31
Proposed branch: lp:~johnsca/charms/trusty/cf-go-router/port-conflicts
Merge into: lp:~cf-charmers/charms/trusty/cf-go-router/trunk
Diff against target: 365 lines (+140/-76)
8 files modified
config.yaml (+4/-1)
hooks/charmhelpers/contrib/cloudfoundry/contexts.py (+17/-6)
hooks/charmhelpers/core/services.py (+93/-56)
hooks/config.py (+3/-2)
hooks/logrouter-relation-changed (+6/-0)
hooks/tests/test_hooks.py (+4/-4)
metadata.yaml (+2/-0)
templates/gorouter.yml (+11/-7)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-go-router/port-conflicts
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+222671@code.launchpad.net

Description of the change

Resolve port conflicts in cf-go-router

https://codereview.appspot.com/103270043/

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

Reviewers: mp+222671_code.launchpad.net,

Message:
Please take a look.

Description:
Resolve port conflicts in cf-go-router

https://code.launchpad.net/~johnsca/charms/trusty/cf-go-router/port-conflicts/+merge/222671

(do not edit description out of merge proposal)

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

Affected files (+134, -75 lines):
   A [revision details]
   M config.yaml
   M hooks/charmhelpers/contrib/cloudfoundry/contexts.py
   M hooks/charmhelpers/core/services.py
   M hooks/config.py
   A hooks/logrouter-relation-changed
   M hooks/tests/test_hooks.py
   M metadata.yaml
   M templates/gorouter.yml

38. By Cory Johns

Removed varz_port option as it doesn't add much value to change

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

Thanks, LGTM with changes

At min we should use the config['port'] the action for the rest might be
a card unless you disagree with the approach.

https://codereview.appspot.com/103270043/diff/20001/hooks/config.py
File hooks/config.py (right):

https://codereview.appspot.com/103270043/diff/20001/hooks/config.py#newcode39
hooks/config.py:39: 'ports': [80],
if this is a config option (and it can be because we expose this one)
then this can't be hard-coded here.

config['port']

The harder issue is that we then need to track the previous value if we
are to close the old port.

We (and it doesn't have to be now) could use a config context that
called callbacks here as callback(new_value, old_value=None) so we have
a chance to do something with the old saved context. seem reasonable?

https://codereview.appspot.com/103270043/

39. By Cory Johns

Removed hard-coded port and updated charm-helpers for port changing awareness logic

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

On 2014/06/11 22:23:56, benjamin.saller wrote:
> hooks/config.py:39: 'ports': [80],
> if this is a config option (and it can be because we expose this one)
then this
> can't be hard-coded here.

> The harder issue is that we then need to track the previous value if
we are to
> close the old port.

Thanks, good catches. I added the logic to close the old port when
switching to charm-helpers because it should always happen, and I feel
like it should be transparent to the charm author.

https://codereview.appspot.com/103270043/

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

LGTM

Nice fix for the port handling

https://codereview.appspot.com/103270043/diff/40001/hooks/charmhelpers/core/services.py
File hooks/charmhelpers/core/services.py (right):

https://codereview.appspot.com/103270043/diff/40001/hooks/charmhelpers/core/services.py#newcode348
hooks/charmhelpers/core/services.py:348: for port in new_ports:
Cool, thanks

https://codereview.appspot.com/103270043/diff/40001/hooks/config.py
File hooks/config.py (right):

https://codereview.appspot.com/103270043/diff/40001/hooks/config.py#newcode39
hooks/config.py:39: 'ports': [hookenv.config('port')],
funny that we use the hookenv.config()['domain'] above and this here. I
didn't recall that it even did per-key lookup :)

https://codereview.appspot.com/103270043/

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

*** Submitted:

Resolve port conflicts in cf-go-router

R=benjamin.saller
CC=
https://codereview.appspot.com/103270043

https://codereview.appspot.com/103270043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'config.yaml'
--- config.yaml 2014-05-27 21:50:52 +0000
+++ config.yaml 2014-06-12 14:01:13 +0000
@@ -25,4 +25,7 @@
25 description: |25 description: |
26 Key ID to import to the apt keyring to support use with arbitary source26 Key ID to import to the apt keyring to support use with arbitary source
27 configuration from outside of Launchpad archives or PPA's.27 configuration from outside of Launchpad archives or PPA's.
2828 port:
29 type: int
30 default: 80
31 description: Port for the router to listen on.
2932
=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py'
--- hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-27 21:50:52 +0000
+++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-06-12 14:01:13 +0000
@@ -33,7 +33,7 @@
3333
34class NatsRelation(RelationContext):34class NatsRelation(RelationContext):
35 interface = 'nats'35 interface = 'nats'
36 required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']36 required_keys = ['address', 'port', 'user', 'password']
3737
3838
39class MysqlRelation(RelationContext):39class MysqlRelation(RelationContext):
@@ -44,9 +44,10 @@
44 def get_data(self):44 def get_data(self):
45 RelationContext.get_data(self)45 RelationContext.get_data(self)
46 if self.is_ready():46 if self.is_ready():
47 if 'port' not in self['db']:47 for unit in self['db']:
48 self['db']['port'] = '3306'48 if 'port' not in unit:
49 self['db']['dsn'] = self.dsn_template.format(**self['db'])49 unit['port'] = '3306'
50 unit['dsn'] = self.dsn_template.format(**unit)
5051
5152
52class RouterRelation(RelationContext):53class RouterRelation(RelationContext):
@@ -56,9 +57,19 @@
5657
57class LogRouterRelation(RelationContext):58class LogRouterRelation(RelationContext):
58 interface = 'logrouter'59 interface = 'logrouter'
59 required_keys = ['shared-secret', 'logrouter-address']60 required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
6061
6162
62class LoggregatorRelation(RelationContext):63class LoggregatorRelation(RelationContext):
63 interface = 'loggregator'64 interface = 'loggregator'
64 required_keys = ['shared_secret', 'loggregator_address']65 required_keys = ['address', 'incoming_port', 'outgoing_port']
66
67
68class EtcdRelation(RelationContext):
69 interface = 'etcd'
70 required_keys = ['hostname', 'port']
71
72
73class CloudControllerRelation(RelationContext):
74 interface = 'cc'
75 required_keys = ['hostname', 'port', 'user', 'password']
6576
=== modified file 'hooks/charmhelpers/core/services.py'
--- hooks/charmhelpers/core/services.py 2014-05-29 17:27:32 +0000
+++ hooks/charmhelpers/core/services.py 2014-06-12 14:01:13 +0000
@@ -11,6 +11,14 @@
11 """11 """
12 Register a list of services, given their definitions.12 Register a list of services, given their definitions.
1313
14 Traditional charm authoring is focused on implementing hooks. That is,
15 the charm author is thinking in terms of "What hook am I handling; what
16 does this hook need to do?" However, in most cases, the real question
17 should be "Do I have the information I need to configure and start this
18 piece of software and, if so, what are the steps for doing so." The
19 ServiceManager framework tries to bring the focus to the data and the
20 setup tasks, in the most declarative way possible.
21
14 Service definitions are dicts in the following formats (all keys except22 Service definitions are dicts in the following formats (all keys except
15 'service' are optional):23 'service' are optional):
1624
@@ -67,28 +75,28 @@
67 a mongodb relation and which runs a custom `db_migrate` function prior to75 a mongodb relation and which runs a custom `db_migrate` function prior to
68 restarting the service, and a Runit serivce called spadesd.76 restarting the service, and a Runit serivce called spadesd.
6977
70 >>> manager = services.ServiceManager([78 manager = services.ServiceManager([
71 ... {79 {
72 ... 'service': 'bingod',80 'service': 'bingod',
73 ... 'ports': [80, 443],81 'ports': [80, 443],
74 ... 'required_data': [MongoRelation(), config()],82 'required_data': [MongoRelation(), config(), {'my': 'data'}],
75 ... 'data_ready': [83 'data_ready': [
76 ... services.template(source='bingod.conf'),84 services.template(source='bingod.conf'),
77 ... services.template(source='bingod.ini',85 services.template(source='bingod.ini',
78 ... target='/etc/bingod.ini',86 target='/etc/bingod.ini',
79 ... owner='bingo', perms=0400),87 owner='bingo', perms=0400),
80 ... ],88 ],
81 ... },89 },
82 ... {90 {
83 ... 'service': 'spadesd',91 'service': 'spadesd',
84 ... 'data_ready': services.template(source='spadesd_run.j2',92 'data_ready': services.template(source='spadesd_run.j2',
85 ... target='/etc/sv/spadesd/run',93 target='/etc/sv/spadesd/run',
86 ... perms=0555),94 perms=0555),
87 ... 'start': runit_start,95 'start': runit_start,
88 ... 'stop': runit_stop,96 'stop': runit_stop,
89 ... },97 },
90 ... ])98 ])
91 ... manager.manage()99 manager.manage()
92 """100 """
93 self.services = {}101 self.services = {}
94 for service in services or []:102 for service in services or []:
@@ -213,55 +221,73 @@
213 interface = None221 interface = None
214 required_keys = []222 required_keys = []
215223
224 def __init__(self, *args, **kwargs):
225 super(RelationContext, self).__init__(*args, **kwargs)
226 self.get_data()
227
216 def __bool__(self):228 def __bool__(self):
217 """229 """
218 Updates the data and returns True if all of the required_keys are available.230 Returns True if all of the required_keys are available.
219 """231 """
220 self.get_data()
221 return self.is_ready()232 return self.is_ready()
222233
223 __nonzero__ = __bool__234 __nonzero__ = __bool__
224235
236 def __repr__(self):
237 return super(RelationContext, self).__repr__()
238
225 def is_ready(self):239 def is_ready(self):
226 """240 """
227 Returns True if all of the required_keys are available.241 Returns True if all of the `required_keys` are available from any units.
228 """242 """
229 return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys))243 ready = len(self.get(self.interface, [])) > 0
244 if not ready:
245 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
246 return ready
247
248 def _is_ready(self, unit_data):
249 """
250 Helper method that tests a set of relation data and returns True if
251 all of the `required_keys` are present.
252 """
253 return set(unit_data.keys()).issuperset(set(self.required_keys))
230254
231 def get_data(self):255 def get_data(self):
232 """256 """
233 Retrieve the relation data and store it under `self[self.interface]`.257 Retrieve the relation data for each unit involved in a realtion and,
234258 if complete, store it in a list under `self[self.interface]`. This
235 If there are more than one units related on the desired interface,259 is automatically called when the RelationContext is instantiated.
236 then each unit will have its data stored under `self[self.interface][unit_id]`260
237 and one of the units with complete information will chosen at random261 The units are sorted lexographically first by the service ID, then by
238 to fill the values at `self[self.interface]`.262 the unit ID. Thus, if an interface has two other services, 'db:1'
239263 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
240264 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
241 For example:265 set of data, the relation data for the units will be stored in the
242266 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
243 {267
244 'foo': 'bar',268 If you only care about a single unit on the relation, you can just
245 'unit/0': {269 access it as `{{ interface[0]['key'] }}`. However, if you can at all
246 'foo': 'bar',270 support multiple units on a relation, you should iterate over the list,
247 },271 like:
248 'unit/1': {272
249 'foo': 'baz',273 {% for unit in interface -%}
250 },274 {{ unit['key'] }}{% if not loop.last %},{% endif %}
251 }275 {%- endfor %}
276
277 Note that since all sets of relation data from all related services and
278 units are in a single list, if you need to know which service or unit a
279 set of data came from, you'll need to extend this class to preserve
280 that information.
252 """281 """
253 if not hookenv.relation_ids(self.interface):282 if not hookenv.relation_ids(self.interface):
254 return283 return
255284
256 ns = self.setdefault(self.interface, {})285 ns = self.setdefault(self.interface, [])
257 required = set(self.required_keys)286 for rid in sorted(hookenv.relation_ids(self.interface)):
258 for rid in hookenv.relation_ids(self.interface):287 for unit in sorted(hookenv.related_units(rid)):
259 for unit in hookenv.related_units(rid):
260 reldata = hookenv.relation_get(rid=rid, unit=unit)288 reldata = hookenv.relation_get(rid=rid, unit=unit)
261 unit_ns = ns.setdefault(unit, {})289 if self._is_ready(reldata):
262 unit_ns.update(reldata)290 ns.append(reldata)
263 if set(reldata.keys()).issuperset(required):
264 ns.update(reldata)
265291
266292
267class ManagerCallback(object):293class ManagerCallback(object):
@@ -308,7 +334,18 @@
308 """334 """
309 def __call__(self, manager, service_name, event_name):335 def __call__(self, manager, service_name, event_name):
310 service = manager.get_service(service_name)336 service = manager.get_service(service_name)
311 for port in service.get('ports', []):337 new_ports = service.get('ports', [])
338 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
339 if os.path.exists(port_file):
340 with open(port_file) as fp:
341 old_ports = fp.read().split(',')
342 for old_port in old_ports:
343 old_port = int(old_port)
344 if old_port not in new_ports:
345 hookenv.close_port(old_port)
346 with open(port_file, 'w') as fp:
347 fp.write(','.join(str(port) for port in new_ports))
348 for port in new_ports:
312 if event_name == 'start':349 if event_name == 'start':
313 hookenv.open_port(port)350 hookenv.open_port(port)
314 elif event_name == 'stop':351 elif event_name == 'stop':
@@ -316,6 +353,6 @@
316353
317354
318# Convenience aliases355# Convenience aliases
319template = TemplateCallback356render_template = template = TemplateCallback
320open_ports = PortManagerCallback()357open_ports = PortManagerCallback()
321close_ports = PortManagerCallback()358close_ports = PortManagerCallback()
322359
=== modified file 'hooks/config.py'
--- hooks/config.py 2014-06-03 03:50:30 +0000
+++ hooks/config.py 2014-06-12 14:01:13 +0000
@@ -36,10 +36,11 @@
36SERVICES = [36SERVICES = [
37 {37 {
38 'service': 'gorouter',38 'service': 'gorouter',
39 'ports': [80, 443],39 'ports': [hookenv.config('port')],
40 'required_data': [40 'required_data': [
41 {'domain': domain()},41 {'domain': domain(), 'config': hookenv.config(), 'varz_port': 8081},
42 contexts.NatsRelation(),42 contexts.NatsRelation(),
43 contexts.LogRouterRelation(),
43 ],44 ],
44 'data_ready': [45 'data_ready': [
45 services.template(source='gorouter.conf',46 services.template(source='gorouter.conf',
4647
=== added file 'hooks/logrouter-relation-changed'
--- hooks/logrouter-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/logrouter-relation-changed 2014-06-12 14:01:13 +0000
@@ -0,0 +1,6 @@
1#!/usr/bin/env python
2from charmhelpers.core import services
3import config
4
5manager = services.ServiceManager(config.SERVICES)
6manager.manage()
07
=== modified file 'hooks/tests/test_hooks.py'
--- hooks/tests/test_hooks.py 2014-06-03 03:50:30 +0000
+++ hooks/tests/test_hooks.py 2014-06-12 14:01:13 +0000
@@ -14,10 +14,10 @@
14 @mock.patch('hooks.hooks.host.write_file')14 @mock.patch('hooks.hooks.host.write_file')
15 def test_emit_router_config(self, write_file):15 def test_emit_router_config(self, write_file):
16 emit_router_config({16 emit_router_config({
17 'nats_address': 'addy',17 'address': 'addy',
18 'nats_port': 123,18 'port': 123,
19 'nats_password': 'password',19 'password': 'password',
20 'nats_user': 'user'})20 'user': 'user'})
2121
22 args, kw = write_file.call_args_list[0]22 args, kw = write_file.call_args_list[0]
23 self.assertEqual(23 self.assertEqual(
2424
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-05-12 07:19:21 +0000
+++ metadata.yaml 2014-06-12 14:01:13 +0000
@@ -13,3 +13,5 @@
13requires:13requires:
14 nats:14 nats:
15 interface: nats15 interface: nats
16 logrouter:
17 interface: logrouter
1618
=== modified file 'templates/gorouter.yml'
--- templates/gorouter.yml 2014-04-17 15:12:52 +0000
+++ templates/gorouter.yml 2014-06-12 14:01:13 +0000
@@ -1,11 +1,11 @@
1nats:1nats:
2 - host: "{{ nats['nats_address'] }}"2 - host: "{{ nats[0]['address'] }}"
3 port: {{ nats['nats_port'] }}3 port: {{ nats[0]['port'] }}
4 user: "{{ nats['nats_user'] }}"4 user: "{{ nats[0]['user'] }}"
5 pass: "{{ nats['nats_password'] }}"5 pass: "{{ nats[0]['password'] }}"
66
7status:7status:
8 port: 80818 port: {{ varz_port }}
9 user: user9 user: user
10 pass: password10 pass: password
1111
@@ -13,7 +13,11 @@
13 file: /var/vcap/sys/log/gorouter/gorouter.log13 file: /var/vcap/sys/log/gorouter/gorouter.log
14 level: info14 level: info
1515
16port: 8016loggregatorConfig:
17 url: {{ logrouter[0]['address'] }}:{{ logrouter[0]['incoming_port'] }}
18 shared_secret: {{ logrouter[0]['shared_secret'] }}
19
20port: {{ config['port'] }}
17index: 121index: 1
18pidfile: /var/vcap/sys/run/gorouter/gorouter.pid22pidfile: /var/vcap/sys/run/gorouter/gorouter.pid
19go_max_procs: 823go_max_procs: 8
@@ -23,6 +27,6 @@
23publish_start_message_interval: 3027publish_start_message_interval: 30
24prune_stale_droplets_interval: 3028prune_stale_droplets_interval: 30
25droplet_stale_threshold: 12029droplet_stale_threshold: 120
26publish_active_apps_interval: 0 30publish_active_apps_interval: 0
2731
28endpoint_timeout: 6032endpoint_timeout: 60

Subscribers

People subscribed via source and target branches