From 77ef172f6589e811b19bc1eb234adc2c5f0bd039 Mon Sep 17 00:00:00 2001
From: Jay Pipes <jaypipes@gmail.com>
Date: Mon, 26 Sep 2016 20:52:49 -0400
Subject: [PATCH] placement: add nested resource providers

Adds initial support for storing the relationship between parent and
child resource providers. Nested resource providers are essential for
expressing certain types of resources -- in particular SR-IOV physical
functions and certain SR-IOV fully-programmable gate arrays. The
resources that these providers expose are of resource class
SRIOV_NET_VF and we will need a way of indicating that the physical
function providing these virtual function resources is tagged with
certain traits (representing vendor_id, product_id or the physical
network the PF is attached to).

The compute host is a resource provider which has an SR-IOV-enabled
physical function (NIC) as a child resource provider. The physical
function has an inventory containing some total amount of SRIOV_NET_VF
resources. These SRIOV_NET_VF resources are allocated to zero or more
consumers (instances) on the compute host.

                    compute host (parent resource provider)
                         |
                         |
                      SR-IOV PF  (child resource provider)
                         :
                        / \
                       /   \
                    VF1    VF2   (inventory of child provider)

The resource provider model gets two new fields:

 - root_provider_uuid: The "top" or "root" of the tree of nested
   providers
 - parent_provider_uuid: The immediate parent of the provider, or None
   if the provider is a root provider.

The database schema adds two new columns to the resource_providers
table that contain the internal integer IDs that correspond to the
user-facing UUID values:

 - root_provider_id
 - parent_provider_id

The root_provider_uuid field is non-nullable in the ResourceProvider
object definition, and this code includes an online data migration to
automatically populate the root_provider_id field with the value of the
resource_providers.id field for any resource providers already in the
DB.

The root_provider_id field value is populated automatically when a
provider is created. If the parent provider UUID is set, then the
root_provider_id is set to the root_provider_id value of the parent. If
parent is unset, root_provider_id is set to the value of the id
attribute of the provider being created. The corresponding UUID values
for root and parent provider are fetched in the queries that retrieves
resource provider data using two self-referential joins.

The root_provider_id column allows us to do extremely quick lookups of
an entire tree of providers without needing to perform any recursive
database queries.

Logic in this patch ensures that no resource provider can be deleted if
any of its children have any allocations active on them. We also check
to ensure that when created or updated, a resource provider's parent
provider UUID actually points to an existing provider.

It's important to point out that qualitative trait information is only
associated with a resource provider entity, not the resources that
resource provider has in its inventory. This is the reason why nested
resource providers are necessary. In the case of things like NUMA nodes
or SRIOV physical functions, if a compute host had multiple SRIOV
physical functions, each associated with a different network trait,
there would be no way to differentiate between the SRIOV_NET_VF
resources that those multiple SRIOV physical functions provided if the
containing compute host had a single inventory item containing the
total number of VFs exposed by both PFs.

Change-Id: I2d8df57f77a03cde898d9ec792c5d59b75f61204
blueprint: nested-resource-providers
Co-Authored-By: Moshe Levi <moshele@mellanox.com>
---
 .../versions/051_nested_resource_providers.py | 50 +++++++++++++++++++
 nova/db/sqlalchemy/api_models.py              | 11 ++++
 2 files changed, 61 insertions(+)
 create mode 100644 nova/db/sqlalchemy/api_migrations/migrate_repo/versions/051_nested_resource_providers.py

diff --git a/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/051_nested_resource_providers.py b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/051_nested_resource_providers.py
new file mode 100644
index 000000000..4745a1341
--- /dev/null
+++ b/nova/db/sqlalchemy/api_migrations/migrate_repo/versions/051_nested_resource_providers.py
@@ -0,0 +1,50 @@
+#    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.
+
+from sqlalchemy import Column
+from sqlalchemy import ForeignKey
+from sqlalchemy import Index
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import Table
+
+
+def upgrade(migrate_engine):
+    meta = MetaData()
+    meta.bind = migrate_engine
+
+    resource_providers = Table('resource_providers', meta, autoload=True)
+    columns_to_add = [
+            ('root_provider_id',
+                Column('root_provider_id', Integer,
+                       ForeignKey('resource_providers.id'))),
+            ('parent_provider_id',
+                Column('parent_provider_id', Integer,
+                       ForeignKey('resource_providers.id'))),
+    ]
+    for col_name, column in columns_to_add:
+        if not hasattr(resource_providers.c, col_name):
+            resource_providers.create_column(column)
+
+    indexed_columns = set()
+    for idx in resource_providers.indexes:
+        for c in idx.columns:
+            indexed_columns.add(c.name)
+
+    if 'root_provider_id' not in indexed_columns:
+        index = Index('resource_providers_root_provider_id_idx',
+                resource_providers.c.root_provider_id)
+        index.create()
+    if 'parent_provider_id' not in indexed_columns:
+        index = Index('resource_providers_parent_provider_id_idx',
+                resource_providers.c.parent_provider_id)
+        index.create()
diff --git a/nova/db/sqlalchemy/api_models.py b/nova/db/sqlalchemy/api_models.py
index 398260699..7e5e7b19f 100644
--- a/nova/db/sqlalchemy/api_models.py
+++ b/nova/db/sqlalchemy/api_models.py
@@ -293,6 +293,10 @@ class ResourceProvider(API_BASE):
         schema.UniqueConstraint('uuid',
             name='uniq_resource_providers0uuid'),
         Index('resource_providers_name_idx', 'name'),
+        Index('resource_providers_root_provider_id_idx',
+              'root_provider_id'),
+        Index('resource_providers_parent_provider_id_idx',
+              'parent_provider_id'),
         schema.UniqueConstraint('name',
             name='uniq_resource_providers0name')
     )
@@ -301,6 +305,13 @@ class ResourceProvider(API_BASE):
     uuid = Column(String(36), nullable=False)
     name = Column(Unicode(200), nullable=True)
     generation = Column(Integer, default=0)
+    # Represents the root of the "tree" that the provider belongs to
+    root_provider_id = Column(Integer, ForeignKey('resource_providers.id'),
+        nullable=True)
+    # The immediate parent provider of this provider, or NULL if there is no
+    # parent. If parent_provider_id == NULL then root_provider_id == id
+    parent_provider_id = Column(Integer, ForeignKey('resource_providers.id'),
+        nullable=True)
 
 
 class Inventory(API_BASE):