# Copyright (c) 2015-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
#

import copy
import json
import os
import pecan
from pecan import rest
import six
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan

from fm_api import constants as fm_constants
from fm_api import fm_api

from oslo_log import log
from oslo_utils import excutils
from sysinv._i18n import _
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import link
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.api.controllers.v1.query import Query
from sysinv.api.policies import service_parameter as sp_policy
from sysinv import objects
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import kubernetes
from sysinv.common import policy
from sysinv.common import service_parameter
from sysinv.common import utils as cutils
from sysinv.openstack.common.rpc import common as rpc_common

from tsconfig.tsconfig import INITIAL_K8S_CONFIG_COMPLETE

LOG = log.getLogger(__name__)

k8s_volumes_sections = [
    constants.SERVICE_PARAM_SECTION_KUBERNETES_APISERVER_VOLUMES,
    constants.SERVICE_PARAM_SECTION_KUBERNETES_CONTROLLER_MANAGER_VOLUMES,
    constants.SERVICE_PARAM_SECTION_KUBERNETES_SCHEDULER_VOLUMES]


def delete_k8s_configmap(parameter, kube_operator):
    """
    The function removes the k8s configmap resource associated with an
    extra-volume service parameter.

    Parameters
    ----------
    parameter : dict
        Dictionary with service-parameter extra-volume data.

    Raises
    ------
    wsme.exc.ClientSideError
    """
    _volume, _noConfigMap = service_parameter.parse_volume_string_to_dict(parameter)

    # only removes the configmaps if k8s has been initialized
    # this case is only valid during the bootstrap scenario. When the ansible
    # playbook fails before the k8s service is initialized, during a replay
    # it will fail when trying to remove configmaps that don't exist yet.
    if not os.path.isfile(INITIAL_K8S_CONFIG_COMPLETE):
        return

    # only delete configmaps for 'File' type since
    # 'DirectoryorCreate' type has no associated configmaps
    pathType = _volume['pathType']
    if pathType != 'File':
        return

    configmap_name = service_parameter.get_k8s_configmap_name(parameter)
    namespace = 'kube-system'
    try:
        kube_operator.kube_delete_config_map(
            name=configmap_name, namespace=namespace)
    except Exception as e:
        msg = _("Failure deleting configmap: %s." % e)
        raise wsme.exc.ClientSideError(msg)


def create_k8s_configmap(parameter, kube_operator):
    """
    The function creates the k8s configmap resource associated with an
    extra-volume service parameter.

    Parameters
    ----------
    parameter : dict
        Dictionary with service-parameter extra-volume data.

    Raises
    ------
    wsme.exc.ClientSideError
    """
    _volume, noConfigmap = service_parameter.parse_volume_string_to_dict(parameter)

    # skip k8s configmap creation if 'noConfigmap' flag exists
    # this case is used only during bootstrap initialization
    if noConfigmap == 1:
        # removes noConfigmap flag from the service parameter value, as it is
        # no longer needed during runtime operation
        parameter['value'] = json.dumps(_volume)
        return parameter

    # only create configmaps for 'File' type
    # 'DirectoryorCreate' type has no associated configmaps
    pathType = _volume['pathType']
    if pathType != 'File':
        return parameter

    filename = _volume['hostPath']
    configmap_name = service_parameter.get_k8s_configmap_name(parameter)
    namespace = 'kube-system'

    try:
        # verifying if file exists
        if not os.path.isfile(filename):
            msg = _("File not found.")
            raise wsme.exc.ClientSideError(msg)

        # verify if configmap exists
        if kube_operator.kube_get_config_map(configmap_name, namespace):
            msg = ("Failed to create configmap %s, "
                   "configmap exists" % (configmap_name))
            raise wsme.exc.ClientSideError(msg)

        # create configmap
        kube_operator.kube_create_config_map_from_file(
            namespace, configmap_name, filename)

        # verify configmap
        try:
            configmap = kube_operator.kube_read_config_map(
                name=configmap_name, namespace=namespace)
            if configmap is None:
                msg = _("configmap not found.")
                raise wsme.exc.ClientSideError(msg)
            return parameter
        except Exception as e:
            msg = _("checking configmap: %s." % e)
            raise wsme.exc.ClientSideError(msg)

    except Exception as e:
        msg = _("Configmap add failed: %s. Service parameter add aborted." % e)
        raise wsme.exc.ClientSideError(msg)


def update_k8s_configmap(parameter, kube_operator):
    """
    The function updates the k8s configmap resource associated with an
    extra-volume service parameter.

    Parameters
    ----------
    parameter : dict
        Dictionary with service-parameter extra-volume data.

    Raises
    ------
    wsme.exc.ClientSideError
    """
    _volume, _noConfigMap = service_parameter.parse_volume_string_to_dict(parameter)

    # only update configmaps for 'File' type since
    # 'DirectoryorCreate' type has no associated configmaps
    pathType = _volume['pathType']
    if pathType != 'File':
        return

    delete_k8s_configmap(parameter, kube_operator)
    create_k8s_configmap(parameter, kube_operator)


class ServiceParameterPatchType(types.JsonPatchType):
    @staticmethod
    def mandatory_attrs():
        return ['/uuid']


class ServiceParameter(base.APIBase):
    """API representation of a Service Parameter instance.

    This class enforces type checking and value constraints, and converts
    between the internal object model and the API representation of a service
    parameter.
    """

    id = int
    "Unique ID for this entry"

    uuid = types.uuid
    "Unique UUID for this entry"

    service = wtypes.text
    "Name of a service."

    section = wtypes.text
    "Name of a section."

    name = wtypes.text
    "Name of a parameter"

    value = wtypes.text
    "Value of a parameter"

    personality = wtypes.text
    "The host personality to which the parameter is restricted."

    resource = wtypes.text
    "The puppet resource"

    links = [link.Link]
    "A list containing a self link and associated links"

    def __init__(self, **kwargs):
        self.fields = list(objects.service_parameter.fields.keys())
        for k in self.fields:
            if not hasattr(self, k):
                continue
            setattr(self, k, kwargs.get(k, wtypes.Unset))

    @classmethod
    def convert_with_links(cls, rpc_service_parameter, expand=True):
        parm = ServiceParameter(**rpc_service_parameter.as_dict())
        if not expand:
            parm.unset_fields_except(['uuid', 'service', 'section',
                                      'name', 'value', 'personality', 'resource'])

        parm.links = [link.Link.make_link('self', pecan.request.host_url,
                                          'parameters', parm.uuid),
                      link.Link.make_link('bookmark',
                                          pecan.request.host_url,
                                          'parameters', parm.uuid,
                                          bookmark=True)
                      ]
        return parm


class ServiceParameterCollection(collection.Collection):
    """API representation of a collection of service parameters."""

    parameters = [ServiceParameter]
    "A list containing Service Parameter objects"

    def __init__(self, **kwargs):
        self._type = 'parameters'

    @classmethod
    def convert_with_links(cls, rpc_service_parameter, limit, url=None,
                           expand=False,
                           **kwargs):
        collection = ServiceParameterCollection()
        collection.parameters = [ServiceParameter.convert_with_links(p, expand)
                                 for p in rpc_service_parameter]
        collection.next = collection.get_next(limit, url=url, **kwargs)
        return collection


LOCK_NAME = 'ServiceParameterController'


class ServiceParameterController(rest.RestController):
    """REST controller for ServiceParameter."""

    _custom_actions = {
        'apply': ['POST'],
    }

    def __init__(self, parent=None, **kwargs):
        self._parent = parent
        self.kube_operator = kubernetes.KubeOperator()

    def _get_service_parameter_collection(self, marker=None, limit=None,
                                          sort_key=None, sort_dir=None,
                                          expand=False, resource_url=None,
                                          q=None):
        limit = utils.validate_limit(limit)
        sort_dir = utils.validate_sort_dir(sort_dir)
        kwargs = {}
        if q is not None:
            for i in q:
                if i.op == 'eq':
                    kwargs[i.field] = i.value
        marker_obj = None
        if marker:
            marker_obj = objects.service_parameter.get_by_uuid(
                pecan.request.context, marker)

        if q is None:
            parms = pecan.request.dbapi.service_parameter_get_list(
                limit=limit, marker=marker_obj,
                sort_key=sort_key, sort_dir=sort_dir)
        else:
            kwargs['limit'] = limit
            kwargs['sort_key'] = sort_key
            kwargs['sort_dir'] = sort_dir
            parms = pecan.request.dbapi.service_parameter_get_all(**kwargs)

        # Before we can return the service parameter collection,
        # we need to ensure that the list does not contain any
        # "protected" service parameters which may need to be
        # obfuscated.
        for idx, svc_param in enumerate(parms):
            service = svc_param['service']
            section = svc_param['section']
            name = svc_param['name']

            if service in service_parameter.SERVICE_PARAMETER_SCHEMA \
                    and section in service_parameter.SERVICE_PARAMETER_SCHEMA[service]:
                schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
                if service_parameter.SERVICE_PARAM_PROTECTED in schema:
                    # atleast one parameter is to be protected
                    if name in schema[service_parameter.SERVICE_PARAM_PROTECTED]:
                        parms[idx]['value'] = service_parameter.SERVICE_VALUE_PROTECTION_MASK

        return ServiceParameterCollection.convert_with_links(
            parms, limit, url=resource_url, expand=expand,
            sort_key=sort_key, sort_dir=sort_dir)

    def _get_updates(self, patch):
        """Retrieve the updated attributes from the patch request."""
        updates = {}
        for p in patch:
            attribute = p['path'] if p['path'][0] != '/' else p['path'][1:]
            updates[attribute] = p['value']
        return updates

    @wsme_pecan.wsexpose(ServiceParameterCollection, [Query],
                         types.uuid, wtypes.text,
                         wtypes.text, wtypes.text, wtypes.text)
    def get_all(self, q=None, marker=None, limit=None,
                sort_key='id', sort_dir='asc'):
        """Retrieve a list of service parameters."""
        if q is None:
            q = []
        sort_key = ['section', 'name']
        return self._get_service_parameter_collection(marker, limit,
                                                      sort_key,
                                                      sort_dir, q=q)

    @wsme_pecan.wsexpose(ServiceParameter, types.uuid)
    def get_one(self, uuid):
        """Retrieve information about the given parameter."""
        rpc_parameter = objects.service_parameter.get_by_uuid(
            pecan.request.context, uuid)

        # Before we can return the service parameter, we need
        # to ensure that it is not a "protected" parameter
        # which may need to be obfuscated.
        service = rpc_parameter['service']
        section = rpc_parameter['section']
        name = rpc_parameter['name']

        if service in service_parameter.SERVICE_PARAMETER_SCHEMA \
                and section in service_parameter.SERVICE_PARAMETER_SCHEMA[service]:
            schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
            if service_parameter.SERVICE_PARAM_PROTECTED in schema:
                # parameter is to be protected
                if name in schema[service_parameter.SERVICE_PARAM_PROTECTED]:
                    rpc_parameter['value'] = service_parameter.SERVICE_VALUE_PROTECTION_MASK

        return ServiceParameter.convert_with_links(rpc_parameter)

    @staticmethod
    def _check_read_only_parameter(svc_param):
        """Check the read-only attribute of service parameter"""
        service = svc_param['service']
        section = svc_param['section']
        name = svc_param['name']

        schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
        readonly_parameters = schema.get(service_parameter.SERVICE_PARAM_READONLY, [])
        if name in readonly_parameters:
            msg = _("The parameter '%s' is readonly." % name)
            raise wsme.exc.ClientSideError(msg)

    @staticmethod
    def _check_parameter_syntax(svc_param):
        """Check the attributes of service parameter"""
        service = svc_param['service']
        section = svc_param['section']
        name = svc_param['name']
        value = svc_param['value']

        if service == constants.SERVICE_TYPE_PTP:
            msg = _("%s service is deprecated" % service)
            raise wsme.exc.ClientSideError(msg)

        schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
        parameters = (schema.get(service_parameter.SERVICE_PARAM_MANDATORY, []) +
                      schema.get(service_parameter.SERVICE_PARAM_OPTIONAL, []))
        has_wildcard = (constants.SERVICE_PARAM_NAME_WILDCARD in parameters)
        if name not in parameters and not has_wildcard:
            msg = _("The parameter name %s is invalid for "
                    "service %s section %s"
                    % (name, service, section))
            raise wsme.exc.ClientSideError(msg)

        if not value:
            msg = _("The service parameter value is mandatory")
            raise wsme.exc.ClientSideError(msg)

        if len(value) > service_parameter.SERVICE_PARAMETER_MAX_LENGTH:
            msg = _("The service parameter value is restricted to at most %d "
                    "characters." % service_parameter.SERVICE_PARAMETER_MAX_LENGTH)
            raise wsme.exc.ClientSideError(msg)

        validators = schema.get(service_parameter.SERVICE_PARAM_VALIDATOR, {})
        validator = validators.get(name)
        if callable(validator):
            validator(name, value)

    @staticmethod
    def _check_custom_parameter_syntax(svc_param):
        """Check the attributes of custom service parameter"""
        service = svc_param['service']
        section = svc_param['section']
        name = svc_param['name']
        value = svc_param['value']
        personality = svc_param['personality']
        resource = svc_param['resource']

        if service == constants.SERVICE_TYPE_PTP:
            msg = _("%s service is deprecated" % service)
            raise wsme.exc.ClientSideError(msg)

        if personality is not None and personality not in constants.PERSONALITIES:
            msg = _("%s is not a supported personality type" % personality)
            raise wsme.exc.ClientSideError(msg)

        if len(resource) > service_parameter.SERVICE_PARAMETER_MAX_LENGTH:
            msg = _("The custom resource option is restricted to at most %d "
                    "characters." % service_parameter.SERVICE_PARAMETER_MAX_LENGTH)
            raise wsme.exc.ClientSideError(msg)

        if service in service_parameter.SERVICE_PARAMETER_SCHEMA \
                and section in service_parameter.SERVICE_PARAMETER_SCHEMA[service]:
            schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
            parameters = (schema.get(service_parameter.SERVICE_PARAM_MANDATORY, []) +
                          schema.get(service_parameter.SERVICE_PARAM_OPTIONAL, []))
            if name in parameters:
                msg = _("The parameter name %s is reserved for "
                        "service %s section %s, and cannot be customized"
                        % (name, service, section))
                raise wsme.exc.ClientSideError(msg)

            if value is not None and len(value) > service_parameter.SERVICE_PARAMETER_MAX_LENGTH:
                msg = _("The service parameter value is restricted to at most %d "
                        "characters." % service_parameter.SERVICE_PARAMETER_MAX_LENGTH)
                raise wsme.exc.ClientSideError(msg)

        mapped_resource = service_parameter.map_resource(resource)
        if mapped_resource is not None:
            msg = _("The specified resource is reserved for "
                    "service=%s section=%s name=%s and cannot "
                    "be customized."
                    % (mapped_resource.get('service'),
                       mapped_resource.get('section'),
                       mapped_resource.get('name')))
            raise wsme.exc.ClientSideError(msg)

    def post_custom_resource(self, body, personality, resource):
        """Create new custom Service Parameter."""

        if resource is None:
            raise wsme.exc.ClientSideError(_("Unspecified resource"))

        service = body.get('service')
        if not service:
            raise wsme.exc.ClientSideError("Unspecified service name")

        section = body.get('section')
        if not section:
            raise wsme.exc.ClientSideError(_("Unspecified section name."))

        new_records = []
        parameters = body.get('parameters')
        if not parameters:
            raise wsme.exc.ClientSideError(_("Unspecified parameters."))

        if len(parameters) > 1:
            msg = _("Cannot specify multiple parameters with custom resource.")
            raise wsme.exc.ClientSideError(msg)

        for name, value in parameters.items():
            new_record = {
                'service': service,
                'section': section,
                'name': name,
                'value': value,
                'personality': personality,
                'resource': resource,
            }
            self._check_custom_parameter_syntax(new_record)

            existing = False
            try:
                pecan.request.dbapi.service_parameter_get_one(
                    service, section, name,
                    personality, resource)
                existing = True
            except exception.NotFound:
                pass
            except exception.MultipleResults:
                # We'll check/handle this in the "finally" block
                existing = True
            finally:
                if existing:
                    msg = _("Service parameter add failed: "
                            "Parameter already exists: "
                            "service=%s section=%s name=%s "
                            "personality=%s resource=%s"
                            % (service, section, name,
                               personality, resource))
                    raise wsme.exc.ClientSideError(msg)

            new_records.append(new_record)

        svc_params = []
        for n in new_records:
            try:
                new_parm = pecan.request.dbapi.service_parameter_create(n)
            except exception.NotFound:
                msg = _("Service parameter add failed:  "
                        "service %s section %s name %s value %s"
                        " personality %s resource %s"
                        % (service, section, n.name, n.value, personality, resource))
                raise wsme.exc.ClientSideError(msg)
            svc_params.append(new_parm)

        try:
            pecan.request.rpcapi.update_service_config(
                pecan.request.context, service, section=section)
        except rpc_common.RemoteError as e:
            # rollback create service parameters
            for p in svc_params:
                try:
                    pecan.request.dbapi.service_parameter_destroy_uuid(p.uuid)
                    LOG.warn(_("Rollback service parameter create: "
                               "destroy uuid {}".format(p.uuid)))
                except exception.SysinvException:
                    pass
            raise wsme.exc.ClientSideError(str(e.value))
        except Exception as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(e)

        return ServiceParameterCollection.convert_with_links(
            svc_params, limit=None, url=None, expand=False,
            sort_key='id', sort_dir='asc')

    @cutils.synchronized(LOCK_NAME)
    @wsme_pecan.wsexpose(ServiceParameterCollection, body=types.apidict)
    def post(self, body):
        """Create new Service Parameter."""

        resource = body.get('resource')
        personality = body.get('personality')

        if personality is not None or resource is not None:
            return self.post_custom_resource(body, personality, resource)

        service = self._get_service(body)

        section = body.get('section')
        if not section:
            raise wsme.exc.ClientSideError(_("Unspecified section name."))
        elif section not in service_parameter.SERVICE_PARAMETER_SCHEMA[service]:
            msg = _("Invalid service section %s." % section)
            raise wsme.exc.ClientSideError(msg)

        new_records = []
        parameters = body.get('parameters')
        if not parameters:
            raise wsme.exc.ClientSideError(_("Unspecified parameters."))

        for name, value in parameters.items():
            new_record = {
                'service': service,
                'section': section,
                'name': name,
                'value': value,
            }
            self._check_parameter_syntax(new_record)

            existing = False
            try:
                pecan.request.dbapi.service_parameter_get_one(
                    service, section, name)
                existing = True
            except exception.NotFound:
                pass
            except exception.MultipleResults:
                # We'll check/handle this in the "finally" block
                existing = True
            finally:
                if existing:
                    msg = _("Service parameter add failed: "
                            "Parameter already exists: "
                            "service=%s section=%s name=%s"
                            % (service, section, name))
                    raise wsme.exc.ClientSideError(msg)

            new_records.append(new_record)

        svc_params = []
        for n in new_records:
            # If the parameter being created belongs to the kube_apiserver_volumes,
            # kube_controller_manager_volumes or scheduler_volumes sections and
            # does not define a directory volume, a K8s configmap is
            # created from the configuration file pointed by the parameter.
            if n['section'] in k8s_volumes_sections:
                n = create_k8s_configmap(n, self.kube_operator)

            # creating new service-parameter in the sysinv database
            try:
                new_parm = pecan.request.dbapi.service_parameter_create(n)
            except exception.NotFound:
                msg = _("Service parameter add failed:  "
                        "service %s section %s name %s value %s"
                        % (service, section, n.name, n.value))
                raise wsme.exc.ClientSideError(msg)
            svc_params.append(new_parm)

        try:
            # Pass name to update_service_config only in case the parameters are the Intel
            # NIC driver version and intel_pstate.
            new_name = None
            if section == constants.SERVICE_PARAM_SECTION_PLATFORM_CONFIG and \
                    name == constants.SERVICE_PARAM_NAME_PLAT_CONFIG_INTEL_NIC_DRIVER_VERSION:
                new_name = name

            elif section == constants.SERVICE_PARAM_SECTION_PLATFORM_CONFIG and \
                    name == constants.SERVICE_PARAM_NAME_PLAT_CONFIG_INTEL_PSTATE:
                new_name = name

            pecan.request.rpcapi.update_service_config(
                pecan.request.context, service, section=section, name=new_name)
        except rpc_common.RemoteError as e:
            # rollback create service parameters
            for p in svc_params:
                try:
                    pecan.request.dbapi.service_parameter_destroy_uuid(p.uuid)
                    LOG.warn(_("Rollback service parameter create: "
                               "destroy uuid {}".format(p.uuid)))
                except exception.SysinvException:
                    pass
            raise wsme.exc.ClientSideError(str(e.value))
        except Exception as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(e)

        return ServiceParameterCollection.convert_with_links(
            svc_params, limit=None, url=None, expand=False,
            sort_key='id', sort_dir='asc')

    def patch_custom_resource(self, uuid, patch, personality, resource):
        """Updates attributes of Service Parameter."""

        parameter = objects.service_parameter.get_by_uuid(
            pecan.request.context, uuid)

        parameter = parameter.as_dict()
        old_parameter = copy.deepcopy(parameter)

        updates = self._get_updates(patch)
        parameter.update(updates)

        self._check_custom_parameter_syntax(parameter)

        updated_parameter = pecan.request.dbapi.service_parameter_update(
            uuid, updates)

        try:
            pecan.request.rpcapi.update_service_config(
                pecan.request.context,
                parameter['service'],
                section=parameter['section'])
        except rpc_common.RemoteError as e:
            # rollback service parameter update
            try:
                pecan.request.dbapi.service_parameter_update(uuid, old_parameter)
                LOG.warn(_("Rollback service parameter update: "
                           "uuid={}, old_values={}".format(uuid, old_parameter)))
            except exception.SysinvException:
                pass
            raise wsme.exc.ClientSideError(str(e.value))

        return ServiceParameter.convert_with_links(updated_parameter)

    @cutils.synchronized(LOCK_NAME)
    @wsme.validate(types.uuid, [ServiceParameterPatchType])
    @wsme_pecan.wsexpose(ServiceParameter, types.uuid,
                         body=[ServiceParameterPatchType])
    def patch(self, uuid, patch):
        """Updates attributes of Service Parameter."""

        parameter = objects.service_parameter.get_by_uuid(
            pecan.request.context, uuid)
        if parameter.personality is not None or parameter.resource is not None:
            return self.patch_custom_resource(uuid,
                                              patch,
                                              parameter.personality,
                                              parameter.resource)

        parameter = parameter.as_dict()
        old_parameter = copy.deepcopy(parameter)

        updates = self._get_updates(patch)
        parameter.update(updates)

        self._check_parameter_syntax(parameter)
        self._check_read_only_parameter(parameter)

        updated_parameter = pecan.request.dbapi.service_parameter_update(
            uuid, updates)

        # If the parameter being updated belongs to the kube_apiserver_volumes,
        # kube_controller_manager_volumes or scheduler_volumes sections and
        # does not define a directory volume, the K8s configmap is
        # updated from the configuration file pointed by the parameter.
        if parameter['section'] in k8s_volumes_sections:
            update_k8s_configmap(parameter, self.kube_operator)

        try:
            if parameter['name'] not in service_parameter.DB_ONLY_SERVICE_PARAMETERS:
                pecan.request.rpcapi.update_service_config(
                    pecan.request.context,
                    parameter['service'],
                    section=parameter['section'],
                    name=parameter['name'])
        except rpc_common.RemoteError as e:
            # rollback service parameter update
            try:
                pecan.request.dbapi.service_parameter_update(uuid, old_parameter)
                LOG.warn(_("Rollback service parameter update: "
                           "uuid={}, old_values={}".format(uuid, old_parameter)))
            except exception.SysinvException:
                pass
            raise wsme.exc.ClientSideError(str(e.value))

        # Before we can return the service parameter, we need
        # to ensure that this updated parameter is not "protected"
        # which may need to be obfuscated.
        service = updated_parameter['service']
        section = updated_parameter['section']
        name = updated_parameter['name']

        if service in service_parameter.SERVICE_PARAMETER_SCHEMA \
                and section in service_parameter.SERVICE_PARAMETER_SCHEMA[service]:
            schema = service_parameter.SERVICE_PARAMETER_SCHEMA[service][section]
            if service_parameter.SERVICE_PARAM_PROTECTED in schema:
                # parameter is to be protected
                if name in schema[service_parameter.SERVICE_PARAM_PROTECTED]:
                    updated_parameter['value'] = service_parameter.SERVICE_VALUE_PROTECTION_MASK

        return ServiceParameter.convert_with_links(updated_parameter)

    @cutils.synchronized(LOCK_NAME)
    @wsme_pecan.wsexpose(None, types.uuid, status_code=204)
    def delete(self, uuid):
        """Delete a Service Parameter instance."""
        parameter = objects.service_parameter.get_by_uuid(pecan.request.context, uuid)

        if parameter.section == \
                constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE:
            msg = _("Platform Maintenance Parameter '%s' is required." %
                    parameter.name)
            raise wsme.exc.ClientSideError(msg)

        # delete parameter from sysinv database
        pecan.request.dbapi.service_parameter_destroy_uuid(uuid)

        # if the parameter being deleted has an associated K8s configmap,
        # delete it using the K8s API.
        if parameter.section in k8s_volumes_sections:
            delete_k8s_configmap(parameter.as_dict(), self.kube_operator)

        try:
            # Pass name to update_service_config only in case the parameter is the Intel
            # NIC driver version
            name = None
            if parameter.section == constants.SERVICE_PARAM_SECTION_PLATFORM_CONFIG and \
                    parameter.name == constants.SERVICE_PARAM_NAME_PLAT_CONFIG_INTEL_NIC_DRIVER_VERSION:
                name = parameter.name

            elif parameter.section == constants.SERVICE_PARAM_SECTION_PLATFORM_CONFIG and \
                   parameter.name == constants.SERVICE_PARAM_NAME_PLAT_CONFIG_INTEL_PSTATE:
                name = parameter.name

            pecan.request.rpcapi.update_service_config(
                pecan.request.context,
                parameter.service,
                section=parameter.section,
                name=name)
        except rpc_common.RemoteError as e:
            # rollback destroy service parameter
            try:
                parameter = parameter.as_dict()
                pecan.request.dbapi.service_parameter_create(parameter)
                LOG.warn(_("Rollback service parameter destroy: "
                           "create parameter with values={}".format(parameter)))
                # rollback parameter has a different uuid
            except exception.SysinvException:
                pass
            raise wsme.exc.ClientSideError(str(e.value))

    @staticmethod
    def _service_parameter_apply_semantic_check_mtce():
        """Semantic checks for the Platform Maintenance Service Type """
        hbs_failure_threshold = pecan.request.dbapi.service_parameter_get_one(
            service=constants.SERVICE_TYPE_PLATFORM,
            section=constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
            name=constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_THRESHOLD)

        hbs_degrade_threshold = pecan.request.dbapi.service_parameter_get_one(
            service=constants.SERVICE_TYPE_PLATFORM,
            section=constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
            name=constants.SERVICE_PARAM_PLAT_MTCE_HBS_DEGRADE_THRESHOLD)

        if int(hbs_degrade_threshold.value) >= int(hbs_failure_threshold.value):
            msg = _("Unable to apply service parameters. "
                    "Service parameter '%s' should be greater than '%s' "
                    % (
                        constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_THRESHOLD,
                        constants.SERVICE_PARAM_PLAT_MTCE_HBS_DEGRADE_THRESHOLD
                    ))
            raise wsme.exc.ClientSideError(msg)

    @staticmethod
    def _service_parameter_apply_semantic_check_http():
        """Semantic checks for the HTTP Service Type """

        # check if a patching operation in progress
        fm = fm_api.FaultAPIs()

        alarms = fm.get_faults_by_id(fm_constants.
                                     FM_ALARM_ID_PATCH_IN_PROGRESS)
        if alarms is not None:
            msg = _("Unable to apply %s service parameters. "
                    "A patching operation is in progress."
                    % constants.SERVICE_TYPE_HTTP)
            raise wsme.exc.ClientSideError(msg)

        # check if a device image update operation is in progress
        alarms = fm.get_faults_by_id(fm_constants.
                                     FM_ALARM_ID_DEVICE_IMAGE_UPDATE_IN_PROGRESS)
        if alarms is not None:
            msg = _("Unable to apply %s service parameters. "
                    "A device image update operation is in progress. "
                    "Please try again later when the operation is complete."
                    % constants.SERVICE_TYPE_HTTP)
            raise wsme.exc.ClientSideError(msg)

        # check if all hosts are unlocked/enabled
        hosts = pecan.request.dbapi.ihost_get_list()
        for host in hosts:
            if (host['administrative'] == constants.ADMIN_UNLOCKED and
                        host['operational'] == constants.OPERATIONAL_ENABLED):
                continue
            else:
                # the host name might be None for a newly discovered host
                if not host['hostname']:
                    host_id = host['uuid']
                else:
                    host_id = host['hostname']
                raise wsme.exc.ClientSideError(
                    _("Host %s must be unlocked and enabled." % host_id))

    @staticmethod
    def _service_parameter_apply_semantic_check_kubernetes():
        """Semantic checks for the Platform Kubernetes Service Type """
        try:
            oidc_issuer_url = pecan.request.dbapi.service_parameter_get_one(
                service=constants.SERVICE_TYPE_KUBERNETES,
                section=constants.SERVICE_PARAM_SECTION_KUBERNETES_APISERVER,
                name=constants.SERVICE_PARAM_NAME_OIDC_ISSUER_URL)
        except exception.NotFound:
            oidc_issuer_url = None

        try:
            oidc_client_id = pecan.request.dbapi.service_parameter_get_one(
                service=constants.SERVICE_TYPE_KUBERNETES,
                section=constants.SERVICE_PARAM_SECTION_KUBERNETES_APISERVER,
                name=constants.SERVICE_PARAM_NAME_OIDC_CLIENT_ID)
        except exception.NotFound:
            oidc_client_id = None

        try:
            oidc_username_claim = pecan.request.dbapi.service_parameter_get_one(
                service=constants.SERVICE_TYPE_KUBERNETES,
                section=constants.SERVICE_PARAM_SECTION_KUBERNETES_APISERVER,
                name=constants.SERVICE_PARAM_NAME_OIDC_USERNAME_CLAIM)
        except exception.NotFound:
            oidc_username_claim = None

        try:
            oidc_groups_claim = pecan.request.dbapi.service_parameter_get_one(
                service=constants.SERVICE_TYPE_KUBERNETES,
                section=constants.SERVICE_PARAM_SECTION_KUBERNETES_APISERVER,
                name=constants.SERVICE_PARAM_NAME_OIDC_GROUPS_CLAIM)
        except exception.NotFound:
            oidc_groups_claim = None

        if not ((not oidc_issuer_url and not oidc_client_id and
                 not oidc_username_claim and not oidc_groups_claim) or
                (oidc_issuer_url and oidc_client_id and
                 oidc_username_claim and not oidc_groups_claim) or
                (oidc_issuer_url and oidc_client_id and
                 oidc_username_claim and oidc_groups_claim)):
            msg = _("Unable to apply service parameters. Please choose one of "
                    "the valid Kubernetes OIDC parameter setups: (None) or "
                    "(oidc-issuer-url, oidc-client-id, oidc-username-claim) or "
                    "(the previous 3 plus oidc-groups-claim)")
            raise wsme.exc.ClientSideError(msg)

    def _service_parameter_apply_semantic_check(self, service):
        """Semantic checks for the service-parameter-apply command """

        # Check if all the mandatory parameters have been configured
        for section, schema in service_parameter.SERVICE_PARAMETER_SCHEMA[service].items():
            mandatory = schema.get(service_parameter.SERVICE_PARAM_MANDATORY, [])
            for name in mandatory:
                try:
                    pecan.request.dbapi.service_parameter_get_one(
                        service=service, section=section, name=name)
                except exception.NotFound:
                    msg = _("Unable to apply service parameters. "
                            "Missing service parameter '%s' for service '%s' "
                            "in section '%s'." % (name, service, section))
                    raise wsme.exc.ClientSideError(msg)

        # Apply service specific semantic checks
        if service == constants.SERVICE_TYPE_PLATFORM:
            self._service_parameter_apply_semantic_check_mtce()

        elif service == constants.SERVICE_TYPE_HTTP:
            self._service_parameter_apply_semantic_check_http()

        elif service == constants.SERVICE_TYPE_KUBERNETES:
            self._service_parameter_apply_semantic_check_kubernetes()

        elif service == constants.SERVICE_TYPE_PTP:
            msg = _("%s service is deprecated" % service)
            raise wsme.exc.ClientSideError(msg)

    def _get_service(self, body):
        service = body.get('service') or ""
        if not service:
            raise wsme.exc.ClientSideError("Unspecified service name")
        if body['service'] not in service_parameter.SERVICE_PARAMETER_SCHEMA:
            msg = _("Invalid service name %s." % body['service'])
            raise wsme.exc.ClientSideError(msg)
        return service

    def _get_section(self, body):
        section = body.get('section') or ""
        return section

    @cutils.synchronized(LOCK_NAME)
    @wsme_pecan.wsexpose('json', body=six.text_type)
    def apply(self, body):
        """ Apply the service parameters."""
        service = self._get_service(body)
        section = self._get_section(body)

        self._service_parameter_apply_semantic_check(service)
        try:
            pecan.request.rpcapi.update_service_config(
                pecan.request.context, service, section=section, do_apply=True)
        except rpc_common.RemoteError as e:
            raise wsme.exc.ClientSideError(str(e.value))
        except Exception as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(e)

    def enforce_policy(self, method_name, request):
        """Check policy rules for each action of this controller."""
        context_dict = request.context.to_dict()
        if method_name == "apply":
            policy.authorize(sp_policy.POLICY_ROOT % "apply", {}, context_dict)
        elif method_name == "delete":
            policy.authorize(sp_policy.POLICY_ROOT % "delete", {}, context_dict)
        elif method_name in ["get_all", "get_one"]:
            policy.authorize(sp_policy.POLICY_ROOT % "get", {}, context_dict)
        elif method_name == "patch":
            policy.authorize(sp_policy.POLICY_ROOT % "modify", {}, context_dict)
        elif method_name == "post":
            policy.authorize(sp_policy.POLICY_ROOT % "add", {}, context_dict)
        else:
            raise exception.PolicyNotFound()
