From 200ba35d57e0140aeb3f922063c31ce121cf65e5 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Wed, 1 Aug 2018 15:31:24 +0200 Subject: [PATCH 1/2] Add functions: run command on leader, get leader settings --- unit_tests/test_zaza_model.py | 11 +++++++ .../utilities/test_zaza_utilities_juju.py | 33 +++++++++++++++++++ zaza/model.py | 28 ++++++++++++++++ zaza/utilities/juju.py | 17 ++++++++++ 4 files changed, 89 insertions(+) diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index 95bd7cb..d06c5d5 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -67,6 +67,7 @@ class TestModel(ut_utils.BaseTestCase): self.unit2.name = 'app/4' self.unit2.entity_id = 'app/4' self.unit2.machine = 'machine7' + self.unit2.run.side_effect = _run self.unit1.run.side_effect = _run self.unit1.scp_to.side_effect = _scp_to self.unit2.scp_to.side_effect = _scp_to @@ -255,6 +256,16 @@ class TestModel(ut_utils.BaseTestCase): expected) self.unit1.run.assert_called_once_with(cmd, timeout=None) + def test_run_on_leader(self): + self.patch_object(model, 'get_juju_model', return_value='mname') + expected = {'Code': '0', 'Stderr': '', 'Stdout': 'RESULT'} + self.cmd = cmd = 'somecommand someargument' + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.assertEqual(model.run_on_leader('app', cmd), + expected) + self.unit2.run.assert_called_once_with(cmd, timeout=None) + def test_get_relation_id(self): self.patch_object(model, 'get_juju_model', return_value='mname') self.patch_object(model, 'Model') diff --git a/unit_tests/utilities/test_zaza_utilities_juju.py b/unit_tests/utilities/test_zaza_utilities_juju.py index b43988d..db49cb7 100644 --- a/unit_tests/utilities/test_zaza_utilities_juju.py +++ b/unit_tests/utilities/test_zaza_utilities_juju.py @@ -194,3 +194,36 @@ class TestJujuUtils(ut_utils.BaseTestCase): 'aunit/0', 'relation-get --format=yaml -r "42" - "otherunit/0"') self.assertFalse(self.yaml.load.called) + + def test_leader_get(self): + self.patch_object(juju_utils, 'yaml') + self.patch_object(juju_utils, 'model') + data = {'foo': 'bar'} + self.model.run_on_leader.return_value = { + 'Code': 0, 'Stdout': str(data)} + juju_utils.leader_get('application') + self.model.run_on_leader.assert_called_with( + 'application', 'leader-get --format=yaml ') + self.yaml.load.assert_called_with(str(data)) + + def test_leader_get_key(self): + self.patch_object(juju_utils, 'yaml') + self.patch_object(juju_utils, 'model') + data = {'foo': 'bar'} + self.model.run_on_leader.return_value = { + 'Code': 0, 'Stdout': data['foo']} + juju_utils.leader_get('application', 'foo') + self.model.run_on_leader.assert_called_with( + 'application', 'leader-get --format=yaml foo') + self.yaml.load.assert_called_with(data['foo']) + + def test_leader_get_fails(self): + self.patch_object(juju_utils, 'yaml') + self.patch_object(juju_utils, 'model') + self.model.run_on_leader.return_value = { + 'Code': 1, 'Stderr': 'ERROR'} + with self.assertRaises(Exception): + juju_utils.leader_get('application') + self.model.run_on_leader.assert_called_with( + 'application', 'leader-get --format=yaml ') + self.assertFalse(self.yaml.load.called) diff --git a/zaza/model.py b/zaza/model.py index 944a74b..445fe8a 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -228,6 +228,34 @@ async def async_run_on_unit(unit_name, command, model_name=None, timeout=None): run_on_unit = sync_wrapper(async_run_on_unit) +async def async_run_on_leader(application_name, command, model_name=None, + timeout=None): + """Juju run on leader unit. + + :param application_name: Application to match + :type application_name: str + :param command: Command to execute + :type command: str + :param model_name: Name of model unit is in + :type model_name: str + :param timeout: How long in seconds to wait for command to complete + :type timeout: int + :returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''} + :rtype: dict + """ + async with run_in_model(model_name) as model: + for unit in model.applications[application_name].units: + is_leader = await unit.is_leader_from_status() + if is_leader: + action = await unit.run(command, timeout=timeout) + if action.data.get('results'): + return action.data.get('results') + else: + return {} + +run_on_leader = sync_wrapper(async_run_on_leader) + + async def async_get_unit_time(unit_name, model_name=None, timeout=None): """Get the current time (in seconds since Epoch) on the given unit. diff --git a/zaza/utilities/juju.py b/zaza/utilities/juju.py index 8d724c2..b1a1f5b 100644 --- a/zaza/utilities/juju.py +++ b/zaza/utilities/juju.py @@ -228,3 +228,20 @@ def get_relation_from_unit(entity, remote_entity, remote_interface_name): else: raise Exception('Error running remote command: "{}"' .format(result.get("Stderr"))) + + +def leader_get(application, key=''): + """Get leader settings from leader unit of named application. + + :param application: Application to get leader settings from. + :type application: str + :returns: dict with leader settings + :rtype: dict + """ + result = model.run_on_leader(application, + 'leader-get --format=yaml {}'.format(key)) + 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"))) From 97c8c69675f83c8f7ac6d925f53a57d6b2b25165 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Wed, 1 Aug 2018 15:36:10 +0200 Subject: [PATCH 2/2] Improve password security Retrieve password from leader settings on keystone leader unit. --- unit_tests/utilities/test_zaza_utilities_openstack.py | 2 ++ zaza/utilities/openstack.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 705b378..599a1e2 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -158,10 +158,12 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.patch_object(openstack_utils, 'get_application_config_option') self.patch_object(openstack_utils, 'get_keystone_ip') self.patch_object(openstack_utils, "get_current_os_versions") + self.patch_object(openstack_utils.juju_utils, 'leader_get') self.get_keystone_ip.return_value = '127.0.0.1' self.get_relation_id.return_value = None self.get_application_config_option.return_value = None + self.leader_get.return_value = 'openstack' if tls_relation or ssl_cert: port = 35357 transport = 'https' diff --git a/zaza/utilities/openstack.py b/zaza/utilities/openstack.py index 0a26eea..c8c767c 100644 --- a/zaza/utilities/openstack.py +++ b/zaza/utilities/openstack.py @@ -1245,6 +1245,8 @@ def get_overcloud_auth(): elif api_version is None: api_version = 2 + password = juju_utils.leader_get('keystone', 'admin_passwd') + if api_version == 2: # V2 Explicitly, or None when charm does not possess the config key logging.info('Using keystone API V2 for overcloud auth') @@ -1252,7 +1254,7 @@ def get_overcloud_auth(): 'OS_AUTH_URL': '%s://%s:%i/v2.0' % (transport, address, port), 'OS_TENANT_NAME': 'admin', 'OS_USERNAME': 'admin', - 'OS_PASSWORD': 'openstack', + 'OS_PASSWORD': password, 'OS_REGION_NAME': 'RegionOne', 'API_VERSION': 2, } @@ -1262,7 +1264,7 @@ def get_overcloud_auth(): auth_settings = { 'OS_AUTH_URL': '%s://%s:%i/v3' % (transport, address, port), 'OS_USERNAME': 'admin', - 'OS_PASSWORD': 'openstack', + 'OS_PASSWORD': password, 'OS_REGION_NAME': 'RegionOne', 'OS_DOMAIN_NAME': 'admin_domain', 'OS_USER_DOMAIN_NAME': 'admin_domain',