summaryrefslogtreecommitdiffstats
path: root/charms/trusty/ceilometer-agent/unit_tests
diff options
context:
space:
mode:
authorStuart Mackie <wsmackie@juniper.net>2016-10-07 12:24:58 -0700
committerStuart Mackie <wsmackie@juniper.net>2016-10-07 12:24:58 -0700
commit4faa7f927149a5c4ef7a03523f7bc14523cb9baa (patch)
tree0be55aa0809cc395e45baeae63db660b4e72fe83 /charms/trusty/ceilometer-agent/unit_tests
parent82f1a7eb5535b30a95b1e71ff18c315d40d1e6f0 (diff)
Charms for Contrail 3.1 with Mitaka
Change-Id: Id37f3b9743d1974e31fcd7cd9c54be41bb0c47fb Signed-off-by: Stuart Mackie <wsmackie@juniper.net>
Diffstat (limited to 'charms/trusty/ceilometer-agent/unit_tests')
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/__init__.py17
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_actions.py78
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_actions_openstack_upgrade.py69
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_contexts.py59
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_hooks.py127
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_utils.py116
-rw-r--r--charms/trusty/ceilometer-agent/unit_tests/test_utils.py128
7 files changed, 594 insertions, 0 deletions
diff --git a/charms/trusty/ceilometer-agent/unit_tests/__init__.py b/charms/trusty/ceilometer-agent/unit_tests/__init__.py
new file mode 100644
index 0000000..6c9ae40
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+sys.path.append('actions')
+sys.path.append('hooks')
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_actions.py b/charms/trusty/ceilometer-agent/unit_tests/test_actions.py
new file mode 100644
index 0000000..6f37c83
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_actions.py
@@ -0,0 +1,78 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+from mock import patch
+
+from test_utils import CharmTestCase
+
+with patch('ceilometer_utils.register_configs') as configs:
+ configs.return_value = 'test-config'
+ import actions
+
+
+class PauseTestCase(CharmTestCase):
+
+ def setUp(self):
+ super(PauseTestCase, self).setUp(
+ actions, ["pause_unit_helper"])
+
+ def test_pauses_services(self):
+ actions.pause([])
+ self.pause_unit_helper.assert_called_once_with('test-config')
+
+
+class ResumeTestCase(CharmTestCase):
+
+ def setUp(self):
+ super(ResumeTestCase, self).setUp(
+ actions, ["resume_unit_helper"])
+
+ def test_pauses_services(self):
+ actions.resume([])
+ self.resume_unit_helper.assert_called_once_with('test-config')
+
+
+class MainTestCase(CharmTestCase):
+
+ def setUp(self):
+ super(MainTestCase, self).setUp(actions, ["action_fail"])
+
+ def test_invokes_action(self):
+ dummy_calls = []
+
+ def dummy_action(args):
+ dummy_calls.append(True)
+
+ with mock.patch.dict(actions.ACTIONS, {"foo": dummy_action}):
+ actions.main(["foo"])
+ self.assertEqual(dummy_calls, [True])
+
+ def test_unknown_action(self):
+ """Unknown actions aren't a traceback."""
+ exit_string = actions.main(["foo"])
+ self.assertEqual("Action foo undefined", exit_string)
+
+ def test_failing_action(self):
+ """Actions which traceback trigger action_fail() calls."""
+ dummy_calls = []
+
+ self.action_fail.side_effect = dummy_calls.append
+
+ def dummy_action(args):
+ raise ValueError("uh oh")
+
+ with mock.patch.dict(actions.ACTIONS, {"foo": dummy_action}):
+ actions.main(["foo"])
+ self.assertEqual(dummy_calls, ["uh oh"])
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_actions_openstack_upgrade.py b/charms/trusty/ceilometer-agent/unit_tests/test_actions_openstack_upgrade.py
new file mode 100644
index 0000000..7628232
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_actions_openstack_upgrade.py
@@ -0,0 +1,69 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from mock import patch
+import os
+
+os.environ['JUJU_UNIT_NAME'] = 'ceilometer'
+
+with patch('ceilometer_utils.register_configs') as register_configs:
+ import openstack_upgrade
+
+from test_utils import (
+ CharmTestCase
+)
+
+TO_PATCH = [
+ 'config_changed',
+ 'do_openstack_upgrade',
+]
+
+
+class TestCinderUpgradeActions(CharmTestCase):
+
+ def setUp(self):
+ super(TestCinderUpgradeActions, self).setUp(openstack_upgrade,
+ TO_PATCH)
+
+ @patch('charmhelpers.contrib.openstack.utils.juju_log')
+ @patch('charmhelpers.contrib.openstack.utils.config')
+ @patch('charmhelpers.contrib.openstack.utils.action_set')
+ @patch('charmhelpers.contrib.openstack.utils.git_install_requested')
+ @patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
+ def test_openstack_upgrade_true(self, upgrade_avail, git_requested,
+ action_set, config, log):
+ git_requested.return_value = False
+ upgrade_avail.return_value = True
+ config.return_value = True
+
+ openstack_upgrade.openstack_upgrade()
+
+ self.assertTrue(self.do_openstack_upgrade.called)
+ self.assertTrue(self.config_changed.called)
+
+ @patch('charmhelpers.contrib.openstack.utils.juju_log')
+ @patch('charmhelpers.contrib.openstack.utils.config')
+ @patch('charmhelpers.contrib.openstack.utils.action_set')
+ @patch('charmhelpers.contrib.openstack.utils.git_install_requested')
+ @patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
+ def test_openstack_upgrade_false(self, upgrade_avail, git_requested,
+ action_set, config, log):
+ git_requested.return_value = False
+ upgrade_avail.return_value = True
+ config.return_value = False
+
+ openstack_upgrade.openstack_upgrade()
+
+ self.assertFalse(self.do_openstack_upgrade.called)
+ self.assertFalse(self.config_changed.called)
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_contexts.py b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_contexts.py
new file mode 100644
index 0000000..fdbbf15
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_contexts.py
@@ -0,0 +1,59 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ceilometer_contexts as contexts
+from test_utils import CharmTestCase
+
+TO_PATCH = [
+ 'relation_get',
+ 'relation_ids',
+ 'related_units',
+]
+
+
+class CeilometerContextsTest(CharmTestCase):
+
+ def setUp(self):
+ super(CeilometerContextsTest, self).setUp(contexts, TO_PATCH)
+ self.relation_get.side_effect = self.test_relation.get
+
+ def tearDown(self):
+ super(CeilometerContextsTest, self).tearDown()
+
+ def test_ceilometer_service_context(self):
+ self.relation_ids.return_value = ['ceilometer-service:0']
+ self.related_units.return_value = ['ceilometer/0']
+ data = {
+ 'debug': True,
+ 'verbose': False,
+ 'rabbitmq_host': 'foo',
+ 'rabbitmq_user': 'bar',
+ 'rabbitmq_password': 'baz',
+ 'rabbitmq_virtual_host': 'openstack',
+ 'rabbit_ssl_ca': None,
+ 'rabbit_ssl_port': None,
+ 'auth_protocol': 'http',
+ 'auth_host': 'keystone',
+ 'auth_port': '80',
+ 'admin_tenant_name': 'admin',
+ 'admin_user': 'admin',
+ 'admin_password': 'password',
+ 'metering_secret': 'secret'
+ }
+ self.test_relation.set(data)
+ self.assertEquals(contexts.CeilometerServiceContext()(), data)
+
+ def test_ceilometer_service_context_not_related(self):
+ self.relation_ids.return_value = []
+ self.assertEquals(contexts.CeilometerServiceContext()(), {})
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_hooks.py b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_hooks.py
new file mode 100644
index 0000000..67a933b
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_hooks.py
@@ -0,0 +1,127 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+from mock import patch, MagicMock
+
+import ceilometer_utils
+# Patch out register_configs for import of hooks
+_register_configs = ceilometer_utils.register_configs
+ceilometer_utils.register_configs = MagicMock()
+
+import ceilometer_hooks as hooks
+
+# Renable old function
+ceilometer_utils.register_configs = _register_configs
+
+from test_utils import CharmTestCase
+
+TO_PATCH = [
+ 'configure_installation_source',
+ 'apt_install',
+ 'apt_update',
+ 'config',
+ 'filter_installed_packages',
+ 'CONFIGS',
+ 'relation_set',
+ 'openstack_upgrade_available',
+ 'do_openstack_upgrade',
+ 'update_nrpe_config',
+ 'is_relation_made',
+]
+
+
+class CeilometerHooksTest(CharmTestCase):
+
+ def setUp(self):
+ super(CeilometerHooksTest, self).setUp(hooks, TO_PATCH)
+ self.config.side_effect = self.test_config.get
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_configure_source(self, mock_config):
+ self.test_config.set('openstack-origin', 'cloud:precise-havana')
+ hooks.hooks.execute(['hooks/install'])
+ self.configure_installation_source.\
+ assert_called_with('cloud:precise-havana')
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_install_hook(self, mock_config):
+ self.filter_installed_packages.return_value = \
+ hooks.CEILOMETER_AGENT_PACKAGES
+ hooks.hooks.execute(['hooks/install'])
+ self.assertTrue(self.configure_installation_source.called)
+ self.apt_update.assert_called_with(fatal=True)
+ self.apt_install.assert_called_with(hooks.CEILOMETER_AGENT_PACKAGES,
+ fatal=True)
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_ceilometer_changed(self, mock_config):
+ hooks.hooks.execute(['hooks/ceilometer-service-relation-changed'])
+ self.assertTrue(self.CONFIGS.write_all.called)
+ self.assertTrue(self.update_nrpe_config.called)
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_ceilometer_changed_no_nrpe(self, mock_config):
+ self.is_relation_made.return_value = False
+
+ hooks.hooks.execute(['hooks/ceilometer-service-relation-changed'])
+ self.assertTrue(self.CONFIGS.write_all.called)
+ self.assertFalse(self.update_nrpe_config.called)
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_nova_ceilometer_joined(self, mock_config):
+ hooks.hooks.execute(['hooks/nova-ceilometer-relation-joined'])
+ self.relation_set.assert_called_with(
+ subordinate_configuration=json.dumps(
+ ceilometer_utils.NOVA_SETTINGS))
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_config_changed_no_upgrade(self, mock_config):
+ self.openstack_upgrade_available.return_value = False
+ hooks.hooks.execute(['hooks/config-changed'])
+ self.openstack_upgrade_available.\
+ assert_called_with('ceilometer-common')
+ self.assertFalse(self.do_openstack_upgrade.called)
+ self.assertTrue(self.CONFIGS.write_all.called)
+ self.assertTrue(self.update_nrpe_config.called)
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_config_changed_upgrade(self, mock_config):
+ self.openstack_upgrade_available.return_value = True
+ hooks.hooks.execute(['hooks/config-changed'])
+ self.openstack_upgrade_available.\
+ assert_called_with('ceilometer-common')
+ self.assertTrue(self.do_openstack_upgrade.called)
+ self.assertTrue(self.CONFIGS.write_all.called)
+ self.assertTrue(self.update_nrpe_config.called)
+
+ def test_config_changed_with_openstack_upgrade_action(self):
+ self.openstack_upgrade_available.return_value = True
+ self.test_config.set('action-managed-upgrade', True)
+
+ hooks.hooks.execute(['hooks/config-changed'])
+
+ self.assertFalse(self.do_openstack_upgrade.called)
+
+ @patch('charmhelpers.core.hookenv.config')
+ def test_config_changed_no_nrpe(self, mock_config):
+ self.openstack_upgrade_available.return_value = False
+ self.is_relation_made.return_value = False
+
+ hooks.hooks.execute(['hooks/config-changed'])
+ self.openstack_upgrade_available.\
+ assert_called_with('ceilometer-common')
+ self.assertFalse(self.do_openstack_upgrade.called)
+ self.assertTrue(self.CONFIGS.write_all.called)
+ self.assertFalse(self.update_nrpe_config.called)
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_utils.py b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_utils.py
new file mode 100644
index 0000000..01ba074
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_ceilometer_utils.py
@@ -0,0 +1,116 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from mock import call, MagicMock, patch
+
+import ceilometer_utils as utils
+
+from test_utils import CharmTestCase
+
+TO_PATCH = [
+ 'get_os_codename_package',
+ 'templating',
+ 'CeilometerServiceContext',
+ 'config',
+ 'get_os_codename_install_source',
+ 'configure_installation_source',
+ 'apt_install',
+ 'apt_update',
+ 'apt_upgrade',
+ 'log'
+]
+
+
+class CeilometerUtilsTest(CharmTestCase):
+
+ def setUp(self):
+ super(CeilometerUtilsTest, self).setUp(utils, TO_PATCH)
+
+ def tearDown(self):
+ super(CeilometerUtilsTest, self).tearDown()
+
+ def test_register_configs(self):
+ configs = utils.register_configs()
+ calls = []
+ for conf in utils.CONFIG_FILES:
+ calls.append(call(conf,
+ utils.CONFIG_FILES[conf]['hook_contexts']))
+ configs.register.assert_has_calls(calls, any_order=True)
+
+ def test_restart_map(self):
+ restart_map = utils.restart_map()
+ self.assertEquals(restart_map,
+ {'/etc/ceilometer/ceilometer.conf': [
+ 'ceilometer-agent-compute']})
+
+ def test_do_openstack_upgrade(self):
+ self.config.side_effect = self.test_config.get
+ self.test_config.set('openstack-origin', 'cloud:precise-havana')
+ self.get_os_codename_install_source.return_value = 'havana'
+ configs = MagicMock()
+ utils.do_openstack_upgrade(configs)
+ configs.set_release.assert_called_with(openstack_release='havana')
+ self.assertTrue(self.log.called)
+ self.apt_update.assert_called_with(fatal=True)
+ dpkg_opts = [
+ '--option', 'Dpkg::Options::=--force-confnew',
+ '--option', 'Dpkg::Options::=--force-confdef',
+ ]
+ self.apt_install.assert_called_with(
+ packages=utils.CEILOMETER_AGENT_PACKAGES,
+ options=dpkg_opts, fatal=True
+ )
+ self.configure_installation_source.assert_called_with(
+ 'cloud:precise-havana'
+ )
+
+ def test_assess_status(self):
+ with patch.object(utils, 'assess_status_func') as asf:
+ callee = MagicMock()
+ asf.return_value = callee
+ utils.assess_status('test-config')
+ asf.assert_called_once_with('test-config')
+ callee.assert_called_once_with()
+
+ @patch.object(utils, 'REQUIRED_INTERFACES')
+ @patch.object(utils, 'services')
+ @patch.object(utils, 'make_assess_status_func')
+ def test_assess_status_func(self,
+ make_assess_status_func,
+ services,
+ REQUIRED_INTERFACES):
+ services.return_value = 's1'
+ utils.assess_status_func('test-config')
+ # ports=None whilst port checks are disabled.
+ make_assess_status_func.assert_called_once_with(
+ 'test-config', REQUIRED_INTERFACES, services='s1', ports=None)
+
+ def test_pause_unit_helper(self):
+ with patch.object(utils, '_pause_resume_helper') as prh:
+ utils.pause_unit_helper('random-config')
+ prh.assert_called_once_with(utils.pause_unit, 'random-config')
+ with patch.object(utils, '_pause_resume_helper') as prh:
+ utils.resume_unit_helper('random-config')
+ prh.assert_called_once_with(utils.resume_unit, 'random-config')
+
+ @patch.object(utils, 'services')
+ def test_pause_resume_helper(self, services):
+ f = MagicMock()
+ services.return_value = 's1'
+ with patch.object(utils, 'assess_status_func') as asf:
+ asf.return_value = 'assessor'
+ utils._pause_resume_helper(f, 'some-config')
+ asf.assert_called_once_with('some-config')
+ # ports=None whilst port checks are disabled.
+ f.assert_called_once_with('assessor', services='s1', ports=None)
diff --git a/charms/trusty/ceilometer-agent/unit_tests/test_utils.py b/charms/trusty/ceilometer-agent/unit_tests/test_utils.py
new file mode 100644
index 0000000..09d77a6
--- /dev/null
+++ b/charms/trusty/ceilometer-agent/unit_tests/test_utils.py
@@ -0,0 +1,128 @@
+# Copyright 2016 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import unittest
+import os
+import yaml
+import io
+
+from contextlib import contextmanager
+from mock import patch
+
+
+@contextmanager
+def mock_open(filename, contents=None):
+ ''' Slightly simpler mock of open to return contents for filename '''
+ def mock_file(*args):
+ if args[0] == filename:
+ return io.StringIO(contents)
+ else:
+ return open(*args)
+ with patch('__builtin__.open', mock_file):
+ yield
+
+
+def load_config():
+ '''
+ Walk backwords from __file__ looking for config.yaml, load and return the
+ 'options' section'
+ '''
+ config = None
+ f = __file__
+ while config is None:
+ d = os.path.dirname(f)
+ if os.path.isfile(os.path.join(d, 'config.yaml')):
+ config = os.path.join(d, 'config.yaml')
+ break
+ f = d
+
+ if not config:
+ logging.error('Could not find config.yaml in any parent directory '
+ 'of %s. ' % file)
+ raise Exception
+
+ return yaml.safe_load(open(config).read())['options']
+
+
+def get_default_config():
+ '''
+ Load default charm config from config.yaml return as a dict.
+ If no default is set in config.yaml, its value is None.
+ '''
+ default_config = {}
+ config = load_config()
+ for k, v in config.iteritems():
+ if 'default' in v:
+ default_config[k] = v['default']
+ else:
+ default_config[k] = None
+ return default_config
+
+
+class CharmTestCase(unittest.TestCase):
+
+ def setUp(self, obj, patches):
+ super(CharmTestCase, self).setUp()
+ self.patches = patches
+ self.obj = obj
+ self.test_config = TestConfig()
+ self.test_relation = TestRelation()
+ self.patch_all()
+
+ def patch(self, method):
+ _m = patch.object(self.obj, method)
+ mock = _m.start()
+ self.addCleanup(_m.stop)
+ return mock
+
+ def patch_all(self):
+ for method in self.patches:
+ setattr(self, method, self.patch(method))
+
+
+class TestConfig(object):
+
+ def __init__(self):
+ self.config = get_default_config()
+
+ def get(self, attr):
+ try:
+ return self.config[attr]
+ except KeyError:
+ return None
+
+ def get_all(self):
+ return self.config
+
+ def set(self, attr, value):
+ if attr not in self.config:
+ raise KeyError
+ self.config[attr] = value
+
+
+class TestRelation(object):
+
+ def __init__(self, relation_data={}):
+ self.relation_data = relation_data
+
+ def set(self, relation_data):
+ self.relation_data = relation_data
+
+ def get(self, attr=None, unit=None, rid=None):
+ if attr is None:
+ return self.relation_data
+ elif attr in self.relation_data:
+ return self.relation_data[attr]
+ return None