From 6d315598aa29ddda8dd3090170f2ef7d030e13ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 5 Apr 2017 09:28:55 -0500 Subject: [PATCH] Add support for downloading clouds.yaml files python-openstackclient, shade, openstacksdk and Ansible's OpenStack modules all support reading client config information from a file called clouds.yaml instead of from environment variables set from openrc files. Unfortunately, the only thing horizon currently offers for download is old-style openrc files. Add support for downloading clouds.yaml files. Change-Id: I0611dd44524b746ad993bff7435ec8628a83a762 --- doc/source/topics/settings.rst | 23 +++++ .../dashboards/project/api_access/tables.py | 11 ++- .../templates/api_access/clouds.yaml.template | 37 ++++++++ .../dashboards/project/api_access/urls.py | 2 + .../dashboards/project/api_access/views.py | 33 ++++++- openstack_dashboard/settings.py | 6 ++ openstack_dashboard/test/tests/templates.py | 92 +++++++++++++++++++ .../add-clouds-yaml-f72b9a0b7df2aa79.yaml | 6 ++ 8 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 openstack_dashboard/dashboards/project/api_access/templates/api_access/clouds.yaml.template create mode 100644 releasenotes/notes/add-clouds-yaml-f72b9a0b7df2aa79.yaml diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 81d8b96efb..d7f4389a3e 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -796,6 +796,29 @@ When set, enables the instance action "Retrieve password" allowing password retr from metadata service. +``OPENSTACK_CLOUDS_YAML_NAME`` +------------------------------ + +.. versionadded:: 12.0.0(Pike) + +Default: ``openstack`` + +The name of the entry to put into the user's clouds.yaml file. + + +``OPENSTACK_CLOUDS_YAML_PROFILE`` +--------------------------------- + +.. versionadded:: 12.0.0(Pike) + +Default: None + +If set, the name of the `vendor profile`_ from `os-client-config`_. + +.. _vendor profile: https://docs.openstack.org/developer/os-client-config/vendor-support.html +.. _os-client-config: https://docs.openstack.org/developer/os-client-config + + ``OPENSTACK_ENDPOINT_TYPE`` --------------------------- diff --git a/openstack_dashboard/dashboards/project/api_access/tables.py b/openstack_dashboard/dashboards/project/api_access/tables.py index 23469ece8b..5317b6b6fb 100644 --- a/openstack_dashboard/dashboards/project/api_access/tables.py +++ b/openstack_dashboard/dashboards/project/api_access/tables.py @@ -44,6 +44,14 @@ class DownloadEC2(tables.LinkAction): return api.base.is_service_enabled(request, 'ec2') +class DownloadCloudsYaml(tables.LinkAction): + name = "download_clouds_yaml" + verbose_name = _("Download OpenStack clouds.yaml File") + verbose_name_plural = _("Download OpenStack clouds.yaml File") + icon = "download" + url = "horizon:project:api_access:clouds.yaml" + + class DownloadOpenRC(tables.LinkAction): name = "download_openrc" verbose_name = _("Download OpenStack RC File v3") @@ -106,5 +114,6 @@ class EndpointsTable(tables.DataTable): name = "endpoints" verbose_name = _("API Endpoints") multi_select = False - table_actions = (DownloadOpenRCv2, DownloadOpenRC, DownloadEC2, + table_actions = (DownloadCloudsYaml, DownloadOpenRCv2, DownloadOpenRC, + DownloadEC2, ViewCredentials, RecreateCredentials) diff --git a/openstack_dashboard/dashboards/project/api_access/templates/api_access/clouds.yaml.template b/openstack_dashboard/dashboards/project/api_access/templates/api_access/clouds.yaml.template new file mode 100644 index 0000000000..efd5968fa7 --- /dev/null +++ b/openstack_dashboard/dashboards/project/api_access/templates/api_access/clouds.yaml.template @@ -0,0 +1,37 @@ +# This is a clouds.yaml file, which can be used by OpenStack tools as a source +# of configuration on how to connect to a cloud. If this is your only cloud, +# just put this file in ~/.config/openstack/clouds.yaml and tools like +# python-openstackclient will just work with no further config. (You will need +# to add your password to the auth section) +# If you have more than one cloud account, add the cloud entry to the clouds +# section of your existing file and you can refer to them by name with +# OS_CLOUD={{ cloud_name }} or --os-cloud={{ cloud_name }} +clouds: + {{ cloud_name }}: + {% if profile %} + profile: {{ profile }} + {% endif %} + auth: + {% if not profile %} + auth_url: {{ auth_url }} + {% endif %} + username: "{{ user.username }}" + project_id: {{ tenant_id }} + project_name: "{{ tenant_name }}" + {% if user_domain_name %} + user_domain_name: "{{ user_domain_name }}" + {% endif %} + {% if not profile %} + {% if regions %} + regions: + {% for r in regions %} + - {{ r }} + {% endfor %} + {% else %} + {% if region %} + region_name: "{{ region }}" + {% endif %} + {% endif %} + interface: "{{ interface }}" + identity_api_version: {{ os_identity_api_version }} + {% endif %} diff --git a/openstack_dashboard/dashboards/project/api_access/urls.py b/openstack_dashboard/dashboards/project/api_access/urls.py index 66ffcfee9d..cbcee5bbcd 100644 --- a/openstack_dashboard/dashboards/project/api_access/urls.py +++ b/openstack_dashboard/dashboards/project/api_access/urls.py @@ -22,6 +22,8 @@ from openstack_dashboard.dashboards.project.api_access import views urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), url(r'^ec2/$', views.download_ec2_bundle, name='ec2'), + url(r'^clouds.yaml/$', + views.download_clouds_yaml_file, name='clouds.yaml'), url(r'^openrc/$', views.download_rc_file, name='openrc'), url(r'^openrcv2/$', views.download_rc_file_v2, name='openrcv2'), url(r'^view_credentials/$', views.CredentialsView.as_view(), diff --git a/openstack_dashboard/dashboards/project/api_access/views.py b/openstack_dashboard/dashboards/project/api_access/views.py index d5cba0d6fb..ab7b99542f 100644 --- a/openstack_dashboard/dashboards/project/api_access/views.py +++ b/openstack_dashboard/dashboards/project/api_access/views.py @@ -17,6 +17,7 @@ import logging import tempfile import zipfile +from django.conf import settings from django.core.urlresolvers import reverse_lazy from django import http from django import shortcuts @@ -143,14 +144,40 @@ def download_rc_file(request): return _download_rc_file_for_template(request, context, template) -def _download_rc_file_for_template(request, context, template): +def download_clouds_yaml_file(request): + template = 'project/api_access/clouds.yaml.template' + context = _get_openrc_credentials(request) + context['cloud_name'] = getattr( + settings, "OPENSTACK_CLOUDS_YAML_NAME", 'openstack') + context['profile'] = getattr( + settings, "OPENSTACK_CLOUDS_YAML_PROFILE", None) + context['regions'] = [ + region_tuple[1] for region_tuple in getattr( + settings, "AVAILABLE_REGIONS", []) + ] + + if utils.get_keystone_version() >= 3: + # make v3 specific changes + context['user_domain_name'] = request.user.user_domain_name + # sanity fix for removing v2.0 from the url if present + context['auth_url'], _ = utils.fix_auth_url_version_prefix( + context['auth_url']) + context['os_identity_api_version'] = 3 + context['os_auth_version'] = 3 + + return _download_rc_file_for_template(request, context, template, + 'clouds.yaml') + + +def _download_rc_file_for_template(request, context, template, filename=None): try: response = shortcuts.render(request, template, context, content_type="text/plain") - tenant_name = context['tenant_name'] - disposition = 'attachment; filename="%s-openrc.sh"' % tenant_name + if not filename: + filename = '%s-openrc.sh' % context['tenant_name'] + disposition = 'attachment; filename="%s"' % filename response['Content-Disposition'] = disposition.encode('utf-8') response['Content-Length'] = str(len(response.content)) return response diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 3fd2419ecd..34e63f2f6f 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -239,6 +239,12 @@ LOCALE_PATHS = [ 'openstack_dashboard/locale', ] +# Set OPENSTACK_CLOUDS_YAML_NAME to provide a nicer name for this cloud for +# the clouds.yaml file than "openstack". +OPENSTACK_CLOUDS_YAML_NAME = 'openstack' +# If this cloud has a vendor profile in os-client-config, put it's name here. +OPENSTACK_CLOUDS_YAML_PROFILE = '' + OPENSTACK_KEYSTONE_DEFAULT_ROLE = '_member_' DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter' diff --git a/openstack_dashboard/test/tests/templates.py b/openstack_dashboard/test/tests/templates.py index c0d7f20d1d..852482f9a3 100644 --- a/openstack_dashboard/test/tests/templates.py +++ b/openstack_dashboard/test/tests/templates.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import yaml + from django import template from django.template import loader @@ -95,3 +97,93 @@ class TemplateRenderTest(test.TestCase): template.Context(context)) self.assertIn("OS_REGION_NAME=\"\"", out) + + def test_clouds_yaml_set_region(self): + context = { + "cloud_name": "openstack", + "user": FakeUser(), + "tenant_id": "some-cool-id", + "auth_url": "http://example.com", + "tenant_name": "Tenant", + "region": "Colorado"} + out = yaml.load(loader.render_to_string( + 'project/api_access/clouds.yaml.template', + context, + template.Context(context))) + + self.assertIn('clouds', out) + self.assertIn('openstack', out['clouds']) + self.assertNotIn('profile', out['clouds']['openstack']) + self.assertEqual( + "http://example.com", + out['clouds']['openstack']['auth']['auth_url']) + self.assertEqual("Colorado", out['clouds']['openstack']['region_name']) + self.assertNotIn('regions', out['clouds']['openstack']) + + def test_clouds_yaml_region_not_set(self): + context = { + "cloud_name": "openstack", + "user": FakeUser(), + "tenant_id": "some-cool-id", + "auth_url": "http://example.com", + "tenant_name": "Tenant"} + out = yaml.load(loader.render_to_string( + 'project/api_access/clouds.yaml.template', + context, + template.Context(context))) + + self.assertIn('clouds', out) + self.assertIn('openstack', out['clouds']) + self.assertNotIn('profile', out['clouds']['openstack']) + self.assertEqual( + "http://example.com", + out['clouds']['openstack']['auth']['auth_url']) + self.assertNotIn('region_name', out['clouds']['openstack']) + self.assertNotIn('regions', out['clouds']['openstack']) + + def test_clouds_yaml_regions(self): + regions = ['region1', 'region2'] + context = { + "cloud_name": "openstack", + "user": FakeUser(), + "tenant_id": "some-cool-id", + "auth_url": "http://example.com", + "tenant_name": "Tenant", + "regions": regions} + out = yaml.load(loader.render_to_string( + 'project/api_access/clouds.yaml.template', + context, + template.Context(context))) + + self.assertIn('clouds', out) + self.assertIn('openstack', out['clouds']) + self.assertNotIn('profile', out['clouds']['openstack']) + self.assertEqual( + "http://example.com", + out['clouds']['openstack']['auth']['auth_url']) + self.assertNotIn('region_name', out['clouds']['openstack']) + self.assertIn('regions', out['clouds']['openstack']) + self.assertEqual(regions, out['clouds']['openstack']['regions']) + + def test_clouds_yaml_profile(self): + regions = ['region1', 'region2'] + context = { + "cloud_name": "openstack", + "user": FakeUser(), + "profile": "example", + "tenant_id": "some-cool-id", + "auth_url": "http://example.com", + "tenant_name": "Tenant", + "regions": regions} + out = yaml.load(loader.render_to_string( + 'project/api_access/clouds.yaml.template', + context, + template.Context(context))) + + self.assertIn('clouds', out) + self.assertIn('openstack', out['clouds']) + self.assertIn('profile', out['clouds']['openstack']) + self.assertEqual('example', out['clouds']['openstack']['profile']) + self.assertNotIn('auth_url', out['clouds']['openstack']['auth']) + self.assertNotIn('region_name', out['clouds']['openstack']) + self.assertNotIn('regions', out['clouds']['openstack']) diff --git a/releasenotes/notes/add-clouds-yaml-f72b9a0b7df2aa79.yaml b/releasenotes/notes/add-clouds-yaml-f72b9a0b7df2aa79.yaml new file mode 100644 index 0000000000..dd7e7ce2c2 --- /dev/null +++ b/releasenotes/notes/add-clouds-yaml-f72b9a0b7df2aa79.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add support for horizon offering a clouds.yaml file + for download along with the openrc files. For more + information on clouds.yaml, see + https://docs.openstack.org/developer/os-client-config