387 lines
14 KiB
Python
387 lines
14 KiB
Python
# Copyright (c) 2018 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import json
|
|
import os
|
|
import pecan
|
|
from pecan import rest
|
|
import wsme
|
|
import wsmeext.pecan as wsme_pecan
|
|
from wsme import types as wtypes
|
|
|
|
from oslo_log import log
|
|
from sysinv._i18n import _
|
|
from sysinv import objects
|
|
from sysinv.api.controllers.v1 import base
|
|
from sysinv.api.controllers.v1 import collection
|
|
from sysinv.api.controllers.v1 import types
|
|
from sysinv.api.controllers.v1 import utils
|
|
from sysinv.api.controllers.v1 import vim_api
|
|
from sysinv.common import constants
|
|
from sysinv.common import exception
|
|
from sysinv.common import utils as cutils
|
|
from sysinv.helm import common
|
|
from sysinv.openstack.common import excutils
|
|
from sysinv.openstack.common.rpc import common as rpc_common
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class LabelPatchType(types.JsonPatchType):
|
|
@staticmethod
|
|
def mandatory_attrs():
|
|
return []
|
|
|
|
|
|
class Label(base.APIBase):
|
|
"""API representation of host label Configuration.
|
|
|
|
Kubernetes labels are assigned to nodes(ie. hosts)
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation of
|
|
a host label.
|
|
"""
|
|
|
|
uuid = types.uuid
|
|
"Unique UUID for this label"
|
|
|
|
label_key = wtypes.text
|
|
"Represents a label key assigned to the host"
|
|
|
|
label_value = wtypes.text
|
|
"Represents a label value assigned to the host"
|
|
|
|
host_id = int
|
|
"Represent the host_id the label belongs to"
|
|
|
|
host_uuid = types.uuid
|
|
"The uuid of the host this label belongs to"
|
|
|
|
def __init__(self, **kwargs):
|
|
self.fields = objects.label.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_label, expand=False):
|
|
label = Label(**rpc_label.as_dict())
|
|
if not expand:
|
|
label.unset_fields_except(['uuid',
|
|
'host_uuid',
|
|
'label_key',
|
|
'label_value'])
|
|
|
|
# do not expose the id attribute
|
|
label.host_id = wtypes.Unset
|
|
|
|
return label
|
|
|
|
|
|
class LabelCollection(collection.Collection):
|
|
"""API representation of a collection of labels."""
|
|
|
|
labels = [Label]
|
|
"A list containing label objects"
|
|
|
|
def __init__(self, **kwargs):
|
|
self._type = 'labels'
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_labels, limit, url=None,
|
|
expand=False, **kwargs):
|
|
collection = LabelCollection()
|
|
collection.labels = [Label.convert_with_links(p, expand)
|
|
for p in rpc_labels]
|
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
|
return collection
|
|
|
|
|
|
LOCK_NAME = 'LabelController'
|
|
|
|
|
|
class LabelController(rest.RestController):
|
|
"""REST controller for labels."""
|
|
|
|
_custom_actions = {
|
|
'detail': ['GET'],
|
|
}
|
|
|
|
def __init__(self, from_ihosts=False):
|
|
self._from_ihosts = from_ihosts
|
|
|
|
def _get_labels_collection(self, host_uuid, marker, limit, sort_key,
|
|
sort_dir, expand=False, resource_url=None):
|
|
if self._from_ihosts and not host_uuid:
|
|
raise exception.InvalidParameterValue(_(
|
|
"Host id not specified."))
|
|
|
|
limit = utils.validate_limit(limit)
|
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
|
marker_obj = None
|
|
if marker:
|
|
marker_obj = objects.label.get_by_uuid(
|
|
pecan.request.context,
|
|
marker)
|
|
if self._from_ihosts:
|
|
host_label = pecan.request.dbapi.label_get_by_host(
|
|
host_uuid, limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
else:
|
|
if host_uuid:
|
|
host_label = pecan.request.dbapi.label_get_by_host(
|
|
host_uuid, limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
else:
|
|
host_label = pecan.request.dbapi.label_get_list(
|
|
limit, marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
|
|
return LabelCollection.convert_with_links(host_label, limit,
|
|
url=resource_url,
|
|
expand=expand,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
|
|
@wsme_pecan.wsexpose(LabelCollection, types.uuid, types.uuid,
|
|
int, wtypes.text, wtypes.text)
|
|
def get_all(self, uuid=None, marker=None, limit=None,
|
|
sort_key='id', sort_dir='asc'):
|
|
"""Retrieve a list of labels."""
|
|
return self._get_labels_collection(uuid,
|
|
marker, limit, sort_key, sort_dir)
|
|
|
|
@wsme_pecan.wsexpose(LabelCollection, types.uuid, types.uuid, int,
|
|
wtypes.text, wtypes.text)
|
|
def detail(self, uuid=None, marker=None, limit=None,
|
|
sort_key='id', sort_dir='asc'):
|
|
"""Retrieve a list of devices with detail."""
|
|
|
|
# NOTE: /detail should only work against collections
|
|
parent = pecan.request.path.split('/')[:-1][-1]
|
|
if parent != "labels":
|
|
raise exception.HTTPNotFound
|
|
|
|
expand = True
|
|
resource_url = '/'.join(['labels', 'detail'])
|
|
return self._get_labels_collection(uuid, marker, limit, sort_key,
|
|
sort_dir, expand, resource_url)
|
|
|
|
@wsme_pecan.wsexpose(Label, types.uuid)
|
|
def get_one(self, label_uuid):
|
|
"""Retrieve information about the given label."""
|
|
|
|
try:
|
|
sp_label = objects.label.get_by_uuid(
|
|
pecan.request.context,
|
|
label_uuid)
|
|
except exception.InvalidParameterValue:
|
|
raise wsme.exc.ClientSideError(
|
|
_("No label found for %s" % label_uuid))
|
|
|
|
return Label.convert_with_links(sp_label)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(LabelCollection, types.uuid, types.boolean,
|
|
body=types.apidict)
|
|
def post(self, uuid, overwrite=False, body=None):
|
|
"""Assign label(s) to a host.
|
|
"""
|
|
if self._from_ihosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
LOG.info("patch_data: %s" % body)
|
|
host = objects.host.get_by_uuid(pecan.request.context, uuid)
|
|
|
|
_check_host_locked(host, body.keys())
|
|
|
|
_semantic_check_worker_labels(body)
|
|
|
|
_semantic_check_k8s_plugins_labels(host, body)
|
|
|
|
existing_labels = {}
|
|
for label_key in body.keys():
|
|
label = None
|
|
try:
|
|
label = pecan.request.dbapi.label_query(host.id, label_key)
|
|
except exception.HostLabelNotFoundByKey:
|
|
pass
|
|
if label:
|
|
if overwrite:
|
|
existing_labels.update({label_key: label.uuid})
|
|
else:
|
|
raise wsme.exc.ClientSideError(
|
|
_("Label %s exists for host %s. Use overwrite option to assign a new value." % (
|
|
label_key, host.hostname)))
|
|
|
|
try:
|
|
pecan.request.rpcapi.update_kubernetes_label(
|
|
pecan.request.context,
|
|
host.uuid,
|
|
body
|
|
)
|
|
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)
|
|
|
|
new_records = []
|
|
for key, value in body.items():
|
|
values = {
|
|
'host_id': host.id,
|
|
'label_key': key,
|
|
'label_value': value
|
|
}
|
|
try:
|
|
if existing_labels.get(key, None):
|
|
# Update the value
|
|
label_uuid = existing_labels.get(key)
|
|
new_label = pecan.request.dbapi.label_update(label_uuid, {'label_value': value})
|
|
else:
|
|
new_label = pecan.request.dbapi.label_create(uuid, values)
|
|
new_records.append(new_label)
|
|
except exception.HostLabelAlreadyExists:
|
|
# We should not be here
|
|
raise wsme.exc.ClientSideError(_("Error creating label %s") % label_key)
|
|
|
|
try:
|
|
vim_api.vim_host_update(
|
|
None,
|
|
uuid,
|
|
host.hostname,
|
|
constants.VIM_DEFAULT_TIMEOUT_IN_SECS)
|
|
except Exception as e:
|
|
LOG.warn(_("No response vim_api host:%s e=%s" %
|
|
(host.hostname, e)))
|
|
pass
|
|
|
|
return LabelCollection.convert_with_links(
|
|
new_records, limit=None, url=None, expand=False,
|
|
sort_key='id', sort_dir='asc')
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
|
def delete(self, uuid):
|
|
"""Delete a host label."""
|
|
if self._from_ihosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
lbl_obj = objects.label.get_by_uuid(pecan.request.context, uuid)
|
|
host = objects.host.get_by_uuid(pecan.request.context, lbl_obj.host_id)
|
|
|
|
_check_host_locked(host, [lbl_obj.label_key])
|
|
|
|
label_dict = {lbl_obj.label_key: None}
|
|
|
|
try:
|
|
pecan.request.rpcapi.update_kubernetes_label(
|
|
pecan.request.context,
|
|
host.uuid,
|
|
label_dict)
|
|
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)
|
|
|
|
try:
|
|
pecan.request.dbapi.label_destroy(lbl_obj.uuid)
|
|
except exception.HostLabelNotFound:
|
|
msg = _("Delete host label failed: host %s label %s=%s"
|
|
% (host.hostname, lbl_obj.label_key, lbl_obj.label_value))
|
|
raise wsme.exc.ClientSideError(msg)
|
|
|
|
try:
|
|
vim_api.vim_host_update(
|
|
None,
|
|
host.uuid,
|
|
host.hostname,
|
|
constants.VIM_DEFAULT_TIMEOUT_IN_SECS)
|
|
except Exception as e:
|
|
LOG.warn(_("No response vim_api host:%s e=%s" %
|
|
(host.hostname, e)))
|
|
pass
|
|
|
|
|
|
###########
|
|
# UTILS
|
|
###########
|
|
def _check_host_locked(host, host_labels):
|
|
if host.administrative != constants.ADMIN_LOCKED:
|
|
# check if host has any labels which require host-lock
|
|
labels_requiring_lock = \
|
|
[common.LABEL_CONTROLLER,
|
|
common.LABEL_COMPUTE_LABEL,
|
|
common.LABEL_OPENVSWITCH,
|
|
common.LABEL_REMOTE_STORAGE,
|
|
common.LABEL_SRIOVDP,
|
|
constants.KUBE_TOPOLOGY_MANAGER_LABEL,
|
|
constants.KUBE_CPU_MANAGER_LABEL]
|
|
|
|
lock_required_labels = [x for x in host_labels
|
|
if x in labels_requiring_lock]
|
|
|
|
if lock_required_labels:
|
|
raise wsme.exc.ClientSideError(
|
|
"Host %s must be locked for label(s)=%s." %
|
|
(host.hostname, lock_required_labels))
|
|
|
|
|
|
def _semantic_check_worker_labels(body):
|
|
"""
|
|
Perform semantic checks to ensure the worker labels are valid.
|
|
"""
|
|
for label_key, label_value in body.items():
|
|
if label_key == constants.KUBE_TOPOLOGY_MANAGER_LABEL:
|
|
if label_value not in constants.KUBE_TOPOLOGY_MANAGER_VALUES:
|
|
raise wsme.exc.ClientSideError(
|
|
_(
|
|
"Invalid value for %s label." % constants.KUBE_TOPOLOGY_MANAGER_LABEL))
|
|
elif label_key == constants.KUBE_CPU_MANAGER_LABEL:
|
|
if label_value not in constants.KUBE_CPU_MANAGER_VALUES:
|
|
raise wsme.exc.ClientSideError(
|
|
_(
|
|
"Invalid value for %s label." % constants.KUBE_CPU_MANAGER_LABEL))
|
|
|
|
|
|
def _get_system_enabled_k8s_plugins():
|
|
if not os.path.isfile(constants.ENABLED_KUBE_PLUGINS):
|
|
return None
|
|
|
|
with open(constants.ENABLED_KUBE_PLUGINS) as f:
|
|
return json.loads(f.read())
|
|
|
|
|
|
def _semantic_check_intel_gpu_plugins_labels(host):
|
|
pci_devices = pecan.request.dbapi.pci_device_get_by_host(host.id)
|
|
for pci_device in pci_devices:
|
|
if ("VGA" in pci_device.pclass and pci_device.driver == "i915"):
|
|
return
|
|
|
|
raise wsme.exc.ClientSideError("Host %s does not support Intel GPU device plugin." % (host.hostname))
|
|
|
|
|
|
def _semantic_check_k8s_plugins_labels(host, body):
|
|
"""
|
|
Perform hardware checks to ensure k8s plugins labels are valid on particular node.
|
|
"""
|
|
plugins = _get_system_enabled_k8s_plugins()
|
|
if plugins is None:
|
|
return
|
|
|
|
for label_key, label_value in body.items():
|
|
label = label_key + "=" + label_value
|
|
if label in plugins.values():
|
|
if label == constants.KUBE_INTEL_GPU_DEVICE_PLUGIN_LABEL:
|
|
_semantic_check_intel_gpu_plugins_labels(host)
|