hardware: Update and correct typing information

This is going to be used extensively in forthcoming patches. Lay the
groundwork now. This requires some minor tweaks of code that mypy found
confusing along with unit tests for coverage gaps it exposed.

Part of blueprint use-pcpu-and-vcpu-in-one-instance

Change-Id: Ied35762c353a084398ab8032a8efe6eada69dd9b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2020-04-06 17:27:51 +01:00
parent f5f7c25401
commit 26c1567a16
3 changed files with 123 additions and 58 deletions

View File

@@ -1 +1,2 @@
nova/virt/hardware.py
nova/virt/libvirt/__init__.py nova/virt/libvirt/__init__.py

View File

@@ -1185,6 +1185,32 @@ class NUMATopologyTest(test.NoDBTestCase):
}, },
"expect": exception.ImageNUMATopologyIncomplete, "expect": exception.ImageNUMATopologyIncomplete,
}, },
{
# Request missing mem.1
"flavor": objects.Flavor(vcpus=8, memory_mb=2048,
extra_specs={
"hw:numa_nodes": 2,
"hw:numa_cpus.0": "0-3",
"hw:numa_cpus.1": "4-7",
"hw:numa_mem.0": "1576",
}),
"image": {
},
"expect": exception.ImageNUMATopologyIncomplete,
},
{
# Request missing cpu.1
"flavor": objects.Flavor(vcpus=8, memory_mb=2048,
extra_specs={
"hw:numa_nodes": 2,
"hw:numa_cpus.0": "0-3",
"hw:numa_mem.0": "1024",
"hw:numa_mem.1": "1024",
}),
"image": {
},
"expect": exception.ImageNUMATopologyIncomplete,
},
{ {
# Image attempts to override flavor # Image attempts to override flavor
"flavor": objects.Flavor(vcpus=8, memory_mb=2048, "flavor": objects.Flavor(vcpus=8, memory_mb=2048,

View File

@@ -17,7 +17,7 @@ import fractions
import itertools import itertools
import math import math
import re import re
from typing import List, Optional, Set, Tuple import typing as ty
import os_resource_classes as orc import os_resource_classes as orc
import os_traits import os_traits
@@ -90,7 +90,7 @@ def get_cpu_shared_set():
return shared_ids return shared_ids
def parse_cpu_spec(spec): def parse_cpu_spec(spec: str) -> ty.Set[int]:
"""Parse a CPU set specification. """Parse a CPU set specification.
Each element in the list is either a single CPU number, a range of Each element in the list is either a single CPU number, a range of
@@ -101,8 +101,8 @@ def parse_cpu_spec(spec):
:returns: a set of CPU indexes :returns: a set of CPU indexes
""" """
cpuset_ids = set() cpuset_ids: ty.Set[int] = set()
cpuset_reject_ids = set() cpuset_reject_ids: ty.Set[int] = set()
for rule in spec.split(','): for rule in spec.split(','):
rule = rule.strip() rule = rule.strip()
# Handle multi ',' # Handle multi ','
@@ -152,7 +152,10 @@ def parse_cpu_spec(spec):
return cpuset_ids return cpuset_ids
def format_cpu_spec(cpuset, allow_ranges=True): def format_cpu_spec(
cpuset: ty.Set[int],
allow_ranges: bool = True,
) -> str:
"""Format a libvirt CPU range specification. """Format a libvirt CPU range specification.
Format a set/list of CPU indexes as a libvirt CPU range Format a set/list of CPU indexes as a libvirt CPU range
@@ -161,6 +164,8 @@ def format_cpu_spec(cpuset, allow_ranges=True):
index explicitly. index explicitly.
:param cpuset: set (or list) of CPU indexes :param cpuset: set (or list) of CPU indexes
:param allow_ranges: Whether we should attempt to detect continuous ranges
of CPUs.
:returns: a formatted CPU range string :returns: a formatted CPU range string
""" """
@@ -168,7 +173,7 @@ def format_cpu_spec(cpuset, allow_ranges=True):
# trying to do range negations to minimize the overall # trying to do range negations to minimize the overall
# spec string length # spec string length
if allow_ranges: if allow_ranges:
ranges = [] ranges: ty.List[ty.List[int]] = []
previndex = None previndex = None
for cpuindex in sorted(cpuset): for cpuindex in sorted(cpuset):
if previndex is None or previndex != (cpuindex - 1): if previndex is None or previndex != (cpuindex - 1):
@@ -552,7 +557,9 @@ def _sort_possible_cpu_topologies(possible, wanttopology):
# We don't use python's sort(), since we want to # We don't use python's sort(), since we want to
# preserve the sorting done when populating the # preserve the sorting done when populating the
# 'possible' list originally # 'possible' list originally
scores = collections.defaultdict(list) scores: ty.Dict[int, ty.List['objects.VirtCPUTopology']] = (
collections.defaultdict(list)
)
for topology in possible: for topology in possible:
score = _score_cpu_topology(topology, wanttopology) score = _score_cpu_topology(topology, wanttopology)
scores[score].append(topology) scores[score].append(topology)
@@ -731,7 +738,9 @@ def _pack_instance_onto_cores(host_cell, instance_cell,
# We build up a data structure that answers the question: 'Given the # We build up a data structure that answers the question: 'Given the
# number of threads I want to pack, give me a list of all the available # number of threads I want to pack, give me a list of all the available
# sibling sets (or groups thereof) that can accommodate it' # sibling sets (or groups thereof) that can accommodate it'
sibling_sets = collections.defaultdict(list) sibling_sets: ty.Dict[int, ty.List[ty.Set[int]]] = (
collections.defaultdict(list)
)
for sib in host_cell.free_siblings: for sib in host_cell.free_siblings:
for threads_no in range(1, len(sib) + 1): for threads_no in range(1, len(sib) + 1):
sibling_sets[threads_no].append(sib) sibling_sets[threads_no].append(sib)
@@ -1175,7 +1184,12 @@ def _numa_fit_instance_cell(host_cell, instance_cell, limit_cell=None,
return instance_cell return instance_cell
def _get_flavor_image_meta(key, flavor, image_meta, default=None): def _get_flavor_image_meta(
key: str,
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
default: ty.Any = None,
) -> ty.Tuple[ty.Any, ty.Any]:
"""Extract both flavor- and image-based variants of metadata.""" """Extract both flavor- and image-based variants of metadata."""
flavor_key = ':'.join(['hw', key]) flavor_key = ':'.join(['hw', key])
image_key = '_'.join(['hw', key]) image_key = '_'.join(['hw', key])
@@ -1186,7 +1200,11 @@ def _get_flavor_image_meta(key, flavor, image_meta, default=None):
return flavor_policy, image_policy return flavor_policy, image_policy
def get_mem_encryption_constraint(flavor, image_meta, machine_type=None): def get_mem_encryption_constraint(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
machine_type: ty.Optional[str] = None,
) -> bool:
"""Return a boolean indicating whether encryption of guest memory was """Return a boolean indicating whether encryption of guest memory was
requested, either via the hw:mem_encryption extra spec or the requested, either via the hw:mem_encryption extra spec or the
hw_mem_encryption image property (or both). hw_mem_encryption image property (or both).
@@ -1323,7 +1341,10 @@ def _check_mem_encryption_machine_type(image_meta, machine_type=None):
reason=_("q35 type is required for SEV to work")) reason=_("q35 type is required for SEV to work"))
def _get_numa_pagesize_constraint(flavor, image_meta): def _get_numa_pagesize_constraint(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[int]:
"""Return the requested memory page size """Return the requested memory page size
:param flavor: a Flavor object to read extra specs from :param flavor: a Flavor object to read extra specs from
@@ -1388,8 +1409,10 @@ def _get_constraint_mappings_from_flavor(flavor, key, func):
return hw_numa_map or None return hw_numa_map or None
def _get_numa_cpu_constraint(flavor, image_meta): def _get_numa_cpu_constraint(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[List[Set[int]]] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[ty.List[ty.Set[int]]]:
"""Validate and return the requested guest NUMA-guest CPU mapping. """Validate and return the requested guest NUMA-guest CPU mapping.
Extract the user-provided mapping of guest CPUs to guest NUMA nodes. For Extract the user-provided mapping of guest CPUs to guest NUMA nodes. For
@@ -1418,8 +1441,10 @@ def _get_numa_cpu_constraint(flavor, image_meta):
return flavor_cpu_list return flavor_cpu_list
def _get_numa_mem_constraint(flavor, image_meta): def _get_numa_mem_constraint(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[List[Set[int]]] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[ty.List[int]]:
"""Validate and return the requested guest NUMA-guest memory mapping. """Validate and return the requested guest NUMA-guest memory mapping.
Extract the user-provided mapping of guest memory to guest NUMA nodes. For Extract the user-provided mapping of guest memory to guest NUMA nodes. For
@@ -1448,8 +1473,10 @@ def _get_numa_mem_constraint(flavor, image_meta):
return flavor_mem_list return flavor_mem_list
def _get_numa_node_count_constraint(flavor, image_meta): def _get_numa_node_count_constraint(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[int] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[int]:
"""Validate and return the requested NUMA nodes. """Validate and return the requested NUMA nodes.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
@@ -1475,8 +1502,10 @@ def _get_numa_node_count_constraint(flavor, image_meta):
# NOTE(stephenfin): This must be public as it's used elsewhere # NOTE(stephenfin): This must be public as it's used elsewhere
def get_cpu_policy_constraint(flavor, image_meta): def get_cpu_policy_constraint(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[str] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[str]:
"""Validate and return the requested CPU policy. """Validate and return the requested CPU policy.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
@@ -1517,8 +1546,10 @@ def get_cpu_policy_constraint(flavor, image_meta):
# NOTE(stephenfin): This must be public as it's used elsewhere # NOTE(stephenfin): This must be public as it's used elsewhere
def get_cpu_thread_policy_constraint(flavor, image_meta): def get_cpu_thread_policy_constraint(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[str] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[str]:
"""Validate and return the requested CPU thread policy. """Validate and return the requested CPU thread policy.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
@@ -1616,8 +1647,9 @@ def is_realtime_enabled(flavor):
return strutils.bool_from_string(flavor_rt) return strutils.bool_from_string(flavor_rt)
def _get_vcpu_pcpu_resources(flavor): def _get_vcpu_pcpu_resources(
# type: (objects.Flavor) -> Tuple[bool, bool] flavor: 'objects.Flavor',
) -> ty.Tuple[int, int]:
requested_vcpu = 0 requested_vcpu = 0
requested_pcpu = 0 requested_pcpu = 0
@@ -1635,11 +1667,13 @@ def _get_vcpu_pcpu_resources(flavor):
# this is handled elsewhere # this is handled elsewhere
pass pass
return (requested_vcpu, requested_pcpu) return requested_vcpu, requested_pcpu
def _get_hyperthreading_trait(flavor, image_meta): def _get_hyperthreading_trait(
# type: (objects.Flavor, objects.ImageMeta) -> Optional[str] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[str]:
for key, val in flavor.get('extra_specs', {}).items(): for key, val in flavor.get('extra_specs', {}).items():
if re.match('trait([1-9][0-9]*)?:%s' % os_traits.HW_CPU_HYPERTHREADING, if re.match('trait([1-9][0-9]*)?:%s' % os_traits.HW_CPU_HYPERTHREADING,
key): key):
@@ -1649,9 +1683,13 @@ def _get_hyperthreading_trait(flavor, image_meta):
'traits_required', []): 'traits_required', []):
return 'required' return 'required'
return None
def _get_realtime_constraint(flavor, image_meta):
# type: (objects.Flavor, objects.ImageMeta) -> Optional[str] def _get_realtime_constraint(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[str]:
"""Validate and return the requested realtime CPU mask. """Validate and return the requested realtime CPU mask.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
@@ -1666,15 +1704,17 @@ def _get_realtime_constraint(flavor, image_meta):
return image_mask or flavor_mask return image_mask or flavor_mask
def vcpus_realtime_topology(flavor, image_meta): def vcpus_realtime_topology(
# type: (objects.Flavor, objects.ImageMeta) -> List[int] flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Set[int]:
"""Determines instance vCPUs used as RT for a given spec. """Determines instance vCPUs used as RT for a given spec.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
:param image_meta: ``nova.objects.ImageMeta`` instance :param image_meta: ``nova.objects.ImageMeta`` instance
:raises: exception.RealtimeMaskNotFoundOrInvalid if mask was not found or :raises: exception.RealtimeMaskNotFoundOrInvalid if mask was not found or
is invalid. is invalid.
:returns: The realtime CPU mask requested, else None. :returns: The realtime CPU mask requested.
""" """
mask = _get_realtime_constraint(flavor, image_meta) mask = _get_realtime_constraint(flavor, image_meta)
if not mask: if not mask:
@@ -1688,8 +1728,9 @@ def vcpus_realtime_topology(flavor, image_meta):
# NOTE(stephenfin): This must be public as it's used elsewhere # NOTE(stephenfin): This must be public as it's used elsewhere
def get_emulator_thread_policy_constraint(flavor): def get_emulator_thread_policy_constraint(
# type: (objects.Flavor) -> Optional[str] flavor: 'objects.Flavor',
) -> ty.Optional[str]:
"""Validate and return the requested emulator threads policy. """Validate and return the requested emulator threads policy.
:param flavor: ``nova.objects.Flavor`` instance :param flavor: ``nova.objects.Flavor`` instance
@@ -1701,7 +1742,7 @@ def get_emulator_thread_policy_constraint(flavor):
'hw:emulator_threads_policy') 'hw:emulator_threads_policy')
if not emu_threads_policy: if not emu_threads_policy:
return return None
if emu_threads_policy not in fields.CPUEmulatorThreadsPolicy.ALL: if emu_threads_policy not in fields.CPUEmulatorThreadsPolicy.ALL:
raise exception.InvalidEmulatorThreadsPolicy( raise exception.InvalidEmulatorThreadsPolicy(
@@ -1794,22 +1835,20 @@ def numa_get_constraints(flavor, image_meta):
cpu_list = _get_numa_cpu_constraint(flavor, image_meta) cpu_list = _get_numa_cpu_constraint(flavor, image_meta)
mem_list = _get_numa_mem_constraint(flavor, image_meta) mem_list = _get_numa_mem_constraint(flavor, image_meta)
# If one property list is specified both must be if cpu_list is None and mem_list is None:
if ((cpu_list is None and mem_list is not None) or
(cpu_list is not None and mem_list is None)):
raise exception.ImageNUMATopologyIncomplete()
# If any node has data set, all nodes must have data set
if ((cpu_list is not None and len(cpu_list) != nodes) or
(mem_list is not None and len(mem_list) != nodes)):
raise exception.ImageNUMATopologyIncomplete()
if cpu_list is None:
numa_topology = _get_numa_topology_auto( numa_topology = _get_numa_topology_auto(
nodes, flavor) nodes, flavor)
else: elif cpu_list is not None and mem_list is not None:
# If any node has data set, all nodes must have data set
if len(cpu_list) != nodes or len(mem_list) != nodes:
raise exception.ImageNUMATopologyIncomplete()
numa_topology = _get_numa_topology_manual( numa_topology = _get_numa_topology_manual(
nodes, flavor, cpu_list, mem_list) nodes, flavor, cpu_list, mem_list
)
else:
# If one property list is specified both must be
raise exception.ImageNUMATopologyIncomplete()
# We currently support same pagesize for all cells. # We currently support same pagesize for all cells.
for c in numa_topology.cells: for c in numa_topology.cells:
@@ -1912,11 +1951,10 @@ def numa_get_constraints(flavor, image_meta):
def _numa_cells_support_network_metadata( def _numa_cells_support_network_metadata(
host_topology, # type: objects.NUMATopology host_topology: 'objects.NUMATopology',
chosen_host_cells, # type: List[objects.NUMACell] chosen_host_cells: ty.List['objects.NUMACell'],
network_metadata # type: objects.NetworkMetadata network_metadata: 'objects.NetworkMetadata',
): ) -> bool:
# type: (...) -> bool
"""Determine whether the cells can accept the network requests. """Determine whether the cells can accept the network requests.
:param host_topology: The entire host topology, used to find non-chosen :param host_topology: The entire host topology, used to find non-chosen
@@ -1932,12 +1970,12 @@ def _numa_cells_support_network_metadata(
if not network_metadata: if not network_metadata:
return True return True
required_physnets = None # type: Set[str] required_physnets: ty.Set[str] = set()
if 'physnets' in network_metadata: if 'physnets' in network_metadata:
# use set() to avoid modifying the original data structure # use set() to avoid modifying the original data structure
required_physnets = set(network_metadata.physnets) required_physnets = set(network_metadata.physnets)
required_tunnel = False # type: bool required_tunnel: bool = False
if 'tunneled' in network_metadata: if 'tunneled' in network_metadata:
required_tunnel = network_metadata.tunneled required_tunnel = network_metadata.tunneled
@@ -2045,8 +2083,8 @@ def numa_fit_instance_to_host(
# depending on whether we want packing/spreading over NUMA nodes # depending on whether we want packing/spreading over NUMA nodes
for host_cell_perm in itertools.permutations( for host_cell_perm in itertools.permutations(
host_cells, len(instance_topology)): host_cells, len(instance_topology)):
chosen_instance_cells = [] chosen_instance_cells: ty.List['objects.InstanceNUMACell'] = []
chosen_host_cells = [] chosen_host_cells: ty.List['objects.NUMACell'] = []
for host_cell, instance_cell in zip( for host_cell, instance_cell in zip(
host_cell_perm, instance_topology.cells): host_cell_perm, instance_topology.cells):
try: try:
@@ -2096,14 +2134,14 @@ def numa_get_reserved_huge_pages():
:raises: exception.InvalidReservedMemoryPagesOption when :raises: exception.InvalidReservedMemoryPagesOption when
reserved_huge_pages option is not correctly set. reserved_huge_pages option is not correctly set.
:returns: a list of dict ordered by NUMA node ids; keys of dict :returns: A dict of dicts keyed by NUMA node IDs; keys of child dict
are pages size and values of the number reserved. are pages size and values of the number reserved.
""" """
if not CONF.reserved_huge_pages: if not CONF.reserved_huge_pages:
return {} return {}
try: try:
bucket = collections.defaultdict(dict) bucket: ty.Dict[int, ty.Dict[int, int]] = collections.defaultdict(dict)
for cfg in CONF.reserved_huge_pages: for cfg in CONF.reserved_huge_pages:
try: try:
pagesize = int(cfg['size']) pagesize = int(cfg['size'])