Extend sysinv to assign kubernetes labels to nodes
Add CLI commands and restapi to assign and remove kubernetes labels to hosts. Change-Id: I3f68fcd331d5539429f86dfd4bf7514dc963bedb Signed-off-by: David Sullivan <david.sullivan@windriver.com> Story: 2002845 Task: 22793 Depends-On: https://review.openstack.org/#/c/595875/
This commit is contained in:
parent
19e20118d8
commit
54142cc5e7
@ -49,6 +49,7 @@ from cgtsclient.v1 import istor
|
||||
from cgtsclient.v1 import isystem
|
||||
from cgtsclient.v1 import itrapdest
|
||||
from cgtsclient.v1 import iuser
|
||||
from cgtsclient.v1 import label
|
||||
from cgtsclient.v1 import license
|
||||
from cgtsclient.v1 import lldp_agent
|
||||
from cgtsclient.v1 import lldp_neighbour
|
||||
@ -146,3 +147,4 @@ class Client(http.HTTPClient):
|
||||
self.storage_ceph_external = \
|
||||
storage_ceph_external.StorageCephExternalManager(self)
|
||||
self.helm = helm.HelmManager(self)
|
||||
self.label = label.KubernetesLabelManager(self)
|
||||
|
41
sysinv/cgts-client/cgts-client/cgtsclient/v1/label.py
Normal file
41
sysinv/cgts-client/cgts-client/cgtsclient/v1/label.py
Normal file
@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
|
||||
from cgtsclient.common import base
|
||||
|
||||
|
||||
class KubernetesLabel(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<KubernetesLabel %s>" % self._info
|
||||
|
||||
|
||||
class KubernetesLabelManager(base.Manager):
|
||||
resource_class = KubernetesLabel
|
||||
|
||||
@staticmethod
|
||||
def _path(label_id=None):
|
||||
return '/v1/labels/%s' % label_id if label_id else \
|
||||
'/v1/labels'
|
||||
|
||||
def list(self, ihost_id):
|
||||
path = '/v1/ihosts/%s/labels' % ihost_id
|
||||
return self._list(path, "labels")
|
||||
|
||||
def get(self, label_id):
|
||||
path = '/v1/labels/%s' % label_id
|
||||
try:
|
||||
return self._list(path)[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def assign(self, host_uuid, label):
|
||||
return self._create(self._path(host_uuid), label)
|
||||
|
||||
def remove(self, uuid):
|
||||
return self._delete(self._path(uuid))
|
90
sysinv/cgts-client/cgts-client/cgtsclient/v1/label_shell.py
Normal file
90
sysinv/cgts-client/cgts-client/cgtsclient/v1/label_shell.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
||||
|
||||
from cgtsclient.common import utils
|
||||
from cgtsclient import exc
|
||||
from cgtsclient.v1 import ihost as ihost_utils
|
||||
|
||||
|
||||
def _print_label_show(obj):
|
||||
fields = ['uuid', 'host_uuid', 'label']
|
||||
data = [(f, getattr(obj, f, '')) for f in fields]
|
||||
utils.print_tuple_list(data)
|
||||
|
||||
|
||||
@utils.arg('hostnameorid',
|
||||
metavar='<hostname or id>',
|
||||
help="Name or ID of host [REQUIRED]")
|
||||
def do_host_label_list(cc, args):
|
||||
"""List kubernetes labels assigned to a host."""
|
||||
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
|
||||
host_label = cc.label.list(ihost.uuid)
|
||||
for i in host_label[:]:
|
||||
setattr(i, 'hostname', ihost.hostname)
|
||||
field_labels = ['hostname', 'label', ]
|
||||
fields = ['hostname', 'label', ]
|
||||
utils.print_list(host_label, fields, field_labels, sortby=1)
|
||||
|
||||
|
||||
@utils.arg('hostnameorid',
|
||||
metavar='<hostname or id>',
|
||||
help="Name or ID of host [REQUIRED]")
|
||||
@utils.arg('attributes',
|
||||
metavar='<name=value>',
|
||||
nargs='+',
|
||||
action='append',
|
||||
default=[],
|
||||
help="List of Kubernetes labels")
|
||||
def do_host_label_assign(cc, args):
|
||||
"""Update the Kubernetes labels on a host."""
|
||||
attributes = utils.extract_keypairs(args)
|
||||
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
|
||||
new_labels = cc.label.assign(ihost.uuid, attributes)
|
||||
for p in new_labels.labels:
|
||||
uuid = p['uuid']
|
||||
if uuid is not None:
|
||||
try:
|
||||
label_obj = cc.label.get(uuid)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError('Host label not found: %s' % uuid)
|
||||
_print_label_show(label_obj)
|
||||
|
||||
|
||||
@utils.arg('hostnameorid',
|
||||
metavar='<hostname or id>',
|
||||
help="Name or ID of host [REQUIRED]")
|
||||
@utils.arg('attributes',
|
||||
metavar='<name>',
|
||||
nargs='+',
|
||||
action='append',
|
||||
default=[],
|
||||
help="List of Kubernetes label keys")
|
||||
def do_host_label_remove(cc, args):
|
||||
"""Remove Kubernetes label(s) from a host"""
|
||||
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
|
||||
for i in args.attributes[0]:
|
||||
lbl = _find_host_label(cc, ihost, i)
|
||||
if lbl:
|
||||
cc.label.remove(lbl.uuid)
|
||||
print 'Deleted host label %s for host %s' % (i, ihost.hostname)
|
||||
|
||||
|
||||
def _find_host_label(cc, host, label):
|
||||
host_labels = cc.label.list(host.uuid)
|
||||
for lbl in host_labels:
|
||||
if lbl.host_uuid == host.uuid and lbl.label.split('=')[0] == label:
|
||||
break
|
||||
else:
|
||||
lbl = None
|
||||
print('Host label not found: host %s, label key %s ' %
|
||||
(host.hostname, label))
|
||||
return lbl
|
@ -38,6 +38,7 @@ from cgtsclient.v1 import isystem_shell
|
||||
from cgtsclient.v1 import itrapdest_shell
|
||||
from cgtsclient.v1 import iuser_shell
|
||||
|
||||
from cgtsclient.v1 import label_shell
|
||||
from cgtsclient.v1 import license_shell
|
||||
from cgtsclient.v1 import lldp_agent_shell
|
||||
from cgtsclient.v1 import lldp_neighbour_shell
|
||||
@ -109,6 +110,7 @@ COMMAND_MODULES = [
|
||||
certificate_shell,
|
||||
storage_tier_shell,
|
||||
helm_shell,
|
||||
label_shell,
|
||||
]
|
||||
|
||||
|
||||
|
@ -37,3 +37,4 @@ python-magnumclient>=2.0.0 # Apache-2.0
|
||||
psutil
|
||||
simplejson>=2.2.0 # MIT
|
||||
rpm
|
||||
kubernetes # Apache-2.0
|
||||
|
@ -36,6 +36,7 @@ from sysinv.api.controllers.v1 import firewallrules
|
||||
from sysinv.api.controllers.v1 import health
|
||||
from sysinv.api.controllers.v1 import helm_charts
|
||||
from sysinv.api.controllers.v1 import host
|
||||
from sysinv.api.controllers.v1 import label
|
||||
from sysinv.api.controllers.v1 import interface
|
||||
from sysinv.api.controllers.v1 import link
|
||||
from sysinv.api.controllers.v1 import lldp_agent
|
||||
@ -225,6 +226,9 @@ class V1(base.APIBase):
|
||||
license = [link.Link]
|
||||
"Links to the license resource "
|
||||
|
||||
label = [link.Link]
|
||||
"Links to the label resource "
|
||||
|
||||
@classmethod
|
||||
def convert(self):
|
||||
v1 = V1()
|
||||
@ -703,6 +707,13 @@ class V1(base.APIBase):
|
||||
'license', '',
|
||||
bookmark=True)]
|
||||
|
||||
v1.labels = [link.Link.make_link('self',
|
||||
pecan.request.host_url,
|
||||
'labels', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'labels', '',
|
||||
bookmark=True)]
|
||||
return v1
|
||||
|
||||
|
||||
@ -765,6 +776,7 @@ class Controller(rest.RestController):
|
||||
sdn_controller = sdn_controller.SDNControllerController()
|
||||
firewallrules = firewallrules.FirewallRulesController()
|
||||
license = license.LicenseController()
|
||||
labels = label.LabelController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
def get(self):
|
||||
|
@ -65,6 +65,7 @@ from sysinv.api.controllers.v1 import pv as pv_api
|
||||
from sysinv.api.controllers.v1 import sensor as sensor_api
|
||||
from sysinv.api.controllers.v1 import sensorgroup
|
||||
from sysinv.api.controllers.v1 import storage
|
||||
from sysinv.api.controllers.v1 import label
|
||||
from sysinv.api.controllers.v1 import link
|
||||
from sysinv.api.controllers.v1 import lldp_agent
|
||||
from sysinv.api.controllers.v1 import lldp_neighbour
|
||||
@ -483,6 +484,9 @@ class Host(base.APIBase):
|
||||
lldp_neighbours = [link.Link]
|
||||
"Links to the collection of LldpNeighbours on this ihost"
|
||||
|
||||
labels = [link.Link]
|
||||
"Links to the collection of labels assigned to this host"
|
||||
|
||||
boot_device = wtypes.text
|
||||
rootfs_device = wtypes.text
|
||||
install_output = wtypes.text
|
||||
@ -748,6 +752,16 @@ class Host(base.APIBase):
|
||||
bookmark=True)
|
||||
]
|
||||
|
||||
uhost.labels = [link.Link.make_link('self',
|
||||
pecan.request.host_url,
|
||||
'ihosts',
|
||||
uhost.uuid + "/labels"),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'ihosts',
|
||||
uhost.uuid + "/labels",
|
||||
bookmark=True)
|
||||
]
|
||||
# Don't expose the vsc_controllers field if we are not configured with
|
||||
# the nuage_vrs vswitch or we are not a compute node.
|
||||
vswitch_type = utils.get_vswitch_type()
|
||||
@ -1036,6 +1050,9 @@ class HostController(rest.RestController):
|
||||
from_ihosts=True)
|
||||
"Expose lldp_neighbours as a sub-element of ihosts"
|
||||
|
||||
labels = label.LabelController(from_ihosts=True)
|
||||
"Expose labels as a sub-element of ihosts"
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
'bulk_add': ['POST'],
|
||||
|
293
sysinv/sysinv/sysinv/sysinv/api/controllers/v1/label.py
Normal file
293
sysinv/sysinv/sysinv/sysinv/api/controllers/v1/label.py
Normal file
@ -0,0 +1,293 @@
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import pecan
|
||||
import re
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
from pecan import rest
|
||||
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.common import exception
|
||||
from sysinv.common import utils as cutils
|
||||
from sysinv.openstack.common import excutils
|
||||
from sysinv.openstack.common import log
|
||||
from sysinv.openstack.common.gettextutils import _
|
||||
from sysinv.openstack.common.rpc import common as rpc_common
|
||||
from wsme import types as wtypes
|
||||
|
||||
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 = wtypes.text
|
||||
"Represents a label 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'])
|
||||
|
||||
# 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)
|
||||
|
||||
@staticmethod
|
||||
def _check_label_validity(label):
|
||||
"""Perform checks on validity of label
|
||||
"""
|
||||
expr = re.compile("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")
|
||||
if not expr.match(label):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _check_duplicate_label(host, label_key):
|
||||
"""Perform checks whether label already exists
|
||||
"""
|
||||
try:
|
||||
pecan.request.dbapi.label_query(host.id, label_key)
|
||||
except exception.HostLabelNotFoundByKey:
|
||||
return None
|
||||
raise exception.HostLabelAlreadyExists(host=host.hostname,
|
||||
label=label_key)
|
||||
|
||||
@cutils.synchronized(LOCK_NAME)
|
||||
@wsme_pecan.wsexpose(LabelCollection, types.uuid,
|
||||
body=types.apidict)
|
||||
def post(self, uuid, body):
|
||||
"""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)
|
||||
|
||||
new_records = []
|
||||
for key, value in body.iteritems():
|
||||
values = {
|
||||
'host_id': host.id,
|
||||
'label': "=".join([key, str(value)])
|
||||
}
|
||||
# syntax check
|
||||
if not self._check_label_validity(values['label']):
|
||||
msg = _("Label must consist of alphanumeric characters, "
|
||||
"'-', '_' or '.', and must start and end with an "
|
||||
"alphanumeric character with an optional DNS "
|
||||
"subdomain prefix and '/'")
|
||||
raise wsme.exc.ClientSideError(msg)
|
||||
|
||||
# check for duplicate
|
||||
self._check_duplicate_label(host, key)
|
||||
|
||||
try:
|
||||
new_label = pecan.request.dbapi.label_create(uuid, values)
|
||||
except exception.HostLabelAlreadyExists:
|
||||
msg = _("Host label add failed: "
|
||||
"host %s label %s "
|
||||
% (host.hostname, values['label']))
|
||||
raise wsme.exc.ClientSideError(msg)
|
||||
new_records.append(new_label)
|
||||
|
||||
try:
|
||||
pecan.request.rpcapi.update_kubernetes_label(
|
||||
pecan.request.context,
|
||||
host.uuid,
|
||||
body
|
||||
)
|
||||
except rpc_common.RemoteError as e:
|
||||
# rollback
|
||||
for p in new_records:
|
||||
try:
|
||||
pecan.request.dbapi.label_destroy(p.uuid)
|
||||
LOG.warn(_("Rollback host label 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 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)
|
||||
label_dict = {lbl_obj.label.split('=')[0]: 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"
|
||||
% (host.hostname, lbl_obj.label.split('=')[0]))
|
||||
raise wsme.exc.ClientSideError(msg)
|
@ -1139,6 +1139,19 @@ class StorageBackendNotFoundByName(NotFound):
|
||||
message = _("StorageBackend %(name)s not found")
|
||||
|
||||
|
||||
class HostLabelNotFound(NotFound):
|
||||
message = _("Host label %(uuid)s could not be found.")
|
||||
|
||||
|
||||
class HostLabelAlreadyExists(Conflict):
|
||||
message = _("Host label %(label)s already "
|
||||
"exists on this host %(host)s.")
|
||||
|
||||
|
||||
class HostLabelNotFoundByKey(NotFound):
|
||||
message = _("Host label %(label)s could not be found.")
|
||||
|
||||
|
||||
class PickleableException(Exception):
|
||||
"""
|
||||
Pickleable Exception
|
||||
|
48
sysinv/sysinv/sysinv/sysinv/common/kubernetes.py
Normal file
48
sysinv/sysinv/sysinv/sysinv/common/kubernetes.py
Normal file
@ -0,0 +1,48 @@
|
||||
#
|
||||
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
||||
""" System Inventory Kubernetes Utilities and helper functions."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from kubernetes import config
|
||||
from kubernetes import client
|
||||
from kubernetes.client import Configuration
|
||||
from sysinv.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KubeOperator(object):
|
||||
|
||||
def __init__(self, dbapi):
|
||||
self._dbapi = dbapi
|
||||
self._kube_client = None
|
||||
|
||||
def _get_kubernetesclient(self):
|
||||
if not self._kube_client:
|
||||
config.load_kube_config('/etc/kubernetes/admin.conf')
|
||||
|
||||
# Workaround: Turn off SSL/TLS verification
|
||||
c = Configuration()
|
||||
c.verify_ssl = False
|
||||
Configuration.set_default(c)
|
||||
|
||||
self._kube_client = client.CoreV1Api()
|
||||
return self._kube_client
|
||||
|
||||
def kube_patch_node(self, name, body):
|
||||
try:
|
||||
api_response = self._get_kubernetesclient().patch_node(name, body)
|
||||
LOG.debug("Response: %s" % api_response)
|
||||
except Exception as e:
|
||||
LOG.error("Kubernetes exception: %s" % e)
|
||||
raise
|
@ -71,6 +71,7 @@ from sysinv.common import constants
|
||||
from sysinv.common import exception
|
||||
from sysinv.common import fm
|
||||
from sysinv.common import health
|
||||
from sysinv.common import kubernetes
|
||||
from sysinv.common import retrying
|
||||
from sysinv.common import service
|
||||
from sysinv.common import utils as cutils
|
||||
@ -153,6 +154,7 @@ class ConductorManager(service.PeriodicService):
|
||||
self._ceph = None
|
||||
self._ceph_api = ceph.CephWrapper(
|
||||
endpoint='http://localhost:5001/api/v0.1/')
|
||||
self._kube = None
|
||||
|
||||
self._openstack = None
|
||||
self._api_token = None
|
||||
@ -178,6 +180,7 @@ class ConductorManager(service.PeriodicService):
|
||||
self._puppet = puppet.PuppetOperator(self.dbapi)
|
||||
self._ceph = iceph.CephOperator(self.dbapi)
|
||||
self._helm = helm.HelmOperator(self.dbapi)
|
||||
self._kube = kubernetes.KubeOperator(self.dbapi)
|
||||
|
||||
# create /var/run/sysinv if required. On DOR, the manifests
|
||||
# may not run to create this volatile directory.
|
||||
@ -10170,3 +10173,27 @@ class ConductorManager(service.PeriodicService):
|
||||
}
|
||||
"""
|
||||
return self._helm.get_helm_application_overrides(app_name, cnamespace)
|
||||
|
||||
def update_kubernetes_label(self, context,
|
||||
host_uuid, label_dict):
|
||||
"""Synchronously, have the conductor update kubernetes label
|
||||
per host.
|
||||
|
||||
:param context: request context.
|
||||
:param host_uuid: uuid or id of the host
|
||||
:param label_dict: a dictionary of host label attributes
|
||||
|
||||
"""
|
||||
LOG.info("update_kubernetes_label: label_dict=%s" % label_dict)
|
||||
try:
|
||||
host = self.dbapi.ihost_get(host_uuid)
|
||||
except exception.ServerNotFound:
|
||||
LOG.error("Cannot find host by id %s" % host_uuid)
|
||||
return
|
||||
body = {
|
||||
'metadata': {
|
||||
'labels': {}
|
||||
}
|
||||
}
|
||||
body['metadata']['labels'].update(label_dict)
|
||||
self._kube.kube_patch_node(host.hostname, body)
|
||||
|
@ -1631,3 +1631,15 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
|
||||
self.make_msg('get_helm_application_overrides',
|
||||
app_name=app_name,
|
||||
cnamespace=cnamespace))
|
||||
|
||||
def update_kubernetes_label(self, context, host_uuid, label_dict):
|
||||
"""Synchronously, have the conductor update kubernetes label.
|
||||
|
||||
:param context: request context.
|
||||
:param host_uuid: uuid or id of the host
|
||||
:param label_dict: a dictionary of kubernetes labels
|
||||
"""
|
||||
return self.call(context,
|
||||
self.make_msg('update_kubernetes_label',
|
||||
host_uuid=host_uuid,
|
||||
label_dict=label_dict))
|
||||
|
@ -1082,6 +1082,25 @@ def add_lldp_tlv_filter_by_agent(query, agentid):
|
||||
models.LldpTlvs.agent_id == models.LldpAgents.id)
|
||||
return query.filter(models.LldpAgents.uuid == agentid)
|
||||
|
||||
|
||||
def add_label_filter_by_host(query, hostid):
|
||||
"""Adds a label-specific ihost filter to a query.
|
||||
|
||||
Filters results by host id if supplied value is an integer,
|
||||
otherwise attempts to filter results by host uuid.
|
||||
|
||||
:param query: Initial query to add filter to.
|
||||
:param hostid: host id or uuid to filter results by.
|
||||
:return: Modified query.
|
||||
"""
|
||||
if utils.is_int_like(hostid):
|
||||
return query.filter_by(host_id=hostid)
|
||||
|
||||
elif utils.is_uuid_like(hostid):
|
||||
query = query.join(models.ihost)
|
||||
return query.filter(models.ihost.uuid == hostid)
|
||||
|
||||
|
||||
class Connection(api.Connection):
|
||||
"""SqlAlchemy connection."""
|
||||
|
||||
@ -7267,3 +7286,96 @@ class Connection(api.Connection):
|
||||
raise exception.HelmOverrideNotFound(name=name,
|
||||
namespace=namespace)
|
||||
query.delete()
|
||||
|
||||
def _label_get(self, label_id):
|
||||
query = model_query(models.Label)
|
||||
query = add_identity_filter(query, label_id)
|
||||
|
||||
try:
|
||||
result = query.one()
|
||||
except NoResultFound:
|
||||
raise exception.HostLabelNotFound(uuid=label_id)
|
||||
return result
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_create(self, host_uuid, values):
|
||||
|
||||
if not values.get('uuid'):
|
||||
values['uuid'] = uuidutils.generate_uuid()
|
||||
values['host_uuid'] = host_uuid
|
||||
|
||||
host_label = models.Label()
|
||||
host_label.update(values)
|
||||
with _session_for_write() as session:
|
||||
try:
|
||||
session.add(host_label)
|
||||
session.flush()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
LOG.error("Failed to add host label %s. "
|
||||
"Already exists with this uuid" %
|
||||
(values['label']))
|
||||
raise exception.HostLabelAlreadyExists(
|
||||
label=values['label'], host=values['host_uuid'])
|
||||
return self._label_get(values['uuid'])
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_get(self, uuid):
|
||||
query = model_query(models.Label)
|
||||
query = query.filter_by(uuid=uuid)
|
||||
try:
|
||||
result = query.one()
|
||||
except NoResultFound:
|
||||
raise exception.InvalidParameterValue(
|
||||
err="No label entry found for %s" % uuid)
|
||||
return result
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_get_all(self, hostid=None):
|
||||
query = model_query(models.Label, read_deleted="no")
|
||||
if hostid:
|
||||
query = query.filter_by(host_id=hostid)
|
||||
return query.all()
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_update(self, uuid, values):
|
||||
with _session_for_write() as session:
|
||||
query = model_query(models.Label, session=session)
|
||||
query = query.filter_by(uuid=uuid)
|
||||
|
||||
count = query.update(values, synchronize_session='fetch')
|
||||
if count == 0:
|
||||
raise exception.HostLabelNotFound(uuid)
|
||||
return query.one()
|
||||
|
||||
def label_destroy(self, uuid):
|
||||
with _session_for_write() as session:
|
||||
query = model_query(models.Label, session=session)
|
||||
query = query.filter_by(uuid=uuid)
|
||||
try:
|
||||
query.one()
|
||||
except NoResultFound:
|
||||
raise exception.HostLabelNotFound(uuid)
|
||||
query.delete()
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_get_by_host(self, host,
|
||||
limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
query = model_query(models.Label)
|
||||
query = add_label_filter_by_host(query, host)
|
||||
return _paginate_query(models.Label, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def _label_query(self, host_id, values, session=None):
|
||||
query = model_query(models.Label, session=session)
|
||||
query = query.filter(models.Label.host_id == host_id)
|
||||
query = query.filter(models.Label.label.startswith(values))
|
||||
try:
|
||||
result = query.one()
|
||||
except NoResultFound:
|
||||
raise exception.HostLabelNotFoundByKey(label=values)
|
||||
return result
|
||||
|
||||
@objects.objectify(objects.label)
|
||||
def label_query(self, host_id, values):
|
||||
return self._label_query(host_id, values)
|
||||
|
@ -2,9 +2,7 @@
|
||||
#
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# The right to copy, distribute, modify, or otherwise make use
|
||||
# of this software may be licensed only pursuant to the terms
|
||||
# of an applicable Wind River license agreement.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from sqlalchemy import Column, MetaData, Table, Boolean
|
||||
|
@ -0,0 +1,51 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from sqlalchemy import Column, MetaData, Table
|
||||
from sqlalchemy import DateTime, Integer, String
|
||||
from sqlalchemy import ForeignKey
|
||||
from sysinv.openstack.common import log
|
||||
|
||||
ENGINE = 'InnoDB'
|
||||
CHARSET = 'utf8'
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""Perform sysinv database upgrade for host label
|
||||
"""
|
||||
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
Table('i_host', meta, autoload=True)
|
||||
|
||||
label = Table(
|
||||
'label',
|
||||
meta,
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
|
||||
Column('id', Integer, primary_key=True, nullable=False),
|
||||
Column('uuid', String(36), unique=True),
|
||||
|
||||
Column('host_id', Integer, ForeignKey('i_host.id',
|
||||
ondelete='CASCADE')),
|
||||
|
||||
Column('label', String(255)),
|
||||
|
||||
mysql_engine=ENGINE,
|
||||
mysql_charset=CHARSET,
|
||||
)
|
||||
label.create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
# As per other openstack components, downgrade is
|
||||
# unsupported in this release.
|
||||
raise NotImplementedError('SysInv database downgrade is unsupported.')
|
@ -1606,3 +1606,15 @@ class HelmOverrides(Base):
|
||||
namespace = Column(String(255), nullable=False)
|
||||
user_overrides = Column(Text, nullable=True)
|
||||
UniqueConstraint('name', 'namespace', name='u_name_namespace')
|
||||
|
||||
|
||||
class Label(Base):
|
||||
__tablename__ = 'label'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36))
|
||||
host_id = Column(Integer, ForeignKey('i_host.id',
|
||||
ondelete='CASCADE'))
|
||||
host = relationship("ihost", lazy="joined", join_depth=1)
|
||||
label = Column(String(255))
|
||||
UniqueConstraint('host_id', 'label', name='u_host_label')
|
||||
|
@ -43,6 +43,7 @@ from sysinv.objects import interface_ethernet
|
||||
from sysinv.objects import interface_virtual
|
||||
from sysinv.objects import interface_vlan
|
||||
from sysinv.objects import journal
|
||||
from sysinv.objects import label
|
||||
from sysinv.objects import lldp_agent
|
||||
from sysinv.objects import lldp_neighbour
|
||||
from sysinv.objects import lldp_tlv
|
||||
@ -177,6 +178,7 @@ storage_external = storage_external.StorageExternal
|
||||
storage_tier = storage_tier.StorageTier
|
||||
storage_ceph_external = storage_ceph_external.StorageCephExternal
|
||||
helm_overrides = helm_overrides.HelmOverrides
|
||||
label = label.Label
|
||||
|
||||
__all__ = (system,
|
||||
cluster,
|
||||
@ -226,6 +228,7 @@ __all__ = (system,
|
||||
host_upgrade,
|
||||
network,
|
||||
service_parameter,
|
||||
label,
|
||||
lldp_agent,
|
||||
lldp_neighbour,
|
||||
lldp_tlv,
|
||||
|
38
sysinv/sysinv/sysinv/sysinv/objects/label.py
Normal file
38
sysinv/sysinv/sysinv/sysinv/objects/label.py
Normal file
@ -0,0 +1,38 @@
|
||||
#
|
||||
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# coding=utf-8
|
||||
#
|
||||
|
||||
from sysinv.db import api as db_api
|
||||
from sysinv.objects import base
|
||||
from sysinv.objects import utils
|
||||
|
||||
|
||||
class Label(base.SysinvObject):
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
fields = {
|
||||
'uuid': utils.str_or_none,
|
||||
'label': utils.str_or_none,
|
||||
'host_id': utils.int_or_none,
|
||||
'host_uuid': utils.str_or_none,
|
||||
}
|
||||
|
||||
_foreign_fields = {'host_uuid': 'host:uuid'}
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_uuid(cls, context, uuid):
|
||||
return cls.dbapi.label_get(uuid)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_host_id(cls, context, host_id):
|
||||
return cls.dbapi.label_get_by_host(host_id)
|
||||
|
||||
def save_changes(self, context, updates):
|
||||
self.dbapi.label_update(self.uuid, updates)
|
Loading…
Reference in New Issue
Block a user