Skip to content

Commit b10482c

Browse files
committed
Support resource_class and traits in PCI alias
The [pci]alias configuration option now accepts two new optional fields: * resource_class: that can be used to request PCI device by placement RC name. * traits: a comma separated list of placement trait names that can be used to filter placement PCI resource provider by traits. These fields has the matching counterpart in [pci]device_spec implemented already. These fields are matched by the Placement GET allocation_candidates query therefore these fields are ignored when PCI device pools are matched against IntancePCIRequest by nova. Note that InstancePCIRequest object spec field is defined as a list of dicts. But in reality nova creates the request always with a single dict. So we restricted the placement logic to handle a single spec. blueprint: pci-device-tracking-in-placement Change-Id: I5c8f05c3c5d7597175e60b29e4ab2f22e6496ecd
1 parent 7459213 commit b10482c

File tree

11 files changed

+449
-48
lines changed

11 files changed

+449
-48
lines changed

doc/source/admin/pci-passthrough.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,21 @@ removed and VFs from the same PF is configured (or vice versa) then
442442
nova-compute will refuse to start as it would create a situation where both
443443
the PF and its VFs are made available for consumption.
444444

445+
If a flavor requests multiple ``type-VF`` devices via
446+
:nova:extra-spec:`pci_passthrough:alias` then it is important to consider the
447+
value of :nova:extra-spec:`group_policy` as well. The value ``none``
448+
allows nova to select VFs from the same parent PF to fulfill the request. The
449+
value ``isolate`` restricts nova to select each VF from a different parent PF
450+
to fulfill the request. If :nova:extra-spec:`group_policy` is not provided in
451+
such flavor then it will defaulted to ``none``.
452+
453+
Symmetrically with the ``resource_class`` and ``traits`` fields of
454+
:oslo.config:option:`pci.device_spec` the :oslo.config:option:`pci.alias`
455+
configuration option supports requesting devices by Placement resource class
456+
name via the ``resource_class`` field and also support requesting traits to
457+
be present on the selected devices via the ``traits`` field in the alias. If
458+
the ``resource_class`` field is not specified in the alias then it is defaulted
459+
by nova to ``CUSTOM_PCI_<vendor_id>_<product_id>``.
445460

446461
For deeper technical details please read the `nova specification. <https://specs.openstack.org/openstack/nova-specs/specs/zed/approved/pci-device-tracking-in-placement.html>`_
447462

nova/compute/pci_placement_translator.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,50 @@ def _normalize_traits(traits: ty.List[str]) -> ty.List[str]:
6565
return list(standard_traits) + custom_traits
6666

6767

68-
def _get_traits_for_dev(
69-
dev_spec_tags: ty.Dict[str, str],
70-
) -> ty.Set[str]:
68+
def get_traits(traits_str: str) -> ty.Set[str]:
69+
"""Return a normalized set of placement standard and custom traits from
70+
a string of comma separated trait names.
71+
"""
7172
# traits is a comma separated list of placement trait names
72-
traits_str = dev_spec_tags.get("traits")
7373
if not traits_str:
74-
return {os_traits.COMPUTE_MANAGED_PCI_DEVICE}
74+
return set()
75+
return set(_normalize_traits(traits_str.split(',')))
7576

76-
traits = traits_str.split(',')
77-
return set(_normalize_traits(traits)) | {
77+
78+
def _get_traits_for_dev(
79+
dev_spec_tags: ty.Dict[str, str],
80+
) -> ty.Set[str]:
81+
return get_traits(dev_spec_tags.get("traits", "")) | {
7882
os_traits.COMPUTE_MANAGED_PCI_DEVICE
7983
}
8084

8185

86+
def _normalize_resource_class(rc: str) -> str:
87+
rc = rc.upper()
88+
if (
89+
rc not in os_resource_classes.STANDARDS and
90+
not os_resource_classes.is_custom(rc)
91+
):
92+
rc = os_resource_classes.normalize_name(rc)
93+
# mypy: normalize_name will return non None for non None input
94+
assert rc
95+
96+
return rc
97+
98+
99+
def get_resource_class(
100+
requested_name: ty.Optional[str], vendor_id: str, product_id: str
101+
) -> str:
102+
"""Return the normalized resource class name based on what is requested
103+
or if nothing is requested then generated from the vendor_id and product_id
104+
"""
105+
if requested_name:
106+
rc = _normalize_resource_class(requested_name)
107+
else:
108+
rc = f"CUSTOM_PCI_{vendor_id}_{product_id}".upper()
109+
return rc
110+
111+
82112
def _get_rc_for_dev(
83113
dev: pci_device.PciDevice,
84114
dev_spec_tags: ty.Dict[str, str],
@@ -91,23 +121,8 @@ def _get_rc_for_dev(
91121
The user specified resource class is normalized if it is not already an
92122
acceptable standard or custom resource class.
93123
"""
94-
# Either use the resource class from the config or the vendor_id and
95-
# product_id of the device to generate the RC
96124
rc = dev_spec_tags.get("resource_class")
97-
if rc:
98-
rc = rc.upper()
99-
if (
100-
rc not in os_resource_classes.STANDARDS and
101-
not os_resource_classes.is_custom(rc)
102-
):
103-
rc = os_resource_classes.normalize_name(rc)
104-
# mypy: normalize_name will return non None for non None input
105-
assert rc
106-
107-
else:
108-
rc = f"CUSTOM_PCI_{dev.vendor_id}_{dev.product_id}".upper()
109-
110-
return rc
125+
return get_resource_class(rc, dev.vendor_id, dev.product_id)
111126

112127

113128
class PciResourceProvider:

nova/conf/pci.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,32 @@
6767
Required NUMA affinity of device. Valid values are: ``legacy``,
6868
``preferred`` and ``required``.
6969
70+
``resource_class``
71+
The optional Placement resource class name that is used
72+
to track the requested PCI devices in Placement. It can be a standard
73+
resource class from the ``os-resource-classes`` lib. Or can be any string.
74+
In that case Nova will normalize it to a proper Placement resource class by
75+
making it upper case, replacing any consecutive character outside of
76+
``[A-Z0-9_]`` with a single '_', and prefixing the name with ``CUSTOM_`` if
77+
not yet prefixed. The maximum allowed length is 255 character including the
78+
prefix. If ``resource_class`` is not provided Nova will generate it from
79+
``vendor_id`` and ``product_id`` values of the alias in the form of
80+
``CUSTOM_PCI_{vendor_id}_{product_id}``. The ``resource_class`` requested
81+
in the alias is matched against the ``resource_class`` defined in the
82+
``[pci]device_spec``.
83+
84+
``traits``
85+
An optional comma separated list of Placement trait names requested to be
86+
present on the resource provider that fulfills this alias. Each trait can
87+
be a standard trait from ``os-traits`` lib or can be any string. If it is
88+
not a standard trait then Nova will normalize the trait name by making it
89+
upper case, replacing any consecutive character outside of ``[A-Z0-9_]``
90+
with a single '_', and prefixing the name with ``CUSTOM_`` if not yet
91+
prefixed. The maximum allowed length of a trait name is 255 character
92+
including the prefix. Every trait in ``traits`` requested in the alias
93+
ensured to be in the list of traits provided in the ``traits`` field of
94+
the ``[pci]device_spec`` when scheduling the request.
95+
7096
* Supports multiple aliases by repeating the option (not by specifying
7197
a list value)::
7298

nova/objects/request_spec.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
import copy
1616
import itertools
17+
import typing as ty
1718

1819
import os_resource_classes as orc
1920
from oslo_log import log as logging
2021
from oslo_serialization import jsonutils
2122
from oslo_utils import versionutils
2223

24+
from nova.compute import pci_placement_translator
2325
from nova.db.api import api as api_db_api
2426
from nova.db.api import models as api_models
2527
from nova import exception
@@ -474,14 +476,16 @@ def to_legacy_filter_properties_dict(self):
474476
return filt_props
475477

476478
@staticmethod
477-
def _rc_from_request(pci_request: 'objects.InstancePCIRequest') -> str:
478-
# FIXME(gibi): refactor this and the copy of the logic from the
479-
# translator to a common function
480-
# FIXME(gibi): handle directly requested resource_class
481-
# ??? can there be more than one spec???
482-
spec = pci_request.spec[0]
483-
rc = f"CUSTOM_PCI_{spec['vendor_id']}_{spec['product_id']}".upper()
484-
return rc
479+
def _rc_from_request(spec: ty.Dict[str, ty.Any]) -> str:
480+
return pci_placement_translator.get_resource_class(
481+
spec.get("resource_class"),
482+
spec.get("vendor_id"),
483+
spec.get("product_id"),
484+
)
485+
486+
@staticmethod
487+
def _traits_from_request(spec: ty.Dict[str, ty.Any]) -> ty.Set[str]:
488+
return pci_placement_translator.get_traits(spec.get("traits", ""))
485489

486490
# This is here temporarily until the PCI placement scheduling is under
487491
# implementation. When that is done there will be a config option
@@ -501,6 +505,34 @@ def _generate_request_groups_from_pci_requests(self):
501505
# cycle.
502506
continue
503507

508+
if len(pci_request.spec) != 1:
509+
# We are instantiating InstancePCIRequest objects with spec in
510+
# two cases:
511+
# 1) when a neutron port is translated to InstancePCIRequest
512+
# object in
513+
# nova.network.neutron.API.create_resource_requests
514+
# 2) when the pci_passthrough:alias flavor extra_spec is
515+
# translated to InstancePCIRequest objects in
516+
# nova.pci.request._get_alias_from_config which enforces the
517+
# json schema defined in nova.pci.request.
518+
#
519+
# In both cases only a single dict is added to the spec list.
520+
# If we ever want to add support for multiple specs per request
521+
# then we have to solve the issue that each spec can request a
522+
# different resource class from placement. The only place in
523+
# nova that currently handles multiple specs per request is
524+
# nova.pci.utils.pci_device_prop_match() and it considers them
525+
# as alternatives. So specs with different resource classes
526+
# would mean alternative resource_class requests. This cannot
527+
# be expressed today in the allocation_candidate query towards
528+
# placement.
529+
raise ValueError(
530+
"PCI tracking in placement does not support multiple "
531+
"specs per PCI request"
532+
)
533+
534+
spec = pci_request.spec[0]
535+
504536
# The goal is to translate InstancePCIRequest to RequestGroup. Each
505537
# InstancePCIRequest can be fulfilled from the whole RP tree. And
506538
# a flavor based InstancePCIRequest might request more than one
@@ -533,9 +565,13 @@ def _generate_request_groups_from_pci_requests(self):
533565
# per requested device. So for InstancePCIRequest(count=2) we need
534566
# to generate two separate RequestGroup(RC:1) objects.
535567

536-
# FIXME(gibi): make sure that if we have count=2 requests then
537-
# group_policy=none is in the request as group_policy=isolate
538-
# would prevent allocating two VFs from the same PF.
568+
# NOTE(gibi): If we have count=2 requests then the multiple
569+
# RequestGroup split below only works if group_policy is set to
570+
# none as group_policy=isolate would prevent allocating two VFs
571+
# from the same PF. Fortunately
572+
# nova.scheduler.utils.resources_from_request_spec() already
573+
# defaults group_policy to none if it is not specified in the
574+
# flavor and there are multiple RequestGroups in the RequestSpec.
539575

540576
for i in range(pci_request.count):
541577
rg = objects.RequestGroup(
@@ -546,8 +582,11 @@ def _generate_request_groups_from_pci_requests(self):
546582
# as we split count >= 2 requests to independent groups
547583
# each group will have a resource request of one
548584
resources={
549-
self._rc_from_request(pci_request): 1}
550-
# FIXME(gibi): handle traits requested from alias
585+
self._rc_from_request(spec): 1
586+
},
587+
required_traits=self._traits_from_request(spec),
588+
# TODO(gibi): later we can add support for complex trait
589+
# queries here including forbidden_traits.
551590
)
552591
self.requested_resources.append(rg)
553592

nova/pci/request.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@
106106
"type": "string",
107107
"enum": list(obj_fields.PCINUMAAffinityPolicy.ALL),
108108
},
109+
"resource_class": {
110+
"type": "string",
111+
},
112+
"traits": {
113+
"type": "string",
114+
},
109115
},
110116
"required": ["name"],
111117
}
@@ -114,7 +120,7 @@
114120
def _get_alias_from_config() -> Alias:
115121
"""Parse and validate PCI aliases from the nova config.
116122
117-
:returns: A dictionary where the keys are device names and the values are
123+
:returns: A dictionary where the keys are alias names and the values are
118124
tuples of form ``(numa_policy, specs)``. ``numa_policy`` describes the
119125
required NUMA affinity of the device(s), while ``specs`` is a list of
120126
PCI device specs.

nova/pci/stats.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ class PciDeviceStats(object):
6464
"""
6565

6666
pool_keys = ['product_id', 'vendor_id', 'numa_node', 'dev_type']
67+
# these can be specified in the [pci]device_spec and can be requested via
68+
# the PCI alias, but they are matched by the placement
69+
# allocation_candidates query, so we can ignore them during pool creation
70+
# and during filtering here
71+
ignored_tags = ['resource_class', 'traits']
6772

6873
def __init__(
6974
self,
@@ -135,7 +140,9 @@ def _create_pool_keys_from_dev(
135140
tags = devspec.get_tags()
136141
pool = {k: getattr(dev, k) for k in self.pool_keys}
137142
if tags:
138-
pool.update(tags)
143+
pool.update(
144+
{k: v for k, v in tags.items() if k not in self.ignored_tags}
145+
)
139146
# NOTE(gibi): parent_ifname acts like a tag during pci claim but
140147
# not provided as part of the whitelist spec as it is auto detected
141148
# by the virt driver.
@@ -313,7 +320,13 @@ def _filter_pools_for_spec(
313320
:returns: A list of pools that can be used to support the request if
314321
this is possible.
315322
"""
316-
request_specs = request.spec
323+
324+
def ignore_keys(spec):
325+
return {
326+
k: v for k, v in spec.items() if k not in self.ignored_tags
327+
}
328+
329+
request_specs = [ignore_keys(spec) for spec in request.spec]
317330
return [
318331
pool for pool in pools
319332
if utils.pci_device_prop_match(pool, request_specs)

0 commit comments

Comments
 (0)