Merge "Integrate portgroups with ports to support LAG"
This commit is contained in:
commit
e3bedc4ead
@ -2,6 +2,11 @@
|
|||||||
REST API Version History
|
REST API Version History
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
**1.24**
|
||||||
|
|
||||||
|
Added new endpoints '/v1/nodes/<node>/portgroups' and '/v1/portgroups/<portgroup>/ports'.
|
||||||
|
Added new field ``port.portgroup_uuid``.
|
||||||
|
|
||||||
**1.23**
|
**1.23**
|
||||||
|
|
||||||
Added '/v1/portgroups/ endpoint.
|
Added '/v1/portgroups/ endpoint.
|
||||||
|
@ -30,6 +30,7 @@ from ironic.api.controllers import base
|
|||||||
from ironic.api.controllers import link
|
from ironic.api.controllers import link
|
||||||
from ironic.api.controllers.v1 import collection
|
from ironic.api.controllers.v1 import collection
|
||||||
from ironic.api.controllers.v1 import port
|
from ironic.api.controllers.v1 import port
|
||||||
|
from ironic.api.controllers.v1 import portgroup
|
||||||
from ironic.api.controllers.v1 import types
|
from ironic.api.controllers.v1 import types
|
||||||
from ironic.api.controllers.v1 import utils as api_utils
|
from ironic.api.controllers.v1 import utils as api_utils
|
||||||
from ironic.api.controllers.v1 import versions
|
from ironic.api.controllers.v1 import versions
|
||||||
@ -746,6 +747,9 @@ class Node(base.APIBase):
|
|||||||
ports = wsme.wsattr([link.Link], readonly=True)
|
ports = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""Links to the collection of ports on this node"""
|
"""Links to the collection of ports on this node"""
|
||||||
|
|
||||||
|
portgroups = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""Links to the collection of portgroups on this node"""
|
||||||
|
|
||||||
states = wsme.wsattr([link.Link], readonly=True)
|
states = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""Links to endpoint for retrieving and setting node states"""
|
"""Links to endpoint for retrieving and setting node states"""
|
||||||
|
|
||||||
@ -775,7 +779,8 @@ class Node(base.APIBase):
|
|||||||
setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset))
|
setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(node, url, fields=None, show_states_links=True):
|
def _convert_with_links(node, url, fields=None, show_states_links=True,
|
||||||
|
show_portgroups=True):
|
||||||
# NOTE(lucasagomes): Since we are able to return a specified set of
|
# NOTE(lucasagomes): Since we are able to return a specified set of
|
||||||
# fields the "uuid" can be unset, so we need to save it in another
|
# fields the "uuid" can be unset, so we need to save it in another
|
||||||
# variable to use when building the links
|
# variable to use when building the links
|
||||||
@ -795,6 +800,13 @@ class Node(base.APIBase):
|
|||||||
link.Link.make_link('bookmark', url, 'nodes',
|
link.Link.make_link('bookmark', url, 'nodes',
|
||||||
node_uuid + "/states",
|
node_uuid + "/states",
|
||||||
bookmark=True)]
|
bookmark=True)]
|
||||||
|
if show_portgroups:
|
||||||
|
node.portgroups = [
|
||||||
|
link.Link.make_link('self', url, 'nodes',
|
||||||
|
node_uuid + "/portgroups"),
|
||||||
|
link.Link.make_link('bookmark', url, 'nodes',
|
||||||
|
node_uuid + "/portgroups",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
# NOTE(lucasagomes): The numeric ID should not be exposed to
|
# NOTE(lucasagomes): The numeric ID should not be exposed to
|
||||||
# the user, it's internal only.
|
# the user, it's internal only.
|
||||||
@ -841,9 +853,11 @@ class Node(base.APIBase):
|
|||||||
hide_fields_in_newer_versions(node)
|
hide_fields_in_newer_versions(node)
|
||||||
show_states_links = (
|
show_states_links = (
|
||||||
api_utils.allow_links_node_states_and_driver_properties())
|
api_utils.allow_links_node_states_and_driver_properties())
|
||||||
|
show_portgroups = api_utils.allow_portgroups_subcontrollers()
|
||||||
return cls._convert_with_links(node, pecan.request.public_url,
|
return cls._convert_with_links(node, pecan.request.public_url,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
show_states_links=show_states_links)
|
show_states_links=show_states_links,
|
||||||
|
show_portgroups=show_portgroups)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls, expand=True):
|
def sample(cls, expand=True):
|
||||||
@ -1059,7 +1073,8 @@ class NodesController(rest.RestController):
|
|||||||
'clean_step', 'raid_config', 'target_raid_config']
|
'clean_step', 'raid_config', 'target_raid_config']
|
||||||
|
|
||||||
_subcontroller_map = {
|
_subcontroller_map = {
|
||||||
'ports': port.PortsController
|
'ports': port.PortsController,
|
||||||
|
'portgroups': portgroup.PortgroupsController,
|
||||||
}
|
}
|
||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
@ -1071,6 +1086,9 @@ class NodesController(rest.RestController):
|
|||||||
if remainder:
|
if remainder:
|
||||||
subcontroller = self._subcontroller_map.get(remainder[0])
|
subcontroller = self._subcontroller_map.get(remainder[0])
|
||||||
if subcontroller:
|
if subcontroller:
|
||||||
|
if (remainder[0] == 'portgroups' and
|
||||||
|
not api_utils.allow_portgroups_subcontrollers()):
|
||||||
|
pecan.abort(http_client.NOT_FOUND)
|
||||||
return subcontroller(node_ident=ident), remainder[1:]
|
return subcontroller(node_ident=ident), remainder[1:]
|
||||||
|
|
||||||
def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated,
|
def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated,
|
||||||
|
@ -49,6 +49,9 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
if not api_utils.allow_port_advanced_net_fields():
|
if not api_utils.allow_port_advanced_net_fields():
|
||||||
obj.pxe_enabled = wsme.Unset
|
obj.pxe_enabled = wsme.Unset
|
||||||
obj.local_link_connection = wsme.Unset
|
obj.local_link_connection = wsme.Unset
|
||||||
|
# if requested version is < 1.24, hide portgroup_uuid field
|
||||||
|
if not api_utils.allow_portgroups_subcontrollers():
|
||||||
|
obj.portgroup_uuid = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
class Port(base.APIBase):
|
class Port(base.APIBase):
|
||||||
@ -59,6 +62,7 @@ class Port(base.APIBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_node_uuid = None
|
_node_uuid = None
|
||||||
|
_portgroup_uuid = None
|
||||||
|
|
||||||
def _get_node_uuid(self):
|
def _get_node_uuid(self):
|
||||||
return self._node_uuid
|
return self._node_uuid
|
||||||
@ -83,6 +87,36 @@ class Port(base.APIBase):
|
|||||||
elif value == wtypes.Unset:
|
elif value == wtypes.Unset:
|
||||||
self._node_uuid = wtypes.Unset
|
self._node_uuid = wtypes.Unset
|
||||||
|
|
||||||
|
def _get_portgroup_uuid(self):
|
||||||
|
return self._portgroup_uuid
|
||||||
|
|
||||||
|
def _set_portgroup_uuid(self, value):
|
||||||
|
if value and self._portgroup_uuid != value:
|
||||||
|
if not api_utils.allow_portgroups_subcontrollers():
|
||||||
|
self._portgroup_uuid = wtypes.Unset
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
portgroup = objects.Portgroup.get(pecan.request.context, value)
|
||||||
|
if portgroup.node_id != self.node_id:
|
||||||
|
raise exception.BadRequest(_('Port can not be added to a '
|
||||||
|
'portgroup belonging to a '
|
||||||
|
'different node.'))
|
||||||
|
self._portgroup_uuid = portgroup.uuid
|
||||||
|
# NOTE(lucasagomes): Create the portgroup_id attribute
|
||||||
|
# on-the-fly to satisfy the api ->
|
||||||
|
# rpc object conversion.
|
||||||
|
self.portgroup_id = portgroup.id
|
||||||
|
except exception.PortgroupNotFound as e:
|
||||||
|
# Change error code because 404 (NotFound) is inappropriate
|
||||||
|
# response for a POST request to create a Port
|
||||||
|
e.code = http_client.BAD_REQUEST # BadRequest
|
||||||
|
raise e
|
||||||
|
elif value == wtypes.Unset:
|
||||||
|
self._portgroup_uuid = wtypes.Unset
|
||||||
|
elif value is None and api_utils.allow_portgroups_subcontrollers():
|
||||||
|
# This is to output portgroup_uuid field if API version allows this
|
||||||
|
self._portgroup_uuid = None
|
||||||
|
|
||||||
uuid = types.uuid
|
uuid = types.uuid
|
||||||
"""Unique UUID for this port"""
|
"""Unique UUID for this port"""
|
||||||
|
|
||||||
@ -99,6 +133,10 @@ class Port(base.APIBase):
|
|||||||
mandatory=True)
|
mandatory=True)
|
||||||
"""The UUID of the node this port belongs to"""
|
"""The UUID of the node this port belongs to"""
|
||||||
|
|
||||||
|
portgroup_uuid = wsme.wsproperty(types.uuid, _get_portgroup_uuid,
|
||||||
|
_set_portgroup_uuid, mandatory=False)
|
||||||
|
"""The UUID of the portgroup this port belongs to"""
|
||||||
|
|
||||||
pxe_enabled = types.boolean
|
pxe_enabled = types.boolean
|
||||||
"""Indicates whether pxe is enabled or disabled on the node."""
|
"""Indicates whether pxe is enabled or disabled on the node."""
|
||||||
|
|
||||||
@ -114,6 +152,9 @@ class Port(base.APIBase):
|
|||||||
# NOTE(lucasagomes): node_uuid is not part of objects.Port.fields
|
# NOTE(lucasagomes): node_uuid is not part of objects.Port.fields
|
||||||
# because it's an API-only attribute
|
# because it's an API-only attribute
|
||||||
fields.append('node_uuid')
|
fields.append('node_uuid')
|
||||||
|
# NOTE: portgroup_uuid is not part of objects.Port.fields
|
||||||
|
# because it's an API-only attribute
|
||||||
|
fields.append('portgroup_uuid')
|
||||||
for field in fields:
|
for field in fields:
|
||||||
# Add fields we expose.
|
# Add fields we expose.
|
||||||
if hasattr(self, field):
|
if hasattr(self, field):
|
||||||
@ -127,6 +168,14 @@ class Port(base.APIBase):
|
|||||||
self.fields.append('node_id')
|
self.fields.append('node_id')
|
||||||
setattr(self, 'node_uuid', kwargs.get('node_id', wtypes.Unset))
|
setattr(self, 'node_uuid', kwargs.get('node_id', wtypes.Unset))
|
||||||
|
|
||||||
|
# NOTE: portgroup_id is an attribute created on-the-fly
|
||||||
|
# by _set_portgroup_uuid(), it needs to be present in the fields so
|
||||||
|
# that as_dict() will contain portgroup_id field when converting it
|
||||||
|
# before saving it in the database.
|
||||||
|
self.fields.append('portgroup_id')
|
||||||
|
setattr(self, 'portgroup_uuid', kwargs.get('portgroup_id',
|
||||||
|
wtypes.Unset))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(port, url, fields=None):
|
def _convert_with_links(port, url, fields=None):
|
||||||
# NOTE(lucasagomes): Since we are able to return a specified set of
|
# NOTE(lucasagomes): Since we are able to return a specified set of
|
||||||
@ -139,6 +188,9 @@ class Port(base.APIBase):
|
|||||||
# never expose the node_id attribute
|
# never expose the node_id attribute
|
||||||
port.node_id = wtypes.Unset
|
port.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
# never expose the portgroup_id attribute
|
||||||
|
port.portgroup_id = wtypes.Unset
|
||||||
|
|
||||||
port.links = [link.Link.make_link('self', url,
|
port.links = [link.Link.make_link('self', url,
|
||||||
'ports', port_uuid),
|
'ports', port_uuid),
|
||||||
link.Link.make_link('bookmark', url,
|
link.Link.make_link('bookmark', url,
|
||||||
@ -174,6 +226,7 @@ class Port(base.APIBase):
|
|||||||
# NOTE(lucasagomes): node_uuid getter() method look at the
|
# NOTE(lucasagomes): node_uuid getter() method look at the
|
||||||
# _node_uuid variable
|
# _node_uuid variable
|
||||||
sample._node_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
sample._node_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||||
|
sample._portgroup_uuid = '037d9a52-af89-4560-b5a3-a33283295ba2'
|
||||||
fields = None if expand else _DEFAULT_RETURN_FIELDS
|
fields = None if expand else _DEFAULT_RETURN_FIELDS
|
||||||
return cls._convert_with_links(sample, 'http://localhost:6385',
|
return cls._convert_with_links(sample, 'http://localhost:6385',
|
||||||
fields=fields)
|
fields=fields)
|
||||||
@ -223,13 +276,14 @@ class PortsController(rest.RestController):
|
|||||||
|
|
||||||
advanced_net_fields = ['pxe_enabled', 'local_link_connection']
|
advanced_net_fields = ['pxe_enabled', 'local_link_connection']
|
||||||
|
|
||||||
def __init__(self, node_ident=None):
|
def __init__(self, node_ident=None, portgroup_ident=None):
|
||||||
super(PortsController, self).__init__()
|
super(PortsController, self).__init__()
|
||||||
self.parent_node_ident = node_ident
|
self.parent_node_ident = node_ident
|
||||||
|
self.parent_portgroup_ident = portgroup_ident
|
||||||
|
|
||||||
def _get_ports_collection(self, node_ident, address, marker, limit,
|
def _get_ports_collection(self, node_ident, address, portgroup_ident,
|
||||||
sort_key, sort_dir, resource_url=None,
|
marker, limit, sort_key, sort_dir,
|
||||||
fields=None):
|
resource_url=None, fields=None):
|
||||||
|
|
||||||
limit = api_utils.validate_limit(limit)
|
limit = api_utils.validate_limit(limit)
|
||||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||||
@ -245,7 +299,23 @@ class PortsController(rest.RestController):
|
|||||||
"sorting") % {'key': sort_key})
|
"sorting") % {'key': sort_key})
|
||||||
|
|
||||||
node_ident = self.parent_node_ident or node_ident
|
node_ident = self.parent_node_ident or node_ident
|
||||||
if node_ident:
|
portgroup_ident = self.parent_portgroup_ident or portgroup_ident
|
||||||
|
|
||||||
|
if node_ident and portgroup_ident:
|
||||||
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
|
if portgroup_ident:
|
||||||
|
# FIXME: Since all we need is the portgroup ID, we can
|
||||||
|
# make this more efficient by only querying
|
||||||
|
# for that column. This will get cleaned up
|
||||||
|
# as we move to the object interface.
|
||||||
|
portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
||||||
|
ports = objects.Port.list_by_portgroup_id(pecan.request.context,
|
||||||
|
portgroup.id, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif node_ident:
|
||||||
# FIXME(comstud): Since all we need is the node ID, we can
|
# FIXME(comstud): Since all we need is the node ID, we can
|
||||||
# make this more efficient by only querying
|
# make this more efficient by only querying
|
||||||
# for that column. This will get cleaned up
|
# for that column. This will get cleaned up
|
||||||
@ -285,9 +355,10 @@ class PortsController(rest.RestController):
|
|||||||
@METRICS.timer('PortsController.get_all')
|
@METRICS.timer('PortsController.get_all')
|
||||||
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
|
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
|
||||||
types.macaddress, types.uuid, int, wtypes.text,
|
types.macaddress, types.uuid, int, wtypes.text,
|
||||||
wtypes.text, types.listtype)
|
wtypes.text, types.listtype, types.uuid_or_name)
|
||||||
def get_all(self, node=None, node_uuid=None, address=None, marker=None,
|
def get_all(self, node=None, node_uuid=None, address=None, marker=None,
|
||||||
limit=None, sort_key='id', sort_dir='asc', fields=None):
|
limit=None, sort_key='id', sort_dir='asc', fields=None,
|
||||||
|
portgroup=None):
|
||||||
"""Retrieve a list of ports.
|
"""Retrieve a list of ports.
|
||||||
|
|
||||||
Note that the 'node_uuid' interface is deprecated in favour
|
Note that the 'node_uuid' interface is deprecated in favour
|
||||||
@ -308,14 +379,23 @@ class PortsController(rest.RestController):
|
|||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param fields: Optional, a list with a specified set of fields
|
:param fields: Optional, a list with a specified set of fields
|
||||||
of the resource to be returned.
|
of the resource to be returned.
|
||||||
:raises: NotAcceptable
|
:param portgroup: UUID or name of a portgroup, to get only ports
|
||||||
|
for that portgroup.
|
||||||
|
:raises: NotAcceptable, HTTPNotFound
|
||||||
"""
|
"""
|
||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:get', cdict, cdict)
|
policy.authorize('baremetal:port:get', cdict, cdict)
|
||||||
|
|
||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
if (fields and not api_utils.allow_port_advanced_net_fields() and
|
if fields:
|
||||||
set(fields).intersection(self.advanced_net_fields)):
|
if (not api_utils.allow_port_advanced_net_fields() and
|
||||||
|
set(fields).intersection(self.advanced_net_fields)):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
if ('portgroup_uuid' in fields and not
|
||||||
|
api_utils.allow_portgroups_subcontrollers()):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
if portgroup and not api_utils.allow_portgroups_subcontrollers():
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
if fields is None:
|
if fields is None:
|
||||||
@ -329,16 +409,16 @@ class PortsController(rest.RestController):
|
|||||||
not uuidutils.is_uuid_like(node)):
|
not uuidutils.is_uuid_like(node)):
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
return self._get_ports_collection(node_uuid or node, address, marker,
|
return self._get_ports_collection(node_uuid or node, address,
|
||||||
limit, sort_key, sort_dir,
|
portgroup, marker, limit, sort_key,
|
||||||
fields=fields)
|
sort_dir, fields=fields)
|
||||||
|
|
||||||
@METRICS.timer('PortsController.detail')
|
@METRICS.timer('PortsController.detail')
|
||||||
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
|
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
|
||||||
types.macaddress, types.uuid, int, wtypes.text,
|
types.macaddress, types.uuid, int, wtypes.text,
|
||||||
wtypes.text)
|
wtypes.text, types.uuid_or_name)
|
||||||
def detail(self, node=None, node_uuid=None, address=None, marker=None,
|
def detail(self, node=None, node_uuid=None, address=None, marker=None,
|
||||||
limit=None, sort_key='id', sort_dir='asc'):
|
limit=None, sort_key='id', sort_dir='asc', portgroup=None):
|
||||||
"""Retrieve a list of ports with detail.
|
"""Retrieve a list of ports with detail.
|
||||||
|
|
||||||
Note that the 'node_uuid' interface is deprecated in favour
|
Note that the 'node_uuid' interface is deprecated in favour
|
||||||
@ -350,6 +430,8 @@ class PortsController(rest.RestController):
|
|||||||
node.
|
node.
|
||||||
:param address: MAC address of a port, to get the port which has
|
:param address: MAC address of a port, to get the port which has
|
||||||
this MAC address.
|
this MAC address.
|
||||||
|
:param portgroup: UUID or name of a portgroup, to get only ports
|
||||||
|
for that portgroup.
|
||||||
:param marker: pagination marker for large data sets.
|
:param marker: pagination marker for large data sets.
|
||||||
:param limit: maximum number of resources to return in a single result.
|
:param limit: maximum number of resources to return in a single result.
|
||||||
This value cannot be larger than the value of max_limit
|
This value cannot be larger than the value of max_limit
|
||||||
@ -362,6 +444,9 @@ class PortsController(rest.RestController):
|
|||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:get', cdict, cdict)
|
policy.authorize('baremetal:port:get', cdict, cdict)
|
||||||
|
|
||||||
|
if portgroup and not api_utils.allow_portgroups_subcontrollers():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
if not node_uuid and node:
|
if not node_uuid and node:
|
||||||
# We're invoking this interface using positional notation, or
|
# We're invoking this interface using positional notation, or
|
||||||
# explicitly using 'node'. Try and determine which one.
|
# explicitly using 'node'. Try and determine which one.
|
||||||
@ -376,9 +461,9 @@ class PortsController(rest.RestController):
|
|||||||
raise exception.HTTPNotFound()
|
raise exception.HTTPNotFound()
|
||||||
|
|
||||||
resource_url = '/'.join(['ports', 'detail'])
|
resource_url = '/'.join(['ports', 'detail'])
|
||||||
return self._get_ports_collection(node_uuid or node, address, marker,
|
return self._get_ports_collection(node_uuid or node, address,
|
||||||
limit, sort_key, sort_dir,
|
portgroup, marker, limit, sort_key,
|
||||||
resource_url)
|
sort_dir, resource_url)
|
||||||
|
|
||||||
@METRICS.timer('PortsController.get_one')
|
@METRICS.timer('PortsController.get_one')
|
||||||
@expose.expose(Port, types.uuid, types.listtype)
|
@expose.expose(Port, types.uuid, types.listtype)
|
||||||
@ -388,12 +473,12 @@ class PortsController(rest.RestController):
|
|||||||
:param port_uuid: UUID of a port.
|
:param port_uuid: UUID of a port.
|
||||||
:param fields: Optional, a list with a specified set of fields
|
:param fields: Optional, a list with a specified set of fields
|
||||||
of the resource to be returned.
|
of the resource to be returned.
|
||||||
:raises: NotAcceptable
|
:raises: NotAcceptable, HTTPNotFound
|
||||||
"""
|
"""
|
||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:get', cdict, cdict)
|
policy.authorize('baremetal:port:get', cdict, cdict)
|
||||||
|
|
||||||
if self.parent_node_ident:
|
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
@ -407,18 +492,21 @@ class PortsController(rest.RestController):
|
|||||||
"""Create a new port.
|
"""Create a new port.
|
||||||
|
|
||||||
:param port: a port within the request body.
|
:param port: a port within the request body.
|
||||||
:raises: NotAcceptable
|
:raises: NotAcceptable, HTTPNotFound
|
||||||
"""
|
"""
|
||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:create', cdict, cdict)
|
policy.authorize('baremetal:port:create', cdict, cdict)
|
||||||
|
|
||||||
if self.parent_node_ident:
|
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
pdict = port.as_dict()
|
pdict = port.as_dict()
|
||||||
if not api_utils.allow_port_advanced_net_fields():
|
if (not api_utils.allow_port_advanced_net_fields() and
|
||||||
if set(pdict).intersection(self.advanced_net_fields):
|
set(pdict).intersection(self.advanced_net_fields)):
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
if (not api_utils.allow_portgroups_subcontrollers() and
|
||||||
|
'portgroup_uuid' in pdict):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
new_port = objects.Port(pecan.request.context,
|
new_port = objects.Port(pecan.request.context,
|
||||||
**pdict)
|
**pdict)
|
||||||
@ -436,19 +524,26 @@ class PortsController(rest.RestController):
|
|||||||
|
|
||||||
:param port_uuid: UUID of a port.
|
:param port_uuid: UUID of a port.
|
||||||
:param patch: a json PATCH document to apply to this port.
|
:param patch: a json PATCH document to apply to this port.
|
||||||
:raises: NotAcceptable
|
:raises: NotAcceptable, HTTPNotFound
|
||||||
"""
|
"""
|
||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:update', cdict, cdict)
|
policy.authorize('baremetal:port:update', cdict, cdict)
|
||||||
|
|
||||||
if self.parent_node_ident:
|
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
if not api_utils.allow_port_advanced_net_fields():
|
|
||||||
for field in self.advanced_net_fields:
|
fields_to_check = set()
|
||||||
field_path = '/%s' % field
|
for field in self.advanced_net_fields + ['portgroup_uuid']:
|
||||||
if (api_utils.get_patch_values(patch, field_path) or
|
field_path = '/%s' % field
|
||||||
api_utils.is_path_removed(patch, field_path)):
|
if (api_utils.get_patch_values(patch, field_path) or
|
||||||
raise exception.NotAcceptable()
|
api_utils.is_path_removed(patch, field_path)):
|
||||||
|
fields_to_check.add(field)
|
||||||
|
if (fields_to_check.intersection(self.advanced_net_fields) and
|
||||||
|
not api_utils.allow_port_advanced_net_fields()):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
if ('portgroup_uuid' in fields_to_check and
|
||||||
|
not api_utils.allow_portgroups_subcontrollers()):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid)
|
rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid)
|
||||||
try:
|
try:
|
||||||
@ -458,10 +553,18 @@ class PortsController(rest.RestController):
|
|||||||
# not present in the API object
|
# not present in the API object
|
||||||
# 2) Add node_uuid
|
# 2) Add node_uuid
|
||||||
port_dict['node_uuid'] = port_dict.pop('node_id', None)
|
port_dict['node_uuid'] = port_dict.pop('node_id', None)
|
||||||
|
# NOTE(vsaienko):
|
||||||
|
# 1) Remove portgroup_id because it's an internal value and
|
||||||
|
# not present in the API object
|
||||||
|
# 2) Add portgroup_uuid
|
||||||
|
port_dict['portgroup_uuid'] = port_dict.pop('portgroup_id', None)
|
||||||
port = Port(**api_utils.apply_jsonpatch(port_dict, patch))
|
port = Port(**api_utils.apply_jsonpatch(port_dict, patch))
|
||||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
raise exception.PatchError(patch=patch, reason=e)
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
if api_utils.is_path_removed(patch, '/portgroup_uuid'):
|
||||||
|
rpc_port.portgroup_id = None
|
||||||
|
|
||||||
# Update only the fields that have changed
|
# Update only the fields that have changed
|
||||||
for field in objects.Port.fields:
|
for field in objects.Port.fields:
|
||||||
try:
|
try:
|
||||||
@ -489,12 +592,14 @@ class PortsController(rest.RestController):
|
|||||||
"""Delete a port.
|
"""Delete a port.
|
||||||
|
|
||||||
:param port_uuid: UUID of a port.
|
:param port_uuid: UUID of a port.
|
||||||
|
:raises OperationNotPermitted, HTTPNotFound
|
||||||
"""
|
"""
|
||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:port:delete', cdict, cdict)
|
policy.authorize('baremetal:port:delete', cdict, cdict)
|
||||||
|
|
||||||
if self.parent_node_ident:
|
if self.parent_node_ident or self.parent_portgroup_ident:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_port = objects.Port.get_by_uuid(pecan.request.context,
|
rpc_port = objects.Port.get_by_uuid(pecan.request.context,
|
||||||
port_uuid)
|
port_uuid)
|
||||||
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
||||||
|
@ -21,6 +21,7 @@ from wsme import types as wtypes
|
|||||||
from ironic.api.controllers import base
|
from ironic.api.controllers import base
|
||||||
from ironic.api.controllers import link
|
from ironic.api.controllers import link
|
||||||
from ironic.api.controllers.v1 import collection
|
from ironic.api.controllers.v1 import collection
|
||||||
|
from ironic.api.controllers.v1 import port
|
||||||
from ironic.api.controllers.v1 import types
|
from ironic.api.controllers.v1 import types
|
||||||
from ironic.api.controllers.v1 import utils as api_utils
|
from ironic.api.controllers.v1 import utils as api_utils
|
||||||
from ironic.api import expose
|
from ironic.api import expose
|
||||||
@ -93,6 +94,9 @@ class Portgroup(base.APIBase):
|
|||||||
"""Indicates whether ports of this portgroup may be used as
|
"""Indicates whether ports of this portgroup may be used as
|
||||||
single NIC ports"""
|
single NIC ports"""
|
||||||
|
|
||||||
|
ports = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""Links to the collection of ports of this portgroup"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.fields = []
|
self.fields = []
|
||||||
fields = list(objects.Portgroup.fields)
|
fields = list(objects.Portgroup.fields)
|
||||||
@ -216,6 +220,30 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
|
|
||||||
invalid_sort_key_list = ['extra', 'internal_info']
|
invalid_sort_key_list = ['extra', 'internal_info']
|
||||||
|
|
||||||
|
_subcontroller_map = {
|
||||||
|
'ports': port.PortsController,
|
||||||
|
}
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def _lookup(self, ident, subres, *remainder):
|
||||||
|
if not api_utils.allow_portgroups():
|
||||||
|
pecan.abort(http_client.NOT_FOUND)
|
||||||
|
try:
|
||||||
|
ident = types.uuid_or_name.validate(ident)
|
||||||
|
except exception.InvalidUuidOrName as e:
|
||||||
|
pecan.abort(http_client.BAD_REQUEST, e.args[0])
|
||||||
|
subcontroller = self._subcontroller_map.get(subres)
|
||||||
|
if subcontroller:
|
||||||
|
if api_utils.allow_portgroups_subcontrollers():
|
||||||
|
return subcontroller(
|
||||||
|
portgroup_ident=ident,
|
||||||
|
node_ident=self.parent_node_ident), remainder
|
||||||
|
pecan.abort(http_client.NOT_FOUND)
|
||||||
|
|
||||||
|
def __init__(self, node_ident=None):
|
||||||
|
super(PortgroupsController, self).__init__()
|
||||||
|
self.parent_node_ident = node_ident
|
||||||
|
|
||||||
def _get_portgroups_collection(self, node_ident, address,
|
def _get_portgroups_collection(self, node_ident, address,
|
||||||
marker, limit, sort_key, sort_dir,
|
marker, limit, sort_key, sort_dir,
|
||||||
resource_url=None, fields=None):
|
resource_url=None, fields=None):
|
||||||
@ -244,6 +272,8 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
_("The sort_key value %(key)s is an invalid field for "
|
_("The sort_key value %(key)s is an invalid field for "
|
||||||
"sorting") % {'key': sort_key})
|
"sorting") % {'key': sort_key})
|
||||||
|
|
||||||
|
node_ident = self.parent_node_ident or node_ident
|
||||||
|
|
||||||
if node_ident:
|
if node_ident:
|
||||||
# FIXME: Since all we need is the node ID, we can
|
# FIXME: Since all we need is the node ID, we can
|
||||||
# make this more efficient by only querying
|
# make this more efficient by only querying
|
||||||
@ -367,6 +397,9 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:portgroup:get', cdict, cdict)
|
policy.authorize('baremetal:portgroup:get', cdict, cdict)
|
||||||
|
|
||||||
|
if self.parent_node_ident:
|
||||||
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
||||||
return Portgroup.convert_with_links(rpc_portgroup, fields=fields)
|
return Portgroup.convert_with_links(rpc_portgroup, fields=fields)
|
||||||
|
|
||||||
@ -383,6 +416,9 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:portgroup:create', cdict, cdict)
|
policy.authorize('baremetal:portgroup:create', cdict, cdict)
|
||||||
|
|
||||||
|
if self.parent_node_ident:
|
||||||
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
if (portgroup.name and
|
if (portgroup.name and
|
||||||
not api_utils.is_valid_logical_name(portgroup.name)):
|
not api_utils.is_valid_logical_name(portgroup.name)):
|
||||||
error_msg = _("Cannot create portgroup with invalid name "
|
error_msg = _("Cannot create portgroup with invalid name "
|
||||||
@ -413,6 +449,9 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:portgroup:update', cdict, cdict)
|
policy.authorize('baremetal:portgroup:update', cdict, cdict)
|
||||||
|
|
||||||
|
if self.parent_node_ident:
|
||||||
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
||||||
|
|
||||||
names = api_utils.get_patch_values(patch, '/name')
|
names = api_utils.get_patch_values(patch, '/name')
|
||||||
@ -473,6 +512,9 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
cdict = pecan.request.context.to_dict()
|
cdict = pecan.request.context.to_dict()
|
||||||
policy.authorize('baremetal:portgroup:delete', cdict, cdict)
|
policy.authorize('baremetal:portgroup:delete', cdict, cdict)
|
||||||
|
|
||||||
|
if self.parent_node_ident:
|
||||||
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
||||||
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
||||||
rpc_portgroup.node_id)
|
rpc_portgroup.node_id)
|
||||||
|
@ -422,6 +422,16 @@ def allow_portgroups():
|
|||||||
versions.MINOR_23_PORTGROUPS)
|
versions.MINOR_23_PORTGROUPS)
|
||||||
|
|
||||||
|
|
||||||
|
def allow_portgroups_subcontrollers():
|
||||||
|
"""Check if portgroups can be used as subcontrollers.
|
||||||
|
|
||||||
|
Version 1.24 of the API added support for Portgroups as
|
||||||
|
subcontrollers
|
||||||
|
"""
|
||||||
|
return (pecan.request.version.minor >=
|
||||||
|
versions.MINOR_24_PORTGROUPS_SUBCONTROLLERS)
|
||||||
|
|
||||||
|
|
||||||
def get_controller_reserved_names(cls):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -53,6 +53,8 @@ BASE_VERSION = 1
|
|||||||
# v1.21: Add node.resource_class
|
# v1.21: Add node.resource_class
|
||||||
# v1.22: Ramdisk lookup and heartbeat endpoints.
|
# v1.22: Ramdisk lookup and heartbeat endpoints.
|
||||||
# v1.23: Add portgroup support.
|
# v1.23: Add portgroup support.
|
||||||
|
# v1.24: Add subcontrollers: node.portgroup, portgroup.ports.
|
||||||
|
# Add port.portgroup_uuid field.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -78,11 +80,12 @@ MINOR_20_NETWORK_INTERFACE = 20
|
|||||||
MINOR_21_RESOURCE_CLASS = 21
|
MINOR_21_RESOURCE_CLASS = 21
|
||||||
MINOR_22_LOOKUP_HEARTBEAT = 22
|
MINOR_22_LOOKUP_HEARTBEAT = 22
|
||||||
MINOR_23_PORTGROUPS = 23
|
MINOR_23_PORTGROUPS = 23
|
||||||
|
MINOR_24_PORTGROUPS_SUBCONTROLLERS = 24
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
||||||
# what the version has changed.
|
# what the version has changed.
|
||||||
MINOR_MAX_VERSION = MINOR_23_PORTGROUPS
|
MINOR_MAX_VERSION = MINOR_24_PORTGROUPS_SUBCONTROLLERS
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -223,7 +223,7 @@ class BaseApiTest(base.DbTestCase):
|
|||||||
print('GOT:%s' % response)
|
print('GOT:%s' % response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def validate_link(self, link, bookmark=False):
|
def validate_link(self, link, bookmark=False, headers=None):
|
||||||
"""Checks if the given link can get correct data."""
|
"""Checks if the given link can get correct data."""
|
||||||
# removes the scheme and net location parts of the link
|
# removes the scheme and net location parts of the link
|
||||||
url_parts = list(urlparse.urlparse(link))
|
url_parts = list(urlparse.urlparse(link))
|
||||||
@ -235,7 +235,7 @@ class BaseApiTest(base.DbTestCase):
|
|||||||
|
|
||||||
full_path = urlparse.urlunparse(url_parts)
|
full_path = urlparse.urlunparse(url_parts)
|
||||||
try:
|
try:
|
||||||
self.get_json(full_path, path_prefix='')
|
self.get_json(full_path, path_prefix='', headers=headers)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
@ -459,6 +459,47 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
data = self.get_json('/nodes/%s' % node.uuid)
|
data = self.get_json('/nodes/%s' % node.uuid)
|
||||||
self.assertIn('ports', data)
|
self.assertIn('ports', data)
|
||||||
|
|
||||||
|
def test_portgroups_subresource(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
headers = {'X-OpenStack-Ironic-API-Version': '1.24'}
|
||||||
|
for id_ in range(2):
|
||||||
|
obj_utils.create_test_portgroup(self.context, node_id=node.id,
|
||||||
|
name="pg-%s" % id_,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='52:54:00:cf:2d:3%s' % id_)
|
||||||
|
|
||||||
|
data = self.get_json('/nodes/%s/portgroups' % node.uuid,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(2, len(data['portgroups']))
|
||||||
|
self.assertNotIn('next', data)
|
||||||
|
|
||||||
|
# Test collection pagination
|
||||||
|
data = self.get_json('/nodes/%s/portgroups?limit=1' % node.uuid,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(1, len(data['portgroups']))
|
||||||
|
self.assertIn('next', data)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_link(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.uuid,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.24'})
|
||||||
|
self.assertIn('portgroups', data.keys())
|
||||||
|
|
||||||
|
def test_portgroups_subresource_link_hidden_for_older_versions(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.uuid,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.20'})
|
||||||
|
self.assertNotIn('portgroups', data.keys())
|
||||||
|
|
||||||
|
def test_portgroups_subresource_old_api_version(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s/portgroups' % node.uuid, expect_errors=True,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.23'})
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
def test_ports_subresource(self):
|
def test_ports_subresource(self):
|
||||||
node = obj_utils.create_test_node(self.context)
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
|
||||||
@ -497,6 +538,15 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('Expected a logical name or UUID',
|
self.assertIn('Expected a logical name or UUID',
|
||||||
response.json['error_message'])
|
response.json['error_message'])
|
||||||
|
|
||||||
|
def test_ports_subresource_via_portgroups_subres_not_allowed(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=node.id)
|
||||||
|
response = self.get_json('/nodes/%s/portgroups/%s/ports' % (
|
||||||
|
node.uuid, pg.uuid), expect_errors=True,
|
||||||
|
headers={api_base.Version.string: '1.24'})
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
@mock.patch.object(timeutils, 'utcnow')
|
@mock.patch.object(timeutils, 'utcnow')
|
||||||
def _test_node_states(self, mock_utcnow, api_version=None):
|
def _test_node_states(self, mock_utcnow, api_version=None):
|
||||||
fake_state = 'fake-state'
|
fake_state = 'fake-state'
|
||||||
@ -1210,6 +1260,15 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
'op': 'add'}], expect_errors=True)
|
'op': 'add'}], expect_errors=True)
|
||||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
def test_patch_portgroups_subresource(self):
|
||||||
|
response = self.patch_json(
|
||||||
|
'/nodes/%s/portgroups/9bb50f13-0b8d-4ade-ad2d-d91fefdef9cc' %
|
||||||
|
self.node.uuid,
|
||||||
|
[{'path': '/extra/foo', 'value': 'bar',
|
||||||
|
'op': 'add'}], expect_errors=True,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.24'})
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
def test_remove_uuid(self):
|
def test_remove_uuid(self):
|
||||||
response = self.patch_json('/nodes/%s' % self.node.uuid,
|
response = self.patch_json('/nodes/%s' % self.node.uuid,
|
||||||
[{'path': '/uuid', 'op': 'remove'}],
|
[{'path': '/uuid', 'op': 'remove'}],
|
||||||
@ -1929,6 +1988,15 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
def test_post_portgroups_subresource(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
pgdict = test_api_utils.portgroup_post_data(node_id=None)
|
||||||
|
pgdict['node_uuid'] = node.uuid
|
||||||
|
response = self.post_json(
|
||||||
|
'/nodes/%s/portgroups' % node.uuid, pgdict, expect_errors=True,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.24'})
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
def test_create_node_no_mandatory_field_driver(self):
|
def test_create_node_no_mandatory_field_driver(self):
|
||||||
ndict = test_api_utils.post_get_test_node()
|
ndict = test_api_utils.post_get_test_node()
|
||||||
del ndict['driver']
|
del ndict['driver']
|
||||||
@ -2134,6 +2202,16 @@ class TestDelete(test_api_base.BaseApiTest):
|
|||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
def test_delete_portgroup_subresource(self):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context, node_id=node.id)
|
||||||
|
response = self.delete(
|
||||||
|
'/nodes/%(node_uuid)s/portgroups/%(pg_uuid)s' %
|
||||||
|
{'node_uuid': node.uuid, 'pg_uuid': pg.uuid},
|
||||||
|
expect_errors=True,
|
||||||
|
headers={'X-OpenStack-Ironic-API-Version': '1.24'})
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||||
def test_delete_associated(self, mock_dn):
|
def test_delete_associated(self, mock_dn):
|
||||||
node = obj_utils.create_test_node(
|
node = obj_utils.create_test_node(
|
||||||
|
@ -171,6 +171,21 @@ class TestListPortgroups(test_api_base.BaseApiTest):
|
|||||||
uuids = [n['uuid'] for n in data['portgroups']]
|
uuids = [n['uuid'] for n in data['portgroups']]
|
||||||
six.assertCountEqual(self, portgroups, uuids)
|
six.assertCountEqual(self, portgroups, uuids)
|
||||||
|
|
||||||
|
def test_links(self):
|
||||||
|
uuid = uuidutils.generate_uuid()
|
||||||
|
obj_utils.create_test_portgroup(self.context,
|
||||||
|
uuid=uuid,
|
||||||
|
node_id=self.node.id)
|
||||||
|
data = self.get_json('/portgroups/%s' % uuid, headers=self.headers)
|
||||||
|
self.assertIn('links', data)
|
||||||
|
self.assertIn('ports', data)
|
||||||
|
self.assertEqual(2, len(data['links']))
|
||||||
|
self.assertIn(uuid, data['links'][0]['href'])
|
||||||
|
for l in data['links']:
|
||||||
|
bookmark = l['rel'] == 'bookmark'
|
||||||
|
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark,
|
||||||
|
headers=self.headers))
|
||||||
|
|
||||||
def test_collection_links(self):
|
def test_collection_links(self):
|
||||||
portgroups = []
|
portgroups = []
|
||||||
for id_ in range(5):
|
for id_ in range(5):
|
||||||
@ -204,6 +219,47 @@ class TestListPortgroups(test_api_base.BaseApiTest):
|
|||||||
next_marker = data['portgroups'][-1]['uuid']
|
next_marker = data['portgroups'][-1]['uuid']
|
||||||
self.assertIn(next_marker, data['next'])
|
self.assertIn(next_marker, data['next'])
|
||||||
|
|
||||||
|
def test_ports_subresource(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
node_id=self.node.id)
|
||||||
|
|
||||||
|
for id_ in range(2):
|
||||||
|
obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
portgroup_id=pg.id,
|
||||||
|
address='52:54:00:cf:2d:3%s' % id_)
|
||||||
|
|
||||||
|
data = self.get_json('/portgroups/%s/ports' % pg.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(2, len(data['ports']))
|
||||||
|
self.assertNotIn('next', data.keys())
|
||||||
|
|
||||||
|
data = self.get_json('/portgroups/%s/ports/detail' % pg.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(2, len(data['ports']))
|
||||||
|
self.assertNotIn('next', data.keys())
|
||||||
|
|
||||||
|
# Test collection pagination
|
||||||
|
data = self.get_json('/portgroups/%s/ports?limit=1' % pg.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(1, len(data['ports']))
|
||||||
|
self.assertIn('next', data.keys())
|
||||||
|
|
||||||
|
# Test get one old api version, /portgroups controller not allowed
|
||||||
|
response = self.get_json('/portgroups/%s/ports/%s' % (
|
||||||
|
pg.uuid, uuidutils.generate_uuid()),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
# Test get one not allowed to access to /portgroups/<uuid>/ports/<uuid>
|
||||||
|
response = self.get_json(
|
||||||
|
'/portgroups/%s/ports/%s' % (pg.uuid, uuidutils.generate_uuid()),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
def test_ports_subresource_no_portgroups_allowed(self):
|
def test_ports_subresource_no_portgroups_allowed(self):
|
||||||
pg = obj_utils.create_test_portgroup(self.context,
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
@ -220,11 +276,32 @@ class TestListPortgroups(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
|
||||||
|
def test_get_all_ports_by_portgroup_uuid(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
portgroup_id=pg.id)
|
||||||
|
data = self.get_json('/portgroups/%s/ports' % pg.uuid,
|
||||||
|
headers={api_base.Version.string: '1.24'})
|
||||||
|
self.assertEqual(port.uuid, data['ports'][0]['uuid'])
|
||||||
|
|
||||||
|
def test_ports_subresource_not_allowed(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
response = self.get_json('/portgroups/%s/ports' % pg.uuid,
|
||||||
|
expect_errors=True,
|
||||||
|
headers={api_base.Version.string: '1.23'})
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
self.assertIn('The resource could not be found.',
|
||||||
|
response.json['error_message'])
|
||||||
|
|
||||||
def test_ports_subresource_portgroup_not_found(self):
|
def test_ports_subresource_portgroup_not_found(self):
|
||||||
non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
|
non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
|
||||||
response = self.get_json('/portgroups/%s/ports' % non_existent_uuid,
|
response = self.get_json('/portgroups/%s/ports' % non_existent_uuid,
|
||||||
expect_errors=True, headers=self.headers)
|
expect_errors=True, headers=self.headers)
|
||||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
self.assertIn('Portgroup %s could not be found.' % non_existent_uuid,
|
||||||
|
response.json['error_message'])
|
||||||
|
|
||||||
def test_portgroup_by_address(self):
|
def test_portgroup_by_address(self):
|
||||||
address_template = "aa:bb:cc:dd:ee:f%d"
|
address_template = "aa:bb:cc:dd:ee:f%d"
|
||||||
@ -753,6 +830,29 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||||
expected_location)
|
expected_location)
|
||||||
|
|
||||||
|
@mock.patch.object(timeutils, 'utcnow', autospec=True)
|
||||||
|
def test_create_portgroup_v123(self, mock_utcnow):
|
||||||
|
pdict = apiutils.post_get_test_portgroup()
|
||||||
|
test_time = datetime.datetime(2000, 1, 1, 0, 0)
|
||||||
|
mock_utcnow.return_value = test_time
|
||||||
|
headers = {api_base.Version.string: "1.23"}
|
||||||
|
response = self.post_json('/portgroups', pdict,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
result = self.get_json('/portgroups/%s' % pdict['uuid'],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(pdict['uuid'], result['uuid'])
|
||||||
|
self.assertEqual(pdict['node_uuid'], result['node_uuid'])
|
||||||
|
self.assertFalse(result['updated_at'])
|
||||||
|
return_created_at = timeutils.parse_isotime(
|
||||||
|
result['created_at']).replace(tzinfo=None)
|
||||||
|
self.assertEqual(test_time, return_created_at)
|
||||||
|
# Check location header
|
||||||
|
self.assertIsNotNone(response.location)
|
||||||
|
expected_location = '/v1/portgroups/%s' % pdict['uuid']
|
||||||
|
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||||
|
expected_location)
|
||||||
|
|
||||||
def test_create_portgroup_invalid_api_version(self):
|
def test_create_portgroup_invalid_api_version(self):
|
||||||
pdict = apiutils.post_get_test_portgroup()
|
pdict = apiutils.post_get_test_portgroup()
|
||||||
response = self.post_json(
|
response = self.post_json(
|
||||||
|
@ -42,18 +42,23 @@ from ironic.tests.unit.objects import utils as obj_utils
|
|||||||
|
|
||||||
|
|
||||||
# NOTE(lucasagomes): When creating a port via API (POST)
|
# NOTE(lucasagomes): When creating a port via API (POST)
|
||||||
# we have to use node_uuid
|
# we have to use node_uuid and portgroup_uuid
|
||||||
def post_get_test_port(**kw):
|
def post_get_test_port(**kw):
|
||||||
port = apiutils.port_post_data(**kw)
|
port = apiutils.port_post_data(**kw)
|
||||||
node = dbutils.get_test_node()
|
node = dbutils.get_test_node()
|
||||||
|
portgroup = dbutils.get_test_portgroup()
|
||||||
port['node_uuid'] = kw.get('node_uuid', node['uuid'])
|
port['node_uuid'] = kw.get('node_uuid', node['uuid'])
|
||||||
|
port['portgroup_uuid'] = kw.get('portgroup_uuid', portgroup['uuid'])
|
||||||
return port
|
return port
|
||||||
|
|
||||||
|
|
||||||
class TestPortObject(base.TestCase):
|
class TestPortObject(base.TestCase):
|
||||||
|
|
||||||
def test_port_init(self):
|
@mock.patch("pecan.request")
|
||||||
port_dict = apiutils.port_post_data(node_id=None)
|
def test_port_init(self, mock_pecan_req):
|
||||||
|
mock_pecan_req.version.minor = 1
|
||||||
|
port_dict = apiutils.port_post_data(node_id=None,
|
||||||
|
portgroup_uuid=None)
|
||||||
del port_dict['extra']
|
del port_dict['extra']
|
||||||
port = api_port.Port(**port_dict)
|
port = api_port.Port(**port_dict)
|
||||||
self.assertEqual(wtypes.Unset, port.extra)
|
self.assertEqual(wtypes.Unset, port.extra)
|
||||||
@ -84,8 +89,24 @@ class TestListPorts(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(port.uuid, data['uuid'])
|
self.assertEqual(port.uuid, data['uuid'])
|
||||||
self.assertIn('extra', data)
|
self.assertIn('extra', data)
|
||||||
self.assertIn('node_uuid', data)
|
self.assertIn('node_uuid', data)
|
||||||
# never expose the node_id
|
# never expose the node_id, port_id, portgroup_id
|
||||||
self.assertNotIn('node_id', data)
|
self.assertNotIn('node_id', data)
|
||||||
|
self.assertNotIn('port_id', data)
|
||||||
|
self.assertNotIn('portgroup_id', data)
|
||||||
|
self.assertNotIn('portgroup_uuid', data)
|
||||||
|
|
||||||
|
def test_get_one_portgroup_is_none(self):
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
||||||
|
data = self.get_json('/ports/%s' % port.uuid,
|
||||||
|
headers={api_base.Version.string: '1.24'})
|
||||||
|
self.assertEqual(port.uuid, data['uuid'])
|
||||||
|
self.assertIn('extra', data)
|
||||||
|
self.assertIn('node_uuid', data)
|
||||||
|
# never expose the node_id, port_id, portgroup_id
|
||||||
|
self.assertNotIn('node_id', data)
|
||||||
|
self.assertNotIn('port_id', data)
|
||||||
|
self.assertNotIn('portgroup_id', data)
|
||||||
|
self.assertIn('portgroup_uuid', data)
|
||||||
|
|
||||||
def test_get_one_custom_fields(self):
|
def test_get_one_custom_fields(self):
|
||||||
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
||||||
@ -146,7 +167,14 @@ class TestListPorts(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
def test_detail(self):
|
def test_detail(self):
|
||||||
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
llc = {'switch_info': 'switch', 'switch_id': 'aa:bb:cc:dd:ee:ff',
|
||||||
|
'port_id': 'Gig0/1'}
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
portgroup_id=portgroup.id,
|
||||||
|
pxe_enabled=False,
|
||||||
|
local_link_connection=llc)
|
||||||
data = self.get_json(
|
data = self.get_json(
|
||||||
'/ports/detail',
|
'/ports/detail',
|
||||||
headers={api_base.Version.string: str(api_v1.MAX_VER)}
|
headers={api_base.Version.string: str(api_v1.MAX_VER)}
|
||||||
@ -157,8 +185,10 @@ class TestListPorts(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('node_uuid', data['ports'][0])
|
self.assertIn('node_uuid', data['ports'][0])
|
||||||
self.assertIn('pxe_enabled', data['ports'][0])
|
self.assertIn('pxe_enabled', data['ports'][0])
|
||||||
self.assertIn('local_link_connection', data['ports'][0])
|
self.assertIn('local_link_connection', data['ports'][0])
|
||||||
# never expose the node_id
|
self.assertIn('portgroup_uuid', data['ports'][0])
|
||||||
|
# never expose the node_id and portgroup_id
|
||||||
self.assertNotIn('node_id', data['ports'][0])
|
self.assertNotIn('node_id', data['ports'][0])
|
||||||
|
self.assertNotIn('portgroup_id', data['ports'][0])
|
||||||
|
|
||||||
def test_detail_against_single(self):
|
def test_detail_against_single(self):
|
||||||
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
|
||||||
@ -353,6 +383,50 @@ class TestListPorts(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(0, mock_get_rpc_node.call_count)
|
self.assertEqual(0, mock_get_rpc_node.call_count)
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, data.status_int)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, data.status_int)
|
||||||
|
|
||||||
|
def test_get_all_by_portgroup_uuid(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
portgroup_id=pg.id)
|
||||||
|
data = self.get_json('/ports/detail?portgroup=%s' % pg.uuid,
|
||||||
|
headers={api_base.Version.string: '1.24'})
|
||||||
|
self.assertEqual(port.uuid, data['ports'][0]['uuid'])
|
||||||
|
self.assertEqual(pg.uuid,
|
||||||
|
data['ports'][0]['portgroup_uuid'])
|
||||||
|
|
||||||
|
def test_get_all_by_portgroup_uuid_older_api_version(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
response = self.get_json(
|
||||||
|
'/ports/detail?portgroup=%s' % pg.uuid,
|
||||||
|
headers={api_base.Version.string: '1.14'},
|
||||||
|
expect_errors=True
|
||||||
|
)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_get_all_by_portgroup_name(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
portgroup_id=pg.id)
|
||||||
|
data = self.get_json('/ports/detail?portgroup=%s' % pg.name,
|
||||||
|
headers={api_base.Version.string: '1.24'})
|
||||||
|
self.assertEqual(port.uuid, data['ports'][0]['uuid'])
|
||||||
|
self.assertEqual(pg.uuid,
|
||||||
|
data['ports'][0]['portgroup_uuid'])
|
||||||
|
self.assertEqual(1, len(data['ports']))
|
||||||
|
|
||||||
|
def test_get_all_by_portgroup_uuid_and_node_uuid(self):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
response = self.get_json(
|
||||||
|
'/ports/detail?portgroup=%s&node=%s' % (pg.uuid, self.node.uuid),
|
||||||
|
headers={api_base.Version.string: '1.24'},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
@mock.patch.object(api_port.PortsController, '_get_ports_collection')
|
@mock.patch.object(api_port.PortsController, '_get_ports_collection')
|
||||||
def test_detail_with_incorrect_api_usage(self, mock_gpc):
|
def test_detail_with_incorrect_api_usage(self, mock_gpc):
|
||||||
# GET /v1/ports/detail specifying node and node_uuid. In this case
|
# GET /v1/ports/detail specifying node and node_uuid. In this case
|
||||||
@ -361,7 +435,22 @@ class TestListPorts(test_api_base.BaseApiTest):
|
|||||||
('test-node', self.node.uuid))
|
('test-node', self.node.uuid))
|
||||||
mock_gpc.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY,
|
mock_gpc.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY,
|
||||||
mock.ANY, mock.ANY, mock.ANY,
|
mock.ANY, mock.ANY, mock.ANY,
|
||||||
mock.ANY)
|
mock.ANY, mock.ANY)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_node_not_found(self):
|
||||||
|
non_existent_uuid = 'eeeeeeee-cccc-aaaa-bbbb-cccccccccccc'
|
||||||
|
response = self.get_json('/portgroups/%s/ports' % non_existent_uuid,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_invalid_ident(self):
|
||||||
|
invalid_ident = '123 123'
|
||||||
|
response = self.get_json('/portgroups/%s/ports' % invalid_ident,
|
||||||
|
headers={api_base.Version.string: '1.24'},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
self.assertIn('Expected a logical name or UUID',
|
||||||
|
response.json['error_message'])
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'update_port')
|
@mock.patch.object(rpcapi.ConductorAPI, 'update_port')
|
||||||
@ -498,6 +587,96 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
def test_add_portgroup_uuid(self, mock_upd):
|
||||||
|
mock_upd.return_value = self.port
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:bb',
|
||||||
|
name='bar')
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
|
[{'path':
|
||||||
|
'/portgroup_uuid',
|
||||||
|
'value': pg.uuid,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_replace_portgroup_uuid(self, mock_upd):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:bb',
|
||||||
|
name='bar')
|
||||||
|
mock_upd.return_value = self.port
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
|
[{'path': '/portgroup_uuid',
|
||||||
|
'value': pg.uuid,
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_replace_portgroup_uuid_remove(self, mock_upd):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:bb',
|
||||||
|
name='bar')
|
||||||
|
mock_upd.return_value = self.port
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
|
[{'path': '/portgroup_uuid',
|
||||||
|
'value': pg.uuid,
|
||||||
|
'op': 'remove'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertIsNone(mock_upd.call_args[0][1].portgroup_id)
|
||||||
|
|
||||||
|
def test_replace_portgroup_uuid_remove_add(self, mock_upd):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:bb',
|
||||||
|
name='bar')
|
||||||
|
pg1 = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:b1',
|
||||||
|
name='bbb')
|
||||||
|
mock_upd.return_value = self.port
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
|
[{'path': '/portgroup_uuid',
|
||||||
|
'value': pg.uuid,
|
||||||
|
'op': 'remove'},
|
||||||
|
{'path': '/portgroup_uuid',
|
||||||
|
'value': pg1.uuid,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(pg1.id, mock_upd.call_args[0][1].portgroup_id)
|
||||||
|
|
||||||
|
def test_replace_portgroup_uuid_old_api(self, mock_upd):
|
||||||
|
pg = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
address='bb:bb:bb:bb:bb:bb',
|
||||||
|
name='bar')
|
||||||
|
mock_upd.return_value = self.port
|
||||||
|
headers = {api_base.Version.string: '1.15'}
|
||||||
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
|
[{'path': '/portgroup_uuid',
|
||||||
|
'value': pg.uuid,
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
def test_add_node_uuid(self, mock_upd):
|
def test_add_node_uuid(self, mock_upd):
|
||||||
mock_upd.return_value = self.port
|
mock_upd.return_value = self.port
|
||||||
response = self.patch_json('/ports/%s' % self.port.uuid,
|
response = self.patch_json('/ports/%s' % self.port.uuid,
|
||||||
@ -729,12 +908,30 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
self.assertFalse(mock_upd.called)
|
self.assertFalse(mock_upd.called)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_patch(self, mock_upd):
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
portgroup_id=portgroup.id,
|
||||||
|
address='52:55:00:cf:2d:31')
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.patch_json(
|
||||||
|
'/portgroups/%(portgroup)s/ports/%(port)s' %
|
||||||
|
{'portgroup': portgroup.uuid, 'port': port.uuid},
|
||||||
|
[{'path': '/address', 'value': '00:00:00:00:00:00',
|
||||||
|
'op': 'replace'}], headers=headers, expect_errors=True)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
|
||||||
|
|
||||||
class TestPost(test_api_base.BaseApiTest):
|
class TestPost(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestPost, self).setUp()
|
super(TestPost, self).setUp()
|
||||||
self.node = obj_utils.create_test_node(self.context)
|
self.node = obj_utils.create_test_node(self.context)
|
||||||
|
self.portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
self.headers = {api_base.Version.string: str(
|
self.headers = {api_base.Version.string: str(
|
||||||
versions.MAX_VERSION_STRING)}
|
versions.MAX_VERSION_STRING)}
|
||||||
|
|
||||||
@ -853,6 +1050,53 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_create_port_portgroup_uuid_not_found(self):
|
||||||
|
pdict = post_get_test_port(
|
||||||
|
portgroup_uuid='1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e')
|
||||||
|
response = self.post_json('/ports', pdict, expect_errors=True,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_create_port_portgroup_uuid_not_found_old_api_version(self):
|
||||||
|
pdict = post_get_test_port(
|
||||||
|
portgroup_uuid='1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e')
|
||||||
|
response = self.post_json('/ports', pdict, expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_create_port_portgroup(self):
|
||||||
|
pdict = post_get_test_port(
|
||||||
|
portgroup_uuid=self.portgroup.uuid,
|
||||||
|
node_uuid=self.node.uuid)
|
||||||
|
|
||||||
|
response = self.post_json('/ports', pdict, headers=self.headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
|
||||||
|
def test_create_port_portgroup_different_nodes(self):
|
||||||
|
pdict = post_get_test_port(
|
||||||
|
portgroup_uuid=self.portgroup.uuid,
|
||||||
|
node_uuid=uuidutils.generate_uuid())
|
||||||
|
|
||||||
|
response = self.post_json('/ports', pdict, headers=self.headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
|
||||||
|
def test_create_port_portgroup_old_api_version(self):
|
||||||
|
pdict = post_get_test_port(
|
||||||
|
portgroup_uuid=self.portgroup.uuid,
|
||||||
|
node_uuid=self.node.uuid
|
||||||
|
)
|
||||||
|
headers = {api_base.Version.string: '1.15'}
|
||||||
|
response = self.post_json('/ports', pdict, expect_errors=True,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
def test_create_port_address_already_exist(self):
|
def test_create_port_address_already_exist(self):
|
||||||
address = 'AA:AA:AA:11:22:33'
|
address = 'AA:AA:AA:11:22:33'
|
||||||
pdict = post_get_test_port(address=address)
|
pdict = post_get_test_port(address=address)
|
||||||
@ -936,11 +1180,20 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
headers = {api_base.Version.string: '1.14'}
|
headers = {api_base.Version.string: '1.14'}
|
||||||
pdict = post_get_test_port(pxe_enabled=False)
|
pdict = post_get_test_port(pxe_enabled=False)
|
||||||
del pdict['local_link_connection']
|
del pdict['local_link_connection']
|
||||||
|
del pdict['portgroup_uuid']
|
||||||
response = self.post_json('/ports', pdict, headers=headers,
|
response = self.post_json('/ports', pdict, headers=headers,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_post(self):
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
pdict = post_get_test_port()
|
||||||
|
response = self.post_json('/portgroups/%s/ports' % self.portgroup.uuid,
|
||||||
|
pdict, headers=headers, expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_port')
|
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_port')
|
||||||
class TestDelete(test_api_base.BaseApiTest):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
@ -975,3 +1228,18 @@ class TestDelete(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.CONFLICT, ret.status_code)
|
self.assertEqual(http_client.CONFLICT, ret.status_code)
|
||||||
self.assertTrue(ret.json['error_message'])
|
self.assertTrue(ret.json['error_message'])
|
||||||
self.assertTrue(mock_dpt.called)
|
self.assertTrue(mock_dpt.called)
|
||||||
|
|
||||||
|
def test_portgroups_subresource_delete(self, mock_dpt):
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
portgroup_id=portgroup.id,
|
||||||
|
address='52:55:00:cf:2d:31')
|
||||||
|
headers = {api_base.Version.string: '1.24'}
|
||||||
|
response = self.delete(
|
||||||
|
'/portgroups/%(portgroup)s/ports/%(port)s' %
|
||||||
|
{'portgroup': portgroup.uuid, 'port': port.uuid},
|
||||||
|
headers=headers, expect_errors=True)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds, starting with REST API version 1.24:
|
||||||
|
|
||||||
|
* the new endpoint `v1/nodes/<node>/portgroups`;
|
||||||
|
* the new endpoint `v1/portgroups/<portgroup>/ports`;
|
||||||
|
* the new field `portgroup_uuid` to a port. This is the UUID
|
||||||
|
of a port group that this port belongs to, or None if it doesn't
|
||||||
|
belong to any port group.
|
Loading…
Reference in New Issue
Block a user