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:
Teresa Ho 2018-07-10 10:37:54 -04:00 committed by David Sullivan
parent 19e20118d8
commit 54142cc5e7
18 changed files with 775 additions and 3 deletions

View File

@ -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)

View 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))

View 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

View File

@ -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,
]

View File

@ -37,3 +37,4 @@ python-magnumclient>=2.0.0 # Apache-2.0
psutil
simplejson>=2.2.0 # MIT
rpm
kubernetes # Apache-2.0

View File

@ -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):

View File

@ -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'],

View 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)

View File

@ -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

View 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

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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.')

View File

@ -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')

View File

@ -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,

View 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)