# Copyright 2013 UnitedStack Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from http import client as http_client

from oslo_log import log
from oslo_utils import uuidutils
from pecan import rest

from ironic import api
from ironic.api.controllers import link
from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import method
from ironic.common import args
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import metrics_utils
from ironic.common import states as ir_states
from ironic import objects

METRICS = metrics_utils.get_metrics_logger(__name__)
LOG = log.getLogger(__name__)


_DEFAULT_RETURN_FIELDS = ['uuid', 'address']


PORT_SCHEMA = {
    'type': 'object',
    'properties': {
        'address': {'type': 'string'},
        'extra': {'type': ['object', 'null']},
        'is_smartnic': {'type': ['string', 'boolean', 'null']},
        'local_link_connection': {'type': ['null', 'object']},
        'node_ident': {'type': 'string'},
        'node_uuid': {'type': 'string'},
        'physical_network': {'type': ['string', 'null'], 'maxLength': 64},
        'portgroup_uuid': {'type': ['string', 'null']},
        'pxe_enabled': {'type': ['string', 'boolean', 'null']},
        'uuid': {'type': ['string', 'null']},
        'name': {'type': ['string', 'null']},
        'description': {'type': ['string', 'null'], 'maxLength': 255},
        'vendor': {'type': ['string', 'null'], 'maxLength': 32},
        'category': {'type': ['string', 'null'], 'maxLength': 80},
        'available_for_dynamic_portgroup': {'type': 'boolean'},
    },
    'required': ['address'],
    'oneOf': [
        {'required': ['node_ident']},
        {'required': ['node_uuid']},
    ],
    'additionalProperties': False,
}


PORT_PATCH_SCHEMA = PORT_SCHEMA

PATCH_ALLOWED_FIELDS = [
    'address',
    'extra',
    'is_smartnic',
    'local_link_connection',
    'node_ident',
    'node_uuid',
    'physical_network',
    'portgroup_uuid',
    'pxe_enabled',
    'name',
    'description',
    'vendor',
    'category',
    'available_for_dynamic_portgroup',
]

PORT_VALIDATOR_EXTRA = args.dict_valid(
    address=args.mac_address,
    node_uuid=args.uuid,
    is_smartnic=args.boolean,
    local_link_connection=api_utils.LOCAL_LINK_VALIDATOR,
    portgroup_uuid=args.uuid,
    pxe_enabled=args.boolean,
    uuid=args.uuid,
    available_for_dynamic_portgroup=args.boolean,
)

PORT_VALIDATOR = args.and_valid(
    args.schema(PORT_SCHEMA),
    PORT_VALIDATOR_EXTRA
)

PORT_PATCH_VALIDATOR = args.and_valid(
    args.schema(PORT_PATCH_SCHEMA),
    PORT_VALIDATOR_EXTRA
)


def hide_fields_in_newer_versions(port):
    # if requested version is < 1.18, hide internal_info field
    if not api_utils.allow_port_internal_info():
        port.pop('internal_info', None)
    # if requested version is < 1.19, hide local_link_connection and
    # pxe_enabled fields
    if not api_utils.allow_port_advanced_net_fields():
        port.pop('pxe_enabled', None)
        port.pop('local_link_connection', None)
    # if requested version is < 1.24, hide portgroup_uuid field
    if not api_utils.allow_portgroups_subcontrollers():
        port.pop('portgroup_uuid', None)
    # if requested version is < 1.34, hide physical_network field.
    if not api_utils.allow_port_physical_network():
        port.pop('physical_network', None)
    # if requested version is < 1.53, hide is_smartnic field.
    if not api_utils.allow_port_is_smartnic():
        port.pop('is_smartnic', None)
    # if requested version is < 1.88, hide name field.
    if not api_utils.allow_port_name():
        port.pop('name', None)
    # note(JayF): if requested version is < 1.90, hide new
    # local_link_connection schema but only check it if we allow advanced
    # net fields, since otherwise we removed local_link_connection above
    # and don't want to re-add it here
    if (not api_utils.allow_ovn_vtep_version()
            and api_utils.allow_port_advanced_net_fields):
        local_link_connection = port.get('local_link_connection', {})
        if any(key for key in local_link_connection.keys()
               if key in api_utils.LOCAL_LINK_OVN_90_FIELDS):
            # note(JayF): In this case, the field *should* exist but should be
            # set to empty. This is because api version clients in this branch
            # expect the key port.local_link_connection to exist even if we
            # cannot set a valid value
            port['local_link_connection'] = {}
    # if requested version is < 1.97, hide description field.
    if not api_utils.allow_port_description():
        port.pop('description', None)
    # if requested version is < 1.100, hide vendor field.
    if not api_utils.allow_port_vendor():
        port.pop('vendor', None)
    # if requested version is < 1.101, hide category field.
    if not api_utils.allow_port_category():
        port.pop('category', None)
    # if requested version is < 1.111, hide
    # available_for_dynamic_portgroup field.
    if not api_utils.allow_port_available_for_dynamic_portgroup():
        port.pop('available_for_dynamic_portgroup', None)


def convert_with_links(rpc_port, fields=None, sanitize=True):
    port = api_utils.object_to_dict(
        rpc_port,
        link_resource='ports',
        fields=(
            'address',
            'extra',
            'internal_info',
            'is_smartnic',
            'local_link_connection',
            'physical_network',
            'pxe_enabled',
            'node_uuid',
            'name',
            'description',
            'vendor',
            'category',
            'available_for_dynamic_portgroup',
        )
    )
    if rpc_port.portgroup_id:
        pg = objects.Portgroup.get(api.request.context, rpc_port.portgroup_id)
        port['portgroup_uuid'] = pg.uuid
    else:
        port['portgroup_uuid'] = None

    _validate_fields(port, fields)

    if not sanitize:
        return port

    port_sanitize(port, fields=fields)

    return port


def _validate_fields(port, fields=None):
    if fields is not None:
        api_utils.check_for_invalid_fields(fields, port)


def port_sanitize(port, fields=None):
    """Removes sensitive and unrequested data.

    Will only keep the fields specified in the ``fields`` parameter.

    :param fields:
        list of fields to preserve, or ``None`` to preserve them all
    :type fields: list of str
    """
    hide_fields_in_newer_versions(port)
    api_utils.sanitize_dict(port, fields)


def list_convert_with_links(rpc_ports, limit, url, fields=None, **kwargs):
    ports = []
    for rpc_port in rpc_ports:
        port = convert_with_links(rpc_port, fields=fields,
                                  sanitize=False)
        # NOTE(dtantsur): node was deleted after we fetched the port
        # list, meaning that the port was also deleted. Skip it.
        if port['node_uuid'] is None:
            continue

        ports.append(port)
    return collection.list_convert_with_links(
        items=ports,
        item_name='ports',
        limit=limit,
        url=url,
        fields=fields,
        sanitize_func=port_sanitize,
        **kwargs
    )


class PortsController(rest.RestController):
    """REST controller for Ports."""

    _custom_actions = {
        'detail': ['GET'],
    }

    invalid_sort_key_list = ['extra', 'internal_info', 'local_link_connection']

    advanced_net_fields = ['pxe_enabled', 'local_link_connection']

    def __init__(self, node_ident=None, portgroup_ident=None):
        super(PortsController, self).__init__()
        self.parent_node_ident = node_ident
        self.parent_portgroup_ident = portgroup_ident

    def _get_ports_collection(self, node_ident, address, portgroup_ident,
                              shard, marker, limit, sort_key, sort_dir,
                              resource_url=None, fields=None, detail=None,
                              project=None, description_contains=None,
                              conductor_groups=None):
        """Retrieve a collection of ports.

        :param node_ident: UUID or name of a node, to get only ports for that
                           node.
        :param address: MAC address of a port, to get the port which has
                        this MAC address.
        :param portgroup_ident: UUID or name of a portgroup, to get only ports
                                for that portgroup.
        :param shard: A comma-separated shard list, to get only ports for those
                       shards
        :param marker: pagination marker for large data sets.
        :param limit: maximum number of resources to return in a single result.
                      This value cannot be larger than the value of max_limit
                      in the [api] section of the ironic configuration, or only
                      max_limit resources will be returned.
        :param sort_key: column to sort results by. Default: id.
        :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
        :param resource_url: Optional, base url to be used for links
        :param fields: Optional, a list with a specified set of fields
            of the resource to be returned.
        :param detail: Optional, show detailed list of ports
        :param project: Optional, filter by project
        :param description_contains: Optional string value to get only ports
                                     with description field contains matching
                                     value.
        :param conductor_groups: conductor groups, to filter the request by.
        :returns: a list of ports.

        """

        limit = api_utils.validate_limit(limit)
        sort_dir = api_utils.validate_sort_dir(sort_dir)

        marker_obj = None
        if marker:
            marker_obj = objects.Port.get_by_uuid(api.request.context,
                                                  marker)

        if sort_key in self.invalid_sort_key_list:
            raise exception.InvalidParameterValue(
                _("The sort_key value %(key)s is an invalid field for "
                  "sorting") % {'key': sort_key})

        node_ident = self.parent_node_ident or node_ident
        portgroup_ident = self.parent_portgroup_ident or portgroup_ident

        exclusive_filters = 0
        for i in [node_ident, portgroup_ident, shard]:
            if i:
                exclusive_filters += 1
            if exclusive_filters > 1:
                raise exception.OperationNotPermitted()

        filters = {}
        if description_contains:
            filters['description_contains'] = description_contains

        if conductor_groups and (node_ident or portgroup_ident or address):
            raise exception.Invalid(
                _("Filtering by conductor_groups cannot be combined with "
                  "node_ident, portgroup_ident, or node address filters."))

        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(api.request.context,
                                                      portgroup.id, limit,
                                                      marker_obj,
                                                      sort_key=sort_key,
                                                      sort_dir=sort_dir,
                                                      project=project,
                                                      filters=filters)
        elif node_ident:
            # FIXME(comstud): Since all we need is the node 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.
            node = api_utils.get_rpc_node(node_ident)
            ports = objects.Port.list_by_node_id(api.request.context,
                                                 node.id, limit, marker_obj,
                                                 sort_key=sort_key,
                                                 sort_dir=sort_dir,
                                                 project=project,
                                                 filters=filters)
        elif address:
            ports = self._get_ports_by_address(address, project=project)
        elif shard:
            ports = objects.Port.list_by_node_shards(api.request.context,
                                                     shard, limit,
                                                     marker_obj, sort_key,
                                                     sort_dir, project=project,
                                                     filters=filters)
        else:
            ports = objects.Port.list(api.request.context, limit,
                                      marker_obj, sort_key=sort_key,
                                      sort_dir=sort_dir, project=project,
                                      conductor_groups=conductor_groups,
                                      filters=filters)
        parameters = {}

        if detail is not None:
            parameters['detail'] = detail

        return list_convert_with_links(ports, limit,
                                       url=resource_url,
                                       fields=fields,
                                       sort_key=sort_key,
                                       sort_dir=sort_dir,
                                       **parameters)

    def _get_ports_by_address(self, address, project=None):
        """Retrieve a port by its address.

        :param address: MAC address of a port, to get the port which has
                        this MAC address.
        :param project: Optional, filter by project
        :returns: a list with the port, or an empty list if no port is found.

        """
        try:
            port = objects.Port.get_by_address(api.request.context, address,
                                               project=project)
            return [port]
        except exception.PortNotFound:
            return []

    def _check_allowed_port_fields(self, fields):
        """Check if fetching a particular field of a port is allowed.

        Check if the required version is being requested for fields
        that are only allowed to be fetched in a particular API version.

        :param fields: list or set of fields to check
        :raises: NotAcceptable if a field is not allowed
        """
        if fields is None:
            return
        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 ('physical_network' in fields
                and not api_utils.allow_port_physical_network()):
            raise exception.NotAcceptable()
        if ('is_smartnic' in fields
                and not api_utils.allow_port_is_smartnic()):
            raise exception.NotAcceptable()
        if ('local_link_connection/network_type' in fields
                and not api_utils.allow_local_link_connection_network_type()):
            raise exception.NotAcceptable()
        if (isinstance(fields, dict)
                and fields.get('local_link_connection') is not None):
            if (not api_utils.allow_local_link_connection_network_type()
                    and 'network_type' in fields['local_link_connection']):
                raise exception.NotAcceptable()
            if (not api_utils.allow_ovn_vtep_version()
                    and 'vtep-logical-switch'
                    in fields['local_link_connection']):
                raise exception.NotAcceptable()
        if ('name' in fields
                and not api_utils.allow_port_name()):
            raise exception.NotAcceptable()
        if ('description' in fields
                and not api_utils.allow_port_description()):
            raise exception.NotAcceptable()
        if ('vendor' in fields
                and not api_utils.allow_port_vendor()):
            raise exception.NotAcceptable()
        if ('category' in fields
                and not api_utils.allow_port_category()):
            raise exception.NotAcceptable()
        allow_dynpg = api_utils.allow_port_available_for_dynamic_portgroup
        if ('available_for_dynamic_portgroup' in fields
                and not allow_dynpg()):
            raise exception.NotAcceptable()

    @METRICS.timer('PortsController.get_all')
    @method.expose()
    @args.validate(node=args.uuid_or_name, node_uuid=args.uuid,
                   address=args.mac_address, marker=args.uuid,
                   limit=args.integer, sort_key=args.string,
                   sort_dir=args.string, fields=args.string_list,
                   portgroup=args.uuid_or_name, detail=args.boolean,
                   shard=args.string_list, description_contains=args.string,
                   conductor_groups=args.string_list)
    def get_all(self, node=None, node_uuid=None, address=None, marker=None,
                limit=None, sort_key='id', sort_dir='asc', fields=None,
                portgroup=None, detail=None, shard=None,
                description_contains=None, conductor_groups=None):
        """Retrieve a list of ports.

        Note that the 'node_uuid' interface is deprecated in favour
        of the 'node' interface

        :param node: UUID or name of a node, to get only ports for that
                           node.
        :param node_uuid: UUID of a node, to get only ports for that
                           node.
        :param address: MAC address of a port, to get the port which has
                        this MAC address.
        :param marker: pagination marker for large data sets.
        :param limit: maximum number of resources to return in a single result.
                      This value cannot be larger than the value of max_limit
                      in the [api] section of the ironic configuration, or only
                      max_limit resources will be returned.
        :param sort_key: column to sort results by. Default: id.
        :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
        :param fields: Optional, a list with a specified set of fields
            of the resource to be returned.
        :param portgroup: UUID or name of a portgroup, to get only ports
                                   for that portgroup.
        :param shard: Optional, a list of shard ids to filter by, only ports
                      associated with nodes in these shards will be returned.
        :param description_contains: Optional string value to get only ports
                                     with description field contains matching
                                     value.
        :param conductor_groups: conductor groups, to filter the request by.
        :raises: NotAcceptable, HTTPNotFound
        """
        project = api_utils.check_port_list_policy(
            parent_node=self.parent_node_ident,
            parent_portgroup=self.parent_portgroup_ident)

        if self.parent_node_ident:
            node = self.parent_node_ident

        if self.parent_portgroup_ident:
            portgroup = self.parent_portgroup_ident

        api_utils.check_allow_specify_fields(fields)
        self._check_allowed_port_fields(fields)
        self._check_allowed_port_fields([sort_key])

        if portgroup and not api_utils.allow_portgroups_subcontrollers():
            raise exception.NotAcceptable()

        api_utils.check_allow_filter_by_shard(shard)

        fields = api_utils.get_request_return_fields(fields, detail,
                                                     _DEFAULT_RETURN_FIELDS)

        extra_args = {'description_contains': description_contains}

        if not node_uuid and node:
            # We're invoking this interface using positional notation, or
            # explicitly using 'node'.  Try and determine which one.
            # Make sure only one interface, node or node_uuid is used
            if (not api_utils.allow_node_logical_names()
                and not uuidutils.is_uuid_like(node)):
                raise exception.NotAcceptable()

        return self._get_ports_collection(node_uuid or node, address,
                                          portgroup, shard, marker, limit,
                                          sort_key, sort_dir,
                                          resource_url='ports',
                                          fields=fields, detail=detail,
                                          project=project,
                                          conductor_groups=conductor_groups,
                                          **extra_args)

    @METRICS.timer('PortsController.detail')
    @method.expose()
    @args.validate(node=args.uuid_or_name, node_uuid=args.uuid,
                   address=args.mac_address, marker=args.uuid,
                   limit=args.integer, sort_key=args.string,
                   sort_dir=args.string, portgroup=args.uuid_or_name,
                   shard=args.string_list, description_contains=args.string,
                   conductor_groups=args.string_list)
    def detail(self, node=None, node_uuid=None, address=None, marker=None,
               limit=None, sort_key='id', sort_dir='asc', portgroup=None,
               shard=None, description_contains=None, conductor_groups=None):
        """Retrieve a list of ports with detail.

        Note that the 'node_uuid' interface is deprecated in favour
        of the 'node' interface

        :param node: UUID or name of a node, to get only ports for that
                     node.
        :param node_uuid: UUID of a node, to get only ports for that
                          node.
        :param address: MAC address of a port, to get the port which has
                        this MAC address.
        :param portgroup: UUID or name of a portgroup, to get only ports
                           for that portgroup.
        :param shard: comma separated list of shards, to only get ports
                      associated with nodes in those shards.
        :param marker: pagination marker for large data sets.
        :param limit: maximum number of resources to return in a single result.
                      This value cannot be larger than the value of max_limit
                      in the [api] section of the ironic configuration, or only
                      max_limit resources will be returned.
        :param sort_key: column to sort results by. Default: id.
        :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
        :param description_contains: Optional string value to get only ports
                                    with description field contains matching
                                    value.
        :param conductor_groups: conductor groups, to filter the request by.
        :raises: NotAcceptable, HTTPNotFound
        """
        project = api_utils.check_port_list_policy(
            parent_node=self.parent_node_ident,
            parent_portgroup=self.parent_portgroup_ident)

        self._check_allowed_port_fields([sort_key])
        if portgroup and not api_utils.allow_portgroups_subcontrollers():
            raise exception.NotAcceptable()

        api_utils.check_allow_filter_by_shard(shard)

        if not node_uuid and node:
            # We're invoking this interface using positional notation, or
            # explicitly using 'node'.  Try and determine which one.
            # Make sure only one interface, node or node_uuid is used
            if (not api_utils.allow_node_logical_names()
                and not uuidutils.is_uuid_like(node)):
                raise exception.NotAcceptable()

        # NOTE(lucasagomes): /detail should only work against collections
        parent = api.request.path.split('/')[:-1][-1]
        if parent != "ports":
            raise exception.HTTPNotFound()

        extra_args = {'description_contains': description_contains}
        return self._get_ports_collection(node_uuid or node, address,
                                          portgroup, shard, marker, limit,
                                          sort_key, sort_dir,
                                          resource_url='ports/detail',
                                          project=project,
                                          conductor_groups=conductor_groups,
                                          **extra_args)

    @METRICS.timer('PortsController.get_one')
    @method.expose()
    @args.validate(port_ident=args.uuid_or_name, fields=args.string_list)
    def get_one(self, port_ident, fields=None):
        """Retrieve information about the given port.

        :param port_ident: UUID or name of a port.
        :param fields: Optional, a list with a specified set of fields
            of the resource to be returned.
        :raises: NotAcceptable, HTTPNotFound
        """
        if self.parent_node_ident or self.parent_portgroup_ident:
            raise exception.MalformedRequestURI()

        rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve(
            'baremetal:port:get', port_ident)

        api_utils.check_allow_specify_fields(fields)
        self._check_allowed_port_fields(fields)

        return convert_with_links(rpc_port, fields=fields)

    @METRICS.timer('PortsController.post')
    @method.expose(status_code=http_client.CREATED)
    @method.body('port')
    @args.validate(port=PORT_VALIDATOR)
    def post(self, port):
        """Create a new port.

        :param port: a port within the request body.
        :raises: NotAcceptable, HTTPNotFound, Conflict
        """
        if self.parent_node_ident or self.parent_portgroup_ident:
            raise exception.MalformedRequestURI()

        # NOTE(lucasagomes): Create the node_id attribute on-the-fly
        #                    to satisfy the api -> rpc object
        #                    conversion.
        # NOTE(TheJulia): The get of the node *does* check if the node
        # can be accessed. We need to be able to get the node regardless
        # in order to perform the actual policy check.
        raise_node_not_found = False
        node = None
        owner = None
        lessee = None
        node_uuid = port.get('node_uuid', None)
        node_ident = port.get('node_ident', None)

        if node_ident:
            if not api_utils.allow_node_ident_as_param_for_port_creation():
                raise exception.NotAcceptable()

        ident = node_ident or node_uuid
        try:
            node = api_utils.get_rpc_node(ident)
            port['node_uuid'] = node['uuid']
            node = api_utils.replace_node_uuid_with_id(port)
            owner = node.owner
            lessee = node.lessee
        except exception.NotFound:
            raise_node_not_found = True

        # While the rule is for the port, the base object that controls access
        # is the node.
        api_utils.check_owner_policy('node', 'baremetal:port:create',
                                     owner, lessee=lessee,
                                     conceal_node=False)
        if raise_node_not_found:
            # Delayed raise of NodeNotFound because we want to check
            # the access policy first.
            raise exception.NodeNotFound(node=node_uuid,
                                         code=http_client.BAD_REQUEST)

        context = api.request.context

        self._check_allowed_port_fields(port)

        portgroup = None
        if port.get('portgroup_uuid'):
            try:
                portgroup = objects.Portgroup.get(api.request.context,
                                                  port.pop('portgroup_uuid'))
                if portgroup.node_id != node.id:
                    raise exception.BadRequest(_('Port can not be added to a '
                                                 'portgroup belonging to a '
                                                 'different node.'))
                # NOTE(lucasagomes): Create the portgroup_id attribute
                #                    on-the-fly to satisfy the api ->
                #                    rpc object conversion.
                port['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

        if port.get('is_smartnic'):
            try:
                api_utils.LOCAL_LINK_SMART_NIC_VALIDATOR(
                    'local_link_connection',
                    port.get('local_link_connection'))
            except exception.Invalid:
                raise exception.Invalid(
                    "Smart NIC port must have port_id "
                    "and hostname in local_link_connection")

        physical_network = port.get('physical_network')
        if physical_network is not None and not physical_network:
            raise exception.Invalid('A non-empty value is required when '
                                    'setting physical_network')

        if (portgroup and (port.get('pxe_enabled'))):
            if not portgroup.standalone_ports_supported:
                msg = _("Port group %s doesn't support standalone ports. "
                        "This port cannot be created as a member of that "
                        "portgroup as the port's 'pxe_enabled' field was "
                        "set to True.")
                raise exception.Conflict(
                    msg % portgroup.uuid)

        # NOTE(yuriyz): UUID is mandatory for notifications payload
        if not port.get('uuid'):
            port['uuid'] = uuidutils.generate_uuid()

        rpc_port = objects.Port(context, **port)

        notify_extra = {
            'node_uuid': node.uuid,
            'portgroup_uuid': portgroup and portgroup.uuid or None
        }
        notify.emit_start_notification(context, rpc_port, 'create',
                                       **notify_extra)
        with notify.handle_error_notification(context, rpc_port, 'create',
                                              **notify_extra):
            topic = api.request.rpcapi.get_topic_for(node)
            new_port = api.request.rpcapi.create_port(context, rpc_port,
                                                      topic)
        notify.emit_end_notification(context, new_port, 'create',
                                     **notify_extra)
        # Set the HTTP Location Header
        api.response.location = link.build_url('ports', new_port.uuid)
        return convert_with_links(new_port)

    @METRICS.timer('PortsController.patch')
    @method.expose()
    @method.body('patch')
    @args.validate(port_ident=args.uuid_or_name, patch=args.patch)
    def patch(self, port_ident, patch):
        """Update an existing port.

        :param port_ident: UUID or name of a port.
        :param patch: a json PATCH document to apply to this port.
        :raises: NotAcceptable, HTTPNotFound
        """
        if self.parent_node_ident or self.parent_portgroup_ident:
            raise exception.MalformedRequestURI()

        api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)

        context = api.request.context
        fields_to_check = set()
        for field in (self.advanced_net_fields
                      + ['portgroup_uuid', 'physical_network',
                         'is_smartnic', 'local_link_connection/network_type']):
            field_path = '/%s' % field
            if (api_utils.get_patch_values(patch, field_path)
                    or api_utils.is_path_removed(patch, field_path)):
                fields_to_check.add(field)
        self._check_allowed_port_fields(fields_to_check)

        rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve(
            'baremetal:port:update', port_ident)

        port_dict = rpc_port.as_dict()
        # NOTE(lucasagomes):
        # 1) Remove node_id because it's an internal value and
        #    not present in the API object
        # 2) Add node_uuid
        port_dict.pop('node_id', None)
        port_dict['node_uuid'] = rpc_node.uuid
        # NOTE(vsaienko):
        # 1) Remove portgroup_id because it's an internal value and
        #    not present in the API object
        # 2) Add portgroup_uuid
        portgroup = None
        if port_dict.get('portgroup_id'):
            portgroup = objects.Portgroup.get_by_id(
                context, port_dict.pop('portgroup_id'))
        port_dict['portgroup_uuid'] = portgroup and portgroup.uuid or None

        port_dict = api_utils.apply_jsonpatch(port_dict, patch)

        try:
            if api_utils.is_path_updated(patch, '/portgroup_uuid'):
                if port_dict.get('portgroup_uuid'):
                    portgroup = objects.Portgroup.get_by_uuid(
                        context, port_dict['portgroup_uuid'])
                else:
                    portgroup = None
        except exception.PortgroupNotFound as e:
            # Change error code because 404 (NotFound) is inappropriate
            # response for a PATCH request to change a Port
            e.code = http_client.BAD_REQUEST  # BadRequest
            raise

        try:
            if port_dict['node_uuid'] != rpc_node.uuid:
                rpc_node = objects.Node.get(
                    api.request.context, port_dict['node_uuid'])
        except exception.NodeNotFound as e:
            # Change error code because 404 (NotFound) is inappropriate
            # response for a PATCH request to change a Port
            e.code = http_client.BAD_REQUEST  # BadRequest
            raise

        api_utils.patched_validate_with_schema(
            port_dict, PORT_PATCH_SCHEMA, PORT_PATCH_VALIDATOR)

        api_utils.patch_update_changed_fields(
            port_dict, rpc_port, fields=objects.Port.fields,
            schema=PORT_PATCH_SCHEMA,
            id_map={
                'node_id': rpc_node.id,
                'portgroup_id': portgroup and portgroup.id or None
            }
        )

        if (rpc_node.provision_state == ir_states.INSPECTING
                and api_utils.allow_inspect_wait_state()):
            msg = _('Cannot update port "%(port)s" on "%(node)s" while it is '
                    'in state "%(state)s".') % {'port': rpc_port.uuid,
                                                'node': rpc_node.uuid,
                                                'state': ir_states.INSPECTING}
            raise exception.ClientSideError(msg,
                                            status_code=http_client.CONFLICT)

        if (api_utils.is_path_updated(patch, '/physical_network')
            and rpc_port['physical_network'] is not None
                and not rpc_port['physical_network']):
            raise exception.Invalid(_('A non-empty value is required when '
                                      'setting physical_network'))

        if (api_utils.allow_portgroup_physical_network()
            and api_utils.is_path_updated(patch, '/physical_network')
            and portgroup is not None):
            raise exception.Invalid(
                _('physical_network cannot be changed while the port belongs '
                  'to a portgroup.'))

        notify_extra = {'node_uuid': rpc_node.uuid,
                        'portgroup_uuid': portgroup and portgroup.uuid or None}
        notify.emit_start_notification(context, rpc_port, 'update',
                                       **notify_extra)
        with notify.handle_error_notification(context, rpc_port, 'update',
                                              **notify_extra):
            topic = api.request.rpcapi.get_topic_for(rpc_node)
            new_port = api.request.rpcapi.update_port(context, rpc_port,
                                                      topic)

        api_port = convert_with_links(new_port)
        notify.emit_end_notification(context, new_port, 'update',
                                     **notify_extra)

        return api_port

    @METRICS.timer('PortsController.delete')
    @method.expose(status_code=http_client.NO_CONTENT)
    @args.validate(port_uuid=args.uuid)
    def delete(self, port_uuid):
        """Delete a port.

        :param port_uuid: UUID of a port.
        :raises: MalformedRequestURI, HTTPNotFound
        """
        if self.parent_node_ident or self.parent_portgroup_ident:
            raise exception.MalformedRequestURI()

        rpc_port, rpc_node = api_utils.check_port_policy_and_retrieve(
            'baremetal:port:delete', port_uuid)

        context = api.request.context

        portgroup_uuid = None
        if rpc_port.portgroup_id:
            portgroup = objects.Portgroup.get_by_id(context,
                                                    rpc_port.portgroup_id)
            portgroup_uuid = portgroup.uuid

        notify_extra = {'node_uuid': rpc_node.uuid,
                        'portgroup_uuid': portgroup_uuid}
        notify.emit_start_notification(context, rpc_port, 'delete',
                                       **notify_extra)
        with notify.handle_error_notification(context, rpc_port, 'delete',
                                              **notify_extra):
            topic = api.request.rpcapi.get_topic_for(rpc_node)
            api.request.rpcapi.destroy_port(context, rpc_port, topic)
        notify.emit_end_notification(context, rpc_port, 'delete',
                                     **notify_extra)
