cf-charmers team mailing list archive
-
cf-charmers team
-
Mailing list archive
-
Message #00361
[Merge] lp:~johnsca/charm-helpers/port-conflicts into lp:~cf-charmers/charm-helpers/cloud-foundry
Cory Johns has proposed merging lp:~johnsca/charm-helpers/port-conflicts into lp:~cf-charmers/charm-helpers/cloud-foundry.
Requested reviews:
Cloud Foundry Charmers (cf-charmers)
For more details, see:
https://code.launchpad.net/~johnsca/charm-helpers/port-conflicts/+merge/222669
CH changes to resolve port conflicts in CF
--
https://code.launchpad.net/~johnsca/charm-helpers/port-conflicts/+merge/222669
Your team Cloud Foundry Charmers is requested to review the proposed merge of lp:~johnsca/charm-helpers/port-conflicts into lp:~cf-charmers/charm-helpers/cloud-foundry.
=== modified file 'charmhelpers/contrib/cloudfoundry/contexts.py'
--- charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-26 18:49:24 +0000
+++ charmhelpers/contrib/cloudfoundry/contexts.py 2014-06-10 15:53:07 +0000
@@ -33,7 +33,7 @@
class NatsRelation(RelationContext):
interface = 'nats'
- required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']
+ required_keys = ['address', 'port', 'user', 'password']
class MysqlRelation(RelationContext):
@@ -44,9 +44,10 @@
def get_data(self):
RelationContext.get_data(self)
if self.is_ready():
- if 'port' not in self['db']:
- self['db']['port'] = '3306'
- self['db']['dsn'] = self.dsn_template.format(**self['db'])
+ for unit in self['db']:
+ if 'port' not in unit:
+ unit['port'] = '3306'
+ unit['dsn'] = self.dsn_template.format(**unit)
class RouterRelation(RelationContext):
@@ -56,9 +57,19 @@
class LogRouterRelation(RelationContext):
interface = 'logrouter'
- required_keys = ['shared-secret', 'logrouter-address']
+ required_keys = ['shared_secret', 'address', 'incoming_port', 'outgoing_port']
class LoggregatorRelation(RelationContext):
interface = 'loggregator'
- required_keys = ['shared_secret', 'loggregator_address']
+ required_keys = ['address', 'incoming_port', 'outgoing_port']
+
+
+class EtcdRelation(RelationContext):
+ interface = 'etcd'
+ required_keys = ['hostname', 'port']
+
+
+class CloudControllerRelation(RelationContext):
+ interface = 'cc'
+ required_keys = ['hostname', 'port', 'user', 'password']
=== modified file 'charmhelpers/core/services.py'
--- charmhelpers/core/services.py 2014-05-29 17:23:48 +0000
+++ charmhelpers/core/services.py 2014-06-10 15:53:07 +0000
@@ -11,6 +11,14 @@
"""
Register a list of services, given their definitions.
+ Traditional charm authoring is focused on implementing hooks. That is,
+ the charm author is thinking in terms of "What hook am I handling; what
+ does this hook need to do?" However, in most cases, the real question
+ should be "Do I have the information I need to configure and start this
+ piece of software and, if so, what are the steps for doing so." The
+ ServiceManager framework tries to bring the focus to the data and the
+ setup tasks, in the most declarative way possible.
+
Service definitions are dicts in the following formats (all keys except
'service' are optional):
@@ -67,28 +75,28 @@
a mongodb relation and which runs a custom `db_migrate` function prior to
restarting the service, and a Runit serivce called spadesd.
- >>> manager = services.ServiceManager([
- ... {
- ... 'service': 'bingod',
- ... 'ports': [80, 443],
- ... 'required_data': [MongoRelation(), config()],
- ... 'data_ready': [
- ... services.template(source='bingod.conf'),
- ... services.template(source='bingod.ini',
- ... target='/etc/bingod.ini',
- ... owner='bingo', perms=0400),
- ... ],
- ... },
- ... {
- ... 'service': 'spadesd',
- ... 'data_ready': services.template(source='spadesd_run.j2',
- ... target='/etc/sv/spadesd/run',
- ... perms=0555),
- ... 'start': runit_start,
- ... 'stop': runit_stop,
- ... },
- ... ])
- ... manager.manage()
+ manager = services.ServiceManager([
+ {
+ 'service': 'bingod',
+ 'ports': [80, 443],
+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
+ 'data_ready': [
+ services.template(source='bingod.conf'),
+ services.template(source='bingod.ini',
+ target='/etc/bingod.ini',
+ owner='bingo', perms=0400),
+ ],
+ },
+ {
+ 'service': 'spadesd',
+ 'data_ready': services.template(source='spadesd_run.j2',
+ target='/etc/sv/spadesd/run',
+ perms=0555),
+ 'start': runit_start,
+ 'stop': runit_stop,
+ },
+ ])
+ manager.manage()
"""
self.services = {}
for service in services or []:
@@ -213,55 +221,73 @@
interface = None
required_keys = []
+ def __init__(self, *args, **kwargs):
+ super(RelationContext, self).__init__(*args, **kwargs)
+ self.get_data()
+
def __bool__(self):
"""
- Updates the data and returns True if all of the required_keys are available.
+ Returns True if all of the required_keys are available.
"""
- self.get_data()
return self.is_ready()
__nonzero__ = __bool__
+ def __repr__(self):
+ return super(RelationContext, self).__repr__()
+
def is_ready(self):
"""
- Returns True if all of the required_keys are available.
- """
- return set(self.get(self.interface, {}).keys()).issuperset(set(self.required_keys))
+ Returns True if all of the `required_keys` are available from any units.
+ """
+ ready = len(self.get(self.interface, [])) > 0
+ if not ready:
+ hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
+ return ready
+
+ def _is_ready(self, unit_data):
+ """
+ Helper method that tests a set of relation data and returns True if
+ all of the `required_keys` are present.
+ """
+ return set(unit_data.keys()).issuperset(set(self.required_keys))
def get_data(self):
"""
- Retrieve the relation data and store it under `self[self.interface]`.
-
- If there are more than one units related on the desired interface,
- then each unit will have its data stored under `self[self.interface][unit_id]`
- and one of the units with complete information will chosen at random
- to fill the values at `self[self.interface]`.
-
-
- For example:
-
- {
- 'foo': 'bar',
- 'unit/0': {
- 'foo': 'bar',
- },
- 'unit/1': {
- 'foo': 'baz',
- },
- }
+ Retrieve the relation data for each unit involved in a realtion and,
+ if complete, store it in a list under `self[self.interface]`. This
+ is automatically called when the RelationContext is instantiated.
+
+ The units are sorted lexographically first by the service ID, then by
+ the unit ID. Thus, if an interface has two other services, 'db:1'
+ and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
+ and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
+ set of data, the relation data for the units will be stored in the
+ order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
+
+ If you only care about a single unit on the relation, you can just
+ access it as `{{ interface[0]['key'] }}`. However, if you can at all
+ support multiple units on a relation, you should iterate over the list,
+ like:
+
+ {% for unit in interface -%}
+ {{ unit['key'] }}{% if not loop.last %},{% endif %}
+ {%- endfor %}
+
+ Note that since all sets of relation data from all related services and
+ units are in a single list, if you need to know which service or unit a
+ set of data came from, you'll need to extend this class to preserve
+ that information.
"""
if not hookenv.relation_ids(self.interface):
return
- ns = self.setdefault(self.interface, {})
- required = set(self.required_keys)
- for rid in hookenv.relation_ids(self.interface):
- for unit in hookenv.related_units(rid):
+ ns = self.setdefault(self.interface, [])
+ for rid in sorted(hookenv.relation_ids(self.interface)):
+ for unit in sorted(hookenv.related_units(rid)):
reldata = hookenv.relation_get(rid=rid, unit=unit)
- unit_ns = ns.setdefault(unit, {})
- unit_ns.update(reldata)
- if set(reldata.keys()).issuperset(required):
- ns.update(reldata)
+ if self._is_ready(reldata):
+ ns.append(reldata)
class ManagerCallback(object):
@@ -316,6 +342,6 @@
# Convenience aliases
-template = TemplateCallback
+render_template = template = TemplateCallback
open_ports = PortManagerCallback()
close_ports = PortManagerCallback()
=== modified file 'tests/contrib/cloudfoundry/test_render_context.py'
--- tests/contrib/cloudfoundry/test_render_context.py 2014-05-26 18:49:24 +0000
+++ tests/contrib/cloudfoundry/test_render_context.py 2014-06-10 15:53:07 +0000
@@ -18,26 +18,25 @@
@mock.patch('charmhelpers.core.hookenv.relation_get')
def test_nats_relation_populated(self, mrel, mid, mrelated):
mid.return_value = ['nats']
- mrel.return_value = {'nats_port': 1234, 'nats_address': 'host',
- 'nats_user': 'user', 'nats_password': 'password'}
+ mrel.return_value = {'port': 1234, 'address': 'host',
+ 'user': 'user', 'password': 'password'}
mrelated.return_value = ['router/0']
n = contexts.NatsRelation()
- expected = {'nats': {'nats_port': 1234, 'nats_address': 'host',
- 'nats_user': 'user', 'nats_password': 'password',
- 'router/0': {'nats_port': 1234, 'nats_address': 'host',
- 'nats_user': 'user', 'nats_password': 'password'}}}
+ expected = {'nats': [{'port': 1234, 'address': 'host',
+ 'user': 'user', 'password': 'password'}]}
self.assertTrue(bool(n))
self.assertEqual(n, expected)
+ self.assertEqual(n['nats'][0]['port'], 1234)
@mock.patch('charmhelpers.core.hookenv.related_units')
@mock.patch('charmhelpers.core.hookenv.relation_ids')
@mock.patch('charmhelpers.core.hookenv.relation_get')
def test_nats_relation_partial(self, mrel, mid, mrelated):
mid.return_value = ['nats']
- mrel.return_value = {'nats_address': 'host'}
+ mrel.return_value = {'address': 'host'}
mrelated.return_value = ['router/0']
n = contexts.NatsRelation()
- self.assertEqual(n, {})
+ self.assertEqual(n, {'nats': []})
class TestRouterRelation(unittest.TestCase):
@@ -56,10 +55,10 @@
mrel.return_value = {'domain': 'example.com'}
mrelated.return_value = ['router/0']
n = contexts.RouterRelation()
- expected = {'router': {'domain': 'example.com',
- 'router/0': {'domain': 'example.com'}}}
+ expected = {'router': [{'domain': 'example.com'}]}
self.assertTrue(bool(n))
self.assertEqual(n, expected)
+ self.assertEqual(n['router'][0]['domain'], 'example.com')
class TestStoredContext(unittest.TestCase):
=== modified file 'tests/core/test_services.py'
--- tests/core/test_services.py 2014-05-29 17:23:48 +0000
+++ tests/core/test_services.py 2014-06-10 15:53:07 +0000
@@ -292,7 +292,9 @@
class TestRelationContext(unittest.TestCase):
def setUp(self):
- self.context = services.RelationContext()
+ with mock.patch.object(services, 'hookenv') as mhookenv:
+ mhookenv.relation_ids.return_value = []
+ self.context = services.RelationContext()
self.context.interface = 'http'
self.context.required_keys = ['foo', 'bar']
@@ -310,17 +312,18 @@
mhookenv.related_units.return_value = []
self.context.get_data()
self.assertFalse(self.context.is_ready())
- self.assertEqual(self.context, {'http': {}})
+ self.assertEqual(self.context, {'http': []})
@mock.patch.object(services, 'hookenv')
def test_incomplete(self, mhookenv):
mhookenv.relation_ids.return_value = ['nginx', 'apache']
mhookenv.related_units.side_effect = lambda i: [i+'/0']
mhookenv.relation_get.side_effect = [{}, {'foo': '1'}]
+ self.context.get_data()
self.assertFalse(bool(self.context))
self.assertEqual(mhookenv.relation_get.call_args_list, [
+ mock.call(rid='apache', unit='apache/0'),
mock.call(rid='nginx', unit='nginx/0'),
- mock.call(rid='apache', unit='apache/0'),
])
@mock.patch.object(services, 'hookenv')
@@ -329,23 +332,17 @@
mhookenv.related_units.side_effect = lambda i: [i+'/0']
mhookenv.relation_get.side_effect = [{'foo': '1'}, {'foo': '2', 'bar': '3'}, {}]
self.context.get_data()
- self.assertEqual(self.context, {'http': {
- 'foo': '2',
- 'bar': '3',
- 'nginx/0': {
- 'foo': '1',
- },
- 'apache/0': {
+ self.assertTrue(self.context.is_ready())
+ self.assertEqual(self.context, {'http': [
+ {
'foo': '2',
'bar': '3',
},
- 'tomcat/0': {
- },
- }})
+ ]})
mhookenv.relation_ids.assert_called_with('http')
self.assertEqual(mhookenv.relation_get.call_args_list, [
+ mock.call(rid='apache', unit='apache/0'),
mock.call(rid='nginx', unit='nginx/0'),
- mock.call(rid='apache', unit='apache/0'),
mock.call(rid='tomcat', unit='tomcat/0'),
])
Follow ups