Merge "DPU modeling - parent_node DB/Model/API"
This commit is contained in:
commit
32532eeda5
@ -107,6 +107,9 @@ supplied when the Node is created, or the resource may be updated later.
|
|||||||
.. versionadded:: 1.82
|
.. versionadded:: 1.82
|
||||||
Introduced the ``shard`` field.
|
Introduced the ``shard`` field.
|
||||||
|
|
||||||
|
.. versionadded: 1.83
|
||||||
|
Introduced the ``parent_node`` field.
|
||||||
|
|
||||||
Normal response codes: 201
|
Normal response codes: 201
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -147,6 +150,7 @@ Request
|
|||||||
- maintenance: req_maintenance
|
- maintenance: req_maintenance
|
||||||
- maintenance_reason: maintenance_reason
|
- maintenance_reason: maintenance_reason
|
||||||
- network_data: network_data
|
- network_data: network_data
|
||||||
|
- parent_node: parent_node
|
||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- retired: retired
|
- retired: retired
|
||||||
@ -212,6 +216,7 @@ microversion 1.81.
|
|||||||
- vendor_interface: vendor_interface
|
- vendor_interface: vendor_interface
|
||||||
- volume: n_volume
|
- volume: n_volume
|
||||||
- conductor_group: conductor_group
|
- conductor_group: conductor_group
|
||||||
|
- parent_node: parent_node
|
||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- conductor: conductor
|
- conductor: conductor
|
||||||
@ -288,6 +293,13 @@ provision state, and maintenance setting for each Node.
|
|||||||
.. versionadded:: 1.82
|
.. versionadded:: 1.82
|
||||||
Introduced the ``shard`` field. Introduced the ``sharded`` request parameter.
|
Introduced the ``shard`` field. Introduced the ``sharded`` request parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 1.83
|
||||||
|
Introduced the ``parent_node`` field and query parameter to identify
|
||||||
|
matching nodes.
|
||||||
|
Introduced the ``include_children`` parameter which allows for all child
|
||||||
|
nodes to be enumerated, which are normally hidden as child nodes are not
|
||||||
|
normally intended for direct consumption by end users.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -317,6 +329,8 @@ Request
|
|||||||
- sort_dir: sort_dir
|
- sort_dir: sort_dir
|
||||||
- sort_key: sort_key
|
- sort_key: sort_key
|
||||||
- detail: detail
|
- detail: detail
|
||||||
|
- parent_node: parent_node
|
||||||
|
- include_children: include_children
|
||||||
|
|
||||||
Response
|
Response
|
||||||
--------
|
--------
|
||||||
@ -461,6 +475,7 @@ Response
|
|||||||
- vendor_interface: vendor_interface
|
- vendor_interface: vendor_interface
|
||||||
- volume: n_volume
|
- volume: n_volume
|
||||||
- conductor_group: conductor_group
|
- conductor_group: conductor_group
|
||||||
|
- parent_node: parent_node
|
||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- owner: owner
|
- owner: owner
|
||||||
@ -527,6 +542,9 @@ only the specified set.
|
|||||||
.. versionadded:: 1.82
|
.. versionadded:: 1.82
|
||||||
Introduced the ``shard`` field.
|
Introduced the ``shard`` field.
|
||||||
|
|
||||||
|
.. versionadded:: 1.83
|
||||||
|
Introduced the ``parent_node`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,404,406
|
Error codes: 400,403,404,406
|
||||||
|
@ -207,6 +207,12 @@ fields_for_conductor:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: array
|
type: array
|
||||||
|
include_children:
|
||||||
|
description: |
|
||||||
|
Wheter to show child nodes in the node list, which are normally hidden.
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
limit:
|
limit:
|
||||||
description: |
|
description: |
|
||||||
Requests a page size of items. Returns a number of items up to a limit
|
Requests a page size of items. Returns a number of items up to a limit
|
||||||
@ -1321,6 +1327,14 @@ owner:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
parent_node:
|
||||||
|
description: |
|
||||||
|
A UUID representing a node which is a parent_node to the present
|
||||||
|
node. If populated, the node will disappear from the normal node
|
||||||
|
list, as it is *not* intended for *normal* usage directly.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
passthru_async:
|
passthru_async:
|
||||||
description: |
|
description: |
|
||||||
If True the passthru function is invoked asynchronously; if False,
|
If True the passthru function is invoked asynchronously; if False,
|
||||||
|
@ -2,6 +2,27 @@
|
|||||||
REST API Version History
|
REST API Version History
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
1.83 (Bobcat)
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
This version adds a concept of child nodes through the use of a
|
||||||
|
``parent_node`` field which can be set on a node.
|
||||||
|
|
||||||
|
Under normal conditions, child nodes are not visible in the normal node
|
||||||
|
list, as they are more for nested resources and not machines which can be
|
||||||
|
freely used outside of an integrated context of the parent node.
|
||||||
|
Think of a "child node" as a node with it's own BMC embedded inside of
|
||||||
|
an existing node.
|
||||||
|
|
||||||
|
Additionally:
|
||||||
|
|
||||||
|
- Adds ``GET /v1/nodes/{node_ident}/children`` to return a list of node
|
||||||
|
UUIDs which represent children, which can acted upon individually.
|
||||||
|
- Adds ``GET /v1/nodes/?include_children=True`` to return a list of all
|
||||||
|
parent nodes and children.
|
||||||
|
- Adds ``GET /v1/nodes?parent_node={node_ident}`` to explicitly request
|
||||||
|
a detailed list of nodes by parent relationship.
|
||||||
|
|
||||||
1.82 (Antelope)
|
1.82 (Antelope)
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
@ -172,6 +172,7 @@ def node_schema():
|
|||||||
]},
|
]},
|
||||||
'network_interface': {'type': ['string', 'null']},
|
'network_interface': {'type': ['string', 'null']},
|
||||||
'owner': {'type': ['string', 'null']},
|
'owner': {'type': ['string', 'null']},
|
||||||
|
'parent_node': {'type': ['string', 'null'], 'maxLength': 36},
|
||||||
'power_interface': {'type': ['string', 'null']},
|
'power_interface': {'type': ['string', 'null']},
|
||||||
'properties': {'type': ['object', 'null']},
|
'properties': {'type': ['object', 'null']},
|
||||||
'raid_interface': {'type': ['string', 'null']},
|
'raid_interface': {'type': ['string', 'null']},
|
||||||
@ -270,7 +271,8 @@ PATCH_ALLOWED_FIELDS = [
|
|||||||
'retired_reason',
|
'retired_reason',
|
||||||
'shard',
|
'shard',
|
||||||
'storage_interface',
|
'storage_interface',
|
||||||
'vendor_interface'
|
'vendor_interface',
|
||||||
|
'parent_node'
|
||||||
]
|
]
|
||||||
|
|
||||||
TRAITS_SCHEMA = {
|
TRAITS_SCHEMA = {
|
||||||
@ -1370,6 +1372,7 @@ def _get_fields_for_node_query(fields=None):
|
|||||||
'network_data',
|
'network_data',
|
||||||
'network_interface',
|
'network_interface',
|
||||||
'owner',
|
'owner',
|
||||||
|
'parent_node',
|
||||||
'power_interface',
|
'power_interface',
|
||||||
'power_state',
|
'power_state',
|
||||||
'properties',
|
'properties',
|
||||||
@ -1977,6 +1980,51 @@ class NodeInventoryController(rest.RestController):
|
|||||||
return inspect_utils.get_inspection_data(node, api.request.context)
|
return inspect_utils.get_inspection_data(node, api.request.context)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeChildrenController(rest.RestController):
|
||||||
|
|
||||||
|
def __init__(self, node_ident):
|
||||||
|
if hasattr(self, 'parent'):
|
||||||
|
# Short circuit any attempt to access
|
||||||
|
# /v1/nodes/<node>/children/<child_node>/children
|
||||||
|
raise exception.HTTPNotFound()
|
||||||
|
if api.request.version.minor < versions.MINOR_83_PARENT_CHILD_NODES:
|
||||||
|
# Minimum Client version is required.
|
||||||
|
raise exception.HTTPNotFound()
|
||||||
|
|
||||||
|
super(NodeChildrenController).__init__()
|
||||||
|
self.parent_node = node_ident
|
||||||
|
|
||||||
|
@METRICS.timer('NodeHistoryController.get_all')
|
||||||
|
@method.expose()
|
||||||
|
def get_all(self):
|
||||||
|
try:
|
||||||
|
# retrieve the parent node and validate access is permitted.
|
||||||
|
rpc_node = api_utils.check_node_policy_and_retrieve(
|
||||||
|
'baremetal:node:get', self.parent_node)
|
||||||
|
except exception.HTTPForbidden:
|
||||||
|
# If access is forbidden, we cannot tell the user they don't
|
||||||
|
# have access.
|
||||||
|
raise exception.HTTPNotFound()
|
||||||
|
|
||||||
|
filters = {}
|
||||||
|
# Extract the project ID or get None if not applicable.
|
||||||
|
project = api_utils.check_list_policy('node', None)
|
||||||
|
url = api.request.public_url
|
||||||
|
if project:
|
||||||
|
filters['project'] = project
|
||||||
|
filters['parent_node'] = rpc_node.uuid
|
||||||
|
nodes = objects.Node.list(api.request.context,
|
||||||
|
filters=filters, fields=['uuid'])
|
||||||
|
node_list = []
|
||||||
|
for node in nodes:
|
||||||
|
node_list.append(node.uuid)
|
||||||
|
# todo, need to check the format for links
|
||||||
|
return {
|
||||||
|
'children': node_list,
|
||||||
|
'links': link.make_link('children', url, 'nodes',
|
||||||
|
'?parent_node={}'.format(rpc_node.uuid))}
|
||||||
|
|
||||||
|
|
||||||
class NodesController(rest.RestController):
|
class NodesController(rest.RestController):
|
||||||
"""REST controller for Nodes."""
|
"""REST controller for Nodes."""
|
||||||
|
|
||||||
@ -2003,6 +2051,10 @@ class NodesController(rest.RestController):
|
|||||||
"""A flag to indicate if the requests to this controller are coming
|
"""A flag to indicate if the requests to this controller are coming
|
||||||
from the top-level resource Chassis"""
|
from the top-level resource Chassis"""
|
||||||
|
|
||||||
|
parent_node = None
|
||||||
|
"""An indicator to signal if this resource is being accessed
|
||||||
|
by a sub-controller."""
|
||||||
|
|
||||||
_custom_actions = {
|
_custom_actions = {
|
||||||
'detail': ['GET'],
|
'detail': ['GET'],
|
||||||
'validate': ['GET'],
|
'validate': ['GET'],
|
||||||
@ -2024,6 +2076,7 @@ class NodesController(rest.RestController):
|
|||||||
'allocation': allocation.NodeAllocationController,
|
'allocation': allocation.NodeAllocationController,
|
||||||
'history': NodeHistoryController,
|
'history': NodeHistoryController,
|
||||||
'inventory': NodeInventoryController,
|
'inventory': NodeInventoryController,
|
||||||
|
'children': NodeChildrenController,
|
||||||
}
|
}
|
||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
@ -2056,6 +2109,7 @@ class NodesController(rest.RestController):
|
|||||||
# behaviour of previous releases for microversions without this
|
# behaviour of previous releases for microversions without this
|
||||||
# endpoint.
|
# endpoint.
|
||||||
return
|
return
|
||||||
|
|
||||||
subcontroller = self._subcontroller_map.get(remainder[0])
|
subcontroller = self._subcontroller_map.get(remainder[0])
|
||||||
if subcontroller:
|
if subcontroller:
|
||||||
return subcontroller(node_ident=ident), remainder[1:]
|
return subcontroller(node_ident=ident), remainder[1:]
|
||||||
@ -2082,7 +2136,8 @@ class NodesController(rest.RestController):
|
|||||||
detail=None, conductor=None, owner=None,
|
detail=None, conductor=None, owner=None,
|
||||||
lessee=None, project=None,
|
lessee=None, project=None,
|
||||||
description_contains=None, shard=None,
|
description_contains=None, shard=None,
|
||||||
sharded=None):
|
sharded=None, include_children=None,
|
||||||
|
parent_node=None):
|
||||||
if self.from_chassis and not chassis_uuid:
|
if self.from_chassis and not chassis_uuid:
|
||||||
raise exception.MissingParameterValue(
|
raise exception.MissingParameterValue(
|
||||||
_("Chassis id not specified."))
|
_("Chassis id not specified."))
|
||||||
@ -2124,7 +2179,9 @@ class NodesController(rest.RestController):
|
|||||||
'description_contains': description_contains,
|
'description_contains': description_contains,
|
||||||
'retired': retired,
|
'retired': retired,
|
||||||
'instance_uuid': instance_uuid,
|
'instance_uuid': instance_uuid,
|
||||||
'sharded': sharded
|
'sharded': sharded,
|
||||||
|
'include_children': include_children,
|
||||||
|
'parent_node': parent_node,
|
||||||
}
|
}
|
||||||
filters = {}
|
filters = {}
|
||||||
for key, value in possible_filters.items():
|
for key, value in possible_filters.items():
|
||||||
@ -2143,6 +2200,7 @@ class NodesController(rest.RestController):
|
|||||||
# map the name for the call, as we did not pickup a specific
|
# map the name for the call, as we did not pickup a specific
|
||||||
# list of fields to return.
|
# list of fields to return.
|
||||||
obj_fields = fields
|
obj_fields = fields
|
||||||
|
|
||||||
# NOTE(TheJulia): When a data set of the nodes list is being
|
# NOTE(TheJulia): When a data set of the nodes list is being
|
||||||
# requested, this method takes approximately 3-3.5% of the time
|
# requested, this method takes approximately 3-3.5% of the time
|
||||||
# when requesting specific fields aligning with Nova's sync
|
# when requesting specific fields aligning with Nova's sync
|
||||||
@ -2176,7 +2234,6 @@ class NodesController(rest.RestController):
|
|||||||
# and we cannot pass a limit of 0 to sqlalchemy
|
# and we cannot pass a limit of 0 to sqlalchemy
|
||||||
# and expect a response.
|
# and expect a response.
|
||||||
limit = 0
|
limit = 0
|
||||||
|
|
||||||
return node_list_convert_with_links(nodes, limit,
|
return node_list_convert_with_links(nodes, limit,
|
||||||
url=resource_url,
|
url=resource_url,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
@ -2269,14 +2326,16 @@ class NodesController(rest.RestController):
|
|||||||
detail=args.boolean, conductor=args.string,
|
detail=args.boolean, conductor=args.string,
|
||||||
owner=args.string, description_contains=args.string,
|
owner=args.string, description_contains=args.string,
|
||||||
lessee=args.string, project=args.string,
|
lessee=args.string, project=args.string,
|
||||||
shard=args.string_list, sharded=args.boolean)
|
shard=args.string_list, sharded=args.boolean,
|
||||||
|
include_children=args.boolean, parent_node=args.string)
|
||||||
def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||||
maintenance=None, retired=None, provision_state=None,
|
maintenance=None, retired=None, provision_state=None,
|
||||||
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
||||||
driver=None, fields=None, resource_class=None, fault=None,
|
driver=None, fields=None, resource_class=None, fault=None,
|
||||||
conductor_group=None, detail=None, conductor=None,
|
conductor_group=None, detail=None, conductor=None,
|
||||||
owner=None, description_contains=None, lessee=None,
|
owner=None, description_contains=None, lessee=None,
|
||||||
project=None, shard=None, sharded=None):
|
project=None, shard=None, sharded=None, include_children=None,
|
||||||
|
parent_node=None):
|
||||||
"""Retrieve a list of nodes.
|
"""Retrieve a list of nodes.
|
||||||
|
|
||||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||||
@ -2342,6 +2401,9 @@ class NodesController(rest.RestController):
|
|||||||
api_utils.check_allow_filter_by_shard(shard)
|
api_utils.check_allow_filter_by_shard(shard)
|
||||||
# Sharded is guarded by the same API version as shard
|
# Sharded is guarded by the same API version as shard
|
||||||
api_utils.check_allow_filter_by_shard(sharded)
|
api_utils.check_allow_filter_by_shard(sharded)
|
||||||
|
api_utils.check_allow_child_node_params(
|
||||||
|
include_children=include_children,
|
||||||
|
parent_node=parent_node)
|
||||||
|
|
||||||
fields = api_utils.get_request_return_fields(fields, detail,
|
fields = api_utils.get_request_return_fields(fields, detail,
|
||||||
_DEFAULT_RETURN_FIELDS)
|
_DEFAULT_RETURN_FIELDS)
|
||||||
@ -2359,7 +2421,10 @@ class NodesController(rest.RestController):
|
|||||||
conductor=conductor,
|
conductor=conductor,
|
||||||
owner=owner, lessee=lessee,
|
owner=owner, lessee=lessee,
|
||||||
shard=shard, sharded=sharded,
|
shard=shard, sharded=sharded,
|
||||||
project=project, **extra_args)
|
project=project,
|
||||||
|
include_children=include_children,
|
||||||
|
parent_node=parent_node,
|
||||||
|
**extra_args)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.detail')
|
@METRICS.timer('NodesController.detail')
|
||||||
@method.expose()
|
@method.expose()
|
||||||
@ -2379,7 +2444,8 @@ class NodesController(rest.RestController):
|
|||||||
driver=None, resource_class=None, fault=None,
|
driver=None, resource_class=None, fault=None,
|
||||||
conductor_group=None, conductor=None, owner=None,
|
conductor_group=None, conductor=None, owner=None,
|
||||||
description_contains=None, lessee=None, project=None,
|
description_contains=None, lessee=None, project=None,
|
||||||
shard=None, sharded=None):
|
shard=None, sharded=None, include_children=None,
|
||||||
|
parent_node=None):
|
||||||
"""Retrieve a list of nodes with detail.
|
"""Retrieve a list of nodes with detail.
|
||||||
|
|
||||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||||
@ -2457,7 +2523,10 @@ class NodesController(rest.RestController):
|
|||||||
conductor=conductor,
|
conductor=conductor,
|
||||||
owner=owner, lessee=lessee,
|
owner=owner, lessee=lessee,
|
||||||
project=project, shard=shard,
|
project=project, shard=shard,
|
||||||
sharded=sharded, **extra_args)
|
sharded=sharded,
|
||||||
|
include_children=include_children,
|
||||||
|
parent_node=parent_node,
|
||||||
|
**extra_args)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.validate')
|
@METRICS.timer('NodesController.validate')
|
||||||
@method.expose()
|
@method.expose()
|
||||||
@ -2639,57 +2708,77 @@ class NodesController(rest.RestController):
|
|||||||
for network_data in network_data_fields:
|
for network_data in network_data_fields:
|
||||||
validate_network_data(network_data)
|
validate_network_data(network_data)
|
||||||
|
|
||||||
|
parent_node = api_utils.get_patch_values(patch, '/parent_node')
|
||||||
|
if parent_node:
|
||||||
|
try:
|
||||||
|
# Verify we can see the parent node
|
||||||
|
api_utils.check_node_policy_and_retrieve(
|
||||||
|
'baremetal:node:get', parent_node[0])
|
||||||
|
except Exception:
|
||||||
|
msg = _("Unable to apply the requested parent_node. "
|
||||||
|
"Requested value was invalid.")
|
||||||
|
raise exception.Invalid(msg)
|
||||||
|
|
||||||
def _authorize_patch_and_get_node(self, node_ident, patch):
|
def _authorize_patch_and_get_node(self, node_ident, patch):
|
||||||
# deal with attribute-specific policy rules
|
# deal with attribute-specific policy rules
|
||||||
policy_checks = []
|
policy_checks = []
|
||||||
generic_update = False
|
generic_update = False
|
||||||
for p in patch:
|
|
||||||
if p['path'].startswith('/instance_info'):
|
|
||||||
policy_checks.append('baremetal:node:update_instance_info')
|
|
||||||
elif p['path'].startswith('/extra'):
|
|
||||||
policy_checks.append('baremetal:node:update_extra')
|
|
||||||
elif (p['path'].startswith('/automated_clean')
|
|
||||||
and strutils.bool_from_string(p['value'], default=None)
|
|
||||||
is False):
|
|
||||||
policy_checks.append('baremetal:node:disable_cleaning')
|
|
||||||
elif p['path'].startswith('/driver_info'):
|
|
||||||
policy_checks.append('baremetal:node:update:driver_info')
|
|
||||||
elif p['path'].startswith('/properties'):
|
|
||||||
policy_checks.append('baremetal:node:update:properties')
|
|
||||||
elif p['path'].startswith('/chassis_uuid'):
|
|
||||||
policy_checks.append('baremetal:node:update:chassis_uuid')
|
|
||||||
elif p['path'].startswith('/instance_uuid'):
|
|
||||||
policy_checks.append('baremetal:node:update:instance_uuid')
|
|
||||||
elif p['path'].startswith('/lessee'):
|
|
||||||
policy_checks.append('baremetal:node:update:lessee')
|
|
||||||
elif p['path'].startswith('/owner'):
|
|
||||||
policy_checks.append('baremetal:node:update:owner')
|
|
||||||
elif p['path'].startswith('/driver'):
|
|
||||||
policy_checks.append('baremetal:node:update:driver_interfaces')
|
|
||||||
elif ((p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[0]
|
|
||||||
in driver_base.ALL_INTERFACES)
|
|
||||||
and (p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[-1]
|
|
||||||
== "interface")):
|
|
||||||
# TODO(TheJulia): Replace the above check with something like
|
|
||||||
# elif (p['path'].lstrip('/').removesuffix('_interface')
|
|
||||||
# when the minimum supported version is Python 3.9.
|
|
||||||
policy_checks.append('baremetal:node:update:driver_interfaces')
|
|
||||||
elif p['path'].startswith('/network_data'):
|
|
||||||
policy_checks.append('baremetal:node:update:network_data')
|
|
||||||
elif p['path'].startswith('/conductor_group'):
|
|
||||||
policy_checks.append('baremetal:node:update:conductor_group')
|
|
||||||
elif p['path'].startswith('/name'):
|
|
||||||
policy_checks.append('baremetal:node:update:name')
|
|
||||||
elif p['path'].startswith('/retired'):
|
|
||||||
policy_checks.append('baremetal:node:update:retired')
|
|
||||||
elif p['path'].startswith('/shard'):
|
|
||||||
policy_checks.append('baremetal:node:update:shard')
|
|
||||||
else:
|
|
||||||
generic_update = True
|
|
||||||
# always do at least one check
|
|
||||||
if generic_update or not policy_checks:
|
|
||||||
policy_checks.append('baremetal:node:update')
|
|
||||||
|
|
||||||
|
paths_to_policy = (
|
||||||
|
('/instance_info', 'baremetal:node:update_instance_info'),
|
||||||
|
('/extra', 'baremetal:node:update_extra'),
|
||||||
|
('/driver_info', 'baremetal:node:update:driver_info'),
|
||||||
|
('/properties', 'baremetal:node:update:properties'),
|
||||||
|
('/chassis_uuid', 'baremetal:node:update:chassis_uuid'),
|
||||||
|
('/instance_uuid', 'baremetal:node:update:instance_uuid'),
|
||||||
|
('/lessee', 'baremetal:node:update:lessee'),
|
||||||
|
('/owner', 'baremetal:node:update:owner'),
|
||||||
|
('/driver', 'baremetal:node:update:driver_interfaces'),
|
||||||
|
('/network_data', 'baremetal:node:update:network_data'),
|
||||||
|
('/conductor_group', 'baremetal:node:update:conductor_group'),
|
||||||
|
('/name', 'baremetal:node:update:name'),
|
||||||
|
('/retired', 'baremetal:node:update:retired'),
|
||||||
|
('/shard', 'baremetal:node:update:shard'),
|
||||||
|
('/parent_node', 'baremetal:node:update:parent_node')
|
||||||
|
)
|
||||||
|
for p in patch:
|
||||||
|
# Process general direct path to policy map
|
||||||
|
rule_match_found = False
|
||||||
|
for check_path, policy_name in paths_to_policy:
|
||||||
|
if p['path'].startswith(check_path):
|
||||||
|
policy_checks.append(policy_name)
|
||||||
|
# Break from the loop as there is no reason to
|
||||||
|
# continue iterating
|
||||||
|
rule_match_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process more advanced checks and conditional behavior checks.
|
||||||
|
if not rule_match_found:
|
||||||
|
if ((p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[0]
|
||||||
|
in driver_base.ALL_INTERFACES)
|
||||||
|
and (p['path'].lstrip('/').rsplit(sep="_", maxsplit=1)[-1]
|
||||||
|
== "interface")):
|
||||||
|
# TODO(TheJulia): Replace the above check with something
|
||||||
|
# like elif (p['path'].lstrip('/').removesuffix(
|
||||||
|
# '_interface') when the minimum supported version is
|
||||||
|
# Python 3.9.
|
||||||
|
policy_checks.append(
|
||||||
|
'baremetal:node:update:driver_interfaces')
|
||||||
|
rule_match_found = True
|
||||||
|
elif (p['path'].startswith('/automated_clean')
|
||||||
|
and strutils.bool_from_string(p['value'], default=None)
|
||||||
|
is False):
|
||||||
|
policy_checks.append('baremetal:node:disable_cleaning')
|
||||||
|
rule_match_found = True
|
||||||
|
if not rule_match_found:
|
||||||
|
generic_update = True
|
||||||
|
# End of loop over patch to determine rules to apply.
|
||||||
|
|
||||||
|
if generic_update or not policy_checks:
|
||||||
|
# General policy check, either we no specific policy to apply
|
||||||
|
# on a node, or we fell through completely, regardless,
|
||||||
|
# we apply the update policy check.
|
||||||
|
policy_checks.append('baremetal:node:update')
|
||||||
return api_utils.check_multiple_node_policies_and_retrieve(
|
return api_utils.check_multiple_node_policies_and_retrieve(
|
||||||
policy_checks, node_ident, with_suffix=True)
|
policy_checks, node_ident, with_suffix=True)
|
||||||
|
|
||||||
|
@ -531,7 +531,6 @@ def get_rpc_node(node_ident):
|
|||||||
# as a UUID.
|
# as a UUID.
|
||||||
if uuidutils.is_uuid_like(node_ident):
|
if uuidutils.is_uuid_like(node_ident):
|
||||||
return objects.Node.get_by_uuid(api.request.context, node_ident)
|
return objects.Node.get_by_uuid(api.request.context, node_ident)
|
||||||
|
|
||||||
# We can refer to nodes by their name, if the client supports it
|
# We can refer to nodes by their name, if the client supports it
|
||||||
if allow_node_logical_names():
|
if allow_node_logical_names():
|
||||||
if is_valid_logical_name(node_ident):
|
if is_valid_logical_name(node_ident):
|
||||||
@ -807,7 +806,8 @@ VERSIONED_FIELDS = {
|
|||||||
'network_data': versions.MINOR_66_NODE_NETWORK_DATA,
|
'network_data': versions.MINOR_66_NODE_NETWORK_DATA,
|
||||||
'boot_mode': versions.MINOR_75_NODE_BOOT_MODE,
|
'boot_mode': versions.MINOR_75_NODE_BOOT_MODE,
|
||||||
'secure_boot': versions.MINOR_75_NODE_BOOT_MODE,
|
'secure_boot': versions.MINOR_75_NODE_BOOT_MODE,
|
||||||
'shard': versions.MINOR_82_NODE_SHARD
|
'shard': versions.MINOR_82_NODE_SHARD,
|
||||||
|
'parent_node': versions.MINOR_83_PARENT_CHILD_NODES
|
||||||
}
|
}
|
||||||
|
|
||||||
for field in V31_FIELDS:
|
for field in V31_FIELDS:
|
||||||
@ -1080,6 +1080,19 @@ def check_allow_filter_by_shard(shard):
|
|||||||
'opr': versions.MINOR_82_NODE_SHARD})
|
'opr': versions.MINOR_82_NODE_SHARD})
|
||||||
|
|
||||||
|
|
||||||
|
def check_allow_child_node_params(include_children=None,
|
||||||
|
parent_node=None):
|
||||||
|
if ((include_children is not None
|
||||||
|
or parent_node is not None)
|
||||||
|
and api.request.version.minor
|
||||||
|
< versions.MINOR_83_PARENT_CHILD_NODES):
|
||||||
|
raise exception.NotAcceptable(_(
|
||||||
|
"Request not acceptable. The minimal required API version "
|
||||||
|
"should be %(base)s.%(opr)s") %
|
||||||
|
{'base': versions.BASE_VERSION,
|
||||||
|
'opr': versions.MINOR_83_PARENT_CHILD_NODES})
|
||||||
|
|
||||||
|
|
||||||
def initial_node_provision_state():
|
def initial_node_provision_state():
|
||||||
"""Return node state to use by default when creating new nodes.
|
"""Return node state to use by default when creating new nodes.
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ BASE_VERSION = 1
|
|||||||
# v1.80: Marker to represent self service node creation/deletion
|
# v1.80: Marker to represent self service node creation/deletion
|
||||||
# v1.81: Add node inventory
|
# v1.81: Add node inventory
|
||||||
# v1.82: Add node sharding capability
|
# v1.82: Add node sharding capability
|
||||||
|
# v1.83: Add child node modeling
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
MINOR_2_AVAILABLE_STATE = 2
|
MINOR_2_AVAILABLE_STATE = 2
|
||||||
@ -203,6 +204,7 @@ MINOR_79_ALLOCATION_NODE_NAME = 79
|
|||||||
MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
|
MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
|
||||||
MINOR_81_NODE_INVENTORY = 81
|
MINOR_81_NODE_INVENTORY = 81
|
||||||
MINOR_82_NODE_SHARD = 82
|
MINOR_82_NODE_SHARD = 82
|
||||||
|
MINOR_83_PARENT_CHILD_NODES = 83
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -210,7 +212,7 @@ MINOR_82_NODE_SHARD = 82
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||||
|
|
||||||
MINOR_MAX_VERSION = MINOR_82_NODE_SHARD
|
MINOR_MAX_VERSION = MINOR_83_PARENT_CHILD_NODES
|
||||||
|
|
||||||
# String representations of the minor and maximum versions
|
# String representations of the minor and maximum versions
|
||||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -1001,6 +1001,14 @@ node_policies = [
|
|||||||
description='Governs if shards can be read via the API clients.',
|
description='Governs if shards can be read via the API clients.',
|
||||||
operations=[{'path': '/shards', 'method': 'GET'}],
|
operations=[{'path': '/shards', 'method': 'GET'}],
|
||||||
),
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name='baremetal:node:update:parent_node',
|
||||||
|
check_str=SYSTEM_MEMBER,
|
||||||
|
scope_types=['system', 'project'],
|
||||||
|
description='Governs if node parent_node field can be updated via '
|
||||||
|
'the API clients.',
|
||||||
|
operations=[{'path': '/nodes/{node_ident}', 'method': 'PATCH'}],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
deprecated_port_reason = """
|
deprecated_port_reason = """
|
||||||
|
@ -574,12 +574,12 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.82',
|
'api': '1.83',
|
||||||
'rpc': '1.55',
|
'rpc': '1.55',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
'BIOSSetting': ['1.1'],
|
'BIOSSetting': ['1.1'],
|
||||||
'Node': ['1.37'],
|
'Node': ['1.38', '1.37'],
|
||||||
'NodeHistory': ['1.0'],
|
'NodeHistory': ['1.0'],
|
||||||
'NodeInventory': ['1.0'],
|
'NodeInventory': ['1.0'],
|
||||||
'Conductor': ['1.3'],
|
'Conductor': ['1.3'],
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
# 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 alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
"""add parent_node field
|
||||||
|
|
||||||
|
Revision ID: fe222f476baf
|
||||||
|
Revises: 4dbec778866e
|
||||||
|
Create Date: 2023-04-10 11:59:29.633401
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fe222f476baf'
|
||||||
|
down_revision = '4dbec778866e'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('nodes', sa.Column('parent_node', sa.String(length=36),
|
||||||
|
nullable=True))
|
||||||
|
op.create_index(
|
||||||
|
'parent_node_idx', 'nodes', ['parent_node'], unique=False)
|
@ -410,7 +410,8 @@ class Connection(api.Connection):
|
|||||||
'sharded': 'shard'}
|
'sharded': 'shard'}
|
||||||
_NODE_FILTERS = ({'chassis_uuid', 'reserved_by_any_of',
|
_NODE_FILTERS = ({'chassis_uuid', 'reserved_by_any_of',
|
||||||
'provisioned_before', 'inspection_started_before',
|
'provisioned_before', 'inspection_started_before',
|
||||||
'description_contains', 'project'}
|
'description_contains', 'project', 'include_children',
|
||||||
|
'parent_node'}
|
||||||
| _NODE_QUERY_FIELDS
|
| _NODE_QUERY_FIELDS
|
||||||
| set(_NODE_IN_QUERY_FIELDS)
|
| set(_NODE_IN_QUERY_FIELDS)
|
||||||
| set(_NODE_NON_NULL_FILTERS))
|
| set(_NODE_NON_NULL_FILTERS))
|
||||||
@ -472,7 +473,17 @@ class Connection(api.Connection):
|
|||||||
project = filters['project']
|
project = filters['project']
|
||||||
query = query.filter((models.Node.owner == project)
|
query = query.filter((models.Node.owner == project)
|
||||||
| (models.Node.lessee == project))
|
| (models.Node.lessee == project))
|
||||||
|
# Determine parent/child node handling
|
||||||
|
if not filters.get('include_children', False):
|
||||||
|
if 'parent_node' in filters:
|
||||||
|
query = query.filter(
|
||||||
|
models.Node.parent_node == filters.get('parent_node')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.filter(models.Node.parent_node == sql.null())
|
||||||
|
|
||||||
|
# The presence of ``include_children`` as a filter results in
|
||||||
|
# a full list of both parents and children being conveyed.
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def _add_allocations_filters(self, query, filters):
|
def _add_allocations_filters(self, query, filters):
|
||||||
|
@ -135,6 +135,7 @@ class NodeBase(Base):
|
|||||||
Index('conductor_group_idx', 'conductor_group'),
|
Index('conductor_group_idx', 'conductor_group'),
|
||||||
Index('resource_class_idx', 'resource_class'),
|
Index('resource_class_idx', 'resource_class'),
|
||||||
Index('shard_idx', 'shard'),
|
Index('shard_idx', 'shard'),
|
||||||
|
Index('parent_node_idx', 'parent_node'),
|
||||||
table_args())
|
table_args())
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
uuid = Column(String(36))
|
uuid = Column(String(36))
|
||||||
@ -211,11 +212,10 @@ class NodeBase(Base):
|
|||||||
storage_interface = Column(String(255), nullable=True)
|
storage_interface = Column(String(255), nullable=True)
|
||||||
power_interface = Column(String(255), nullable=True)
|
power_interface = Column(String(255), nullable=True)
|
||||||
vendor_interface = Column(String(255), nullable=True)
|
vendor_interface = Column(String(255), nullable=True)
|
||||||
|
|
||||||
boot_mode = Column(String(16), nullable=True)
|
boot_mode = Column(String(16), nullable=True)
|
||||||
secure_boot = Column(Boolean, nullable=True)
|
secure_boot = Column(Boolean, nullable=True)
|
||||||
|
|
||||||
shard = Column(String(255), nullable=True)
|
shard = Column(String(255), nullable=True)
|
||||||
|
parent_node = Column(String(36), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class Node(NodeBase):
|
class Node(NodeBase):
|
||||||
|
@ -79,7 +79,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
# Version 1.35: Add network_data field
|
# Version 1.35: Add network_data field
|
||||||
# Version 1.36: Add boot_mode and secure_boot fields
|
# Version 1.36: Add boot_mode and secure_boot fields
|
||||||
# Version 1.37: Add shard field
|
# Version 1.37: Add shard field
|
||||||
VERSION = '1.37'
|
# Version 1.38: Add parent_node field
|
||||||
|
VERSION = '1.38'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -172,6 +173,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
'boot_mode': object_fields.StringField(nullable=True),
|
'boot_mode': object_fields.StringField(nullable=True),
|
||||||
'secure_boot': object_fields.BooleanField(nullable=True),
|
'secure_boot': object_fields.BooleanField(nullable=True),
|
||||||
'shard': object_fields.StringField(nullable=True),
|
'shard': object_fields.StringField(nullable=True),
|
||||||
|
'parent_node': object_fields.StringField(nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def as_dict(self, secure=False, mask_configdrive=True):
|
def as_dict(self, secure=False, mask_configdrive=True):
|
||||||
|
@ -8164,3 +8164,180 @@ class TestNodeShardPatch(test_api_base.BaseApiTest):
|
|||||||
body, expect_errors=True, headers=headers)
|
body, expect_errors=True, headers=headers)
|
||||||
self.mock_update_node.assert_not_called()
|
self.mock_update_node.assert_not_called()
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeChildrenTestCase(test_api_base.BaseApiTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeChildrenTestCase, self).setUp()
|
||||||
|
self.node = obj_utils.create_test_node(self.context, name='din')
|
||||||
|
self.child_node = obj_utils.create_test_node(
|
||||||
|
self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
name='not-yoda',
|
||||||
|
parent_node=self.node.uuid)
|
||||||
|
self.headers = {api_base.Version.string: '1.83'}
|
||||||
|
|
||||||
|
def test_list_nodes(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/', headers=self.headers)
|
||||||
|
self.assertEqual(1, len(response['nodes']))
|
||||||
|
self.assertEqual('din', response['nodes'][0]['name'])
|
||||||
|
|
||||||
|
def test_get_child_node(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s/children' % self.node.uuid, headers=self.headers)
|
||||||
|
self.assertEqual(1, len(response['children']))
|
||||||
|
self.assertEqual(self.child_node.uuid, response['children'][0])
|
||||||
|
|
||||||
|
def test_list_nodes_with_include_children(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/?include_children=True', headers=self.headers)
|
||||||
|
self.assertEqual(2, len(response['nodes']))
|
||||||
|
|
||||||
|
def test_list_nodes_ignores_parent_if_include_children_indicated(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/?include_children=True&parent_node=111',
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(2, len(response['nodes']))
|
||||||
|
|
||||||
|
@mock.patch.object(api_utils, 'check_list_policy', autospec=True)
|
||||||
|
def test_list_nodes_cannot_see_children_if_not_owned(self, mock_policy):
|
||||||
|
project_id = uuidutils.generate_uuid()
|
||||||
|
mock_policy.return_value = project_id
|
||||||
|
self.node['owner'] = project_id
|
||||||
|
self.node.save()
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/?parent_node={}'.format(project_id),
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(0, len(response['nodes']))
|
||||||
|
|
||||||
|
@mock.patch.object(api_utils, 'check_list_policy', autospec=True)
|
||||||
|
def test_list_nodes_with_children_only_parent(self, mock_policy):
|
||||||
|
project_id = uuidutils.generate_uuid()
|
||||||
|
headers = self.headers.copy()
|
||||||
|
mock_policy.return_value = project_id
|
||||||
|
self.node['lessee'] = project_id
|
||||||
|
self.node.save()
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/?include_children=True&'
|
||||||
|
'fields=uuid,lessee,name,parent_node',
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(1, len(response['nodes']))
|
||||||
|
self.assertEqual(self.node.uuid, response['nodes'][0]['uuid'])
|
||||||
|
|
||||||
|
def test_list_nodes_lists_empty_for_specific_parent(self):
|
||||||
|
node = obj_utils.create_test_node(
|
||||||
|
self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
name='kryze',
|
||||||
|
parent_node=self.node.uuid)
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/?parent_node={}'.format(node.uuid), headers=self.headers)
|
||||||
|
self.assertEqual(0, len(response['nodes']))
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'create_node',
|
||||||
|
lambda _api, _ctx, node, _topic: _create_node_locally(node))
|
||||||
|
class TestNodeParentNodePost(test_api_base.BaseApiTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeParentNodePost, self).setUp()
|
||||||
|
self.node = obj_utils.create_test_node(self.context, name='din')
|
||||||
|
|
||||||
|
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
|
||||||
|
autospec=True)
|
||||||
|
self.mock_gtf = p.start()
|
||||||
|
self.mock_gtf.return_value = 'test-topic'
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
self.chassis = obj_utils.create_test_chassis(self.context)
|
||||||
|
|
||||||
|
def test_create_node_with_parent_node(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
ndict['parent_node'] = self.node.uuid
|
||||||
|
headers = {api_base.Version.string: '1.83'}
|
||||||
|
response = self.post_json('/nodes', ndict, headers=headers)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
|
||||||
|
result = self.get_json('/nodes/{}'.format(ndict['uuid']),
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(ndict['uuid'], result['uuid'])
|
||||||
|
self.assertEqual(self.node.uuid, result['parent_node'])
|
||||||
|
|
||||||
|
def test_create_node_with_parent_node_fail_wrong_version(self):
|
||||||
|
headers = {api_base.Version.string: '1.82'}
|
||||||
|
ndict = test_api_utils.post_get_test_node(
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
ndict['parent_node'] = self.node.uuid
|
||||||
|
response = self.post_json(
|
||||||
|
'/nodes', ndict, expect_errors=True, headers=headers)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeParentNodePatch(test_api_base.BaseApiTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeParentNodePatch, self).setUp()
|
||||||
|
self.node = obj_utils.create_test_node(
|
||||||
|
self.context,
|
||||||
|
name='djarin')
|
||||||
|
self.child_node = obj_utils.create_test_node(
|
||||||
|
self.context,
|
||||||
|
name='the_child',
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
|
||||||
|
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
|
||||||
|
autospec=True)
|
||||||
|
self.mock_gtf = p.start()
|
||||||
|
self.mock_gtf.return_value = 'test-topic'
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
p = mock.patch.object(rpcapi.ConductorAPI, 'update_node',
|
||||||
|
autospec=True)
|
||||||
|
self.mock_update_node = p.start()
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
def test_node_add_parent(self):
|
||||||
|
self.mock_update_node.return_value = self.node
|
||||||
|
(self
|
||||||
|
.mock_update_node
|
||||||
|
.return_value
|
||||||
|
.updated_at) = "2013-12-03T06:20:41.184720+00:00"
|
||||||
|
headers = {api_base.Version.string: '1.83'}
|
||||||
|
body = [{
|
||||||
|
'path': '/parent_node',
|
||||||
|
'value': self.node.uuid,
|
||||||
|
'op': 'add',
|
||||||
|
}]
|
||||||
|
|
||||||
|
response = self.patch_json(
|
||||||
|
'/nodes/%s' % self.child_node.uuid, body, headers=headers)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
self.mock_update_node.assert_called_once()
|
||||||
|
|
||||||
|
def test_node_add_parent_node_fail_wrong_version(self):
|
||||||
|
headers = {api_base.Version.string: '1.82'}
|
||||||
|
body = [{
|
||||||
|
'path': '/parent_node',
|
||||||
|
'value': self.node.uuid,
|
||||||
|
'op': 'add',
|
||||||
|
}]
|
||||||
|
|
||||||
|
response = self.patch_json('/nodes/%s' % self.child_node.uuid,
|
||||||
|
body, expect_errors=True, headers=headers)
|
||||||
|
self.mock_update_node.assert_not_called()
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
def test_node_remove_parent(self):
|
||||||
|
self.mock_update_node.return_value = self.node
|
||||||
|
(self
|
||||||
|
.mock_update_node
|
||||||
|
.return_value
|
||||||
|
.updated_at) = "2013-12-03T06:20:41.184720+00:00"
|
||||||
|
headers = {api_base.Version.string: '1.83'}
|
||||||
|
body = [{
|
||||||
|
'path': '/parent_node',
|
||||||
|
'op': 'remove',
|
||||||
|
}]
|
||||||
|
|
||||||
|
response = self.patch_json(
|
||||||
|
'/nodes/%s' % self.child_node.uuid, body, headers=headers)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
self.mock_update_node.assert_called_once()
|
||||||
|
@ -392,6 +392,17 @@ class TestRBACProjectScoped(TestACLBase):
|
|||||||
owner=owner_project_id,
|
owner=owner_project_id,
|
||||||
last_error='meow',
|
last_error='meow',
|
||||||
reservation='lolcats')
|
reservation='lolcats')
|
||||||
|
# invisible child node, rbac + project query enforcement
|
||||||
|
# prevents it from being visible.
|
||||||
|
db_utils.create_test_node(
|
||||||
|
uuid='2b3b8adb-add7-4fd0-8e82-dcb714d848e7',
|
||||||
|
parent_node=owned_node.uuid)
|
||||||
|
# Child node which will appear in child node endpoint
|
||||||
|
# queries.
|
||||||
|
db_utils.create_test_node(
|
||||||
|
uuid='3c3b8adb-edd7-3ed0-8e82-aab714d8411a',
|
||||||
|
parent_node=owned_node.uuid,
|
||||||
|
owner=owner_project_id)
|
||||||
owned_node_port = db_utils.create_test_port(
|
owned_node_port = db_utils.create_test_port(
|
||||||
uuid='ebe30f19-358d-41e1-8d28-fd7357a0164c',
|
uuid='ebe30f19-358d-41e1-8d28-fd7357a0164c',
|
||||||
node_id=owned_node['id'],
|
node_id=owned_node['id'],
|
||||||
|
@ -84,9 +84,8 @@ values:
|
|||||||
X-Roles: service
|
X-Roles: service
|
||||||
owner_project_id: &owner_project_id 70e5e25a-2ca2-4cb1-8ae8-7d8739cee205
|
owner_project_id: &owner_project_id 70e5e25a-2ca2-4cb1-8ae8-7d8739cee205
|
||||||
lessee_project_id: &lessee_project_id f11853c7-fa9c-4db3-a477-c9d8e0dbbf13
|
lessee_project_id: &lessee_project_id f11853c7-fa9c-4db3-a477-c9d8e0dbbf13
|
||||||
owned_node_ident: &owned_node_ident f11853c7-fa9c-4db3-a477-c9d8e0dbbf13
|
owned_node_ident: &owned_node_ident 1ab63b9e-66d7-4cd7-8618-dddd0f9f7881
|
||||||
lessee_node_ident: &lessee_node_ident 38d5abed-c585-4fce-a57e-a2ffc2a2ec6f
|
lessee_node_ident: &lessee_node_ident 38d5abed-c585-4fce-a57e-a2ffc2a2ec6f
|
||||||
|
|
||||||
# Nodes - https://docs.openstack.org/api-ref/baremetal/?expanded=#nodes-nodes
|
# Nodes - https://docs.openstack.org/api-ref/baremetal/?expanded=#nodes-nodes
|
||||||
|
|
||||||
# Based on nodes_post_admin test.
|
# Based on nodes_post_admin test.
|
||||||
@ -3888,3 +3887,62 @@ shard_patch_set_node_shard_disallowed:
|
|||||||
path: /shard
|
path: /shard
|
||||||
value: 'TestShard'
|
value: 'TestShard'
|
||||||
assert_status: 403
|
assert_status: 403
|
||||||
|
|
||||||
|
# Update node parent_node field - baremetal:node:update:parent_node
|
||||||
|
|
||||||
|
parent_node_patch_by_admin:
|
||||||
|
path: '/v1/nodes/{lessee_node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *owner_admin_headers
|
||||||
|
body: &patch_parent_node
|
||||||
|
- op: replace
|
||||||
|
path: /parent_node
|
||||||
|
value: *owned_node_ident
|
||||||
|
assert_status: 403
|
||||||
|
|
||||||
|
parent_node_patch_by_member:
|
||||||
|
path: '/v1/nodes/{lessee_node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *owner_member_headers
|
||||||
|
body: *patch_parent_node
|
||||||
|
assert_status: 403
|
||||||
|
|
||||||
|
parent_node_patch_by_reader:
|
||||||
|
path: '/v1/nodes/{lessee_node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *owner_reader_headers
|
||||||
|
body: *patch_parent_node
|
||||||
|
assert_status: 403
|
||||||
|
|
||||||
|
parent_node_patch_by_manager:
|
||||||
|
path: '/v1/nodes/{lessee_node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *owner_manager_headers
|
||||||
|
body: *patch_parent_node
|
||||||
|
assert_status: 403
|
||||||
|
|
||||||
|
parent_node_patch_by_cannot_see_node:
|
||||||
|
# This node cannot be seen, and also just doesn't exist.
|
||||||
|
# Just to verify we return a 400 on a node we can change.
|
||||||
|
path: '/v1/nodes/{lessee_node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *owner_admin_headers
|
||||||
|
body:
|
||||||
|
- op: replace
|
||||||
|
path: /parent_node
|
||||||
|
value: 'f11853c7-fa9c-4db3-a477-c9d8e0dbbf13'
|
||||||
|
assert_status: 400
|
||||||
|
|
||||||
|
parent_node_children_can_get_list_of_children:
|
||||||
|
path: '/v1/nodes/{owner_node_ident}/children'
|
||||||
|
method: get
|
||||||
|
headers: *owner_reader_headers
|
||||||
|
assert_status: 200
|
||||||
|
assert_list_length:
|
||||||
|
children: 1
|
||||||
|
|
||||||
|
lessee_cannot_get_a_nodes_children:
|
||||||
|
path: '/v1/nodes/{owner_node_ident}/children'
|
||||||
|
method: get
|
||||||
|
headers: *lessee_reader_headers
|
||||||
|
assert_status: 404
|
||||||
|
@ -3,7 +3,7 @@ values:
|
|||||||
# System scoped admin token
|
# System scoped admin token
|
||||||
admin_headers: &admin_headers
|
admin_headers: &admin_headers
|
||||||
X-Auth-Token: 'baremetal-admin-token'
|
X-Auth-Token: 'baremetal-admin-token'
|
||||||
X-Roles: admin
|
X-Roles: admin,member,reader
|
||||||
OpenStack-System-Scope: all
|
OpenStack-System-Scope: all
|
||||||
# System scoped other member token.
|
# System scoped other member token.
|
||||||
scoped_member_headers: &scoped_member_headers
|
scoped_member_headers: &scoped_member_headers
|
||||||
@ -21,7 +21,7 @@ values:
|
|||||||
other_admin_headers: &other_admin_headers
|
other_admin_headers: &other_admin_headers
|
||||||
X-Auth-Token: 'other-admin-token'
|
X-Auth-Token: 'other-admin-token'
|
||||||
X-Project-ID: a1111111111111111111111111111111
|
X-Project-ID: a1111111111111111111111111111111
|
||||||
X-Roles: admin
|
X-Roles: admin,member,reader
|
||||||
X-Project-Name: 'other-project'
|
X-Project-Name: 'other-project'
|
||||||
service_headers: &service_headers
|
service_headers: &service_headers
|
||||||
X-Auth-Token: 'baremetal-service-token'
|
X-Auth-Token: 'baremetal-service-token'
|
||||||
@ -30,6 +30,7 @@ values:
|
|||||||
owner_project_id: &owner_project_id '{owner_project_id}'
|
owner_project_id: &owner_project_id '{owner_project_id}'
|
||||||
other_project_id: &other_project_id '{other_project_id}'
|
other_project_id: &other_project_id '{other_project_id}'
|
||||||
node_ident: &node_ident '{node_ident}'
|
node_ident: &node_ident '{node_ident}'
|
||||||
|
allocated_node_ident: &allocated_node_ident 22e26c0b-03f2-4d2e-ae87-c02d7f33c000
|
||||||
|
|
||||||
# Nodes - https://docs.openstack.org/api-ref/baremetal/?expanded=#nodes-nodes
|
# Nodes - https://docs.openstack.org/api-ref/baremetal/?expanded=#nodes-nodes
|
||||||
|
|
||||||
@ -2313,3 +2314,29 @@ shard_patch_set_node_shard_disallowed:
|
|||||||
headers: *scoped_member_headers
|
headers: *scoped_member_headers
|
||||||
body: *replace_shard
|
body: *replace_shard
|
||||||
assert_status: 403
|
assert_status: 403
|
||||||
|
|
||||||
|
# Update node parent_node field - baremetal:node:update:parent_node
|
||||||
|
|
||||||
|
parent_node_patch_by_admin:
|
||||||
|
path: '/v1/nodes/{node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *admin_headers
|
||||||
|
body: &patch_parent_node
|
||||||
|
- op: replace
|
||||||
|
path: /parent_node
|
||||||
|
value: *allocated_node_ident
|
||||||
|
assert_status: 503
|
||||||
|
|
||||||
|
parent_node_patch_by_member:
|
||||||
|
path: '/v1/nodes/{node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *scoped_member_headers
|
||||||
|
body: *patch_parent_node
|
||||||
|
assert_status: 503
|
||||||
|
|
||||||
|
parent_node_patch_by_reader:
|
||||||
|
path: '/v1/nodes/{node_ident}'
|
||||||
|
method: patch
|
||||||
|
headers: *reader_headers
|
||||||
|
body: *patch_parent_node
|
||||||
|
assert_status: 403
|
||||||
|
@ -236,7 +236,8 @@ def get_test_node(**kw):
|
|||||||
'network_data': kw.get('network_data'),
|
'network_data': kw.get('network_data'),
|
||||||
'boot_mode': kw.get('boot_mode', None),
|
'boot_mode': kw.get('boot_mode', None),
|
||||||
'secure_boot': kw.get('secure_boot', None),
|
'secure_boot': kw.get('secure_boot', None),
|
||||||
'shard': kw.get('shard', None)
|
'shard': kw.get('shard', None),
|
||||||
|
'parent_node': kw.get('parent_node', None)
|
||||||
}
|
}
|
||||||
|
|
||||||
for iface in drivers_base.ALL_INTERFACES:
|
for iface in drivers_base.ALL_INTERFACES:
|
||||||
|
@ -676,7 +676,7 @@ class TestObject(_LocalTest, _TestObject):
|
|||||||
# version bump. It is an MD5 hash of the object fields and remotable methods.
|
# version bump. It is an MD5 hash of the object fields and remotable methods.
|
||||||
# The fingerprint values should only be changed if there is a version bump.
|
# The fingerprint values should only be changed if there is a version bump.
|
||||||
expected_object_fingerprints = {
|
expected_object_fingerprints = {
|
||||||
'Node': '1.37-6b38eb91aec57532547ea8607f95675a',
|
'Node': '1.38-7e7fdaa2c2bb01153ad567c9f1081cb7',
|
||||||
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
||||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||||
'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2',
|
'Port': '1.11-97bf15b61224f26c65e90f007d78bfd2',
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds the concept of ``parent_node`` which allows a "child node", such as
|
||||||
|
an independently managed BMC controlled device deployed within a
|
||||||
|
``parent_node`` as part of API version *1.83*. Child nodes are hidden
|
||||||
|
from normal node lists as they are not "general purpose" machines,
|
||||||
|
but have a specific embedded usage. In this model, RBAC rules also apply
|
||||||
|
so if you wish an ``owner`` or ``lessee`` to have the child node visible,
|
||||||
|
they must also have the the appropriate ``owner`` or ``lessee`` value set
|
||||||
|
matching the parent node.
|
||||||
|
- |
|
||||||
|
Adds a ``/v1/nodes/?include_children=True`` parameter to get a list of
|
||||||
|
all nodes and their children.
|
||||||
|
- |
|
||||||
|
Adds a ``/v1/nodes/?parent_node=<node_ident>`` query parameter to permit
|
||||||
|
retrieval of a list of child nodes assigned to the parent denoted by
|
||||||
|
``<node_ident>``.
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
This upgrade contains an additional field for the ``nodes`` table, named
|
||||||
|
``parent_node``. This update also indexes the ``parent_node`` database
|
||||||
|
column to prevent performance issues in large deployments.
|
Loading…
Reference in New Issue
Block a user