Merge "pci: Move PCI devices and PCI requests into migration context"
This commit is contained in:
commit
a224d5e8c0
@ -22,7 +22,6 @@ from oslo_log import log as logging
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova.i18n import _LI
|
||||
from nova.i18n import _LW
|
||||
from nova import objects
|
||||
from nova.virt import hardware
|
||||
|
||||
@ -201,9 +200,7 @@ class Claim(NopClaim):
|
||||
if host_topology:
|
||||
host_topology = objects.NUMATopology.obj_from_db_obj(
|
||||
host_topology)
|
||||
pci_requests = objects.InstancePCIRequests.get_by_instance_uuid(
|
||||
self.context, self.instance.uuid)
|
||||
|
||||
pci_requests = self._pci_requests
|
||||
pci_stats = None
|
||||
if pci_requests.requests:
|
||||
pci_stats = self.tracker.pci_tracker.stats
|
||||
@ -300,18 +297,3 @@ class MoveClaim(Claim):
|
||||
self.tracker.drop_move_claim(
|
||||
self.context,
|
||||
self.instance, instance_type=self.instance_type)
|
||||
|
||||
def create_migration_context(self):
|
||||
if not self.migration:
|
||||
LOG.warning(
|
||||
_LW("Can't create a migration_context record without a "
|
||||
"migration object specified."),
|
||||
instance=self.instance)
|
||||
return
|
||||
|
||||
mig_context = objects.MigrationContext(
|
||||
context=self.context, instance_uuid=self.instance.uuid,
|
||||
migration_id=self.migration.id,
|
||||
old_numa_topology=self.instance.numa_topology,
|
||||
new_numa_topology=self.claimed_numa_topology)
|
||||
return mig_context
|
||||
|
@ -35,6 +35,7 @@ from nova import objects
|
||||
from nova.objects import base as obj_base
|
||||
from nova.objects import migration as migration_obj
|
||||
from nova.pci import manager as pci_manager
|
||||
from nova.pci import request as pci_request
|
||||
from nova import rpc
|
||||
from nova.scheduler import client as scheduler_client
|
||||
from nova import utils
|
||||
@ -217,15 +218,49 @@ class ResourceTracker(object):
|
||||
"GB", {'flavor': instance.root_gb,
|
||||
'overhead': overhead.get('disk_gb', 0)})
|
||||
|
||||
pci_requests = objects.InstancePCIRequests.\
|
||||
get_by_instance_uuid_and_newness(
|
||||
context, instance.uuid, True)
|
||||
# TODO(moshele): we are recreating the pci requests even if
|
||||
# there was no change on resize. This will cause allocating
|
||||
# the old/new pci device in the resize phase. In the future
|
||||
# we would like to optimise this.
|
||||
new_pci_requests = pci_request.get_pci_requests_from_flavor(
|
||||
new_instance_type)
|
||||
new_pci_requests.instance_uuid = instance.uuid
|
||||
# PCI requests come from two sources: instance flavor and
|
||||
# SR-IOV ports. SR-IOV ports pci_request don't have an alias_name.
|
||||
# On resize merge the SR-IOV ports pci_requests with the new
|
||||
# instance flavor pci_requests.
|
||||
if instance.pci_requests:
|
||||
for request in instance.pci_requests.requests:
|
||||
if request.alias_name is None:
|
||||
new_pci_requests.requests.append(request)
|
||||
claim = claims.MoveClaim(context, instance, new_instance_type,
|
||||
image_meta, self, self.compute_node,
|
||||
pci_requests, overhead=overhead,
|
||||
new_pci_requests, overhead=overhead,
|
||||
limits=limits)
|
||||
|
||||
claim.migration = migration
|
||||
instance.migration_context = claim.create_migration_context()
|
||||
claimed_pci_devices_objs = []
|
||||
if self.pci_tracker:
|
||||
# NOTE(jaypipes): ComputeNode.pci_device_pools is set below
|
||||
# in _update_usage_from_instance().
|
||||
claimed_pci_devices_objs = self.pci_tracker.claim_instance(
|
||||
context, new_pci_requests, claim.claimed_numa_topology)
|
||||
claimed_pci_devices = objects.PciDeviceList(
|
||||
objects=claimed_pci_devices_objs)
|
||||
|
||||
# TODO(jaypipes): Move claimed_numa_topology out of the Claim's
|
||||
# constructor flow so the Claim constructor only tests whether
|
||||
# resources can be claimed, not consume the resources directly.
|
||||
mig_context = objects.MigrationContext(
|
||||
context=context, instance_uuid=instance.uuid,
|
||||
migration_id=migration.id,
|
||||
old_numa_topology=instance.numa_topology,
|
||||
new_numa_topology=claim.claimed_numa_topology,
|
||||
old_pci_devices=instance.pci_devices,
|
||||
new_pci_devices=claimed_pci_devices,
|
||||
old_pci_requests=instance.pci_requests,
|
||||
new_pci_requests=new_pci_requests)
|
||||
instance.migration_context = mig_context
|
||||
instance.save()
|
||||
|
||||
# Mark the resources in-use for the resize landing on this
|
||||
@ -323,9 +358,12 @@ class ResourceTracker(object):
|
||||
usage = self._get_usage_dict(
|
||||
itype, numa_topology=numa_topology)
|
||||
if self.pci_tracker:
|
||||
self.pci_tracker.update_pci_for_migration(context,
|
||||
instance,
|
||||
sign=-1)
|
||||
# free old allocated pci devices
|
||||
old_pci_devices = self._get_migration_context_resource(
|
||||
'pci_devices', instance, prefix='old_')
|
||||
if old_pci_devices:
|
||||
for pci_device in old_pci_devices:
|
||||
self.pci_tracker.free_device(pci_device, instance)
|
||||
self._update_usage(usage, sign=-1)
|
||||
|
||||
ctxt = context.elevated()
|
||||
@ -685,8 +723,9 @@ class ResourceTracker(object):
|
||||
def _get_migration_context_resource(self, resource, instance,
|
||||
prefix='new_', itype=None):
|
||||
migration_context = instance.migration_context
|
||||
if migration_context:
|
||||
return getattr(migration_context, prefix + resource)
|
||||
resource = prefix + resource
|
||||
if migration_context and resource in migration_context:
|
||||
return getattr(migration_context, resource)
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -710,7 +749,7 @@ class ResourceTracker(object):
|
||||
record = self.tracked_instances.get(uuid, None)
|
||||
itype = None
|
||||
numa_topology = None
|
||||
|
||||
sign = 0
|
||||
if same_node:
|
||||
# same node resize. record usage for whichever instance type the
|
||||
# instance is *not* in:
|
||||
@ -720,6 +759,7 @@ class ResourceTracker(object):
|
||||
migration)
|
||||
numa_topology = self._get_migration_context_resource(
|
||||
'numa_topology', instance)
|
||||
sign = 1
|
||||
else:
|
||||
# instance record already has new flavor, hold space for a
|
||||
# possible revert to the old instance type:
|
||||
@ -752,8 +792,9 @@ class ResourceTracker(object):
|
||||
if itype:
|
||||
usage = self._get_usage_dict(
|
||||
itype, numa_topology=numa_topology)
|
||||
if self.pci_tracker:
|
||||
self.pci_tracker.update_pci_for_migration(context, instance)
|
||||
if self.pci_tracker and sign:
|
||||
self.pci_tracker.update_pci_for_instance(
|
||||
context, instance, sign=sign)
|
||||
self._update_usage(usage)
|
||||
if self.pci_tracker:
|
||||
obj = self.pci_tracker.stats.to_device_pools_obj()
|
||||
|
@ -46,6 +46,7 @@ Possible Values:
|
||||
|
||||
Services which consume this:
|
||||
|
||||
* nova-api
|
||||
* nova-compute
|
||||
|
||||
Related options:
|
||||
|
@ -52,6 +52,9 @@ _INSTANCE_OPTIONAL_NON_COLUMN_FIELDS = ['fault', 'flavor', 'old_flavor',
|
||||
_INSTANCE_EXTRA_FIELDS = ['numa_topology', 'pci_requests',
|
||||
'flavor', 'vcpu_model', 'migration_context',
|
||||
'keypairs']
|
||||
# These are fields that applied/drooped by migration_context
|
||||
_MIGRATION_CONTEXT_ATTRS = ['numa_topology', 'pci_requests',
|
||||
'pci_devices']
|
||||
|
||||
# These are fields that can be specified as expected_attrs
|
||||
INSTANCE_OPTIONAL_ATTRS = (_INSTANCE_OPTIONAL_JOINED_FIELDS +
|
||||
@ -878,18 +881,27 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
|
||||
|
||||
def apply_migration_context(self):
|
||||
if self.migration_context:
|
||||
self.numa_topology = self.migration_context.new_numa_topology
|
||||
self._set_migration_context_to_instance(prefix='new_')
|
||||
else:
|
||||
LOG.debug("Trying to apply a migration context that does not "
|
||||
"seem to be set for this instance", instance=self)
|
||||
|
||||
def revert_migration_context(self):
|
||||
if self.migration_context:
|
||||
self.numa_topology = self.migration_context.old_numa_topology
|
||||
self._set_migration_context_to_instance(prefix='old_')
|
||||
else:
|
||||
LOG.debug("Trying to revert a migration context that does not "
|
||||
"seem to be set for this instance", instance=self)
|
||||
|
||||
def _set_migration_context_to_instance(self, prefix):
|
||||
for inst_attr_name in _MIGRATION_CONTEXT_ATTRS:
|
||||
setattr(self, inst_attr_name, None)
|
||||
attr_name = prefix + inst_attr_name
|
||||
if attr_name in self.migration_context:
|
||||
attr_value = getattr(
|
||||
self.migration_context, attr_name)
|
||||
setattr(self, inst_attr_name, attr_value)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mutated_migration_context(self):
|
||||
"""Context manager to temporarily apply the migration context.
|
||||
@ -898,12 +910,15 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
|
||||
context will be saved which can cause incorrect resource tracking, and
|
||||
should be avoided.
|
||||
"""
|
||||
current_numa_topo = self.numa_topology
|
||||
current_values = {}
|
||||
for attr_name in _MIGRATION_CONTEXT_ATTRS:
|
||||
current_values[attr_name] = getattr(self, attr_name)
|
||||
self.apply_migration_context()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.numa_topology = current_numa_topo
|
||||
for attr_name in _MIGRATION_CONTEXT_ATTRS:
|
||||
setattr(self, attr_name, current_values[attr_name])
|
||||
|
||||
@base.remotable
|
||||
def drop_migration_context(self):
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import versionutils
|
||||
|
||||
from nova import db
|
||||
from nova import exception
|
||||
@ -33,7 +34,8 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject):
|
||||
"""
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Add old/new pci_devices and pci_requests
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'instance_uuid': fields.UUIDField(),
|
||||
@ -42,8 +44,25 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject):
|
||||
nullable=True),
|
||||
'old_numa_topology': fields.ObjectField('InstanceNUMATopology',
|
||||
nullable=True),
|
||||
'new_pci_devices': fields.ObjectField('PciDeviceList',
|
||||
nullable=True),
|
||||
'old_pci_devices': fields.ObjectField('PciDeviceList',
|
||||
nullable=True),
|
||||
'new_pci_requests': fields.ObjectField('InstancePCIRequests',
|
||||
nullable=True),
|
||||
'old_pci_requests': fields.ObjectField('InstancePCIRequests',
|
||||
nullable=True),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def obj_make_compatible(cls, primitive, target_version):
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 1):
|
||||
primitive.pop('old_pci_devices', None)
|
||||
primitive.pop('new_pci_devices', None)
|
||||
primitive.pop('old_pci_requests', None)
|
||||
primitive.pop('new_pci_requests', None)
|
||||
|
||||
@classmethod
|
||||
def obj_from_db_obj(cls, db_obj):
|
||||
primitive = jsonutils.loads(db_obj)
|
||||
|
@ -26,7 +26,6 @@ from nova import objects
|
||||
from nova.objects import fields
|
||||
from nova.pci import stats
|
||||
from nova.pci import whitelist
|
||||
from nova.virt import hardware
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -249,16 +248,27 @@ class PciDevTracker(object):
|
||||
self.allocations[instance['uuid']] += devs
|
||||
|
||||
def claim_instance(self, context, pci_requests, instance_numa_topology):
|
||||
if not self.pci_devs or not pci_requests.requests:
|
||||
return
|
||||
devs = []
|
||||
if self.pci_devs and pci_requests.requests:
|
||||
instance_uuid = pci_requests.instance_uuid
|
||||
devs = self._claim_instance(context, pci_requests,
|
||||
instance_numa_topology)
|
||||
if devs:
|
||||
self.claims[instance_uuid] = devs
|
||||
return devs
|
||||
|
||||
instance_uuid = pci_requests.instance_uuid
|
||||
devs = self._claim_instance(context, pci_requests,
|
||||
instance_numa_topology)
|
||||
if devs:
|
||||
self.claims[instance_uuid] = devs
|
||||
return devs
|
||||
return None
|
||||
def free_device(self, dev, instance):
|
||||
"""Free device from pci resource tracker
|
||||
|
||||
:param dev: cloned pci device object that needs to be free
|
||||
:param instance: the instance that this pci device
|
||||
is allocated to
|
||||
"""
|
||||
for pci_dev in self.pci_devs:
|
||||
# find the matching pci device in the pci resource tracker
|
||||
# pci device. Once found one free it.
|
||||
if dev == pci_dev and dev.instance_uuid == instance['uuid']:
|
||||
self._free_device(pci_dev)
|
||||
|
||||
def _free_device(self, dev, instance=None):
|
||||
freed_devs = dev.free(instance)
|
||||
@ -297,27 +307,6 @@ class PciDevTracker(object):
|
||||
if sign == 1:
|
||||
self.allocate_instance(instance)
|
||||
|
||||
def update_pci_for_migration(self, context, instance, sign=1):
|
||||
"""Update instance's pci usage information when it is migrated.
|
||||
|
||||
The caller should hold the COMPUTE_RESOURCE_SEMAPHORE lock.
|
||||
|
||||
:param sign: claim devices for instance when sign is 1, remove
|
||||
the claims when sign is -1
|
||||
"""
|
||||
uuid = instance['uuid']
|
||||
pci_requests = objects.InstancePCIRequests.get_by_instance(
|
||||
context, instance)
|
||||
instance_numa_topology = hardware.instance_topology_from_instance(
|
||||
instance)
|
||||
if sign == 1 and uuid not in self.claims:
|
||||
devs = self._claim_instance(context, pci_requests,
|
||||
instance_numa_topology)
|
||||
if devs:
|
||||
self.claims[uuid] = devs
|
||||
if sign == -1 and uuid in self.claims:
|
||||
self._free_instance(instance)
|
||||
|
||||
def clean_usage(self, instances, migrations, orphans):
|
||||
"""Remove all usages for instances not passed in the parameter.
|
||||
|
||||
|
@ -62,6 +62,7 @@ class ClaimTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(ClaimTestCase, self).setUp()
|
||||
self.context = context.RequestContext('fake-user', 'fake-project')
|
||||
self.instance = None
|
||||
self.resources = self._fake_resources()
|
||||
self.tracker = DummyTracker()
|
||||
self.empty_requests = objects.InstancePCIRequests(
|
||||
@ -224,34 +225,29 @@ class ClaimTestCase(test.NoDBTestCase):
|
||||
|
||||
@mock.patch('nova.pci.stats.PciDeviceStats.support_requests',
|
||||
return_value=True)
|
||||
def test_pci_pass(self, mock_supports):
|
||||
def test_pci_pass(self, mock_pci_supports_requests):
|
||||
request = objects.InstancePCIRequest(count=1,
|
||||
spec=[{'vendor_id': 'v', 'product_id': 'p'}])
|
||||
requests = objects.InstancePCIRequests(requests=[request])
|
||||
|
||||
# Claim.__init__() would raise ComputeResourcesUnavailable
|
||||
# if Claim._test_pci() did not return None.
|
||||
self._claim(requests=requests)
|
||||
mock_supports.assert_called_once_with(requests.requests)
|
||||
mock_pci_supports_requests.assert_called_once_with([request])
|
||||
|
||||
@mock.patch('nova.pci.stats.PciDeviceStats.support_requests',
|
||||
return_value=False)
|
||||
def test_pci_fail(self, mock_supports):
|
||||
def test_pci_fail(self, mock_pci_supports_requests):
|
||||
request = objects.InstancePCIRequest(count=1,
|
||||
spec=[{'vendor_id': 'v', 'product_id': 'p'}])
|
||||
requests = objects.InstancePCIRequests(requests=[request])
|
||||
self.assertRaisesRegex(
|
||||
exception.ComputeResourcesUnavailable,
|
||||
'Claim pci failed.',
|
||||
self._claim, requests=requests)
|
||||
mock_pci_supports_requests.assert_called_once_with([request])
|
||||
|
||||
self.assertRaises(exception.ComputeResourcesUnavailable,
|
||||
self._claim, requests=requests)
|
||||
mock_supports.assert_called_once_with(requests.requests)
|
||||
|
||||
@mock.patch('nova.pci.stats.PciDeviceStats.support_requests',
|
||||
return_value=True)
|
||||
def test_pci_pass_no_requests(self, mock_supports):
|
||||
# Claim.__init__() would raise ComputeResourcesUnavailable
|
||||
# if Claim._test_pci() did not return None.
|
||||
@mock.patch('nova.pci.stats.PciDeviceStats.support_requests')
|
||||
def test_pci_pass_no_requests(self, mock_pci_supports_requests):
|
||||
self._claim()
|
||||
self.assertFalse(mock_supports.called)
|
||||
self.assertFalse(mock_pci_supports_requests.called)
|
||||
|
||||
def test_numa_topology_no_limit(self):
|
||||
huge_instance = objects.InstanceNUMATopology(
|
||||
@ -422,30 +418,6 @@ class MoveClaimTestCase(ClaimTestCase):
|
||||
claim = self._abort()
|
||||
self.assertTrue(claim.tracker.rcalled)
|
||||
|
||||
def test_create_migration_context(self):
|
||||
numa_topology = objects.InstanceNUMATopology(
|
||||
cells=[objects.InstanceNUMACell(
|
||||
id=1, cpuset=set([1, 2]), memory=512)])
|
||||
claim = self._claim(numa_topology=numa_topology)
|
||||
migration = objects.Migration(context=self.context, id=42)
|
||||
claim.migration = migration
|
||||
fake_mig_context = mock.Mock(spec=objects.MigrationContext)
|
||||
|
||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||
return_value=None)
|
||||
@mock.patch('nova.objects.MigrationContext',
|
||||
return_value=fake_mig_context)
|
||||
def _test(ctxt_mock, mock_get_extra):
|
||||
claim.create_migration_context()
|
||||
ctxt_mock.assert_called_once_with(
|
||||
context=self.context, instance_uuid=self.instance.uuid,
|
||||
migration_id=42, old_numa_topology=None,
|
||||
new_numa_topology=mock.ANY)
|
||||
self.assertIsInstance(ctxt_mock.call_args[1]['new_numa_topology'],
|
||||
objects.InstanceNUMATopology)
|
||||
self.assertEqual(migration, claim.migration)
|
||||
_test()
|
||||
|
||||
def test_image_meta(self):
|
||||
claim = self._claim()
|
||||
self.assertIsInstance(claim.image_meta, objects.ImageMeta)
|
||||
|
@ -2928,6 +2928,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
instance = fake_instance.fake_instance_obj(self.context)
|
||||
instance.migration_context = None
|
||||
instance.numa_topology = None
|
||||
instance.pci_requests = None
|
||||
instance.pci_devices = None
|
||||
instance.task_state = task_states.REBUILDING
|
||||
instance.save(expected_task_state=[task_states.REBUILDING])
|
||||
self.compute._rebuild_default_impl(self.context,
|
||||
|
@ -26,8 +26,10 @@ from nova.compute import vm_states
|
||||
from nova import exception as exc
|
||||
from nova import objects
|
||||
from nova.objects import base as obj_base
|
||||
from nova.objects import pci_device
|
||||
from nova.pci import manager as pci_manager
|
||||
from nova import test
|
||||
from nova.tests.unit.objects import test_pci_device as fake_pci_device
|
||||
|
||||
_HOSTNAME = 'fake-host'
|
||||
_NODENAME = 'fake-node'
|
||||
@ -168,6 +170,8 @@ _INSTANCE_FIXTURES = [
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[1]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[1]['ephemeral_gb'],
|
||||
numa_topology=_INSTANCE_NUMA_TOPOLOGIES['2mb'],
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=1,
|
||||
vm_state=vm_states.ACTIVE,
|
||||
power_state=power_state.RUNNING,
|
||||
@ -188,6 +192,8 @@ _INSTANCE_FIXTURES = [
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[2]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[2]['ephemeral_gb'],
|
||||
numa_topology=None,
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=2,
|
||||
vm_state=vm_states.DELETED,
|
||||
power_state=power_state.SHUTDOWN,
|
||||
@ -267,6 +273,8 @@ _MIGRATION_INSTANCE_FIXTURES = {
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[1]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[1]['ephemeral_gb'],
|
||||
numa_topology=_INSTANCE_NUMA_TOPOLOGIES['2mb'],
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=1,
|
||||
vm_state=vm_states.ACTIVE,
|
||||
power_state=power_state.RUNNING,
|
||||
@ -289,6 +297,8 @@ _MIGRATION_INSTANCE_FIXTURES = {
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[2]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[2]['ephemeral_gb'],
|
||||
numa_topology=None,
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=2,
|
||||
vm_state=vm_states.ACTIVE,
|
||||
power_state=power_state.RUNNING,
|
||||
@ -311,6 +321,8 @@ _MIGRATION_INSTANCE_FIXTURES = {
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[2]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[2]['ephemeral_gb'],
|
||||
numa_topology=None,
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=2,
|
||||
vm_state=vm_states.ACTIVE,
|
||||
power_state=power_state.RUNNING,
|
||||
@ -333,6 +345,8 @@ _MIGRATION_INSTANCE_FIXTURES = {
|
||||
root_gb=_INSTANCE_TYPE_FIXTURES[2]['root_gb'],
|
||||
ephemeral_gb=_INSTANCE_TYPE_FIXTURES[2]['ephemeral_gb'],
|
||||
numa_topology=None,
|
||||
pci_requests=None,
|
||||
pci_devices=None,
|
||||
instance_type_id=2,
|
||||
vm_state=vm_states.ACTIVE,
|
||||
power_state=power_state.RUNNING,
|
||||
@ -1384,11 +1398,9 @@ class TestInstanceClaim(BaseTestCase):
|
||||
|
||||
@mock.patch('nova.pci.stats.PciDeviceStats.support_requests',
|
||||
return_value=True)
|
||||
@mock.patch('nova.pci.manager.PciDevTracker.claim_instance')
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance_uuid')
|
||||
@mock.patch('nova.objects.MigrationList.get_in_progress_by_host_and_node')
|
||||
def test_claim_with_pci(self, migr_mock, pci_mock,
|
||||
pci_manager_mock, pci_stats_mock):
|
||||
def test_claim_with_pci(self, migr_mock, pci_mock, pci_stats_mock):
|
||||
# Test that a claim involving PCI requests correctly claims
|
||||
# PCI devices on the host and sends an updated pci_device_pools
|
||||
# attribute of the ComputeNode object.
|
||||
@ -1398,12 +1410,17 @@ class TestInstanceClaim(BaseTestCase):
|
||||
# upon the resource tracker being initialized...
|
||||
self.rt.pci_tracker = pci_manager.PciDevTracker(mock.sentinel.ctx)
|
||||
|
||||
pci_pools = objects.PciDevicePoolList()
|
||||
pci_manager_mock.return_value = pci_pools
|
||||
pci_dev = pci_device.PciDevice.create(
|
||||
None, fake_pci_device.dev_dict)
|
||||
pci_devs = [pci_dev]
|
||||
self.rt.pci_tracker.pci_devs = objects.PciDeviceList(objects=pci_devs)
|
||||
|
||||
request = objects.InstancePCIRequest(count=1,
|
||||
spec=[{'vendor_id': 'v', 'product_id': 'p'}])
|
||||
pci_mock.return_value = objects.InstancePCIRequests(requests=[request])
|
||||
pci_requests = objects.InstancePCIRequests(
|
||||
requests=[request],
|
||||
instance_uuid=self.instance.uuid)
|
||||
pci_mock.return_value = pci_requests
|
||||
|
||||
disk_used = self.instance.root_gb + self.instance.ephemeral_gb
|
||||
expected = copy.deepcopy(_COMPUTE_NODE_FIXTURES[0])
|
||||
@ -1414,7 +1431,7 @@ class TestInstanceClaim(BaseTestCase):
|
||||
"free_ram_mb": expected.memory_mb - self.instance.memory_mb,
|
||||
'running_vms': 1,
|
||||
'vcpus_used': 1,
|
||||
'pci_device_pools': pci_pools,
|
||||
'pci_device_pools': objects.PciDevicePoolList(),
|
||||
'stats': {
|
||||
'io_workload': 0,
|
||||
'num_instances': 1,
|
||||
@ -1429,9 +1446,7 @@ class TestInstanceClaim(BaseTestCase):
|
||||
with mock.patch.object(self.instance, 'save'):
|
||||
self.rt.instance_claim(self.ctx, self.instance, None)
|
||||
update_mock.assert_called_once_with(self.elevated)
|
||||
pci_manager_mock.assert_called_once_with(mock.ANY, # context...
|
||||
pci_mock.return_value,
|
||||
None)
|
||||
pci_stats_mock.assert_called_once_with([request])
|
||||
self.assertTrue(obj_base.obj_equal_prims(expected,
|
||||
self.rt.compute_node))
|
||||
|
||||
|
@ -1266,13 +1266,22 @@ class _TestInstanceObject(object):
|
||||
|
||||
def test_apply_revert_migration_context(self):
|
||||
inst = instance.Instance(context=self.context, uuid=uuids.instance,
|
||||
numa_topology=None)
|
||||
numa_topology=None, pci_requests=None,
|
||||
pci_devices=None)
|
||||
inst.migration_context = test_mig_ctxt.get_fake_migration_context_obj(
|
||||
self.context)
|
||||
inst.apply_migration_context()
|
||||
self.assertIsInstance(inst.numa_topology, objects.InstanceNUMATopology)
|
||||
attrs_type = {'numa_topology': objects.InstanceNUMATopology,
|
||||
'pci_requests': objects.InstancePCIRequests,
|
||||
'pci_devices': objects.PciDeviceList}
|
||||
|
||||
for attr_name in instance._MIGRATION_CONTEXT_ATTRS:
|
||||
value = getattr(inst, attr_name)
|
||||
self.assertIsInstance(value, attrs_type[attr_name])
|
||||
inst.revert_migration_context()
|
||||
self.assertIsNone(inst.numa_topology)
|
||||
for attr_name in instance._MIGRATION_CONTEXT_ATTRS:
|
||||
value = getattr(inst, attr_name)
|
||||
self.assertIsNone(value)
|
||||
|
||||
def test_drop_migration_context(self):
|
||||
inst = instance.Instance(context=self.context, uuid=uuids.instance)
|
||||
@ -1292,15 +1301,29 @@ class _TestInstanceObject(object):
|
||||
fake_obj_numa_topology.obj_clone())
|
||||
numa_topology.cells[0].memory = 1024
|
||||
numa_topology.cells[1].memory = 1024
|
||||
pci_requests = objects.InstancePCIRequests(requests=[
|
||||
objects.InstancePCIRequest(count=1, spec=[])])
|
||||
pci_devices = pci_device.PciDeviceList()
|
||||
|
||||
inst = instance.Instance(context=self.context, uuid=uuids.instance,
|
||||
numa_topology=numa_topology)
|
||||
numa_topology=numa_topology,
|
||||
pci_requests=pci_requests,
|
||||
pci_devices=pci_devices)
|
||||
expected_objs = {'numa_topology': numa_topology,
|
||||
'pci_requests': pci_requests,
|
||||
'pci_devices': pci_devices}
|
||||
inst.migration_context = test_mig_ctxt.get_fake_migration_context_obj(
|
||||
self.context)
|
||||
with inst.mutated_migration_context():
|
||||
self.assertIs(inst.numa_topology,
|
||||
inst.migration_context.new_numa_topology)
|
||||
self.assertIs(numa_topology, inst.numa_topology)
|
||||
for attr_name in instance._MIGRATION_CONTEXT_ATTRS:
|
||||
inst_value = getattr(inst, attr_name)
|
||||
migration_context_value = (
|
||||
getattr(inst.migration_context, 'new_' + attr_name))
|
||||
self.assertIs(inst_value, migration_context_value)
|
||||
|
||||
for attr_name in instance._MIGRATION_CONTEXT_ATTRS:
|
||||
inst_value = getattr(inst, attr_name)
|
||||
self.assertIs(expected_objs[attr_name], inst_value)
|
||||
|
||||
def test_clear_numa_topology(self):
|
||||
numa_topology = (test_instance_numa_topology.
|
||||
|
@ -29,6 +29,12 @@ fake_migration_context_obj.migration_id = 42
|
||||
fake_migration_context_obj.new_numa_topology = (
|
||||
test_instance_numa_topology.fake_obj_numa_topology.obj_clone())
|
||||
fake_migration_context_obj.old_numa_topology = None
|
||||
fake_migration_context_obj.new_pci_devices = objects.PciDeviceList()
|
||||
fake_migration_context_obj.old_pci_devices = None
|
||||
fake_migration_context_obj.new_pci_requests = (
|
||||
objects.InstancePCIRequests(requests=[
|
||||
objects.InstancePCIRequest(count=123, spec=[])]))
|
||||
fake_migration_context_obj.old_pci_requests = None
|
||||
|
||||
fake_db_context = {
|
||||
'created_at': None,
|
||||
@ -48,6 +54,7 @@ def get_fake_migration_context_obj(ctxt):
|
||||
|
||||
|
||||
class _TestMigrationContext(object):
|
||||
|
||||
def _test_get_by_instance_uuid(self, db_data):
|
||||
mig_context = objects.MigrationContext.get_by_instance_uuid(
|
||||
self.context, fake_db_context['instance_uuid'])
|
||||
@ -65,6 +72,14 @@ class _TestMigrationContext(object):
|
||||
mig_context.new_numa_topology.__class__)
|
||||
self.assertIsInstance(expected_mig_context.old_numa_topology,
|
||||
mig_context.old_numa_topology.__class__)
|
||||
self.assertIsInstance(expected_mig_context.new_pci_devices,
|
||||
mig_context.new_pci_devices.__class__)
|
||||
self.assertIsInstance(expected_mig_context.old_pci_devices,
|
||||
mig_context.old_pci_devices.__class__)
|
||||
self.assertIsInstance(expected_mig_context.new_pci_requests,
|
||||
mig_context.new_pci_requests.__class__)
|
||||
self.assertIsInstance(expected_mig_context.old_pci_requests,
|
||||
mig_context.old_pci_requests.__class__)
|
||||
else:
|
||||
self.assertIsNone(mig_context)
|
||||
|
||||
|
@ -1158,7 +1158,7 @@ object_data = {
|
||||
'KeyPair': '1.4-1244e8d1b103cc69d038ed78ab3a8cc6',
|
||||
'KeyPairList': '1.2-58b94f96e776bedaf1e192ddb2a24c4e',
|
||||
'Migration': '1.4-17979b9f2ae7f28d97043a220b2a8350',
|
||||
'MigrationContext': '1.0-d8c2f10069e410f639c49082b5932c92',
|
||||
'MigrationContext': '1.1-9fb17b0b521370957a884636499df52d',
|
||||
'MigrationList': '1.3-55595bfc1a299a5962614d0821a3567e',
|
||||
'MonitorMetric': '1.1-53b1db7c4ae2c531db79761e7acc52ba',
|
||||
'MonitorMetricList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||
|
@ -124,7 +124,7 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
def _fake_pci_device_destroy(self, ctxt, node_id, address):
|
||||
self.destroy_called += 1
|
||||
|
||||
def _create_pci_requests_object(self, mock_get, requests,
|
||||
def _create_pci_requests_object(self, requests,
|
||||
instance_uuid=None):
|
||||
instance_uuid = instance_uuid or uuidsentinel.instance1
|
||||
pci_reqs = []
|
||||
@ -132,7 +132,7 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
pci_req_obj = objects.InstancePCIRequest(count=request['count'],
|
||||
spec=request['spec'])
|
||||
pci_reqs.append(pci_req_obj)
|
||||
mock_get.return_value = objects.InstancePCIRequests(
|
||||
return objects.InstancePCIRequests(
|
||||
instance_uuid=instance_uuid,
|
||||
requests=pci_reqs)
|
||||
|
||||
@ -306,11 +306,11 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(vfs, pf.child_devices)
|
||||
self.assertEqual(vfs[0].parent_device, pf)
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_set_hvdev_changed_stal(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get,
|
||||
def test_set_hvdev_changed_stal(self):
|
||||
pci_requests_obj = self._create_pci_requests_object(
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v1'}]}])
|
||||
self.tracker._claim_instance(None, mock_get.return_value, None)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
fake_pci_3 = dict(fake_pci, address='0000:00:00.2', vendor_id='v2')
|
||||
fake_pci_devs = [copy.deepcopy(fake_pci), copy.deepcopy(fake_pci_2),
|
||||
copy.deepcopy(fake_pci_3)]
|
||||
@ -318,11 +318,10 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(len(self.tracker.stale), 1)
|
||||
self.assertEqual(self.tracker.stale['0000:00:00.2']['vendor_id'], 'v2')
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_update_pci_for_instance_active(self, mock_get):
|
||||
|
||||
self._create_pci_requests_object(mock_get, fake_pci_requests)
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
def test_update_pci_for_instance_active(self):
|
||||
pci_requests_obj = self._create_pci_requests_object(fake_pci_requests)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
self.assertEqual(len(self.tracker.claims[self.inst['uuid']]), 2)
|
||||
self.tracker.update_pci_for_instance(None, self.inst, sign=1)
|
||||
self.assertEqual(len(self.tracker.allocations[self.inst['uuid']]), 2)
|
||||
@ -330,12 +329,12 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(len(free_devs), 1)
|
||||
self.assertEqual(free_devs[0].vendor_id, 'v')
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_update_pci_for_instance_fail(self, mock_get):
|
||||
def test_update_pci_for_instance_fail(self):
|
||||
pci_requests = copy.deepcopy(fake_pci_requests)
|
||||
pci_requests[0]['count'] = 4
|
||||
self._create_pci_requests_object(mock_get, pci_requests)
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
pci_requests_obj = self._create_pci_requests_object(pci_requests)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
self.assertEqual(len(self.tracker.claims[self.inst['uuid']]), 0)
|
||||
devs = self.tracker.update_pci_for_instance(None,
|
||||
self.inst,
|
||||
@ -343,8 +342,7 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(len(self.tracker.allocations[self.inst['uuid']]), 0)
|
||||
self.assertIsNone(devs)
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_pci_claim_instance_with_numa(self, mock_get):
|
||||
def test_pci_claim_instance_with_numa(self):
|
||||
fake_db_dev_3 = dict(fake_db_dev_1, id=4, address='0000:00:00.4')
|
||||
fake_devs_numa = copy.deepcopy(fake_db_devs)
|
||||
fake_devs_numa.append(fake_db_dev_3)
|
||||
@ -352,31 +350,32 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.tracker._set_hvdevs(fake_devs_numa)
|
||||
pci_requests = copy.deepcopy(fake_pci_requests)[:1]
|
||||
pci_requests[0]['count'] = 2
|
||||
self._create_pci_requests_object(mock_get, pci_requests)
|
||||
pci_requests_obj = self._create_pci_requests_object(pci_requests)
|
||||
self.inst.numa_topology = objects.InstanceNUMATopology(
|
||||
cells=[objects.InstanceNUMACell(
|
||||
id=1, cpuset=set([1, 2]), memory=512)])
|
||||
self.tracker.claim_instance(None, mock_get.return_value,
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj,
|
||||
self.inst.numa_topology)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(2, len(free_devs))
|
||||
self.assertEqual('v1', free_devs[0].vendor_id)
|
||||
self.assertEqual('v1', free_devs[1].vendor_id)
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_pci_claim_instance_with_numa_fail(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get, fake_pci_requests)
|
||||
def test_pci_claim_instance_with_numa_fail(self):
|
||||
pci_requests_obj = self._create_pci_requests_object(fake_pci_requests)
|
||||
self.inst.numa_topology = objects.InstanceNUMATopology(
|
||||
cells=[objects.InstanceNUMACell(
|
||||
id=1, cpuset=set([1, 2]), memory=512)])
|
||||
self.assertIsNone(self.tracker.claim_instance(
|
||||
None, mock_get.return_value,
|
||||
self.inst.numa_topology))
|
||||
mock.sentinel.context,
|
||||
pci_requests_obj,
|
||||
self.inst.numa_topology))
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_update_pci_for_instance_deleted(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get, fake_pci_requests)
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
def test_update_pci_for_instance_deleted(self):
|
||||
pci_requests_obj = self._create_pci_requests_object(fake_pci_requests)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 1)
|
||||
self.inst.vm_state = vm_states.DELETED
|
||||
@ -387,25 +386,6 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
dev in self.tracker.pci_devs]),
|
||||
set(['v', 'v1']))
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_update_pci_for_migration_in(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get, fake_pci_requests)
|
||||
self.tracker.update_pci_for_migration(None, self.inst)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 1)
|
||||
self.assertEqual(free_devs[0].vendor_id, 'v')
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_update_pci_for_migration_out(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get, fake_pci_requests)
|
||||
self.tracker.update_pci_for_migration(None, self.inst)
|
||||
self.tracker.update_pci_for_migration(None, self.inst, sign=-1)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 3)
|
||||
self.assertEqual(set([dev.vendor_id for
|
||||
dev in self.tracker.pci_devs]),
|
||||
set(['v', 'v1']))
|
||||
|
||||
@mock.patch.object(objects.PciDevice, 'should_migrate_data',
|
||||
return_value=False)
|
||||
def test_save(self, migrate_mock):
|
||||
@ -436,21 +416,22 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(len(self.tracker.pci_devs), 2)
|
||||
self.assertEqual(self.destroy_called, 1)
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_clean_usage(self, mock_get):
|
||||
def test_clean_usage(self):
|
||||
inst_2 = copy.copy(self.inst)
|
||||
inst_2.uuid = uuidsentinel.instance2
|
||||
migr = {'instance_uuid': 'uuid2', 'vm_state': vm_states.BUILDING}
|
||||
orph = {'uuid': 'uuid3', 'vm_state': vm_states.BUILDING}
|
||||
|
||||
self._create_pci_requests_object(mock_get,
|
||||
pci_requests_obj = self._create_pci_requests_object(
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v'}]}])
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
self.tracker.update_pci_for_instance(None, self.inst, sign=1)
|
||||
self._create_pci_requests_object(mock_get,
|
||||
pci_requests_obj = self._create_pci_requests_object(
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v1'}]}],
|
||||
instance_uuid=inst_2.uuid)
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
self.tracker.update_pci_for_instance(None, inst_2, sign=1)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 1)
|
||||
@ -463,37 +444,11 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
set([dev.vendor_id for dev in free_devs]),
|
||||
set(['v', 'v1']))
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_clean_usage_claims(self, mock_get):
|
||||
inst_2 = copy.copy(self.inst)
|
||||
inst_2.uuid = uuidsentinel.instance2
|
||||
migr = {'instance_uuid': 'uuid2', 'vm_state': vm_states.BUILDING}
|
||||
orph = {'uuid': 'uuid3', 'vm_state': vm_states.BUILDING}
|
||||
|
||||
self._create_pci_requests_object(mock_get,
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v'}]}])
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
self.tracker.update_pci_for_instance(None, self.inst, sign=1)
|
||||
self._create_pci_requests_object(mock_get,
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v1'}]}],
|
||||
instance_uuid=inst_2.uuid)
|
||||
self.tracker.update_pci_for_migration(None, inst_2)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 1)
|
||||
self.tracker.clean_usage([self.inst], [migr], [orph])
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(len(free_devs), 2)
|
||||
self.assertEqual(
|
||||
set([dev.vendor_id for dev in free_devs]),
|
||||
set(['v', 'v1']))
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_clean_usage_no_request_match_no_claims(self, mock_get):
|
||||
def test_clean_usage_no_request_match_no_claims(self):
|
||||
# Tests the case that there is no match for the request so the
|
||||
# claims mapping is set to None for the instance when the tracker
|
||||
# calls clean_usage.
|
||||
self._create_pci_requests_object(mock_get, [])
|
||||
self.tracker.update_pci_for_migration(None, instance=self.inst, sign=1)
|
||||
self.tracker.update_pci_for_instance(None, self.inst, sign=1)
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
self.assertEqual(3, len(free_devs))
|
||||
self.tracker.clean_usage([], [], [])
|
||||
@ -503,11 +458,11 @@ class PciDevTrackerTestCase(test.NoDBTestCase):
|
||||
set([dev.address for dev in free_devs]),
|
||||
set(['0000:00:00.1', '0000:00:00.2', '0000:00:00.3']))
|
||||
|
||||
@mock.patch('nova.objects.InstancePCIRequests.get_by_instance')
|
||||
def test_free_devices(self, mock_get):
|
||||
self._create_pci_requests_object(mock_get,
|
||||
def test_free_devices(self):
|
||||
pci_requests_obj = self._create_pci_requests_object(
|
||||
[{'count': 1, 'spec': [{'vendor_id': 'v'}]}])
|
||||
self.tracker.claim_instance(None, mock_get.return_value, None)
|
||||
self.tracker.claim_instance(mock.sentinel.context,
|
||||
pci_requests_obj, None)
|
||||
self.tracker.update_pci_for_instance(None, self.inst, sign=1)
|
||||
|
||||
free_devs = self.tracker.pci_stats.get_free_devs()
|
||||
|
Loading…
x
Reference in New Issue
Block a user