Coverage for nova/pci/request.py: 98%

94 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2013 Intel Corporation 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16""" Example of a PCI alias:: 

17 

18 | [pci] 

19 | alias = '{ 

20 | "name": "QuickAssist", 

21 | "product_id": "0443", 

22 | "vendor_id": "8086", 

23 | "device_type": "type-PCI", 

24 | "numa_policy": "legacy" 

25 | }' 

26 

27 Aliases with the same name, device_type and numa_policy are ORed:: 

28 

29 | [pci] 

30 | alias = '{ 

31 | "name": "QuickAssist", 

32 | "product_id": "0442", 

33 | "vendor_id": "8086", 

34 | "device_type": "type-PCI", 

35 | }' 

36 

37 These two aliases define a device request meaning: vendor_id is "8086" and 

38 product_id is "0442" or "0443". 

39 """ 

40 

41import typing as ty 

42 

43import jsonschema 

44from oslo_log import log as logging 

45from oslo_serialization import jsonutils 

46from oslo_utils import uuidutils 

47 

48import nova.conf 

49from nova import context as ctx 

50from nova import exception 

51from nova.i18n import _ 

52from nova.network import model as network_model 

53from nova import objects 

54from nova.objects import fields as obj_fields 

55from nova.pci import utils 

56from oslo_utils import strutils 

57 

58Alias = ty.Dict[str, ty.Tuple[str, ty.List[ty.Dict[str, str]]]] 

59 

60PCI_NET_TAG = 'physical_network' 

61PCI_TRUSTED_TAG = 'trusted' 

62PCI_DEVICE_TYPE_TAG = 'dev_type' 

63PCI_REMOTE_MANAGED_TAG = 'remote_managed' 

64 

65DEVICE_TYPE_FOR_VNIC_TYPE = { 

66 network_model.VNIC_TYPE_DIRECT_PHYSICAL: obj_fields.PciDeviceType.SRIOV_PF, 

67 network_model.VNIC_TYPE_VDPA: obj_fields.PciDeviceType.VDPA, 

68} 

69 

70CONF = nova.conf.CONF 

71LOG = logging.getLogger(__name__) 

72 

73_ALIAS_SCHEMA = { 

74 "type": "object", 

75 "additionalProperties": False, 

76 "properties": { 

77 "name": { 

78 "type": "string", 

79 "minLength": 1, 

80 "maxLength": 256, 

81 }, 

82 # TODO(stephenfin): This isn't used anywhere outside of tests and 

83 # should probably be removed. 

84 "capability_type": { 

85 "type": "string", 

86 "enum": ['pci'], 

87 }, 

88 "product_id": { 

89 "type": "string", 

90 "pattern": utils.PCI_VENDOR_PATTERN, 

91 }, 

92 "vendor_id": { 

93 "type": "string", 

94 "pattern": utils.PCI_VENDOR_PATTERN, 

95 }, 

96 "device_type": { 

97 "type": "string", 

98 # NOTE(sean-k-mooney): vDPA devices cannot currently be used with 

99 # alias-based PCI passthrough so we exclude it here 

100 "enum": [ 

101 obj_fields.PciDeviceType.STANDARD, 

102 obj_fields.PciDeviceType.SRIOV_PF, 

103 obj_fields.PciDeviceType.SRIOV_VF, 

104 ], 

105 }, 

106 "numa_policy": { 

107 "type": "string", 

108 "enum": list(obj_fields.PCINUMAAffinityPolicy.ALL), 

109 }, 

110 "resource_class": { 

111 "type": "string", 

112 }, 

113 "traits": { 

114 "type": "string", 

115 }, 

116 "live_migratable": { 

117 "type": "string", 

118 }, 

119 }, 

120 "required": ["name"], 

121} 

122 

123 

124def _get_alias_from_config() -> Alias: 

125 """Parse and validate PCI aliases from the nova config. 

126 

127 :returns: A dictionary where the keys are alias names and the values are 

128 tuples of form ``(numa_policy, specs)``. ``numa_policy`` describes the 

129 required NUMA affinity of the device(s), while ``specs`` is a list of 

130 PCI device specs. 

131 :raises: exception.PciInvalidAlias if two aliases with the same name have 

132 different device types or different NUMA policies. 

133 """ 

134 jaliases = CONF.pci.alias 

135 # map alias name to alias spec list 

136 aliases: Alias = {} 

137 try: 

138 for jsonspecs in jaliases: 

139 spec = jsonutils.loads(jsonspecs) 

140 jsonschema.validate(spec, _ALIAS_SCHEMA) 

141 

142 name = spec.pop('name').strip() 

143 numa_policy = spec.pop('numa_policy', None) 

144 if not numa_policy: 

145 numa_policy = obj_fields.PCINUMAAffinityPolicy.LEGACY 

146 

147 dev_type = spec.pop('device_type', None) 

148 if dev_type: 

149 spec['dev_type'] = dev_type 

150 

151 live_migratable = spec.pop('live_migratable', None) 

152 if live_migratable is not None: 

153 live_migratable = ( 

154 "true" 

155 if strutils.bool_from_string(live_migratable, strict=True) 

156 else "false" 

157 ) 

158 spec['live_migratable'] = live_migratable 

159 

160 if name not in aliases: 

161 aliases[name] = (numa_policy, [spec]) 

162 continue 

163 

164 if aliases[name][0] != numa_policy: 

165 reason = _("NUMA policy mismatch for alias '%s'") % name 

166 raise exception.PciInvalidAlias(reason=reason) 

167 

168 if aliases[name][1][0]['dev_type'] != spec['dev_type']: 

169 reason = _("Device type mismatch for alias '%s'") % name 

170 raise exception.PciInvalidAlias(reason=reason) 

171 

172 aliases[name][1].append(spec) 

173 except exception.PciInvalidAlias: 

174 raise 

175 except jsonschema.exceptions.ValidationError as exc: 

176 raise exception.PciInvalidAlias(reason=exc.message) 

177 except Exception as exc: 

178 raise exception.PciInvalidAlias(reason=str(exc)) 

179 

180 return aliases 

181 

182 

183def _translate_alias_to_requests( 

184 alias_spec: str, affinity_policy: ty.Optional[str] = None, 

185) -> ty.List['objects.InstancePCIRequest']: 

186 """Generate complete pci requests from pci aliases in extra_spec.""" 

187 pci_aliases = _get_alias_from_config() 

188 

189 pci_requests: ty.List[objects.InstancePCIRequest] = [] 

190 for name, count in [spec.split(':') for spec in alias_spec.split(',')]: 

191 name = name.strip() 

192 if name not in pci_aliases: 

193 raise exception.PciRequestAliasNotDefined(alias=name) 

194 

195 numa_policy, spec = pci_aliases[name] 

196 policy = affinity_policy or numa_policy 

197 

198 # NOTE(gibi): InstancePCIRequest has a requester_id field that could 

199 # be filled with the flavor.flavorid but currently there is no special 

200 # handling for InstancePCIRequests created from the flavor. So it is 

201 # left empty. 

202 pci_requests.append(objects.InstancePCIRequest( 

203 count=int(count), 

204 spec=spec, 

205 alias_name=name, 

206 numa_policy=policy, 

207 request_id=uuidutils.generate_uuid(), 

208 )) 

209 return pci_requests 

210 

211 

212def get_instance_pci_request_from_vif( 

213 context: ctx.RequestContext, 

214 instance: 'objects.Instance', 

215 vif: network_model.VIF, 

216) -> ty.Optional['objects.InstancePCIRequest']: 

217 """Given an Instance, return the PCI request associated 

218 to the PCI device related to the given VIF (if any) on the 

219 compute node the instance is currently running. 

220 

221 In this method we assume a VIF is associated with a PCI device 

222 if 'pci_slot' attribute exists in the vif 'profile' dict. 

223 

224 :param context: security context 

225 :param instance: instance object 

226 :param vif: network VIF model object 

227 :raises: raises PciRequestFromVIFNotFound if a pci device is requested 

228 but not found on current host 

229 :return: instance's PCIRequest object associated with the given VIF 

230 or None if no PCI device is requested 

231 """ 

232 

233 # Get PCI device address for VIF if exists 

234 vif_pci_dev_addr = vif['profile'].get('pci_slot') \ 

235 if vif['profile'] else None 

236 

237 if not vif_pci_dev_addr: 

238 return None 

239 

240 try: 

241 cn_id = objects.ComputeNode.get_by_host_and_nodename( 

242 context, 

243 instance.host, 

244 instance.node).id 

245 except exception.NotFound: 

246 LOG.warning("expected to find compute node with host %s " 

247 "and node %s when getting instance PCI request " 

248 "from VIF", instance.host, instance.node) 

249 return None 

250 # Find PCIDevice associated with vif_pci_dev_addr on the compute node 

251 # the instance is running on. 

252 found_pci_dev = None 

253 for pci_dev in instance.pci_devices: 

254 if (pci_dev.compute_node_id == cn_id and 

255 pci_dev.address == vif_pci_dev_addr): 

256 found_pci_dev = pci_dev 

257 break 

258 if not found_pci_dev: 

259 return None 

260 # Find PCIRequest associated with the given PCIDevice in instance 

261 for pci_req in instance.pci_requests.requests: 

262 if pci_req.request_id == found_pci_dev.request_id: 

263 return pci_req 

264 

265 raise exception.PciRequestFromVIFNotFound( 

266 pci_slot=vif_pci_dev_addr, 

267 node_id=cn_id) 

268 

269 

270def get_pci_requests_from_flavor( 

271 flavor: 'objects.Flavor', affinity_policy: ty.Optional[str] = None, 

272) -> 'objects.InstancePCIRequests': 

273 """Validate and return PCI requests. 

274 

275 The ``pci_passthrough:alias`` extra spec describes the flavor's PCI 

276 requests. The extra spec's value is a comma-separated list of format 

277 ``alias_name_x:count, alias_name_y:count, ... ``, where ``alias_name`` is 

278 defined in ``pci.alias`` configurations. 

279 

280 The flavor's requirement is translated into a PCI requests list. Each 

281 entry in the list is an instance of nova.objects.InstancePCIRequests with 

282 four keys/attributes. 

283 

284 - 'spec' states the PCI device properties requirement 

285 - 'count' states the number of devices 

286 - 'alias_name' (optional) is the corresponding alias definition name 

287 - 'numa_policy' (optional) states the required NUMA affinity of the devices 

288 

289 For example, assume alias configuration is:: 

290 

291 { 

292 'vendor_id':'8086', 

293 'device_id':'1502', 

294 'name':'alias_1' 

295 } 

296 

297 While flavor extra specs includes:: 

298 

299 'pci_passthrough:alias': 'alias_1:2' 

300 

301 The returned ``pci_requests`` are:: 

302 

303 [{ 

304 'count':2, 

305 'specs': [{'vendor_id':'8086', 'device_id':'1502'}], 

306 'alias_name': 'alias_1' 

307 }] 

308 

309 :param flavor: The flavor to be checked 

310 :param affinity_policy: pci numa affinity policy 

311 :returns: A list of PCI requests 

312 :rtype: nova.objects.InstancePCIRequests 

313 :raises: exception.PciRequestAliasNotDefined if an invalid PCI alias is 

314 provided 

315 :raises: exception.PciInvalidAlias if the configuration contains invalid 

316 aliases. 

317 """ 

318 pci_requests: ty.List[objects.InstancePCIRequest] = [] 

319 if ('extra_specs' in flavor and 

320 'pci_passthrough:alias' in flavor['extra_specs']): 

321 pci_requests = _translate_alias_to_requests( 

322 flavor['extra_specs']['pci_passthrough:alias'], 

323 affinity_policy=affinity_policy) 

324 

325 return objects.InstancePCIRequests(requests=pci_requests)