← Back to team overview

cf-charmers team mailing list archive

[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