Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion nova/conductor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ def __init__(self):
self.notifier = rpc.get_notifier('compute')
# Help us to record host in EventReporter
self.host = CONF.host
# Volume types from Cinder change rarely; cache the first successful
# list for the conductor process lifetime. None means not yet loaded.
self._cross_hv_volume_types = None

try:
# Test our placement client during initialization
Expand Down Expand Up @@ -278,6 +281,19 @@ def __init__(self):
LOG.error('Fatal error initializing placement client: %s', e)
raise

def get_cross_hv_volume_types(self, context):
"""Return all Cinder volume types, cached for this conductor process.

The first successful call fetches from Cinder and caches the result.
A Cinder failure is not cached: the next cross-HV resize will retry.
The cache is intentionally permanent until conductor restart so that
a misconfigured type cannot silently start working after deployment.
"""
if self._cross_hv_volume_types is None:
self._cross_hv_volume_types = tuple(
self.volume_api.get_all_volume_types(context))
return self._cross_hv_volume_types

@property
def report_client(self):
return report.report_client_singleton()
Expand Down Expand Up @@ -588,7 +604,8 @@ def _build_cold_migrate_task(self, context, instance, flavor, request_spec,
self.compute_rpcapi,
self.query_client, self.report_client,
host_list, self.network_api,
self.volume_api)
self.volume_api,
self.get_cross_hv_volume_types)

def _destroy_build_request(self, context, instance):
# The BuildRequest needs to be stored until the instance is mapped to
Expand Down
67 changes: 61 additions & 6 deletions nova/conductor/tasks/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class MigrationTask(base.TaskBase):
def __init__(self, context, instance, flavor,
request_spec, clean_shutdown, compute_rpcapi,
query_client, report_client, host_list, network_api,
volume_api=None):
volume_api=None, get_volume_types_fn=None):
super(MigrationTask, self).__init__(context, instance)
self.clean_shutdown = clean_shutdown
self.request_spec = request_spec
Expand All @@ -133,6 +133,10 @@ def __init__(self, context, instance, flavor,
self.host_list = host_list
self.network_api = network_api
self.volume_api = volume_api
# Callable(context) -> list of volume type dicts. Provided by
# ComputeTaskManager so the list is cached per conductor process
# rather than fetched from Cinder on every cross-HV resize.
self._get_volume_types_fn = get_volume_types_fn

# Persist things from the happy path so we don't have to look
# them up if we need to roll back
Expand Down Expand Up @@ -363,10 +367,11 @@ def _execute(self):
# Convert image/local roots before the BFV-only prep_resize path.
# Revert keeps the instance BFV-on-VMware.
if self.instance.system_metadata.get('cross_hv_resize') == 'true':
root_bdm = self._get_root_bdm()
bdms = self._get_bdms()
root_bdm = self._get_root_bdm(bdms)
self._validate_cross_hv_root_bdm(root_bdm)
self._validate_cross_hv_attached_volume_types(bdms)
if self._is_image_backed_local_root(root_bdm):
self._validate_cross_hv_manage_config()
self._convert_image_backed_root_to_bfv(root_bdm)

# NOTE: set after the conversion above, which saves the instance
Expand Down Expand Up @@ -415,12 +420,41 @@ def _validate_cross_hv_manage_config():
raise exception.CrossHVConfigurationMissing(
option='fcd_volume_type')

def _get_root_bdm(self):
"""Load and return the root BDM for the instance, or None."""
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
def _get_bdms(self):
"""Load and return all BDMs for the instance."""
return objects.BlockDeviceMappingList.get_by_instance_uuid(
self.context, self.instance.uuid)

def _get_root_bdm(self, bdms=None):
"""Load and return the root BDM for the instance, or None."""
if bdms is None:
bdms = self._get_bdms()
return bdms.root_bdm()

def _get_cross_hv_expected_volume_type(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we cache the result? Volume types should be basically static and I don't think we should rely on the configured volume type becoming available after we rolled out Nova.

@anokfireball anokfireball Aug 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The first successful lookup is now cached for all successive operations. This also allows implicit retries in case initial lookups fail (e.g. cinder down).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there private data that could be available to a non-admin token if the first lookup is by an admin token?
Would it make sense to limit the cached data to id and name, because that's the only attributes we care about? (minimal less memory, mitigating the first concern - future use-cases will have to explicitly add some attributes)

"""Resolve the configured FCD volume type against the type list.

Uses the injected get_volume_types_fn when available so the caller
(ComputeTaskManager) can cache the list for the conductor process
lifetime. Falls back to a direct Cinder call for tests or callers
that do not supply the function.
"""
self._validate_cross_hv_manage_config()
configured_type = CONF.cross_hv.fcd_volume_type

if self._get_volume_types_fn is not None:
volume_types = self._get_volume_types_fn(self.context)
else:
volume_types = self.volume_api.get_all_volume_types(self.context)

for volume_type in volume_types:
if configured_type in (volume_type['id'], volume_type['name']):
return volume_type

raise exception.InvalidCrossHvResizePrecondition(
reason='Configured [cross_hv] fcd_volume_type %s was not found '
'in Cinder volume types.' % configured_type)

def _is_image_backed_local_root(self, root_bdm):
"""Return True if root_bdm is an image-backed local ephemeral disk."""
if root_bdm is None:
Expand Down Expand Up @@ -456,6 +490,27 @@ def _validate_cross_hv_root_bdm(self, root_bdm):
'or */volume root disks are supported.' %
(root_bdm.source_type, root_bdm.destination_type))

def _validate_cross_hv_attached_volume_types(self, bdms):
"""Ensure every attached Cinder volume matches the supported type."""
expected_type = self._get_cross_hv_expected_volume_type()
expected_types = {expected_type['id'], expected_type['name']}
expected_type_name = expected_type['name']
unsupported = []

for bdm in bdms:
if bdm.destination_type != 'volume' or not bdm.volume_id:
continue
volume = self.volume_api.get(self.context, bdm.volume_id)
volume_type = volume.get('volume_type_id') or '<unset>'
if volume_type not in expected_types:
unsupported.append('%s (%s)' % (bdm.volume_id, volume_type))

if unsupported:
raise exception.InvalidCrossHvResizePrecondition(
reason='All attached Cinder volumes must use volume type %s '
'for cross-HV resize. Unsupported volumes: %s' %
(expected_type_name, ', '.join(unsupported)))

def _set_cross_hv_root_detach_state(self, value):
"""Set or clear cross_hv_root_detach_state and save the instance.

Expand Down
Loading