From 018e99d20e769811babf61d87192ec7a76c8f2eb Mon Sep 17 00:00:00 2001 From: Steve McLellan Date: Thu, 14 Jul 2016 11:55:37 -0500 Subject: [PATCH] Allow horizon to function without nova Adds conditional block to nova quotas to exclude them if nova is not enabled; adds 'permission' checks to the project overview and access_and_security panels to only enable them if compute is enabled; adds permission checks on compute and image to the admin overview and metadef panels; disables 'modify quota' and 'view usage' project actions; disables 'update defaults' if there are no quotas available. The 'access and security' panel still appears (under Compute) but tabs other than the keystone endpoint and RC download tab are hidden. Closes-Bug: #1580116 Change-Id: I1b2ddee0395ad9f55692111604b31618c4eaf69e --- openstack_dashboard/api/network.py | 13 +- .../dashboards/admin/defaults/tables.py | 5 + .../dashboards/admin/defaults/tests.py | 6 +- .../dashboards/admin/metadata_defs/panel.py | 1 + .../dashboards/admin/overview/panel.py | 1 + .../dashboards/identity/projects/tables.py | 7 +- .../dashboards/identity/projects/tests.py | 15 ++- .../dashboards/identity/projects/workflows.py | 9 +- .../access_and_security/floating_ips/tests.py | 4 + .../project/images/images/tables.py | 2 + .../project/network_topology/utils.py | 11 +- .../dashboards/project/overview/panel.py | 1 + .../project/volumes/volumes/tables.py | 5 + .../test/api_tests/network_tests.py | 12 ++ openstack_dashboard/test/tests/quotas.py | 120 +++++++++++------- openstack_dashboard/usage/quotas.py | 35 ++++- ...horizon-without-nova-3cd0a84109ed2187.yaml | 9 ++ 17 files changed, 187 insertions(+), 69 deletions(-) create mode 100644 releasenotes/notes/horizon-without-nova-3cd0a84109ed2187.yaml diff --git a/openstack_dashboard/api/network.py b/openstack_dashboard/api/network.py index f5a4dcd994..015bf3f4c6 100644 --- a/openstack_dashboard/api/network.py +++ b/openstack_dashboard/api/network.py @@ -27,18 +27,24 @@ from openstack_dashboard.api import nova class NetworkClient(object): def __init__(self, request): neutron_enabled = base.is_service_enabled(request, 'network') + nova_enabled = base.is_service_enabled(request, 'compute') + self.secgroups, self.floating_ips = None, None if neutron_enabled: self.floating_ips = neutron.FloatingIpManager(request) - else: + elif nova_enabled: self.floating_ips = nova.FloatingIpManager(request) if (neutron_enabled and neutron.is_extension_supported(request, 'security-group')): self.secgroups = neutron.SecurityGroupManager(request) - else: + elif nova_enabled: self.secgroups = nova.SecurityGroupManager(request) + @property + def enabled(self): + return self.floating_ips is not None + def floating_ip_pools_list(request): return NetworkClient(request).floating_ips.list_pools() @@ -88,7 +94,8 @@ def floating_ip_simple_associate_supported(request): def floating_ip_supported(request): - return NetworkClient(request).floating_ips.is_supported() + nwc = NetworkClient(request) + return nwc.enabled and nwc.floating_ips.is_supported() def security_group_list(request): diff --git a/openstack_dashboard/dashboards/admin/defaults/tables.py b/openstack_dashboard/dashboards/admin/defaults/tables.py index 90f11d56f0..bacae70e0a 100644 --- a/openstack_dashboard/dashboards/admin/defaults/tables.py +++ b/openstack_dashboard/dashboards/admin/defaults/tables.py @@ -16,6 +16,8 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables +from openstack_dashboard.usage import quotas + class QuotaFilterAction(tables.FilterAction): def filter(self, table, tenants, filter_string): @@ -36,6 +38,9 @@ class UpdateDefaultQuotas(tables.LinkAction): classes = ("ajax-modal",) icon = "pencil" + def allowed(self, request, _=None): + return quotas.enabled_quotas(request) + def get_quota_name(quota): QUOTA_NAMES = { diff --git a/openstack_dashboard/dashboards/admin/defaults/tests.py b/openstack_dashboard/dashboards/admin/defaults/tests.py index e7f566b06c..036454b712 100644 --- a/openstack_dashboard/dashboards/admin/defaults/tests.py +++ b/openstack_dashboard/dashboards/admin/defaults/tests.py @@ -46,7 +46,9 @@ class ServicesViewTests(test.BaseAdminViewTests): self.mox.StubOutWithMock(api.neutron, 'is_extension_supported') api.cinder.is_volume_service_enabled(IsA(http.HttpRequest)) \ - .AndReturn(True) + .MultipleTimes().AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .MultipleTimes().AndReturn(True) api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .MultipleTimes().AndReturn(neutron_enabled) @@ -57,7 +59,7 @@ class ServicesViewTests(test.BaseAdminViewTests): if neutron_enabled: api.neutron.is_extension_supported( IsA(http.HttpRequest), - 'security-group').AndReturn(neutron_sg_enabled) + 'security-group').MultipleTimes().AndReturn(neutron_sg_enabled) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/admin/metadata_defs/panel.py b/openstack_dashboard/dashboards/admin/metadata_defs/panel.py index 49c9914ddf..2feabdcc2d 100644 --- a/openstack_dashboard/dashboards/admin/metadata_defs/panel.py +++ b/openstack_dashboard/dashboards/admin/metadata_defs/panel.py @@ -24,6 +24,7 @@ class MetadataDefinitions(horizon.Panel): name = _("Metadata Definitions") slug = 'metadata_defs' policy_rules = (("image", "get_metadef_namespaces"),) + permissions = ('openstack.services.image',) @staticmethod def can_register(): diff --git a/openstack_dashboard/dashboards/admin/overview/panel.py b/openstack_dashboard/dashboards/admin/overview/panel.py index d75f9b35ef..6209d3b990 100644 --- a/openstack_dashboard/dashboards/admin/overview/panel.py +++ b/openstack_dashboard/dashboards/admin/overview/panel.py @@ -27,6 +27,7 @@ class Overview(horizon.Panel): name = _("Overview") slug = 'overview' policy_rules = (('identity', 'identity:list_projects'),) + permissions = ('openstack.services.compute',) dashboard.Admin.register(Overview) diff --git a/openstack_dashboard/dashboards/identity/projects/tables.py b/openstack_dashboard/dashboards/identity/projects/tables.py index fc5ba5fa80..82e02f2c32 100644 --- a/openstack_dashboard/dashboards/identity/projects/tables.py +++ b/openstack_dashboard/dashboards/identity/projects/tables.py @@ -24,6 +24,7 @@ from keystoneclient.exceptions import Conflict # noqa from openstack_dashboard import api from openstack_dashboard import policy +from openstack_dashboard.usage import quotas class RescopeTokenToProject(tables.LinkAction): @@ -103,7 +104,8 @@ class UsageLink(tables.LinkAction): policy_rules = (("compute", "compute_extension:simple_tenant_usage:show"),) def allowed(self, request, project): - return request.user.is_superuser + return (request.user.is_superuser and + api.base.is_service_enabled(request, 'compute')) class CreateProject(tables.LinkAction): @@ -153,7 +155,8 @@ class ModifyQuotas(tables.LinkAction): if api.keystone.VERSIONS.active < 3: return True else: - return api.keystone.is_cloud_admin(request) + return (api.keystone.is_cloud_admin(request) and + quotas.enabled_quotas(request)) def get_link_url(self, project): step = 'update_quotas' diff --git a/openstack_dashboard/dashboards/identity/projects/tests.py b/openstack_dashboard/dashboards/identity/projects/tests.py index a20938a27d..e355f57f5c 100644 --- a/openstack_dashboard/dashboards/identity/projects/tests.py +++ b/openstack_dashboard/dashboards/identity/projects/tests.py @@ -53,7 +53,8 @@ PROJECT_DETAIL_URL = reverse('horizon:identity:projects:detail', args=[1]) class TenantsViewTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('domain_get', 'tenant_list', - 'domain_lookup')}) + 'domain_lookup'), + quotas: ('enabled_quotas',)}) def test_index(self): domain = self.domains.get(id="1") api.keystone.domain_get(IsA(http.HttpRequest), '1').AndReturn(domain) @@ -64,6 +65,8 @@ class TenantsViewTests(test.BaseAdminViewTests): .AndReturn([self.tenants.list(), False]) api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: domain.name}) + quotas.enabled_quotas(IsA(http.HttpRequest)).MultipleTimes()\ + .AndReturn(('instances',)) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -72,7 +75,8 @@ class TenantsViewTests(test.BaseAdminViewTests): @test.create_stubs({api.keystone: ('tenant_list', 'get_effective_domain_id', - 'domain_lookup')}) + 'domain_lookup'), + quotas: ('enabled_quotas',)}) def test_index_with_domain_context(self): domain = self.domains.get(id="1") @@ -91,6 +95,7 @@ class TenantsViewTests(test.BaseAdminViewTests): .AndReturn([domain_tenants, False]) api.keystone.domain_lookup(IgnoreArg()).AndReturn({domain.id: domain.name}) + quotas.enabled_quotas(IsA(http.HttpRequest)).AndReturn(('instances',)) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -201,6 +206,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): # init api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .MultipleTimes().AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .MultipleTimes().AndReturn(True) api.cinder.is_volume_service_enabled(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.keystone.get_default_domain(IsA(http.HttpRequest)) \ @@ -1607,12 +1614,14 @@ class UsageViewTests(test.BaseAdminViewTests): class DetailProjectViewTests(test.BaseAdminViewTests): - @test.create_stubs({api.keystone: ('tenant_get',)}) + @test.create_stubs({api.keystone: ('tenant_get',), + quotas: ('enabled_quotas',)}) def test_detail_view(self): project = self.tenants.first() api.keystone.tenant_get(IsA(http.HttpRequest), self.tenant.id) \ .AndReturn(project) + quotas.enabled_quotas(IsA(http.HttpRequest)).AndReturn(('instances',)) self.mox.ReplayAll() res = self.client.get(PROJECT_DETAIL_URL, args=[project.id]) diff --git a/openstack_dashboard/dashboards/identity/projects/workflows.py b/openstack_dashboard/dashboards/identity/projects/workflows.py index 403c4b6875..e8e92836aa 100644 --- a/openstack_dashboard/dashboards/identity/projects/workflows.py +++ b/openstack_dashboard/dashboards/identity/projects/workflows.py @@ -386,9 +386,12 @@ class UpdateProjectGroups(workflows.UpdateMembersStep): class CommonQuotaWorkflow(workflows.Workflow): def _update_project_quota(self, request, data, project_id): disabled_quotas = quotas.get_disabled_quotas(request) - nova_data = {key: data[key] for key in - set(quotas.NOVA_QUOTA_FIELDS) - disabled_quotas} - nova.tenant_quota_update(request, project_id, **nova_data) + + # Update the project quotas. + if api.base.is_service_enabled(request, 'compute'): + nova_data = {key: data[key] for key in + set(quotas.NOVA_QUOTA_FIELDS) - disabled_quotas} + nova.tenant_quota_update(request, project_id, **nova_data) if cinder.is_volume_service_enabled(request): cinder_data = dict([(key, data[key]) for key in diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py index cac4de3281..ff638f31de 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py @@ -374,6 +374,8 @@ class FloatingIpNeutronViewTests(FloatingIpViewTests): .AndReturn(False) api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .MultipleTimes().AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .MultipleTimes().AndReturn(True) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -435,6 +437,8 @@ class FloatingIpNeutronViewTests(FloatingIpViewTests): .AndReturn(False) api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .MultipleTimes().AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .MultipleTimes().AndReturn(True) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) api.nova.flavor_list(IsA(http.HttpRequest)) \ diff --git a/openstack_dashboard/dashboards/project/images/images/tables.py b/openstack_dashboard/dashboards/project/images/images/tables.py index 29a696dba3..cd43932809 100644 --- a/openstack_dashboard/dashboards/project/images/images/tables.py +++ b/openstack_dashboard/dashboards/project/images/images/tables.py @@ -51,6 +51,8 @@ class LaunchImage(tables.LinkAction): return "?".join([base_url, params]) def allowed(self, request, image=None): + if not api.base.is_service_enabled(request, 'compute'): + return False if image and image.container_format not in NOT_LAUNCHABLE_FORMATS: return image.status in ("active",) return False diff --git a/openstack_dashboard/dashboards/project/network_topology/utils.py b/openstack_dashboard/dashboards/project/network_topology/utils.py index 53af9d3a17..d869c6974c 100644 --- a/openstack_dashboard/dashboards/project/network_topology/utils.py +++ b/openstack_dashboard/dashboards/project/network_topology/utils.py @@ -12,6 +12,7 @@ from django.conf import settings +from openstack_dashboard.api import base from openstack_dashboard.usage import quotas @@ -49,8 +50,10 @@ def get_context(request, context=None): _has_permission(request, (("network", "create_router"),))) context['router_quota_exceeded'] = _quota_exceeded(request, 'routers') context['console_type'] = getattr(settings, 'CONSOLE_TYPE', 'AUTO') - context['show_ng_launch'] = getattr( - settings, 'LAUNCH_INSTANCE_NG_ENABLED', True) - context['show_legacy_launch'] = getattr( - settings, 'LAUNCH_INSTANCE_LEGACY_ENABLED', False) + context['show_ng_launch'] = ( + base.is_service_enabled(request, 'compute') and + getattr(settings, 'LAUNCH_INSTANCE_NG_ENABLED', True)) + context['show_legacy_launch'] = ( + base.is_service_enabled(request, 'compute') and + getattr(settings, 'LAUNCH_INSTANCE_LEGACY_ENABLED', False)) return context diff --git a/openstack_dashboard/dashboards/project/overview/panel.py b/openstack_dashboard/dashboards/project/overview/panel.py index d856021f08..d3735c25da 100644 --- a/openstack_dashboard/dashboards/project/overview/panel.py +++ b/openstack_dashboard/dashboards/project/overview/panel.py @@ -24,3 +24,4 @@ import horizon class Overview(horizon.Panel): name = _("Overview") slug = 'overview' + permissions = ('openstack.services.compute',) diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py index fce082e524..a84399a836 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py @@ -58,6 +58,8 @@ class LaunchVolume(tables.LinkAction): return "?".join([base_url, params]) def allowed(self, request, volume=None): + if not api.base.is_service_enabled(request, 'compute'): + return False if getattr(volume, 'bootable', '') == 'true': return volume.status == "available" return False @@ -179,6 +181,9 @@ class EditAttachments(tables.LinkAction): icon = "pencil" def allowed(self, request, volume=None): + if not api.base.is_service_enabled(request, 'compute'): + return False + if volume: project_id = getattr(volume, "os-vol-tenant-attr:tenant_id", None) attach_allowed = \ diff --git a/openstack_dashboard/test/api_tests/network_tests.py b/openstack_dashboard/test/api_tests/network_tests.py index 0d14571161..2938de570f 100644 --- a/openstack_dashboard/test/api_tests/network_tests.py +++ b/openstack_dashboard/test/api_tests/network_tests.py @@ -32,6 +32,8 @@ class NetworkClientTestCase(test.APITestCase): self.mox.StubOutWithMock(api.base, 'is_service_enabled') api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) self.mox.ReplayAll() nc = api.network.NetworkClient(self.request) @@ -42,6 +44,8 @@ class NetworkClientTestCase(test.APITestCase): self.mox.StubOutWithMock(api.base, 'is_service_enabled') api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) self.neutronclient = self.stub_neutronclient() self.neutronclient.list_extensions() \ .AndReturn({'extensions': self.api_extensions.list()}) @@ -55,6 +59,8 @@ class NetworkClientTestCase(test.APITestCase): self.mox.StubOutWithMock(api.base, 'is_service_enabled') api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .AndReturn(True) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) self.neutronclient = self.stub_neutronclient() self.neutronclient.list_extensions().AndReturn({'extensions': []}) self.mox.ReplayAll() @@ -70,6 +76,8 @@ class NetworkApiNovaTestBase(test.APITestCase): self.mox.StubOutWithMock(api.base, 'is_service_enabled') api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ .AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) class NetworkApiNovaSecurityGroupTests(NetworkApiNovaTestBase): @@ -339,6 +347,8 @@ class NetworkApiNeutronSecurityGroupTests(NetworkApiNeutronTestBase): super(NetworkApiNeutronSecurityGroupTests, self).setUp() self.qclient.list_extensions() \ .AndReturn({'extensions': self.api_extensions.list()}) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) self.sg_dict = dict([(sg['id'], sg['name']) for sg in self.api_q_secgroups.list()]) @@ -521,6 +531,8 @@ class NetworkApiNeutronFloatingIpTests(NetworkApiNeutronTestBase): super(NetworkApiNeutronFloatingIpTests, self).setUp() self.qclient.list_extensions() \ .AndReturn({'extensions': self.api_extensions.list()}) + api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ + .AndReturn(True) @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_router': True}) def test_floating_ip_supported(self): diff --git a/openstack_dashboard/test/tests/quotas.py b/openstack_dashboard/test/tests/quotas.py index 0324801a2a..25ea0a9cd3 100644 --- a/openstack_dashboard/test/tests/quotas.py +++ b/openstack_dashboard/test/tests/quotas.py @@ -33,26 +33,36 @@ from openstack_dashboard.usage import quotas class QuotaTests(test.APITestCase): - def get_usages(self, with_volume=True, nova_quotas_enabled=True): - if nova_quotas_enabled: - usages = {'injected_file_content_bytes': {'quota': 1}, - 'metadata_items': {'quota': 1}, - 'injected_files': {'quota': 1}, - 'security_groups': {'quota': 10}, - 'security_group_rules': {'quota': 20}, - 'fixed_ips': {'quota': 10}, - 'ram': {'available': 8976, 'used': 1024, 'quota': 10000}, - 'floating_ips': {'available': 0, 'used': 2, 'quota': 1}, - 'instances': {'available': 8, 'used': 2, 'quota': 10}, - 'cores': {'available': 8, 'used': 2, 'quota': 10}} - else: - inf = float('inf') - usages = {'security_groups': {'available': inf, 'quota': inf}, - 'ram': {'available': inf, 'used': 1024, 'quota': inf}, - 'floating_ips': { - 'available': inf, 'used': 2, 'quota': inf}, - 'instances': {'available': inf, 'used': 2, 'quota': inf}, - 'cores': {'available': inf, 'used': 2, 'quota': inf}} + def get_usages(self, with_volume=True, with_compute=True, + nova_quotas_enabled=True): + usages = {} + if with_compute: + # These are all nova fields; the neutron ones are named slightly + # differently and aren't included in here yet + if nova_quotas_enabled: + usages.update({ + 'injected_file_content_bytes': {'quota': 1}, + 'metadata_items': {'quota': 1}, + 'injected_files': {'quota': 1}, + 'security_groups': {'quota': 10}, + 'security_group_rules': {'quota': 20}, + 'fixed_ips': {'quota': 10}, + 'ram': {'available': 8976, 'used': 1024, 'quota': 10000}, + 'floating_ips': {'available': 0, 'used': 2, 'quota': 1}, + 'instances': {'available': 8, 'used': 2, 'quota': 10}, + 'cores': {'available': 8, 'used': 2, 'quota': 10} + }) + else: + inf = float('inf') + usages.update({ + 'security_groups': {'available': inf, 'quota': inf}, + 'ram': {'available': inf, 'used': 1024, 'quota': inf}, + 'floating_ips': {'available': inf, 'used': 2, + 'quota': inf}, + 'instances': {'available': inf, 'used': 2, 'quota': inf}, + 'cores': {'available': inf, 'used': 2, 'quota': inf} + }) + if with_volume: usages.update({'volumes': {'available': 0, 'used': 4, 'quota': 1}, 'snapshots': {'available': 0, 'used': 3, @@ -78,26 +88,34 @@ class QuotaTests(test.APITestCase): 'tenant_quota_get', 'is_volume_service_enabled')}) def _test_tenant_quota_usages(self, nova_quotas_enabled=True, - with_volume=True): - servers = [s for s in self.servers.list() - if s.tenant_id == self.request.user.tenant_id] + with_compute=True, with_volume=True): cinder.is_volume_service_enabled(IsA(http.HttpRequest)).AndReturn( with_volume) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) - api.nova.flavor_list(IsA(http.HttpRequest)) \ - .AndReturn(self.flavors.list()) - api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ - .AndReturn(self.quotas.first()) - api.network.floating_ip_supported(IsA(http.HttpRequest)) \ - .AndReturn(True) - api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) - search_opts = {'tenant_id': self.request.user.tenant_id} - api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts, - all_tenants=True) \ - .AndReturn([servers, False]) + api.base.is_service_enabled( + IsA(http.HttpRequest), 'compute' + ).MultipleTimes().AndReturn(with_compute) + if with_compute: + servers = [s for s in self.servers.list() + if s.tenant_id == self.request.user.tenant_id] + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + api.network.floating_ip_supported(IsA(http.HttpRequest)) \ + .AndReturn(True) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) + search_opts = {'tenant_id': self.request.user.tenant_id} + api.nova.server_list(IsA(http.HttpRequest), + search_opts=search_opts, + all_tenants=True) \ + .AndReturn([servers, False]) + + if nova_quotas_enabled: + api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ + .AndReturn(self.quotas.first()) + if with_volume: opts = {'all_tenants': 1, 'project_id': self.request.user.tenant_id} @@ -112,7 +130,8 @@ class QuotaTests(test.APITestCase): quota_usages = quotas.tenant_quota_usages(self.request) expected_output = self.get_usages( - nova_quotas_enabled=nova_quotas_enabled, with_volume=with_volume) + nova_quotas_enabled=nova_quotas_enabled, with_volume=with_volume, + with_compute=with_compute) # Compare internal structure of usages to expected. self.assertItemsEqual(expected_output, quota_usages.usages) @@ -125,6 +144,7 @@ class QuotaTests(test.APITestCase): @override_settings(OPENSTACK_HYPERVISOR_FEATURES={'enable_quotas': False}) def test_tenant_quota_usages_wo_nova_quotas(self): self._test_tenant_quota_usages(nova_quotas_enabled=False, + with_compute=True, with_volume=False) @override_settings(OPENSTACK_HYPERVISOR_FEATURES={'enable_quotas': False}) @@ -135,11 +155,15 @@ class QuotaTests(test.APITestCase): False) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) + # Nova enabled but quotas disabled + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').AndReturn(True) self.mox.ReplayAll() result_quotas = quotas.get_disabled_quotas(self.request) expected_quotas = list(quotas.CINDER_QUOTA_FIELDS) + \ - list(quotas.NEUTRON_QUOTA_FIELDS) + list(quotas.NOVA_QUOTA_FIELDS) + list(quotas.NEUTRON_QUOTA_FIELDS) + \ + list(quotas.NOVA_QUOTA_FIELDS) + list(quotas.MISSING_QUOTA_FIELDS) self.assertItemsEqual(result_quotas, expected_quotas) @test.create_stubs({api.nova: ('server_list', @@ -158,6 +182,8 @@ class QuotaTests(test.APITestCase): ).AndReturn(False) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ @@ -197,6 +223,8 @@ class QuotaTests(test.APITestCase): ).AndReturn(False) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ @@ -244,6 +272,8 @@ class QuotaTests(test.APITestCase): ).AndReturn(True) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ @@ -293,6 +323,8 @@ class QuotaTests(test.APITestCase): ).AndReturn(True) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ @@ -332,8 +364,7 @@ class QuotaTests(test.APITestCase): quotas._get_tenant_volume_usages(self.request, {}, [], None) - @test.create_stubs({api.nova: ('tenant_quota_get',), - api.base: ('is_service_enabled',), + @test.create_stubs({api.base: ('is_service_enabled',), api.cinder: ('tenant_quota_get', 'is_volume_service_enabled'), exceptions: ('handle',)}) @@ -343,8 +374,8 @@ class QuotaTests(test.APITestCase): ).AndReturn(True) api.base.is_service_enabled(IsA(http.HttpRequest), 'network').AndReturn(False) - api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ - .AndReturn(self.quotas.first()) + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').AndReturn(False) api.cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndRaise(cinder.cinder_exception.ClientException('test')) exceptions.handle(IsA(http.HttpRequest), @@ -353,17 +384,16 @@ class QuotaTests(test.APITestCase): quotas._get_quota_data(self.request, 'tenant_quota_get') - @test.create_stubs({api.nova: ('tenant_absolute_limits',), - api.base: ('is_service_enabled',), + @test.create_stubs({api.base: ('is_service_enabled',), api.cinder: ('tenant_absolute_limits', 'is_volume_service_enabled'), exceptions: ('handle',)}) def test_tenant_limit_usages_cinder_exception(self): + api.base.is_service_enabled(IsA(http.HttpRequest), + 'compute').AndReturn(False) api.cinder.is_volume_service_enabled( IsA(http.HttpRequest) ).AndReturn(True) - api.nova.tenant_absolute_limits(IsA(http.HttpRequest), - reserved=True).AndReturn({}) api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)) \ .AndRaise(cinder.cinder_exception.ClientException('test')) exceptions.handle(IsA(http.HttpRequest), diff --git a/openstack_dashboard/usage/quotas.py b/openstack_dashboard/usage/quotas.py index 04504ad03b..dfacd58b81 100644 --- a/openstack_dashboard/usage/quotas.py +++ b/openstack_dashboard/usage/quotas.py @@ -141,10 +141,14 @@ def _get_quota_data(request, method_name, disabled_quotas=None, quotasets = [] if not tenant_id: tenant_id = request.user.tenant_id - quotasets.append(getattr(nova, method_name)(request, tenant_id)) - qs = base.QuotaSet() if disabled_quotas is None: disabled_quotas = get_disabled_quotas(request) + + qs = base.QuotaSet() + + if 'instances' not in disabled_quotas: + quotasets.append(getattr(nova, method_name)(request, tenant_id)) + if 'volumes' not in disabled_quotas: try: quotasets.append(getattr(cinder, method_name)(request, tenant_id)) @@ -231,10 +235,6 @@ def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None): def get_disabled_quotas(request): disabled_quotas = set([]) - # Nova - if not nova.can_set_quotas(): - disabled_quotas.update(NOVA_QUOTA_FIELDS) - # Cinder if not cinder.is_volume_service_enabled(request): disabled_quotas.update(CINDER_QUOTA_FIELDS) @@ -260,10 +260,25 @@ def get_disabled_quotas(request): LOG.exception("There was an error checking if the Neutron " "quotas extension is enabled.") + # Nova + if not (base.is_service_enabled(request, 'compute') and + nova.can_set_quotas()): + disabled_quotas.update(NOVA_QUOTA_FIELDS) + # The 'missing' quota fields are all nova (this is hardcoded in + # dashboards.admin.defaults.workflows) + disabled_quotas.update(MISSING_QUOTA_FIELDS) + + # There appear to be no glance quota fields currently return disabled_quotas def _get_tenant_compute_usages(request, usages, disabled_quotas, tenant_id): + # Unlike the other services it can be the case that nova is enabled but + # doesn't support quotas, in which case we still want to get usage info, + # so don't rely on '"instances" in disabled_quotas' as elsewhere + if not base.is_service_enabled(request, 'compute'): + return + if tenant_id: # determine if the user has permission to view across projects # there are cases where an administrator wants to check the quotas @@ -396,7 +411,8 @@ def tenant_limit_usages(request): limits = {} try: - limits.update(nova.tenant_absolute_limits(request, reserved=True)) + if base.is_service_enabled(request, 'compute'): + limits.update(nova.tenant_absolute_limits(request, reserved=True)) except Exception: msg = _("Unable to retrieve compute limit information.") exceptions.handle(request, msg) @@ -419,3 +435,8 @@ def tenant_limit_usages(request): exceptions.handle(request, msg) return limits + + +def enabled_quotas(request): + """Returns the list of quotas available minus those that are disabled""" + return set(QUOTA_FIELDS) - get_disabled_quotas(request) diff --git a/releasenotes/notes/horizon-without-nova-3cd0a84109ed2187.yaml b/releasenotes/notes/horizon-without-nova-3cd0a84109ed2187.yaml new file mode 100644 index 0000000000..ad50f95645 --- /dev/null +++ b/releasenotes/notes/horizon-without-nova-3cd0a84109ed2187.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + Horizon no longer requires Nova (or Glance) to function; + it will run as long as keystone is present (for instance, + swift-only deployments). +features: + Nova and Glance are no longer required in order to run + Horizon. As long as keystone is present, Horizon will + run correctly.