diff --git a/setup.cfg b/setup.cfg index 551db5c9b..4677c20f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ mistral.actions = tripleo.baremetal.update_node_capability = tripleo_common.actions.baremetal:UpdateNodeCapability tripleo.deployment.config = tripleo_common.actions.deployment:OrchestrationDeployAction tripleo.deployment.deploy = tripleo_common.actions.deployment:DeployStackAction + tripleo.deployment.overcloudrc = tripleo_common.actions.deployment:OvercloudRcAction tripleo.heat_capabilities.get = tripleo_common.actions.heat_capabilities:GetCapabilitiesAction tripleo.heat_capabilities.update = tripleo_common.actions.heat_capabilities:UpdateCapabilitiesAction tripleo.parameters.get = tripleo_common.actions.parameters:GetParametersAction diff --git a/tripleo_common/actions/deployment.py b/tripleo_common/actions/deployment.py index b1470b773..e0c831fb2 100644 --- a/tripleo_common/actions/deployment.py +++ b/tripleo_common/actions/deployment.py @@ -19,10 +19,12 @@ import time from heatclient.common import deployment_utils from heatclient import exc as heat_exc from mistral.workflow import utils as mistral_workflow_utils +from mistralclient.api import base as mistralclient_exc from tripleo_common.actions import base from tripleo_common.actions import templates from tripleo_common import constants +from tripleo_common.utils import overcloudrc LOG = logging.getLogger(__name__) @@ -179,3 +181,55 @@ class DeployStackAction(templates.ProcessTemplatesAction): LOG.info("Performing Heat stack update") stack_args['existing'] = 'true' return heat.stacks.update(stack.id, **stack_args) + + +class OvercloudRcAction(base.TripleOAction): + """Generate the overcloudrc and overcloudrc.v3 for a plan + + Given the name of a container, generate the overcloudrc files needed to + access the overcloud via the CLI. + + no_proxy is optional and is a comma-separated string of hosts that + shouldn't be proxied + """ + + def __init__(self, container, no_proxy=""): + self.container = container + self.no_proxy = no_proxy + + def run(self): + orchestration_client = self._get_orchestration_client() + workflow_client = self._get_workflow_client() + + try: + stack = orchestration_client.stacks.get(self.container) + except heat_exc.HTTPNotFound: + error = ( + "The Heat stack {} cound not be found. Make sure you have " + "deployed before calling this action.").format(self.container) + return mistral_workflow_utils.Result(error=error) + + try: + environment = workflow_client.environments.get(self.container) + except mistralclient_exc.APIException: + error = "The Mistral environment {} cound not be found.".format( + self.container) + return mistral_workflow_utils.Result(error=error) + + # We need to check parameter_defaults first for a user provided + # password. If that doesn't exist, we then should look in the + # automatically generated passwords. + # TODO(d0ugal): Abstract this operation somewhere. We shouldn't need to + # know about the structure of the environment to get a password. + try: + parameter_defaults = environment.variables['parameter_defaults'] + passwords = environment.variables['passwords'] + admin_pass = parameter_defaults.get('AdminPassword') + if admin_pass is None: + admin_pass = passwords['AdminPassword'] + except KeyError: + error = ("Unable to find the AdminPassword in the Mistral " + "environment.") + return mistral_workflow_utils.Result(error=error) + + return overcloudrc.create_overcloudrc(stack, self.no_proxy, admin_pass) diff --git a/tripleo_common/tests/actions/test_deployment.py b/tripleo_common/tests/actions/test_deployment.py index d9b2bb0e2..646671bcf 100644 --- a/tripleo_common/tests/actions/test_deployment.py +++ b/tripleo_common/tests/actions/test_deployment.py @@ -14,7 +14,9 @@ # under the License. import mock +from heatclient import exc as heat_exc from mistral.workflow import utils as mistral_workflow_utils +from mistralclient.api import base as mistralclient_exc from swiftclient import exceptions as swiftexceptions from tripleo_common.actions import deployment @@ -258,3 +260,84 @@ class DeployStackActionTest(base.TestCase): template={'heat_template_version': '2016-04-30'}, timeout_mins=1, ) + + +class OvercloudRcActionTestCase(base.TestCase): + + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_workflow_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_orchestration_client') + @mock.patch('mistral.context.ctx') + def test_no_stack(self, mock_context, mock_get_orchestration, + mock_get_workflow): + + not_found = heat_exc.HTTPNotFound() + mock_get_orchestration.return_value.stacks.get.side_effect = not_found + + action = deployment.OvercloudRcAction("overcast") + result = action.run() + + self.assertEqual(result.error, ( + "The Heat stack overcast cound not be found. Make sure you have " + "deployed before calling this action." + )) + + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_workflow_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_orchestration_client') + @mock.patch('mistral.context.ctx') + def test_no_env(self, mock_context, mock_get_orchestration, + mock_get_workflow): + + not_found = mistralclient_exc.APIException() + mock_get_workflow.return_value.environments.get.side_effect = not_found + + action = deployment.OvercloudRcAction("overcast") + result = action.run() + + self.assertEqual( + result.error, + "The Mistral environment overcast cound not be found.") + + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_workflow_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_orchestration_client') + @mock.patch('mistral.context.ctx') + def test_no_password(self, mock_context, mock_get_orchestration, + mock_get_workflow): + + mock_env = mock.MagicMock(variables={}) + mock_get_workflow.return_value.environments.get.return_value = mock_env + + action = deployment.OvercloudRcAction("overcast") + result = action.run() + + self.assertEqual( + result.error, + "Unable to find the AdminPassword in the Mistral environment.") + + @mock.patch('tripleo_common.utils.overcloudrc.create_overcloudrc') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_workflow_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + '_get_orchestration_client') + @mock.patch('mistral.context.ctx') + def test_no_success(self, mock_context, mock_get_orchestration, + mock_get_workflow, mock_create_overcloudrc): + + mock_create_overcloudrc.return_value = { + "overcloudrc": "fake overcloudrc" + } + mock_env = mock.MagicMock(variables={ + "parameter_defaults": {}, + "passwords": {"AdminPassword": "SUPERSECUREPASSWORD"} + }) + mock_get_workflow.return_value.environments.get.return_value = mock_env + + action = deployment.OvercloudRcAction("overcast") + result = action.run() + + self.assertEqual(result, {"overcloudrc": "fake overcloudrc"}) diff --git a/tripleo_common/tests/utils/test_overcloudrc.py b/tripleo_common/tests/utils/test_overcloudrc.py new file mode 100644 index 000000000..f655b89e5 --- /dev/null +++ b/tripleo_common/tests/utils/test_overcloudrc.py @@ -0,0 +1,42 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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 tripleo_common.tests import base +from tripleo_common.utils import overcloudrc + + +class OvercloudRcTest(base.TestCase): + + def test_generate_overcloudrc(self): + + stack = mock.MagicMock() + stack.stack_name = 'overcast' + stack.to_dict.return_value = { + "outputs": [ + {'output_key': 'KeystoneURL', + 'output_value': 'http://foo.com:8000/'}, + {'output_key': 'EndpointMap', + 'output_value': {'KeystoneAdmin': {'host': 'fd00::1'}}}, + ] + } + + result = overcloudrc.create_overcloudrc(stack, "", "AdminPassword") + + self.assertIn("OS_PASSWORD=AdminPassword", result['overcloudrc']) + self.assertIn("OS_PASSWORD=AdminPassword", result['overcloudrc.v3']) + self.assertNotIn("OS_IDENTITY_API_VERSION=3", result['overcloudrc']) + self.assertIn("OS_IDENTITY_API_VERSION=3", result['overcloudrc.v3']) diff --git a/tripleo_common/utils/overcloudrc.py b/tripleo_common/utils/overcloudrc.py new file mode 100644 index 000000000..46e69a933 --- /dev/null +++ b/tripleo_common/utils/overcloudrc.py @@ -0,0 +1,113 @@ +# Copyright 2015 Red Hat, Inc. +# +# 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 socket + +from six.moves import urllib + + +def get_service_ips(stack): + service_ips = {} + for output in stack.to_dict().get('outputs', {}): + service_ips[output['output_key']] = output['output_value'] + return service_ips + + +def get_endpoint_map(stack): + endpoint_map = {} + for output in stack.to_dict().get('outputs', {}): + if output['output_key'] == 'EndpointMap': + endpoint_map = output['output_value'] + break + return endpoint_map + + +def get_endpoint(key, stack): + endpoint_map = get_endpoint_map(stack) + if endpoint_map: + return endpoint_map[key]['host'] + else: + return get_service_ips(stack).get(key + 'Vip') + + +def get_overcloud_endpoint(stack): + for output in stack.to_dict().get('outputs', {}): + if output['output_key'] == 'KeystoneURL': + return output['output_value'] + + +def bracket_ipv6(address): + """Put a bracket around address if it is valid IPv6 + + Return it unchanged if it is a hostname or IPv4 address. + """ + try: + socket.inet_pton(socket.AF_INET6, address) + return "[%s]" % address + except socket.error: + return address + +CLEAR_ENV = """# Clear any old environment that may conflict. +for key in $( set | awk '{FS=\"=\"} /^OS_/ {print $1}' ); do unset $key ; done +""" + + +def create_overcloudrc(stack, no_proxy, admin_password): + """Given the stack and proxy settings, create the overcloudrc + + stack: Heat stack containing the deployed overcloud + no_proxy: a comma-separated string of hosts that shouldn't be proxied + """ + overcloud_endpoint = get_overcloud_endpoint(stack) + overcloud_host = urllib.parse.urlparse(overcloud_endpoint).hostname + overcloud_admin_vip = get_endpoint('KeystoneAdmin', stack) + + no_proxy_list = map(bracket_ipv6, + [no_proxy, overcloud_host, overcloud_admin_vip]) + + rc_params = { + 'NOVA_VERSION': '1.1', + 'COMPUTE_API_VERSION': '1.1', + 'OS_USERNAME': 'admin', + 'OS_PROJECT_NAME': 'admin', + 'OS_NO_CACHE': 'True', + 'OS_CLOUDNAME': stack.stack_name, + 'no_proxy': ','.join(no_proxy_list), + 'PYTHONWARNINGS': ('"ignore:Certificate has no, ignore:A true ' + 'SSLContext object is not available"'), + 'OS_PASSWORD': admin_password, + 'OS_AUTH_URL': overcloud_endpoint, + } + + overcloudrc = CLEAR_ENV + for key, value in rc_params.items(): + line = "export %(key)s=%(value)s\n" % {'key': key, 'value': value} + overcloudrc = overcloudrc + line + + rc_params.update({ + 'OS_AUTH_URL': overcloud_endpoint.replace('/v2.0', '') + '/v3', + 'OS_USER_DOMAIN_NAME': 'Default', + 'OS_PROJECT_DOMAIN_NAME': 'Default', + 'OS_IDENTITY_API_VERSION': '3' + }) + + overcloudrc_v3 = CLEAR_ENV + for key, value in rc_params.items(): + line = "export %(key)s=%(value)s\n" % {'key': key, 'value': value} + overcloudrc_v3 = overcloudrc_v3 + line + + return { + "overcloudrc": overcloudrc, + "overcloudrc.v3": overcloudrc_v3 + }