Skip to content

Commit 395501c

Browse files
committed
nova-manage: Add 'limits migrate_to_unified_limits'
This command aims to help migrate to unified limits quotas by reading legacy quota limits from the Nova database and calling the Keystone API to create corresponding unified limits. Related to blueprint unified-limits-nova-tool-and-docs Change-Id: I5536010ea1212918e61b3f4f22c2077fadc5ebfe
1 parent 065c590 commit 395501c

File tree

6 files changed

+508
-1
lines changed

6 files changed

+508
-1
lines changed

doc/source/cli/nova-manage.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,61 @@ for example.
17591759
- The provided image property value is invalid
17601760

17611761

1762+
Limits Commands
1763+
===============
1764+
1765+
limits migrate_to_unified_limits
1766+
--------------------------------
1767+
1768+
.. program:: nova-manage limits migrate_to_unified_limits
1769+
1770+
.. code-block:: shell
1771+
1772+
nova-manage limits migrate_to_unified_limits [--project-id <project-id>]
1773+
[--region-id <region-id>] [--verbose] [--dry-run]
1774+
1775+
Migrate quota limits from the Nova database to unified limits in Keystone.
1776+
1777+
This command is useful for operators to migrate from legacy quotas to unified
1778+
limits. Limits are migrated by copying them from the Nova database to Keystone
1779+
by creating them using the Keystone API.
1780+
1781+
.. versionadded:: 28.0.0 (2023.2 Bobcat)
1782+
1783+
.. rubric:: Options
1784+
1785+
.. option:: --project-id <project-id>
1786+
1787+
The project ID for which to migrate quota limits.
1788+
1789+
.. option:: --region-id <region-id>
1790+
1791+
The region ID for which to migrate quota limits.
1792+
1793+
.. option:: --verbose
1794+
1795+
Provide verbose output during execution.
1796+
1797+
.. option:: --dry-run
1798+
1799+
Show what limits would be created without actually creating them.
1800+
1801+
.. rubric:: Return codes
1802+
1803+
.. list-table::
1804+
:widths: 20 80
1805+
:header-rows: 1
1806+
1807+
* - Return code
1808+
- Description
1809+
* - 0
1810+
- Command completed successfully
1811+
* - 1
1812+
- An unexpected error occurred
1813+
* - 2
1814+
- Failed to connect to the database
1815+
1816+
17621817
See Also
17631818
========
17641819

nova/cmd/manage.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
from nova.db import migration
5959
from nova import exception
6060
from nova.i18n import _
61+
from nova.limit import local as local_limit
62+
from nova.limit import placement as placement_limit
6163
from nova.network import constants
6264
from nova.network import neutron as neutron_api
6365
from nova import objects
@@ -70,6 +72,7 @@
7072
from nova.objects import pci_device as pci_device_obj
7173
from nova.objects import quotas as quotas_obj
7274
from nova.objects import virtual_interface as virtual_interface_obj
75+
import nova.quota
7376
from nova import rpc
7477
from nova.scheduler.client import report
7578
from nova.scheduler import utils as scheduler_utils
@@ -3367,6 +3370,183 @@ def set(self, instance_uuid=None, image_properties=None):
33673370
return 1
33683371

33693372

3373+
class LimitsCommands():
3374+
3375+
def _create_unified_limits(self, ctxt, legacy_defaults, project_id,
3376+
region_id, output, dry_run):
3377+
return_code = 0
3378+
3379+
# Create registered (default) limits first.
3380+
unified_to_legacy_names = dict(
3381+
**local_limit.LEGACY_LIMITS, **placement_limit.LEGACY_LIMITS)
3382+
3383+
legacy_to_unified_names = dict(
3384+
zip(unified_to_legacy_names.values(),
3385+
unified_to_legacy_names.keys()))
3386+
3387+
# For auth, a section for [keystone] is required in the config:
3388+
#
3389+
# [keystone]
3390+
# region_name = RegionOne
3391+
# user_domain_name = Default
3392+
# password = <password>
3393+
# username = <username>
3394+
# auth_url = http://127.0.0.1/identity
3395+
# auth_type = password
3396+
# system_scope = all
3397+
#
3398+
# The configured user needs 'role:admin and system_scope:all' by
3399+
# default in order to create limits in Keystone.
3400+
keystone_api = utils.get_sdk_adapter('identity')
3401+
3402+
# Service ID is required in unified limits APIs.
3403+
service_id = keystone_api.find_service('nova').id
3404+
3405+
# Retrieve the existing resource limits from Keystone.
3406+
registered_limits = keystone_api.registered_limits(region_id=region_id)
3407+
3408+
unified_defaults = {
3409+
rl.resource_name: rl.default_limit for rl in registered_limits}
3410+
3411+
# f-strings don't seem to work well with the _() translation function.
3412+
msg = f'Found default limits in Keystone: {unified_defaults} ...'
3413+
output(_(msg))
3414+
3415+
# Determine which resource limits are missing in Keystone so that we
3416+
# can create them.
3417+
output(_('Creating default limits in Keystone ...'))
3418+
for resource, rlimit in legacy_defaults.items():
3419+
resource_name = legacy_to_unified_names[resource]
3420+
if resource_name not in unified_defaults:
3421+
msg = f'Creating default limit: {resource_name} = {rlimit}'
3422+
if region_id:
3423+
msg += f' in region {region_id}'
3424+
output(_(msg))
3425+
if not dry_run:
3426+
try:
3427+
keystone_api.create_registered_limit(
3428+
resource_name=resource_name,
3429+
default_limit=rlimit, region_id=region_id,
3430+
service_id=service_id)
3431+
except Exception as e:
3432+
msg = f'Failed to create default limit: {str(e)}'
3433+
print(_(msg))
3434+
return_code = 1
3435+
else:
3436+
existing_rlimit = unified_defaults[resource_name]
3437+
msg = (f'A default limit: {resource_name} = {existing_rlimit} '
3438+
'already exists in Keystone, skipping ...')
3439+
output(_(msg))
3440+
3441+
# Create project limits if there are any.
3442+
if not project_id:
3443+
return return_code
3444+
3445+
output(_('Reading project limits from the Nova API database ...'))
3446+
legacy_projects = objects.Quotas.get_all_by_project(ctxt, project_id)
3447+
legacy_projects.pop('project_id', None)
3448+
msg = f'Found project limits in the database: {legacy_projects} ...'
3449+
output(_(msg))
3450+
3451+
# Retrieve existing limits from Keystone.
3452+
project_limits = keystone_api.limits(
3453+
project_id=project_id, region_id=region_id)
3454+
unified_projects = {
3455+
pl.resource_name: pl.resource_limit for pl in project_limits}
3456+
msg = f'Found project limits in Keystone: {unified_projects} ...'
3457+
output(_(msg))
3458+
3459+
output(_('Creating project limits in Keystone ...'))
3460+
for resource, plimit in legacy_projects.items():
3461+
resource_name = legacy_to_unified_names[resource]
3462+
if resource_name not in unified_projects:
3463+
msg = (
3464+
f'Creating project limit: {resource_name} = {plimit} '
3465+
f'for project {project_id}')
3466+
if region_id:
3467+
msg += f' in region {region_id}'
3468+
output(_(msg))
3469+
if not dry_run:
3470+
try:
3471+
keystone_api.create_limit(
3472+
resource_name=resource_name,
3473+
resource_limit=plimit, project_id=project_id,
3474+
region_id=region_id, service_id=service_id)
3475+
except Exception as e:
3476+
msg = f'Failed to create project limit: {str(e)}'
3477+
print(_(msg))
3478+
return_code = 1
3479+
else:
3480+
existing_plimit = unified_projects[resource_name]
3481+
msg = (f'A project limit: {resource_name} = {existing_plimit} '
3482+
'already exists in Keystone, skipping ...')
3483+
output(_(msg))
3484+
3485+
return return_code
3486+
3487+
@action_description(
3488+
_("Copy quota limits from the Nova API database to Keystone."))
3489+
@args('--project-id', metavar='<project-id>', dest='project_id',
3490+
help='Project ID for which to migrate quota limits')
3491+
@args('--region-id', metavar='<region-id>', dest='region_id',
3492+
help='Region ID for which to migrate quota limits')
3493+
@args('--verbose', action='store_true', dest='verbose', default=False,
3494+
help='Provide verbose output during execution.')
3495+
@args('--dry-run', action='store_true', dest='dry_run', default=False,
3496+
help='Show what limits would be created without actually '
3497+
'creating them.')
3498+
def migrate_to_unified_limits(self, project_id=None, region_id=None,
3499+
verbose=False, dry_run=False):
3500+
"""Migrate quota limits from legacy quotas to unified limits.
3501+
3502+
Return codes:
3503+
* 0: Command completed successfully.
3504+
* 1: An unexpected error occurred.
3505+
* 2: Failed to connect to the database.
3506+
"""
3507+
ctxt = context.get_admin_context()
3508+
3509+
output = lambda msg: None
3510+
if verbose:
3511+
output = lambda msg: print(msg)
3512+
3513+
output(_('Reading default limits from the Nova API database ...'))
3514+
3515+
try:
3516+
# This will look for limits in the 'default' quota class first and
3517+
# then fall back to the [quota] config options.
3518+
legacy_defaults = nova.quota.QUOTAS.get_defaults(ctxt)
3519+
except db_exc.CantStartEngineError:
3520+
print(_('Failed to connect to the database so aborting this '
3521+
'migration attempt. Please check your config file to make '
3522+
'sure that [api_database]/connection and '
3523+
'[database]/connection are set and run this '
3524+
'command again.'))
3525+
return 2
3526+
3527+
# Remove obsolete resource limits.
3528+
for resource in ('fixed_ips', 'floating_ips', 'security_groups',
3529+
'security_group_rules'):
3530+
if resource in legacy_defaults:
3531+
msg = f'Skipping obsolete limit for {resource} ...'
3532+
output(_(msg))
3533+
legacy_defaults.pop(resource)
3534+
3535+
msg = (
3536+
f'Found default limits in the database: {legacy_defaults} ...')
3537+
output(_(msg))
3538+
3539+
try:
3540+
return self._create_unified_limits(
3541+
ctxt, legacy_defaults, project_id, region_id, output, dry_run)
3542+
except Exception as e:
3543+
msg = (f'Unexpected error, see nova-manage.log for the full '
3544+
f'trace: {str(e)}')
3545+
print(_(msg))
3546+
LOG.exception('Unexpected error')
3547+
return 1
3548+
3549+
33703550
CATEGORIES = {
33713551
'api_db': ApiDbCommands,
33723552
'cell_v2': CellV2Commands,
@@ -3375,6 +3555,7 @@ def set(self, instance_uuid=None, image_properties=None):
33753555
'libvirt': LibvirtCommands,
33763556
'volume_attachment': VolumeAttachmentCommands,
33773557
'image_property': ImagePropertyCommands,
3558+
'limits': LimitsCommands,
33783559
}
33793560

33803561

nova/conf/keystone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
def register_opts(conf):
2929
conf.register_group(keystone_group)
3030
confutils.register_ksa_opts(conf, keystone_group.name,
31-
DEFAULT_SERVICE_TYPE, include_auth=False)
31+
DEFAULT_SERVICE_TYPE, include_auth=True)
3232

3333

3434
def list_opts():

nova/tests/fixtures/nova.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,3 +2032,66 @@ def setUp(self):
20322032
self.useFixture(fixtures.MockPatch(
20332033
'futurist.GreenThreadPoolExecutor.shutdown',
20342034
lambda self, wait: real_shutdown(self, wait=True)))
2035+
2036+
2037+
class UnifiedLimitsFixture(fixtures.Fixture):
2038+
def setUp(self):
2039+
super().setUp()
2040+
self.mock_sdk_adapter = mock.Mock()
2041+
real_get_sdk_adapter = utils.get_sdk_adapter
2042+
2043+
def fake_get_sdk_adapter(service_type, **kwargs):
2044+
if service_type == 'identity':
2045+
return self.mock_sdk_adapter
2046+
return real_get_sdk_adapter(service_type, **kwargs)
2047+
2048+
self.useFixture(fixtures.MockPatch(
2049+
'nova.utils.get_sdk_adapter', fake_get_sdk_adapter))
2050+
2051+
self.mock_sdk_adapter.registered_limits.side_effect = (
2052+
self.registered_limits)
2053+
self.mock_sdk_adapter.limits.side_effect = self.limits
2054+
self.mock_sdk_adapter.create_registered_limit.side_effect = (
2055+
self.create_registered_limit)
2056+
self.mock_sdk_adapter.create_limit.side_effect = self.create_limit
2057+
2058+
self.registered_limits_list = []
2059+
self.limits_list = []
2060+
2061+
def registered_limits(self, region_id=None):
2062+
if region_id:
2063+
return [rl for rl in self.registered_limits_list
2064+
if rl.region_id == region_id]
2065+
return self.registered_limits_list
2066+
2067+
def limits(self, project_id=None, region_id=None):
2068+
limits_list = self.limits_list
2069+
if project_id:
2070+
limits_list = [pl for pl in limits_list
2071+
if pl.project_id == project_id]
2072+
if region_id:
2073+
limits_list = [pl for pl in limits_list
2074+
if pl.region_id == region_id]
2075+
return limits_list
2076+
2077+
def create_registered_limit(self, **attrs):
2078+
rl = collections.namedtuple(
2079+
'RegisteredLimit',
2080+
['resource_name', 'default_limit', 'region_id', 'service_id'])
2081+
rl.resource_name = attrs.get('resource_name')
2082+
rl.default_limit = attrs.get('default_limit')
2083+
rl.region_id = attrs.get('region_id')
2084+
rl.service_id = attrs.get('service_id')
2085+
self.registered_limits_list.append(rl)
2086+
2087+
def create_limit(self, **attrs):
2088+
pl = collections.namedtuple(
2089+
'Limit',
2090+
['resource_name', 'resource_limit', 'project_id', 'region_id',
2091+
'service_id'])
2092+
pl.resource_name = attrs.get('resource_name')
2093+
pl.resource_limit = attrs.get('resource_limit')
2094+
pl.project_id = attrs.get('project_id')
2095+
pl.region_id = attrs.get('region_id')
2096+
pl.service_id = attrs.get('service_id')
2097+
self.limits_list.append(pl)

0 commit comments

Comments
 (0)