diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index 9febe6b..397613c 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -72,8 +72,18 @@ class TestModel(ut_utils.BaseTestCase): self.unit1.is_leader_from_status.side_effect = _is_leader(False) self.unit2.is_leader_from_status.side_effect = _is_leader(True) self.units = [self.unit1, self.unit2] + self.relation1 = mock.MagicMock() + self.relation1.id = 42 + self.relation1.matches.side_effect = \ + lambda x: True if x == 'app' else False + self.relation2 = mock.MagicMock() + self.relation2.id = 51 + self.relation2.matches.side_effect = \ + lambda x: True if x == 'app:interface' else False + self.relations = [self.relation1, self.relation2] _units = mock.MagicMock() _units.units = self.units + _units.relations = self.relations self.mymodel = mock.MagicMock() self.mymodel.applications = { 'app': _units @@ -179,6 +189,17 @@ class TestModel(ut_utils.BaseTestCase): expected) self.unit1.run.assert_called_once_with(cmd, timeout=None) + def test_get_relation_id(self): + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.assertEqual(model.get_relation_id('testmodel', 'app', 'app'), 42) + + def test_get_relation_id_interface(self): + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.assertEqual(model.get_relation_id('testmodel', 'app', 'app', + 'interface'), 51) + def test_run_action(self): self.patch_object(model, 'Model') self.patch_object(model, 'get_unit_from_name') diff --git a/unit_tests/utilities/test_zaza_utilities_juju.py b/unit_tests/utilities/test_zaza_utilities_juju.py index 28d95ed..b895db8 100644 --- a/unit_tests/utilities/test_zaza_utilities_juju.py +++ b/unit_tests/utilities/test_zaza_utilities_juju.py @@ -155,3 +155,46 @@ class TestJujuUtils(ut_utils.BaseTestCase): # Fatal failure with self.assertRaises(Exception): juju_utils.remote_run(self.unit, _cmd, fatal=True) + + def test_get_unit_names(self): + self.patch('zaza.model.get_first_unit_name', new_callable=mock.Mock(), + name='_get_first_unit_name') + juju_utils._get_unit_names(['aunit/0', 'otherunit/0']) + self.assertFalse(self._get_first_unit_name.called) + + def test_get_unit_names_called_with_application_name(self): + self.patch_object(juju_utils, 'model') + juju_utils._get_unit_names(['aunit', 'otherunit/0']) + self.model.get_first_unit_name.assert_called() + + def test_get_relation_from_unit(self): + self.patch_object(juju_utils, 'lifecycle_utils') + self.patch_object(juju_utils, '_get_unit_names') + self.patch_object(juju_utils, 'yaml') + self.patch_object(juju_utils, 'model') + self._get_unit_names.return_value = ['aunit/0', 'otherunit/0'] + data = {'foo': 'bar'} + self.model.get_relation_id.return_value = 42 + self.model.run_on_unit.return_value = {'Code': 0, 'Stdout': str(data)} + juju_utils.get_relation_from_unit('aunit/0', 'otherunit/0', + 'arelation') + self.model.run_on_unit.assert_called_with( + self.lifecycle_utils.get_juju_model(), 'aunit/0', + 'relation-get --format=yaml -r "42" - "otherunit/0"') + self.yaml.load.assert_called_with(str(data)) + + def test_get_relation_from_unit_fails(self): + self.patch_object(juju_utils, 'lifecycle_utils') + self.patch_object(juju_utils, '_get_unit_names') + self.patch_object(juju_utils, 'yaml') + self.patch_object(juju_utils, 'model') + self._get_unit_names.return_value = ['aunit/0', 'otherunit/0'] + self.model.get_relation_id.return_value = 42 + self.model.run_on_unit.return_value = {'Code': 1, 'Stderr': 'ERROR'} + with self.assertRaises(Exception): + juju_utils.get_relation_from_unit('aunit/0', 'otherunit/0', + 'arelation') + self.model.run_on_unit.assert_called_with( + self.lifecycle_utils.get_juju_model(), 'aunit/0', + 'relation-get --format=yaml -r "42" - "otherunit/0"') + self.assertFalse(self.yaml.load.called) diff --git a/zaza/charm_tests/dragent/tests.py b/zaza/charm_tests/dragent/tests.py index 23244a9..3d993c7 100644 --- a/zaza/charm_tests/dragent/tests.py +++ b/zaza/charm_tests/dragent/tests.py @@ -2,7 +2,7 @@ import unittest -from zaza.utilities import generic as generic_utils +from zaza.utilities import cli as cli_utils from zaza.charm_tests.dragent import test @@ -12,7 +12,7 @@ class DRAgentTest(unittest.TestCase): @classmethod def setUpClass(cls): - generic_utils.setup_logging() + cli_utils.setup_logging() def test_bgp_routes(self): test.test_bgp_routes(peer_application_name=self.BGP_PEER_APPLICATION) diff --git a/zaza/configure/bgp_speaker.py b/zaza/configure/bgp_speaker.py index 5f56fc4..091c0da 100755 --- a/zaza/configure/bgp_speaker.py +++ b/zaza/configure/bgp_speaker.py @@ -6,6 +6,7 @@ import sys from zaza.utilities import ( cli as cli_utils, openstack as openstack_utils, + juju as juju_utils, ) @@ -24,6 +25,19 @@ def setup_bgp_speaker(peer_application_name, keystone_session=None): :returns: None :rtype: None """ + # Get ASNs from deployment + dr_relation = juju_utils.get_relation_from_unit( + 'neutron-dynamic-routing', + peer_application_name, + 'bgpclient') + peer_asn = dr_relation.get('asn') + logging.debug('peer ASn: "{}"'.format(peer_asn)) + peer_relation = juju_utils.get_relation_from_unit( + peer_application_name, + 'neutron-dynamic-routing', + 'bgp-speaker') + dr_asn = peer_relation.get('asn') + logging.debug('our ASn: "{}"'.format(dr_asn)) # If a session has not been provided, acquire one if not keystone_session: @@ -36,7 +50,7 @@ def setup_bgp_speaker(peer_application_name, keystone_session=None): # Create BGP speaker logging.info("Setting up BGP speaker") bgp_speaker = openstack_utils.create_bgp_speaker( - neutron_client, local_as=12345) + neutron_client, local_as=dr_asn) # Add networks to bgp speaker logging.info("Advertising BGP routes") @@ -53,7 +67,7 @@ def setup_bgp_speaker(peer_application_name, keystone_session=None): logging.info("Setting up BGP peer") bgp_peer = openstack_utils.create_bgp_peer(neutron_client, peer_application_name, - remote_as=10000) + remote_as=peer_asn) # Add peer to bgp speaker logging.info("Adding BGP peer to BGP speaker") openstack_utils.add_peer_to_bgp_speaker( diff --git a/zaza/model.py b/zaza/model.py index 8deda72..9b4b304 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -654,6 +654,35 @@ block_until_file_has_contents = sync_wrapper( async_block_until_file_has_contents) +async def async_get_relation_id(model_name, application_name, + remote_application_name, + remote_interface_name=None): + """ + Get relation id of relation from model + + :param model_name: Name of model to operate on + :type model_name: str + :param application_name: Name of application on this side of relation + :type application_name: str + :param remote_application_name: Name of application on other side of + relation + :type remote_application_name: str + :param remote_interface_name: Name of interface on remote end of relation + :type remote_interface_name: Optional(str) + :returns: Relation id of relation if found or None + :rtype: any + """ + async with run_in_model(model_name) as model: + for rel in model.applications[application_name].relations: + spec = '{}'.format(remote_application_name) + if remote_interface_name is not None: + spec += ':{}'.format(remote_interface_name) + if rel.matches(spec): + return(rel.id) + +get_relation_id = sync_wrapper(async_get_relation_id) + + def main(): # Run the deploy coroutine in an asyncio event loop, using a helper # that abstracts loop creation and teardown. diff --git a/zaza/utilities/juju.py b/zaza/utilities/juju.py index 74d6a6d..66f5521 100644 --- a/zaza/utilities/juju.py +++ b/zaza/utilities/juju.py @@ -1,7 +1,21 @@ #!/usr/bin/env python3 +# Copyright 2018 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 os from pathlib import Path +import yaml from zaza import ( model, @@ -169,3 +183,61 @@ def remote_run(unit, remote_cmd, timeout=None, fatal=None): raise Exception("Error running remote command: {}" .format(result.get("Stderr"))) return result.get("Stderr") + + +def _get_unit_names(names): + """ + Helper function that resolves application names to first unit name of + said application. Any already resolved unit names are returned as-is. + + :param names: List of units/applications to translate + :type names: list(str) + :returns: List of units + :rtype: list(str) + """ + result = [] + for name in names: + if '/' in name: + result.append(name) + else: + result.append( + model.get_first_unit_name(lifecycle_utils.get_juju_model(), + name)) + return result + + +def get_relation_from_unit(entity, remote_entity, remote_interface_name): + """ + Get relation data for relation with `remote_interface_name` between + `entity` and `remote_entity` from the perspective of `entity`. + + `entity` and `remote_entity` may refer to either a application or a + specific unit. If application name is given first unit is found in model. + + :param entity: Application or unit to get relation data from + :type entity: str + :param remote_entity: Application or Unit in the other end of the relation + we want to query + :type remote_entity: str + :param remote_interface_name: Name of interface to query on remote end of + relation + :type remote_interface_name: str + :returns: dict with relation data + :rtype: dict + """ + application = entity.split('/')[0] + remote_application = remote_entity.split('/')[0] + rid = model.get_relation_id(lifecycle_utils.get_juju_model(), application, + remote_application, + remote_interface_name=remote_interface_name) + (unit, remote_unit) = _get_unit_names([entity, remote_entity]) + result = model.run_on_unit( + lifecycle_utils.get_juju_model(), unit, + 'relation-get --format=yaml -r "{}" - "{}"' + .format(rid, remote_unit) + ) + if result and int(result.get('Code')) == 0: + return yaml.load(result.get('Stdout')) + else: + raise Exception('Error running remote command: "{}"' + .format(result.get("Stderr")))