port pci tracker from nova to zun
Change-Id: Ie9f278afb1af68379f8f641eba2e97dd582697cc Depends-On: I93ce4fdec7e6d870ee4e57e73374089ba27becfe Partially-Implements: blueprint support-pcipassthroughfilter
This commit is contained in:
@@ -22,6 +22,7 @@ from zun.common import utils
|
|||||||
from zun.compute import claims
|
from zun.compute import claims
|
||||||
from zun import objects
|
from zun import objects
|
||||||
from zun.objects import base as obj_base
|
from zun.objects import base as obj_base
|
||||||
|
from zun.pci import manager as pci_manager
|
||||||
from zun.scheduler import client as scheduler_client
|
from zun.scheduler import client as scheduler_client
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -36,6 +37,17 @@ class ComputeNodeTracker(object):
|
|||||||
self.tracked_containers = {}
|
self.tracked_containers = {}
|
||||||
self.old_resources = collections.defaultdict(objects.ComputeNode)
|
self.old_resources = collections.defaultdict(objects.ComputeNode)
|
||||||
self.scheduler_client = scheduler_client.SchedulerClient()
|
self.scheduler_client = scheduler_client.SchedulerClient()
|
||||||
|
self.pci_tracker = None
|
||||||
|
|
||||||
|
def _setup_pci_tracker(self, context, compute_node):
|
||||||
|
if not self.pci_tracker:
|
||||||
|
n_id = compute_node.uuid
|
||||||
|
self.pci_tracker = pci_manager.PciDevTracker(context, node_id=n_id)
|
||||||
|
dev_json = self.container_driver.get_pci_resources()
|
||||||
|
self.pci_tracker.update_devices_from_compute_resources(dev_json)
|
||||||
|
|
||||||
|
dev_pools_obj = self.pci_tracker.stats.to_device_pools_obj()
|
||||||
|
compute_node.pci_device_pools = dev_pools_obj
|
||||||
|
|
||||||
def update_available_resources(self, context):
|
def update_available_resources(self, context):
|
||||||
# Check if the compute_node is already registered
|
# Check if the compute_node is already registered
|
||||||
@@ -49,6 +61,7 @@ class ComputeNodeTracker(object):
|
|||||||
node.create(context)
|
node.create(context)
|
||||||
LOG.info('Node created for :%(host)s', {'host': self.host})
|
LOG.info('Node created for :%(host)s', {'host': self.host})
|
||||||
self.container_driver.get_available_resources(node)
|
self.container_driver.get_available_resources(node)
|
||||||
|
self._setup_pci_tracker(context, node)
|
||||||
self.compute_node = node
|
self.compute_node = node
|
||||||
self._update_available_resource(context)
|
self._update_available_resource(context)
|
||||||
# NOTE(sbiswas7): Consider removing the return statement if not needed
|
# NOTE(sbiswas7): Consider removing the return statement if not needed
|
||||||
@@ -175,7 +188,9 @@ class ComputeNodeTracker(object):
|
|||||||
return
|
return
|
||||||
# Persist the stats to the Scheduler
|
# Persist the stats to the Scheduler
|
||||||
self.scheduler_client.update_resource(compute_node)
|
self.scheduler_client.update_resource(compute_node)
|
||||||
# Update pci tracker here
|
|
||||||
|
if self.pci_tracker:
|
||||||
|
self.pci_tracker.save()
|
||||||
|
|
||||||
def _resource_change(self, compute_node):
|
def _resource_change(self, compute_node):
|
||||||
"""Check to see if any resources have changed."""
|
"""Check to see if any resources have changed."""
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ class ContainerDriver(object):
|
|||||||
os_capability_linux.LinuxHost().get_host_numa_topology(numa_topo_obj)
|
os_capability_linux.LinuxHost().get_host_numa_topology(numa_topo_obj)
|
||||||
return numa_topo_obj
|
return numa_topo_obj
|
||||||
|
|
||||||
|
def get_pci_resources(self):
|
||||||
|
return os_capability_linux.LinuxHost().get_pci_resources()
|
||||||
|
|
||||||
def get_host_mem(self):
|
def get_host_mem(self):
|
||||||
return os_capability_linux.LinuxHost().get_host_mem()
|
return os_capability_linux.LinuxHost().get_host_mem()
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,14 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
from zun.common import exception
|
||||||
from zun.common import utils
|
from zun.common import utils
|
||||||
from zun import objects
|
from zun import objects
|
||||||
|
from zun.objects import fields
|
||||||
|
from zun.pci import utils as pci_utils
|
||||||
|
|
||||||
|
|
||||||
class Host(object):
|
class Host(object):
|
||||||
@@ -67,3 +73,154 @@ class Host(object):
|
|||||||
mem_ava = int(mem_free) + int(buffers) + int(cached)
|
mem_ava = int(mem_free) + int(buffers) + int(cached)
|
||||||
mem_used = int(mem_total) - int(mem_ava)
|
mem_used = int(mem_total) - int(mem_ava)
|
||||||
return int(mem_total), int(mem_free), int(mem_ava), int(mem_used)
|
return int(mem_total), int(mem_free), int(mem_ava), int(mem_used)
|
||||||
|
|
||||||
|
def get_pci_resources(self):
|
||||||
|
addresses = []
|
||||||
|
try:
|
||||||
|
output, status = processutils.execute('lspci', '-D', '-nnmm')
|
||||||
|
lines = output.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
columns = line.split()
|
||||||
|
address = columns[0]
|
||||||
|
addresses.append(address)
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='lspci')
|
||||||
|
|
||||||
|
pci_info = []
|
||||||
|
for addr in addresses:
|
||||||
|
pci_info.append(self._get_pci_dev_info(addr))
|
||||||
|
|
||||||
|
return jsonutils.dumps(pci_info)
|
||||||
|
|
||||||
|
def _get_pci_dev_info(self, address):
|
||||||
|
"""Returns a dict of PCI device."""
|
||||||
|
|
||||||
|
def _get_device_type(address):
|
||||||
|
"""Get a PCI device's device type.
|
||||||
|
|
||||||
|
An assignable PCI device can be a normal PCI device,
|
||||||
|
a SR-IOV Physical Function (PF), or a SR-IOV Virtual
|
||||||
|
Function (VF). Only normal PCI devices or SR-IOV VFs
|
||||||
|
are assignable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path = '/sys/bus/pci/devices/' + address + '/'
|
||||||
|
output, status = processutils.execute('ls', path)
|
||||||
|
if "physfn" in output:
|
||||||
|
phys_address = None
|
||||||
|
upath = '/sys/bus/pci/devices/%s/physfn/uevent' % address
|
||||||
|
try:
|
||||||
|
ou, st = processutils.execute('cat', upath)
|
||||||
|
lines = ou.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if 'PCI_SLOT_NAME' in line:
|
||||||
|
columns = line.split("=")
|
||||||
|
phys_address = columns[1]
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='cat')
|
||||||
|
return {'dev_type': fields.PciDeviceType.SRIOV_VF,
|
||||||
|
'parent_addr': phys_address}
|
||||||
|
if "virtfn" in output:
|
||||||
|
return {'dev_type': fields.PciDeviceType.SRIOV_PF}
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='ls')
|
||||||
|
return {'dev_type': fields.PciDeviceType.STANDARD}
|
||||||
|
|
||||||
|
def _get_device_capabilities(device, address):
|
||||||
|
"""Get PCI VF device's additional capabilities.
|
||||||
|
|
||||||
|
If a PCI device is a virtual function, this function reads the PCI
|
||||||
|
parent's network capabilities (must be always a NIC device) and
|
||||||
|
appends this information to the device's dictionary.
|
||||||
|
"""
|
||||||
|
if device.get('dev_type') == fields.PciDeviceType.SRIOV_VF:
|
||||||
|
pcinet_info = self._get_pcinet_info(address)
|
||||||
|
if pcinet_info:
|
||||||
|
return {'capabilities':
|
||||||
|
{'network': pcinet_info.get('capabilities')}}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _get_product_and_vendor(address):
|
||||||
|
try:
|
||||||
|
output, status = processutils.execute('lspci', '-n', '-s',
|
||||||
|
address)
|
||||||
|
value = output.split()[2]
|
||||||
|
result = value.split(":")
|
||||||
|
return result[0], result[1]
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='lspci')
|
||||||
|
|
||||||
|
def _get_numa_node(address):
|
||||||
|
numa_node = None
|
||||||
|
try:
|
||||||
|
output, status = processutils.execute('lspci', '-vmm', '-s',
|
||||||
|
address)
|
||||||
|
lines = output.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if 'NUMANode' in line:
|
||||||
|
numa_node = int(line.split(":")[1])
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='lspci')
|
||||||
|
return numa_node
|
||||||
|
|
||||||
|
dev_name = 'pci_' + address.replace(":", "_").replace(".", "_")
|
||||||
|
product_id, vendor_id = _get_product_and_vendor(address)
|
||||||
|
numa_node = _get_numa_node(address)
|
||||||
|
device = {
|
||||||
|
"dev_id": dev_name,
|
||||||
|
"address": address,
|
||||||
|
"product_id": product_id,
|
||||||
|
"vendor_id": vendor_id,
|
||||||
|
"numa_node": numa_node
|
||||||
|
}
|
||||||
|
device['label'] = 'label_%(vendor_id)s_%(product_id)s' % device
|
||||||
|
device.update(_get_device_type(address))
|
||||||
|
device.update(_get_device_capabilities(device, address))
|
||||||
|
return device
|
||||||
|
|
||||||
|
def _get_pcinet_info(self, vf_address):
|
||||||
|
"""Returns a dict of NET device."""
|
||||||
|
devname = pci_utils.get_net_name_by_vf_pci_address(vf_address)
|
||||||
|
if not devname:
|
||||||
|
return
|
||||||
|
|
||||||
|
ifname = pci_utils.get_ifname_by_pci_address(vf_address)
|
||||||
|
# Features from the that libvirt supported, get them by ethtool -k
|
||||||
|
# Note: I cannot find the rdma feature returned by ethtool, correct me
|
||||||
|
# if the string is wrong.
|
||||||
|
FEATURES_LIST = ['rx-checksumming', 'tx-checksumming',
|
||||||
|
'scatter-gather', 'tcp-segmentation-offload',
|
||||||
|
'generic-segmentation-offload',
|
||||||
|
'generic-receive-offload', 'large-receive-offload',
|
||||||
|
'rx-vlan-offload', 'tx-vlan-offload',
|
||||||
|
'ntuple-filters', 'receive-hashing',
|
||||||
|
'tx-udp_tnl-segmentation', 'rdma']
|
||||||
|
FEATURES_MAP = {'rx-checksumming': 'rx',
|
||||||
|
'tx-checksumming': 'tx',
|
||||||
|
'scatter-gather': 'sg',
|
||||||
|
'tcp-segmentation-offload': 'tso',
|
||||||
|
'generic-segmentation-offload': 'gso',
|
||||||
|
'generic-receive-offload': 'gro',
|
||||||
|
'large-receive-offload': 'lro',
|
||||||
|
'rx-vlan-offload': 'rxvlan',
|
||||||
|
'tx-vlan-offload': 'txvlan',
|
||||||
|
'ntuple-filters': 'ntuple',
|
||||||
|
'receive-hashing': 'rxhash',
|
||||||
|
'tx-udp_tnl-segmentation': 'txudptnl',
|
||||||
|
'rdma': 'rdma'}
|
||||||
|
|
||||||
|
features = []
|
||||||
|
try:
|
||||||
|
output, status = processutils.execute('ethtool', '-k', ifname)
|
||||||
|
lines = output.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
columns = line.split(":")
|
||||||
|
if columns[0].strip() in FEATURES_LIST:
|
||||||
|
if "on" in columns[1].strip():
|
||||||
|
features.append(FEATURES_MAP.get(columns[0].strip()))
|
||||||
|
except processutils.ProcessExecutionError:
|
||||||
|
raise exception.CommandError(cmd='ethtool -k')
|
||||||
|
return {'name': devname,
|
||||||
|
'capabilities': features}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""add timestamp to pci device
|
||||||
|
|
||||||
|
Revision ID: f046346d1d87
|
||||||
|
Revises: ff7b9665d504
|
||||||
|
Create Date: 2017-10-09 15:30:34.922130
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f046346d1d87'
|
||||||
|
down_revision = 'ff7b9665d504'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('pci_device',
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('pci_device',
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True))
|
||||||
|
op.drop_column('pci_device', 'request_id')
|
||||||
|
op.add_column('pci_device', sa.Column('request_id', sa.String(36),
|
||||||
|
nullable=True))
|
||||||
@@ -30,7 +30,8 @@ class ComputeNode(base.ZunPersistentObject, base.ZunObject):
|
|||||||
# Version 1.6: Add mem_used to compute node
|
# Version 1.6: Add mem_used to compute node
|
||||||
# Version 1.7: Change get_by_hostname to get_by_name
|
# Version 1.7: Change get_by_hostname to get_by_name
|
||||||
# Version 1.8: Add pci_device_pools to compute node
|
# Version 1.8: Add pci_device_pools to compute node
|
||||||
VERSION = '1.8'
|
# Version 1.9: Change PciDevicePoolList to ObjectField
|
||||||
|
VERSION = '1.9'
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'uuid': fields.UUIDField(read_only=True, nullable=False),
|
'uuid': fields.UUIDField(read_only=True, nullable=False),
|
||||||
@@ -53,7 +54,7 @@ class ComputeNode(base.ZunPersistentObject, base.ZunObject):
|
|||||||
'labels': fields.DictOfStringsField(nullable=True),
|
'labels': fields.DictOfStringsField(nullable=True),
|
||||||
# NOTE(pmurray): the pci_device_pools field maps to the
|
# NOTE(pmurray): the pci_device_pools field maps to the
|
||||||
# pci_stats field in the database
|
# pci_stats field in the database
|
||||||
'pci_device_pools': fields.ListOfObjectsField('PciDevicePool',
|
'pci_device_pools': fields.ObjectField('PciDevicePoolList',
|
||||||
nullable=True),
|
nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ class ComputeNode(base.ZunPersistentObject, base.ZunObject):
|
|||||||
numa_obj = updates.pop('numa_topology', None)
|
numa_obj = updates.pop('numa_topology', None)
|
||||||
if numa_obj is not None:
|
if numa_obj is not None:
|
||||||
updates['numa_topology'] = numa_obj._to_dict()
|
updates['numa_topology'] = numa_obj._to_dict()
|
||||||
|
self._convert_pci_stats_to_db_format(updates)
|
||||||
dbapi.update_compute_node(context, self.uuid, updates)
|
dbapi.update_compute_node(context, self.uuid, updates)
|
||||||
self.obj_reset_changes(recursive=True)
|
self.obj_reset_changes(recursive=True)
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,15 @@ class PciDevice(base.ZunPersistentObject, base.ZunObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
VERSION = '1.0'
|
# Version 1.1: Change compute_node_uuid to uuid type
|
||||||
|
VERSION = '1.1'
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'uuid': fields.UUIDField(),
|
'uuid': fields.UUIDField(),
|
||||||
# Note(yjiang5): the compute_node_uuid may be None because the pci
|
# Note(yjiang5): the compute_node_uuid may be None because the pci
|
||||||
# device objects are created before the compute node is created in DB
|
# device objects are created before the compute node is created in DB
|
||||||
'compute_node_uuid': fields.IntegerField(nullable=True),
|
'compute_node_uuid': fields.UUIDField(nullable=True),
|
||||||
'address': fields.StringField(),
|
'address': fields.StringField(),
|
||||||
'vendor_id': fields.StringField(),
|
'vendor_id': fields.StringField(),
|
||||||
'product_id': fields.StringField(),
|
'product_id': fields.StringField(),
|
||||||
@@ -192,7 +193,7 @@ class PciDevice(base.ZunPersistentObject, base.ZunObject):
|
|||||||
return pci_device
|
return pci_device
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def save(self, context):
|
def save(self):
|
||||||
if self.status == z_fields.PciDeviceStatus.REMOVED:
|
if self.status == z_fields.PciDeviceStatus.REMOVED:
|
||||||
self.status = z_fields.PciDeviceStatus.DELETED
|
self.status = z_fields.PciDeviceStatus.DELETED
|
||||||
dbapi.destroy_pci_device(self.compute_node_uuid,
|
dbapi.destroy_pci_device(self.compute_node_uuid,
|
||||||
@@ -203,9 +204,9 @@ class PciDevice(base.ZunPersistentObject, base.ZunObject):
|
|||||||
updates['extra_info'] = jsonutils.dumps(updates['extra_info'])
|
updates['extra_info'] = jsonutils.dumps(updates['extra_info'])
|
||||||
|
|
||||||
if updates:
|
if updates:
|
||||||
db_pci = dbapi.update_pci_device(self.compute_node_uuid,
|
dbapi.update_pci_device(self.compute_node_uuid,
|
||||||
self.address, updates)
|
self.address, updates)
|
||||||
self._from_db_object(context, self, db_pci)
|
# self._from_db_object(context, self, db_pci)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _bulk_update_status(dev_list, status):
|
def _bulk_update_status(dev_list, status):
|
||||||
@@ -373,22 +374,21 @@ class PciDevice(base.ZunPersistentObject, base.ZunObject):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _from_db_object_list(db_objects, cls, context):
|
def _from_db_object_list(db_objects, cls, context):
|
||||||
"""Converts a list of database entities to a list of formal objects."""
|
"""Converts a list of database entities to a list of formal objects."""
|
||||||
return [PciDevice._from_db_object(cls(context), obj)
|
return [PciDevice._from_db_object(context, cls(context), obj)
|
||||||
for obj in db_objects]
|
for obj in db_objects]
|
||||||
|
|
||||||
@base.remotable_classmethod
|
@base.remotable_classmethod
|
||||||
def list_by_compute_node(cls, context, node_id):
|
def list_by_compute_node(cls, context, node_id):
|
||||||
db_dev_list = dbapi.get_all_pci_device_by_node(context, node_id)
|
db_dev_list = dbapi.get_all_pci_device_by_node(node_id)
|
||||||
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
||||||
|
|
||||||
@base.remotable_classmethod
|
@base.remotable_classmethod
|
||||||
def list_by_container_uuid(cls, context, uuid):
|
def list_by_container_uuid(cls, context, uuid):
|
||||||
db_dev_list = dbapi.get_all_pci_device_by_container_uuid(context, uuid)
|
db_dev_list = dbapi.get_all_pci_device_by_container_uuid(uuid)
|
||||||
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
||||||
|
|
||||||
@base.remotable_classmethod
|
@base.remotable_classmethod
|
||||||
def list_by_parent_address(cls, context, node_id, parent_addr):
|
def list_by_parent_address(cls, context, node_id, parent_addr):
|
||||||
db_dev_list = dbapi.get_all_pci_device_by_parent_addr(context,
|
db_dev_list = dbapi.get_all_pci_device_by_parent_addr(node_id,
|
||||||
node_id,
|
|
||||||
parent_addr)
|
parent_addr)
|
||||||
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
return PciDevice._from_db_object_list(db_dev_list, cls, context)
|
||||||
|
|||||||
338
zun/pci/manager.py
Normal file
338
zun/pci/manager.py
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# Copyright (c) 2017 Intel, Inc.
|
||||||
|
# Copyright (c) 2017 OpenStack Foundation
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
from zun.common import exception
|
||||||
|
from zun import objects
|
||||||
|
from zun.objects import fields
|
||||||
|
from zun.pci import stats
|
||||||
|
from zun.pci import whitelist
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PciDevTracker(object):
|
||||||
|
"""Manage pci devices in a compute node.
|
||||||
|
|
||||||
|
This class fetches pci passthrough information from compute node
|
||||||
|
and tracks the usage of these devices.
|
||||||
|
|
||||||
|
It's called by compute node resource tracker to allocate and free
|
||||||
|
devices to/from containers, and to update the available pci passthrough
|
||||||
|
devices information from compute node periodically.
|
||||||
|
|
||||||
|
`pci_devs` attribute of this class is the in-memory "master copy" of all
|
||||||
|
devices on each compute node, and all data changes that happen when
|
||||||
|
claiming/allocating/freeing
|
||||||
|
devices HAVE TO be made against container contained in `pci_devs` list,
|
||||||
|
because they are periodically flushed to the DB when the save()
|
||||||
|
method is called.
|
||||||
|
|
||||||
|
It is unsafe to fetch PciDevice objects elsewhere in the code for update
|
||||||
|
purposes as those changes will end up being overwritten when the `pci_devs`
|
||||||
|
are saved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, context, node_id=None):
|
||||||
|
"""Create a pci device tracker.
|
||||||
|
|
||||||
|
If a node_id is passed in, it will fetch pci devices information
|
||||||
|
from database, otherwise, it will create an empty devices list
|
||||||
|
and the resource tracker will update the node_id information later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(PciDevTracker, self).__init__()
|
||||||
|
self.stale = {}
|
||||||
|
self.node_id = node_id
|
||||||
|
self.dev_filter = whitelist.Whitelist(CONF.pci.passthrough_whitelist)
|
||||||
|
self.stats = stats.PciDeviceStats(dev_filter=self.dev_filter)
|
||||||
|
self._context = context
|
||||||
|
if node_id:
|
||||||
|
self.pci_devs = objects.PciDevice.list_by_compute_node(
|
||||||
|
context, node_id)
|
||||||
|
else:
|
||||||
|
self.pci_devs = []
|
||||||
|
self._build_device_tree(self.pci_devs)
|
||||||
|
self._initial_instance_usage()
|
||||||
|
|
||||||
|
def _initial_instance_usage(self):
|
||||||
|
self.allocations = collections.defaultdict(list)
|
||||||
|
self.claims = collections.defaultdict(list)
|
||||||
|
for dev in self.pci_devs:
|
||||||
|
uuid = dev.container_uuid
|
||||||
|
if dev.status == fields.PciDeviceStatus.CLAIMED:
|
||||||
|
self.claims[uuid].append(dev)
|
||||||
|
elif dev.status == fields.PciDeviceStatus.ALLOCATED:
|
||||||
|
self.allocations[uuid].append(dev)
|
||||||
|
elif dev.status == fields.PciDeviceStatus.AVAILABLE:
|
||||||
|
self.stats.add_device(dev)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
for dev in self.pci_devs:
|
||||||
|
if dev.obj_what_changed():
|
||||||
|
dev.save()
|
||||||
|
if dev.status == fields.PciDeviceStatus.DELETED:
|
||||||
|
self.pci_devs.remove(dev)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pci_stats(self):
|
||||||
|
return self.stats
|
||||||
|
|
||||||
|
def update_devices_from_compute_resources(self, devices_json):
|
||||||
|
"""Sync the pci device tracker with compute node information.
|
||||||
|
|
||||||
|
To support pci device hot plug, we sync with the compute node
|
||||||
|
periodically, fetching all devices information from compute node,
|
||||||
|
update the tracker and sync the DB information.
|
||||||
|
|
||||||
|
Devices should not be hot-plugged when assigned to a container,
|
||||||
|
but possibly the compute node has no such guarantee. The best
|
||||||
|
we can do is to give a warning if a device is changed
|
||||||
|
or removed while assigned.
|
||||||
|
|
||||||
|
:param devices_json: The JSON-ified string of device information
|
||||||
|
that is returned from the compute node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
for dev in jsonutils.loads(devices_json):
|
||||||
|
if self.dev_filter.device_assignable(dev):
|
||||||
|
devices.append(dev)
|
||||||
|
self._set_hvdevs(devices)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_device_tree(all_devs):
|
||||||
|
"""Build a tree of devices that represents parent-child relationships.
|
||||||
|
|
||||||
|
We need to have the relationships set up so that we can easily make
|
||||||
|
all the necessary changes to parent/child devices without having to
|
||||||
|
figure it out at each call site.
|
||||||
|
|
||||||
|
This method just adds references to relevant containers already found
|
||||||
|
in `pci_devs` to `child_devices` and `parent_device` fields of each
|
||||||
|
one.
|
||||||
|
|
||||||
|
Currently relationships are considered for SR-IOV PFs/VFs only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensures that devices are ordered in ASC so VFs will come
|
||||||
|
# after their PFs.
|
||||||
|
all_devs.sort(key=lambda x: x.address)
|
||||||
|
|
||||||
|
parents = {}
|
||||||
|
for dev in all_devs:
|
||||||
|
if dev.status in (fields.PciDeviceStatus.REMOVED,
|
||||||
|
fields.PciDeviceStatus.DELETED):
|
||||||
|
# NOTE(ndipanov): Removed devs are pruned from
|
||||||
|
# self.pci_devs on save() so we need to make sure we
|
||||||
|
# are not looking at removed ones as we may build up
|
||||||
|
# the tree sooner than they are pruned.
|
||||||
|
continue
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_PF:
|
||||||
|
dev.child_devices = []
|
||||||
|
parents[dev.address] = dev
|
||||||
|
elif dev.dev_type == fields.PciDeviceType.SRIOV_VF:
|
||||||
|
dev.parent_device = parents.get(dev.parent_addr)
|
||||||
|
if dev.parent_device:
|
||||||
|
parents[dev.parent_addr].child_devices.append(dev)
|
||||||
|
|
||||||
|
def _set_hvdevs(self, devices):
|
||||||
|
exist_addrs = set([dev.address for dev in self.pci_devs])
|
||||||
|
new_addrs = set([dev['address'] for dev in devices])
|
||||||
|
|
||||||
|
for existed in self.pci_devs:
|
||||||
|
if existed.address in exist_addrs - new_addrs:
|
||||||
|
try:
|
||||||
|
existed.remove()
|
||||||
|
except exception.PciDeviceInvalidStatus as e:
|
||||||
|
LOG.warning(("Trying to remove device with %(status)s "
|
||||||
|
"ownership %(instance_uuid)s because of "
|
||||||
|
"%(pci_exception)s"),
|
||||||
|
{'status': existed.status,
|
||||||
|
'container_uuid': existed.container_uuid,
|
||||||
|
'pci_exception': e.format_message()})
|
||||||
|
# Note(yjiang5): remove the device by force so that
|
||||||
|
# db entry is cleaned in next sync.
|
||||||
|
existed.status = fields.PciDeviceStatus.REMOVED
|
||||||
|
else:
|
||||||
|
# Note(yjiang5): no need to update stats if an assigned
|
||||||
|
# device is hot removed.
|
||||||
|
self.stats.remove_device(existed)
|
||||||
|
else:
|
||||||
|
new_value = next((dev for dev in devices if
|
||||||
|
dev['address'] == existed.address))
|
||||||
|
new_value['compute_node_id'] = self.node_id
|
||||||
|
if existed.status in (fields.PciDeviceStatus.CLAIMED,
|
||||||
|
fields.PciDeviceStatus.ALLOCATED):
|
||||||
|
# Pci properties may change while assigned because of
|
||||||
|
# hotplug or config changes. Although normally this should
|
||||||
|
# not happen.
|
||||||
|
|
||||||
|
# As the devices have been assigned to a container,
|
||||||
|
# we defer the change till the container is destroyed.
|
||||||
|
# We will not sync the new properties with database
|
||||||
|
# before that.
|
||||||
|
|
||||||
|
# TODO(yjiang5): Not sure if this is a right policy, but
|
||||||
|
# at least it avoids some confusion and, if needed,
|
||||||
|
# we can add more action like killing the container
|
||||||
|
# by force in future.
|
||||||
|
self.stale[new_value['address']] = new_value
|
||||||
|
else:
|
||||||
|
existed.update_device(new_value)
|
||||||
|
|
||||||
|
for dev in [dev for dev in devices if
|
||||||
|
dev['address'] in new_addrs - exist_addrs]:
|
||||||
|
dev['compute_node_uuid'] = self.node_id
|
||||||
|
dev_obj = objects.PciDevice.create(self._context, dev)
|
||||||
|
self.pci_devs.append(dev_obj)
|
||||||
|
self.stats.add_device(dev_obj)
|
||||||
|
|
||||||
|
self._build_device_tree(self.pci_devs)
|
||||||
|
|
||||||
|
def _claim_container(self, context, pci_requests):
|
||||||
|
devs = self.stats.consume_requests(pci_requests.requests)
|
||||||
|
if not devs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
container_uuid = pci_requests.container_uuid
|
||||||
|
for dev in devs:
|
||||||
|
dev.claim(container_uuid)
|
||||||
|
return devs
|
||||||
|
|
||||||
|
def _allocate_container(self, container, devs):
|
||||||
|
for dev in devs:
|
||||||
|
dev.allocate(container)
|
||||||
|
|
||||||
|
def allocate_container(self, container):
|
||||||
|
devs = self.claims.pop(container['uuid'], [])
|
||||||
|
self._allocate_container(container, devs)
|
||||||
|
if devs:
|
||||||
|
self.allocations[container['uuid']] += devs
|
||||||
|
|
||||||
|
def claim_container(self, context, pci_requests, container_numa_topology):
|
||||||
|
devs = []
|
||||||
|
if self.pci_devs and pci_requests.requests:
|
||||||
|
container_uuid = pci_requests.container_uuid
|
||||||
|
devs = self._claim_container(context, pci_requests,
|
||||||
|
container_numa_topology)
|
||||||
|
if devs:
|
||||||
|
self.claims[container_uuid] = devs
|
||||||
|
return devs
|
||||||
|
|
||||||
|
def free_device(self, dev, container):
|
||||||
|
"""Free device from pci resource tracker
|
||||||
|
|
||||||
|
:param dev: cloned pci device object that needs to be free
|
||||||
|
:param container: the container that this pci device
|
||||||
|
is allocated to
|
||||||
|
"""
|
||||||
|
for pci_dev in self.pci_devs:
|
||||||
|
# Find the matching pci device in the pci resource tracker.
|
||||||
|
# Once found, free it.
|
||||||
|
if (dev.id == pci_dev.id and
|
||||||
|
dev.container_uuid == container['uuid']):
|
||||||
|
self._remove_device_from_pci_mapping(
|
||||||
|
container['uuid'], pci_dev, self.allocations)
|
||||||
|
self._remove_device_from_pci_mapping(
|
||||||
|
container['uuid'], pci_dev, self.claims)
|
||||||
|
self._free_device(pci_dev)
|
||||||
|
break
|
||||||
|
|
||||||
|
def _remove_device_from_pci_mapping(self, container_uuid,
|
||||||
|
pci_device, pci_mapping):
|
||||||
|
"""Remove a PCI device from allocations or claims.
|
||||||
|
|
||||||
|
If there are no more PCI devices, pop the uuid.
|
||||||
|
"""
|
||||||
|
pci_devices = pci_mapping.get(container_uuid, [])
|
||||||
|
if pci_device in pci_devices:
|
||||||
|
pci_devices.remove(pci_device)
|
||||||
|
if len(pci_devices) == 0:
|
||||||
|
pci_mapping.pop(container_uuid, None)
|
||||||
|
|
||||||
|
def _free_device(self, dev, container=None):
|
||||||
|
freed_devs = dev.free(container)
|
||||||
|
stale = self.stale.pop(dev.address, None)
|
||||||
|
if stale:
|
||||||
|
dev.update_device(stale)
|
||||||
|
for dev in freed_devs:
|
||||||
|
self.stats.add_device(dev)
|
||||||
|
|
||||||
|
def _free_container(self, container):
|
||||||
|
for dev in self.pci_devs:
|
||||||
|
if dev.status in (fields.PciDeviceStatus.CLAIMED,
|
||||||
|
fields.PciDeviceStatus.ALLOCATED):
|
||||||
|
if dev.container_uuid == container['uuid']:
|
||||||
|
self._free_device(dev)
|
||||||
|
|
||||||
|
def free_container(self, context, container):
|
||||||
|
if self.allocations.pop(container['uuid'], None):
|
||||||
|
self._free_container(container)
|
||||||
|
elif self.claims.pop(container['uuid'], None):
|
||||||
|
self._free_container(container)
|
||||||
|
|
||||||
|
def update_pci_for_container(self, context, container, sign):
|
||||||
|
"""Update PCI usage information if devices are de/allocated."""
|
||||||
|
if not self.pci_devs:
|
||||||
|
return
|
||||||
|
|
||||||
|
if sign == -1:
|
||||||
|
self.free_container(context, container)
|
||||||
|
if sign == 1:
|
||||||
|
self.allocate_container(container)
|
||||||
|
|
||||||
|
def clean_usage(self, containers, orphans):
|
||||||
|
"""Remove all usages for containers not passed in the parameter.
|
||||||
|
|
||||||
|
The caller should hold the COMPUTE_RESOURCE_SEMAPHORE lock
|
||||||
|
"""
|
||||||
|
existed = set(cnt['uuid'] for cnt in containers)
|
||||||
|
existed |= set(cnt['uuid'] for cnt in orphans)
|
||||||
|
|
||||||
|
# need to copy keys, because the dict is modified in the loop body
|
||||||
|
for uuid in list(self.claims):
|
||||||
|
if uuid not in existed:
|
||||||
|
devs = self.claims.pop(uuid, [])
|
||||||
|
for dev in devs:
|
||||||
|
self._free_device(dev)
|
||||||
|
# need to copy keys, because the dict is modified in the loop body
|
||||||
|
for uuid in list(self.allocations):
|
||||||
|
if uuid not in existed:
|
||||||
|
devs = self.allocations.pop(uuid, [])
|
||||||
|
for dev in devs:
|
||||||
|
self._free_device(dev)
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_pci_devs(cnt, request_id=None):
|
||||||
|
"""Get the devices allocated to one or all requests for a container.
|
||||||
|
|
||||||
|
- For generic PCI request, the request id is None.
|
||||||
|
- For sr-iov networking, the request id is a valid uuid
|
||||||
|
- There are a couple of cases where all the PCI devices allocated to a
|
||||||
|
container need to be returned.
|
||||||
|
"""
|
||||||
|
pci_devices = cnt.pci_devices
|
||||||
|
if pci_devices is None:
|
||||||
|
return []
|
||||||
|
return [device for device in pci_devices
|
||||||
|
if device.request_id == request_id or request_id == 'all']
|
||||||
@@ -181,3 +181,16 @@ def get_vf_num_by_pci_address(pci_addr):
|
|||||||
if vf_num is None:
|
if vf_num is None:
|
||||||
raise exception.PciDeviceNotFoundById(id=pci_addr)
|
raise exception.PciDeviceNotFoundById(id=pci_addr)
|
||||||
return vf_num
|
return vf_num
|
||||||
|
|
||||||
|
|
||||||
|
def get_net_name_by_vf_pci_address(vfaddress):
|
||||||
|
"""Given the VF PCI address, returns the net device name and ifname."""
|
||||||
|
try:
|
||||||
|
mac = get_mac_by_pci_address(vfaddress).split(':')
|
||||||
|
ifname = get_ifname_by_pci_address(vfaddress)
|
||||||
|
return ("net_%(ifname)s_%(mac)s" %
|
||||||
|
{'ifname': ifname, 'mac': '_'.join(mac)}), ifname
|
||||||
|
except Exception:
|
||||||
|
LOG.warning("No net device was found for VF %(vfaddress)s",
|
||||||
|
{'vfaddress': vfaddress})
|
||||||
|
return
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
@@ -128,3 +129,15 @@ class TestCase(base.BaseTestCase):
|
|||||||
return os.path.join(root, project_file)
|
return os.path.join(root, project_file)
|
||||||
else:
|
else:
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
def stub_out(self, old, new):
|
||||||
|
"""Replace a function for the duration of the test.
|
||||||
|
|
||||||
|
Use the monkey patch fixture to replace a function for the
|
||||||
|
duration of a test. Useful when you want to provide fake
|
||||||
|
methods instead of mocks during testing.
|
||||||
|
|
||||||
|
This should be used instead of self.stubs.Set (which is based
|
||||||
|
on mox) going forward.
|
||||||
|
"""
|
||||||
|
self.useFixture(fixtures.MonkeyPatch(old, new))
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import six
|
|||||||
from mock import mock_open
|
from mock import mock_open
|
||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
from zun.common import exception
|
from zun.common import exception
|
||||||
from zun.container.os_capability.linux import os_capability_linux
|
from zun.container.os_capability.linux import os_capability_linux
|
||||||
from zun.tests import base
|
from zun.tests import base
|
||||||
@@ -73,3 +75,82 @@ class TestOSCapability(base.BaseTestCase):
|
|||||||
output = os_capability_linux.LinuxHost().get_host_mem()
|
output = os_capability_linux.LinuxHost().get_host_mem()
|
||||||
used = (3882464 - 3556372)
|
used = (3882464 - 3556372)
|
||||||
self.assertEqual((3882464, 3514608, 3556372, used), output)
|
self.assertEqual((3882464, 3514608, 3556372, used), output)
|
||||||
|
|
||||||
|
@mock.patch('zun.pci.utils.get_ifname_by_pci_address')
|
||||||
|
@mock.patch('zun.pci.utils.get_net_name_by_vf_pci_address')
|
||||||
|
@mock.patch('oslo_concurrency.processutils.execute')
|
||||||
|
def test_get_pci_resource(self, mock_output, mock_netname,
|
||||||
|
mock_ifname):
|
||||||
|
mock_netname.return_value = 'net_enp2s0f3_ec_38_8f_79_11_2b'
|
||||||
|
mock_ifname.return_value = 'enp2s0f3'
|
||||||
|
value1 = '''0000:02:10.7 "Ethernet controller...." ""'''
|
||||||
|
value2 = '02:10.7 0200: 8086:1520 (rev 01)'
|
||||||
|
value3 = '''Slot: 02:10.7
|
||||||
|
Class: Ethernet controller
|
||||||
|
Vendor: Intel Corporation
|
||||||
|
Device: I350 Ethernet Controller Virtual Function
|
||||||
|
Rev: 01
|
||||||
|
NUMANode: 0'''
|
||||||
|
value4 = 'class physfn'
|
||||||
|
value5 = '''DRIVER=igbvf
|
||||||
|
PCI_CLASS=20000
|
||||||
|
PCI_ID=8086:1520
|
||||||
|
PCI_SUBSYS_ID=FFFF:0000
|
||||||
|
PCI_SLOT_NAME=0000:02:10.7
|
||||||
|
MODALIAS=pci:v00008086d00001520sv0000FFFFsd00000000bc02sc00i00'''
|
||||||
|
value6 = '''Features for enp2s0f3:
|
||||||
|
rx-checksumming: on
|
||||||
|
tx-checksumming: on
|
||||||
|
scatter-gather: on
|
||||||
|
tcp-segmentation-offload: on
|
||||||
|
generic-receive-offload: on
|
||||||
|
large-receive-offload: off [fixed]
|
||||||
|
rx-vlan-offload: on
|
||||||
|
tx-vlan-offload: on
|
||||||
|
ntuple-filters: off [fixed]
|
||||||
|
receive-hashing: on
|
||||||
|
highdma: on [fixed]
|
||||||
|
rx-vlan-filter: on [fixed]
|
||||||
|
vlan-challenged: off [fixed]
|
||||||
|
tx-lockless: off [fixed]
|
||||||
|
netns-local: off [fixed]
|
||||||
|
tx-gso-robust: off [fixed]
|
||||||
|
tx-fcoe-segmentation: off [fixed]
|
||||||
|
tx-gre-segmentation: off [fixed]
|
||||||
|
tx-ipip-segmentation: off [fixed]
|
||||||
|
tx-sit-segmentation: off [fixed]
|
||||||
|
tx-udp_tnl-segmentation: off [fixed]
|
||||||
|
tx-mpls-segmentation: off [fixed]
|
||||||
|
rx-fcs: off [fixed]
|
||||||
|
tx-vlan-stag-hw-insert: off [fixed]
|
||||||
|
rx-vlan-stag-hw-parse: off [fixed]
|
||||||
|
rx-vlan-stag-filter: off [fixed]'''
|
||||||
|
values = [(value1, 0),
|
||||||
|
(value2, 0),
|
||||||
|
(value3, 0),
|
||||||
|
(value4, 0),
|
||||||
|
(value5, 0),
|
||||||
|
(value6, 0)]
|
||||||
|
mock_output.side_effect = values
|
||||||
|
expected = {"dev_id": "pci_0000_02_10_7",
|
||||||
|
"address": "0000:02:10.7",
|
||||||
|
"product_id": "8086",
|
||||||
|
"vendor_id": "1520",
|
||||||
|
"numa_node": 0,
|
||||||
|
"label": "label_1520_8086",
|
||||||
|
"dev_type": "VF",
|
||||||
|
"parent_addr": "0000:02:10.7"}
|
||||||
|
output = os_capability_linux.LinuxHost().get_pci_resources()
|
||||||
|
|
||||||
|
pci_infos = jsonutils.loads(output)
|
||||||
|
for pci_info in pci_infos:
|
||||||
|
self.assertEqual(expected['dev_id'], str(pci_info['dev_id']))
|
||||||
|
self.assertEqual(expected['address'], str(pci_info['address']))
|
||||||
|
self.assertEqual(expected['product_id'],
|
||||||
|
str(pci_info['product_id']))
|
||||||
|
self.assertEqual(expected['vendor_id'], str(pci_info['vendor_id']))
|
||||||
|
self.assertEqual(expected['numa_node'], pci_info['numa_node'])
|
||||||
|
self.assertEqual(expected['label'], str(pci_info['label']))
|
||||||
|
self.assertEqual(expected['dev_type'], str(pci_info['dev_type']))
|
||||||
|
self.assertEqual(expected['parent_addr'],
|
||||||
|
str(pci_info['parent_addr']))
|
||||||
|
|||||||
@@ -354,8 +354,8 @@ object_data = {
|
|||||||
'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24',
|
'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24',
|
||||||
'ZunService': '1.1-b1549134bfd5271daec417ca8cabc77e',
|
'ZunService': '1.1-b1549134bfd5271daec417ca8cabc77e',
|
||||||
'Capsule': '1.3-f4c6b8fede0fa9488fc4f77b97601654',
|
'Capsule': '1.3-f4c6b8fede0fa9488fc4f77b97601654',
|
||||||
'PciDevice': '1.0-19fdd11935cda5e92947913f081d9edd',
|
'PciDevice': '1.1-6e3f0851ad1cf12583e6af4df1883979',
|
||||||
'ComputeNode': '1.8-5f5f893df0b514c88a3abaec1dfbb89e',
|
'ComputeNode': '1.9-e8536102d3b28cb3378e9e26f508cd72',
|
||||||
'PciDevicePool': '1.0-3f5ddc3ff7bfa14da7f6c7e9904cc000',
|
'PciDevicePool': '1.0-3f5ddc3ff7bfa14da7f6c7e9904cc000',
|
||||||
'PciDevicePoolList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e'
|
'PciDevicePoolList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e'
|
||||||
}
|
}
|
||||||
|
|||||||
299
zun/tests/unit/pci/test_manager.py
Normal file
299
zun/tests/unit/pci/test_manager.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Copyright (c) 2017 OpenStack Foundation
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
import zun
|
||||||
|
from zun.common import context
|
||||||
|
from zun.objects import fields
|
||||||
|
from zun.pci import manager
|
||||||
|
from zun.tests.unit.db import base
|
||||||
|
from zun.tests.unit.pci import fakes as pci_fakes
|
||||||
|
from zun.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
fake_pci = {
|
||||||
|
'compute_node_uuid': 1,
|
||||||
|
'address': '0000:00:00.1',
|
||||||
|
'product_id': 'p',
|
||||||
|
'vendor_id': 'v',
|
||||||
|
'request_id': None,
|
||||||
|
'status': fields.PciDeviceStatus.AVAILABLE,
|
||||||
|
'dev_type': fields.PciDeviceType.STANDARD,
|
||||||
|
'parent_addr': None,
|
||||||
|
'numa_node': 0}
|
||||||
|
fake_pci_1 = dict(fake_pci, address='0000:00:00.2',
|
||||||
|
product_id='p1', vendor_id='v1')
|
||||||
|
fake_pci_2 = dict(fake_pci, address='0000:00:00.3')
|
||||||
|
|
||||||
|
fake_pci_3 = dict(fake_pci, address='0000:00:01.1',
|
||||||
|
dev_type=fields.PciDeviceType.SRIOV_PF,
|
||||||
|
vendor_id='v2', product_id='p2', numa_node=None)
|
||||||
|
fake_pci_4 = dict(fake_pci, address='0000:00:02.1',
|
||||||
|
dev_type=fields.PciDeviceType.SRIOV_VF,
|
||||||
|
parent_addr='0000:00:01.1',
|
||||||
|
vendor_id='v2', product_id='p2', numa_node=None)
|
||||||
|
fake_pci_5 = dict(fake_pci, address='0000:00:02.2',
|
||||||
|
dev_type=fields.PciDeviceType.SRIOV_VF,
|
||||||
|
parent_addr='0000:00:01.1',
|
||||||
|
vendor_id='v2', product_id='p2', numa_node=None)
|
||||||
|
|
||||||
|
fake_db_dev = {
|
||||||
|
'created_at': None,
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'deleted': None,
|
||||||
|
'id': 1,
|
||||||
|
'uuid': uuidsentinel.pci_device1,
|
||||||
|
'compute_node_uuid': 1,
|
||||||
|
'address': '0000:00:00.1',
|
||||||
|
'vendor_id': 'v',
|
||||||
|
'product_id': 'p',
|
||||||
|
'numa_node': 1,
|
||||||
|
'dev_type': fields.PciDeviceType.STANDARD,
|
||||||
|
'status': fields.PciDeviceStatus.AVAILABLE,
|
||||||
|
'dev_id': 'i',
|
||||||
|
'label': 'l',
|
||||||
|
'container_uuid': None,
|
||||||
|
'extra_info': '{}',
|
||||||
|
'request_id': None,
|
||||||
|
'parent_addr': None,
|
||||||
|
}
|
||||||
|
fake_db_dev_1 = dict(fake_db_dev, vendor_id='v1',
|
||||||
|
uuid=uuidsentinel.pci_device1,
|
||||||
|
product_id='p1', id=2,
|
||||||
|
address='0000:00:00.2',
|
||||||
|
numa_node=0)
|
||||||
|
fake_db_dev_2 = dict(fake_db_dev, id=3, address='0000:00:00.3',
|
||||||
|
uuid=uuidsentinel.pci_device2,
|
||||||
|
numa_node=None, parent_addr='0000:00:00.1')
|
||||||
|
fake_db_devs = [fake_db_dev, fake_db_dev_1, fake_db_dev_2]
|
||||||
|
|
||||||
|
fake_db_dev_3 = dict(fake_db_dev, id=4, address='0000:00:01.1',
|
||||||
|
uuid=uuidsentinel.pci_device3,
|
||||||
|
vendor_id='v2', product_id='p2',
|
||||||
|
numa_node=None, dev_type=fields.PciDeviceType.SRIOV_PF)
|
||||||
|
fake_db_dev_4 = dict(fake_db_dev, id=5, address='0000:00:02.1',
|
||||||
|
uuid=uuidsentinel.pci_device4,
|
||||||
|
numa_node=None, dev_type=fields.PciDeviceType.SRIOV_VF,
|
||||||
|
vendor_id='v2', product_id='p2',
|
||||||
|
parent_addr='0000:00:01.1')
|
||||||
|
fake_db_dev_5 = dict(fake_db_dev, id=6, address='0000:00:02.2',
|
||||||
|
uuid=uuidsentinel.pci_device5,
|
||||||
|
numa_node=None, dev_type=fields.PciDeviceType.SRIOV_VF,
|
||||||
|
vendor_id='v2', product_id='p2',
|
||||||
|
parent_addr='0000:00:01.1')
|
||||||
|
fake_db_devs_tree = [fake_db_dev_3, fake_db_dev_4, fake_db_dev_5]
|
||||||
|
|
||||||
|
|
||||||
|
class PciDevTrackerTestCase(base.DbTestCase):
|
||||||
|
def _fake_get_pci_devices(self, node_id):
|
||||||
|
return self.fake_devs
|
||||||
|
|
||||||
|
def _fake_pci_device_update(self, node_id, address, value):
|
||||||
|
self.update_called += 1
|
||||||
|
self.called_values = value
|
||||||
|
fake_return = copy.deepcopy(fake_db_dev)
|
||||||
|
return fake_return
|
||||||
|
|
||||||
|
def _fake_pci_device_destroy(self, node_id, address):
|
||||||
|
self.destroy_called += 1
|
||||||
|
|
||||||
|
def _create_tracker(self, fake_devs):
|
||||||
|
self.fake_devs = fake_devs
|
||||||
|
self.tracker = manager.PciDevTracker(self.fake_context, 1)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PciDevTrackerTestCase, self).setUp()
|
||||||
|
self.fake_context = context.get_admin_context()
|
||||||
|
self.fake_devs = fake_db_devs[:]
|
||||||
|
self.stub_out('zun.db.api.get_all_pci_device_by_node',
|
||||||
|
self._fake_get_pci_devices)
|
||||||
|
# The fake_pci_whitelist must be called before creating the fake
|
||||||
|
# devices
|
||||||
|
patcher = pci_fakes.fake_pci_whitelist()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self._create_tracker(fake_db_devs[:])
|
||||||
|
|
||||||
|
def test_pcidev_tracker_create(self):
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 3)
|
||||||
|
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||||
|
self.assertEqual(len(free_devs), 3)
|
||||||
|
self.assertEqual(list(self.tracker.stale), [])
|
||||||
|
self.assertEqual(len(self.tracker.stats.pools), 3)
|
||||||
|
self.assertEqual(self.tracker.node_id, 1)
|
||||||
|
for dev in self.tracker.pci_devs:
|
||||||
|
self.assertIsNone(dev.parent_device)
|
||||||
|
self.assertEqual(dev.child_devices, [])
|
||||||
|
|
||||||
|
def test_pcidev_tracker_create_device_tree(self):
|
||||||
|
self._create_tracker(fake_db_devs_tree)
|
||||||
|
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 3)
|
||||||
|
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||||
|
self.assertEqual(len(free_devs), 3)
|
||||||
|
self.assertEqual(list(self.tracker.stale), [])
|
||||||
|
self.assertEqual(len(self.tracker.stats.pools), 2)
|
||||||
|
self.assertEqual(self.tracker.node_id, 1)
|
||||||
|
pf = [dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_PF].pop()
|
||||||
|
vfs = [dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_VF]
|
||||||
|
self.assertEqual(2, len(vfs))
|
||||||
|
|
||||||
|
# Assert we build the device tree correctly
|
||||||
|
self.assertEqual(vfs, pf.child_devices)
|
||||||
|
for vf in vfs:
|
||||||
|
self.assertEqual(vf.parent_device, pf)
|
||||||
|
|
||||||
|
def test_pcidev_tracker_create_device_tree_pf_only(self):
|
||||||
|
self._create_tracker([fake_db_dev_3])
|
||||||
|
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 1)
|
||||||
|
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||||
|
self.assertEqual(len(free_devs), 1)
|
||||||
|
self.assertEqual(list(self.tracker.stale), [])
|
||||||
|
self.assertEqual(len(self.tracker.stats.pools), 1)
|
||||||
|
self.assertEqual(self.tracker.node_id, 1)
|
||||||
|
pf = self.tracker.pci_devs[0]
|
||||||
|
self.assertIsNone(pf.parent_device)
|
||||||
|
self.assertEqual([], pf.child_devices)
|
||||||
|
|
||||||
|
def test_pcidev_tracker_create_device_tree_vf_only(self):
|
||||||
|
self._create_tracker([fake_db_dev_4])
|
||||||
|
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 1)
|
||||||
|
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||||
|
self.assertEqual(len(free_devs), 1)
|
||||||
|
self.assertEqual(list(self.tracker.stale), [])
|
||||||
|
self.assertEqual(len(self.tracker.stats.pools), 1)
|
||||||
|
self.assertEqual(self.tracker.node_id, 1)
|
||||||
|
vf = self.tracker.pci_devs[0]
|
||||||
|
self.assertIsNone(vf.parent_device)
|
||||||
|
self.assertEqual([], vf.child_devices)
|
||||||
|
|
||||||
|
@mock.patch.object(zun.objects.PciDevice, 'list_by_compute_node')
|
||||||
|
def test_pcidev_tracker_create_no_nodeid(self, mock_get_cn):
|
||||||
|
self.tracker = manager.PciDevTracker(self.fake_context)
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 0)
|
||||||
|
self.assertFalse(mock_get_cn.called)
|
||||||
|
|
||||||
|
@mock.patch.object(zun.objects.PciDevice, 'list_by_compute_node')
|
||||||
|
def test_pcidev_tracker_create_with_nodeid(self, mock_get_cn):
|
||||||
|
self.tracker = manager.PciDevTracker(self.fake_context, node_id=1)
|
||||||
|
mock_get_cn.assert_called_once_with(self.fake_context, 1)
|
||||||
|
|
||||||
|
def test_set_hvdev_new_dev(self):
|
||||||
|
fake_pci_3 = dict(fake_pci, address='0000:00:00.4', vendor_id='v2')
|
||||||
|
fake_pci_devs = [copy.deepcopy(fake_pci), copy.deepcopy(fake_pci_1),
|
||||||
|
copy.deepcopy(fake_pci_2), copy.deepcopy(fake_pci_3)]
|
||||||
|
self.tracker._set_hvdevs(fake_pci_devs)
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 4)
|
||||||
|
self.assertEqual(set([dev.address for
|
||||||
|
dev in self.tracker.pci_devs]),
|
||||||
|
set(['0000:00:00.1', '0000:00:00.2',
|
||||||
|
'0000:00:00.3', '0000:00:00.4']))
|
||||||
|
self.assertEqual(set([dev.vendor_id for
|
||||||
|
dev in self.tracker.pci_devs]),
|
||||||
|
set(['v', 'v1', 'v2']))
|
||||||
|
|
||||||
|
def test_set_hvdev_new_dev_tree_maintained(self):
|
||||||
|
# Make sure the device tree is properly maintained when there are new
|
||||||
|
# devices reported by the driver
|
||||||
|
self._create_tracker(fake_db_devs_tree)
|
||||||
|
|
||||||
|
fake_new_device = dict(fake_pci_5, id=12, address='0000:00:02.3')
|
||||||
|
fake_pci_devs = [copy.deepcopy(fake_pci_3),
|
||||||
|
copy.deepcopy(fake_pci_4),
|
||||||
|
copy.deepcopy(fake_pci_5),
|
||||||
|
copy.deepcopy(fake_new_device)]
|
||||||
|
self.tracker._set_hvdevs(fake_pci_devs)
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 4)
|
||||||
|
|
||||||
|
pf = [dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_PF].pop()
|
||||||
|
vfs = [dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_VF]
|
||||||
|
self.assertEqual(3, len(vfs))
|
||||||
|
|
||||||
|
# Assert we build the device tree correctly
|
||||||
|
self.assertEqual(vfs, pf.child_devices)
|
||||||
|
for vf in vfs:
|
||||||
|
self.assertEqual(vf.parent_device, pf)
|
||||||
|
|
||||||
|
def test_set_hvdev_changed(self):
|
||||||
|
fake_pci_v2 = dict(fake_pci, address='0000:00:00.2', vendor_id='v1')
|
||||||
|
fake_pci_devs = [copy.deepcopy(fake_pci), copy.deepcopy(fake_pci_2),
|
||||||
|
copy.deepcopy(fake_pci_v2)]
|
||||||
|
self.tracker._set_hvdevs(fake_pci_devs)
|
||||||
|
self.assertEqual(set([dev.vendor_id for
|
||||||
|
dev in self.tracker.pci_devs]),
|
||||||
|
set(['v', 'v1']))
|
||||||
|
|
||||||
|
def test_set_hvdev_remove(self):
|
||||||
|
self.tracker._set_hvdevs([fake_pci])
|
||||||
|
self.assertEqual(
|
||||||
|
len([dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.status == fields.PciDeviceStatus.REMOVED]),
|
||||||
|
2)
|
||||||
|
|
||||||
|
def test_set_hvdev_remove_tree_maintained(self):
|
||||||
|
# Make sure the device tree is properly maintained when there are
|
||||||
|
# devices removed from the system (not reported by the driver but known
|
||||||
|
# from previous scans)
|
||||||
|
self._create_tracker(fake_db_devs_tree)
|
||||||
|
|
||||||
|
fake_pci_devs = [copy.deepcopy(fake_pci_3), copy.deepcopy(fake_pci_4)]
|
||||||
|
self.tracker._set_hvdevs(fake_pci_devs)
|
||||||
|
self.assertEqual(
|
||||||
|
2,
|
||||||
|
len([dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.status != fields.PciDeviceStatus.REMOVED]))
|
||||||
|
pf = [dev for dev in self.tracker.pci_devs
|
||||||
|
if dev.dev_type == fields.PciDeviceType.SRIOV_PF].pop()
|
||||||
|
vfs = [dev for dev in self.tracker.pci_devs
|
||||||
|
if (dev.dev_type == fields.PciDeviceType.SRIOV_VF and
|
||||||
|
dev.status != fields.PciDeviceStatus.REMOVED)]
|
||||||
|
self.assertEqual(1, len(vfs))
|
||||||
|
|
||||||
|
self.assertEqual(vfs, pf.child_devices)
|
||||||
|
self.assertEqual(vfs[0].parent_device, pf)
|
||||||
|
|
||||||
|
def test_save(self):
|
||||||
|
self.stub_out('zun.db.api.update_pci_device',
|
||||||
|
self._fake_pci_device_update)
|
||||||
|
fake_pci_v3 = dict(fake_pci, address='0000:00:00.2', vendor_id='v3')
|
||||||
|
fake_pci_devs = [copy.deepcopy(fake_pci), copy.deepcopy(fake_pci_2),
|
||||||
|
copy.deepcopy(fake_pci_v3)]
|
||||||
|
self.tracker._set_hvdevs(fake_pci_devs)
|
||||||
|
self.update_called = 0
|
||||||
|
self.tracker.save()
|
||||||
|
self.assertEqual(self.update_called, 3)
|
||||||
|
|
||||||
|
def test_save_removed(self):
|
||||||
|
self.stub_out('zun.db.api.update_pci_device',
|
||||||
|
self._fake_pci_device_update)
|
||||||
|
self.stub_out('zun.db.api.destroy_pci_device',
|
||||||
|
self._fake_pci_device_destroy)
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 3)
|
||||||
|
dev = self.tracker.pci_devs[0]
|
||||||
|
self.destroy_called = 0
|
||||||
|
self.update_called = 0
|
||||||
|
dev.remove()
|
||||||
|
self.tracker.save()
|
||||||
|
self.assertEqual(len(self.tracker.pci_devs), 2)
|
||||||
|
self.assertEqual(self.destroy_called, 1)
|
||||||
@@ -263,3 +263,46 @@ class GetVfNumByPciAddressTestCase(base.TestCase):
|
|||||||
utils.get_vf_num_by_pci_address,
|
utils.get_vf_num_by_pci_address,
|
||||||
self.pci_address
|
self.pci_address
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetNetNameByVfPciAddressTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(GetNetNameByVfPciAddressTestCase, self).setUp()
|
||||||
|
self._get_mac = mock.patch.object(utils, 'get_mac_by_pci_address')
|
||||||
|
self.mock_get_mac = self._get_mac.start()
|
||||||
|
self._get_ifname = mock.patch.object(
|
||||||
|
utils, 'get_ifname_by_pci_address')
|
||||||
|
self.mock_get_ifname = self._get_ifname.start()
|
||||||
|
self.addCleanup(self._get_mac.stop)
|
||||||
|
self.addCleanup(self._get_ifname.stop)
|
||||||
|
|
||||||
|
self.mac = 'ca:fe:ca:fe:ca:fe'
|
||||||
|
self.if_name = 'enp7s0f0'
|
||||||
|
self.pci_address = '0000:07:02.1'
|
||||||
|
|
||||||
|
def test_correct_behaviour(self):
|
||||||
|
ref_net_name = ('net_enp7s0f0_ca_fe_ca_fe_ca_fe', 'enp7s0f0')
|
||||||
|
self.mock_get_mac.return_value = self.mac
|
||||||
|
self.mock_get_ifname.return_value = self.if_name
|
||||||
|
net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
|
||||||
|
self.assertEqual(ref_net_name, net_name)
|
||||||
|
self.mock_get_mac.called_once_with(self.pci_address)
|
||||||
|
self.mock_get_ifname.called_once_with(self.pci_address)
|
||||||
|
|
||||||
|
def test_wrong_mac(self):
|
||||||
|
self.mock_get_mac.side_effect = (
|
||||||
|
exception.PciDeviceNotFoundById(self.pci_address))
|
||||||
|
net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
|
||||||
|
self.assertIsNone(net_name)
|
||||||
|
self.mock_get_mac.called_once_with(self.pci_address)
|
||||||
|
self.mock_get_ifname.assert_not_called()
|
||||||
|
|
||||||
|
def test_wrong_ifname(self):
|
||||||
|
self.mock_get_mac.return_value = self.mac
|
||||||
|
self.mock_get_ifname.side_effect = (
|
||||||
|
exception.PciDeviceNotFoundById(self.pci_address))
|
||||||
|
net_name = utils.get_net_name_by_vf_pci_address(self.pci_address)
|
||||||
|
self.assertIsNone(net_name)
|
||||||
|
self.mock_get_mac.called_once_with(self.pci_address)
|
||||||
|
self.mock_get_ifname.called_once_with(self.pci_address)
|
||||||
|
|||||||
Reference in New Issue
Block a user