metal/inventory/inventory/inventory/api/controllers/v1/sensorgroup.py

752 lines
29 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 UnitedStack Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import copy
import jsonpatch
import pecan
from pecan import rest
import six
import uuid
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from inventory.api.controllers.v1 import base
from inventory.api.controllers.v1 import collection
from inventory.api.controllers.v1 import link
from inventory.api.controllers.v1 import sensor as sensor_api
from inventory.api.controllers.v1 import types
from inventory.api.controllers.v1 import utils
from inventory.common import constants
from inventory.common import exception
from inventory.common import hwmon_api
from inventory.common.i18n import _
from inventory.common import k_host
from inventory.common import utils as cutils
from inventory import objects
from oslo_log import log
from oslo_utils import uuidutils
from six import text_type as unicode
LOG = log.getLogger(__name__)
class SensorGroupPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/host_uuid', 'uuid']
class SensorGroup(base.APIBase):
"""API representation of an Sensor Group
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of an
sensorgroup.
"""
uuid = types.uuid
"Unique UUID for this sensorgroup"
sensorgroupname = wtypes.text
"Represent the name of the sensorgroup. Unique with path per host"
path = wtypes.text
"Represent the path of the sensor. Unique with sensorname per host"
sensortype = wtypes.text
"Represent the sensortype . e.g. Temperature, WatchDog"
datatype = wtypes.text
"Represent the datatype e.g. discrete or analog,"
state = wtypes.text
"Represent the state of the sensorgroup"
possible_states = wtypes.text
"Represent the possible states of the sensorgroup"
algorithm = wtypes.text
"Represent the algorithm of the sensorgroup."
audit_interval_group = int
"Represent the audit interval of the sensorgroup."
actions_critical_choices = wtypes.text
"Represent the configurable critical severity actions of the sensorgroup."
actions_major_choices = wtypes.text
"Represent the configurable major severity actions of the sensorgroup."
actions_minor_choices = wtypes.text
"Represent the configurable minor severity actions of the sensorgroup."
actions_minor_group = wtypes.text
"Represent the minor configured actions of the sensorgroup. CSV."
actions_major_group = wtypes.text
"Represent the major configured actions of the sensorgroup. CSV."
actions_critical_group = wtypes.text
"Represent the critical configured actions of the sensorgroup. CSV."
unit_base_group = wtypes.text
"Represent the unit base of the analog sensorgroup e.g. revolutions"
unit_modifier_group = wtypes.text
"Represent the unit modifier of the analog sensorgroup e.g. 10**2"
unit_rate_group = wtypes.text
"Represent the unit rate of the sensorgroup e.g. /minute"
t_minor_lower_group = wtypes.text
"Represent the minor lower threshold of the analog sensorgroup"
t_minor_upper_group = wtypes.text
"Represent the minor upper threshold of the analog sensorgroup"
t_major_lower_group = wtypes.text
"Represent the major lower threshold of the analog sensorgroup"
t_major_upper_group = wtypes.text
"Represent the major upper threshold of the analog sensorgroup"
t_critical_lower_group = wtypes.text
"Represent the critical lower threshold of the analog sensorgroup"
t_critical_upper_group = wtypes.text
"Represent the critical upper threshold of the analog sensorgroup"
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
six.integer_types)}
"Represent meta data of the sensorgroup"
suppress = wtypes.text
"Represent supress sensor if True, otherwise not suppress sensor"
sensors = wtypes.text
"Represent the sensors of the sensorgroup"
host_id = int
"Represent the host_id the sensorgroup belongs to"
host_uuid = types.uuid
"Represent the UUID of the host the sensorgroup belongs to"
links = [link.Link]
"Represent a list containing a self link and associated sensorgroup links"
sensors = [link.Link]
"Links to the collection of sensors on this sensorgroup"
def __init__(self, **kwargs):
self.fields = objects.SensorGroup.fields.keys()
for k in self.fields:
setattr(self, k, kwargs.get(k))
# 'sensors' is not part of objects.SenorGroups.fields (it's an
# API-only attribute)
self.fields.append('sensors')
setattr(self, 'sensors', kwargs.get('sensors', None))
@classmethod
def convert_with_links(cls, rsensorgroup, expand=True):
sensorgroup = SensorGroup(**rsensorgroup.as_dict())
sensorgroup_fields_common = ['uuid', 'host_id',
'host_uuid',
'sensortype', 'datatype',
'sensorgroupname',
'path',
'state',
'possible_states',
'audit_interval_group',
'algorithm',
'actions_critical_choices',
'actions_major_choices',
'actions_minor_choices',
'actions_minor_group',
'actions_major_group',
'actions_critical_group',
'sensors',
'suppress',
'capabilities',
'created_at', 'updated_at', ]
sensorgroup_fields_analog = ['unit_base_group',
'unit_modifier_group',
'unit_rate_group',
't_minor_lower_group',
't_minor_upper_group',
't_major_lower_group',
't_major_upper_group',
't_critical_lower_group',
't_critical_upper_group', ]
if rsensorgroup.datatype == 'discrete':
sensorgroup_fields = sensorgroup_fields_common
elif rsensorgroup.datatype == 'analog':
sensorgroup_fields = \
sensorgroup_fields_common + sensorgroup_fields_analog
else:
LOG.error(_("Invalid datatype={}").format(rsensorgroup.datatype))
if not expand:
sensorgroup.unset_fields_except(sensorgroup_fields)
if sensorgroup.host_id and not sensorgroup.host_uuid:
host = objects.Host.get_by_uuid(pecan.request.context,
sensorgroup.host_id)
sensorgroup.host_uuid = host.uuid
# never expose the id attribute
sensorgroup.host_id = wtypes.Unset
sensorgroup.id = wtypes.Unset
sensorgroup.links = [
link.Link.make_link('self', pecan.request.host_url,
'sensorgroups',
sensorgroup.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'sensorgroups',
sensorgroup.uuid,
bookmark=True)]
sensorgroup.sensors = [
link.Link.make_link('self',
pecan.request.host_url,
'sensorgroups',
sensorgroup.uuid + "/sensors"),
link.Link.make_link('bookmark',
pecan.request.host_url,
'sensorgroups',
sensorgroup.uuid + "/sensors",
bookmark=True)]
return sensorgroup
class SensorGroupCollection(collection.Collection):
"""API representation of a collection of SensorGroup objects."""
sensorgroups = [SensorGroup]
"A list containing SensorGroup objects"
def __init__(self, **kwargs):
self._type = 'sensorgroups'
@classmethod
def convert_with_links(cls, rsensorgroups, limit, url=None,
expand=False, **kwargs):
collection = SensorGroupCollection()
collection.sensorgroups = [SensorGroup.convert_with_links(p, expand)
for p in rsensorgroups]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
LOCK_NAME = 'SensorGroupController'
class SensorGroupController(rest.RestController):
"""REST controller for SensorGroups."""
sensors = sensor_api.SensorController(from_sensorgroup=True)
"Expose sensors as a sub-element of sensorgroups"
_custom_actions = {
'detail': ['GET'],
'relearn': ['POST'],
}
def __init__(self, from_hosts=False):
self._from_hosts = from_hosts
self._api_token = None
self._hwmon_address = k_host.LOCALHOST_HOSTNAME
self._hwmon_port = constants.HWMON_PORT
def _get_sensorgroups_collection(self, uuid,
marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
if self._from_hosts and not 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.SensorGroup.get_by_uuid(
pecan.request.context,
marker)
if self._from_hosts:
sensorgroups = pecan.request.dbapi.sensorgroup_get_by_host(
uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
if uuid:
sensorgroups = pecan.request.dbapi.sensorgroup_get_by_host(
uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
sensorgroups = pecan.request.dbapi.sensorgroup_get_list(
limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return SensorGroupCollection.convert_with_links(sensorgroups, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(SensorGroupCollection, 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 sensorgroups."""
return self._get_sensorgroups_collection(uuid,
marker, limit,
sort_key, sort_dir)
@wsme_pecan.wsexpose(SensorGroupCollection, 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 sensorgroups with detail."""
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "sensorgroups":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['sensorgroups', 'detail'])
return self._get_sensorgroups_collection(uuid, marker, limit,
sort_key, sort_dir,
expand, resource_url)
@wsme_pecan.wsexpose(SensorGroup, types.uuid)
def get_one(self, sensorgroup_uuid):
"""Retrieve information about the given sensorgroup."""
if self._from_hosts:
raise exception.OperationNotPermitted
rsensorgroup = objects.SensorGroup.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
if rsensorgroup.datatype == 'discrete':
rsensorgroup = objects.SensorGroupDiscrete.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
elif rsensorgroup.datatype == 'analog':
rsensorgroup = objects.SensorGroupAnalog.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
else:
LOG.error(_("Invalid datatype={}").format(rsensorgroup.datatype))
return SensorGroup.convert_with_links(rsensorgroup)
@staticmethod
def _new_sensorgroup_semantic_checks(sensorgroup):
datatype = sensorgroup.as_dict().get('datatype') or ""
sensortype = sensorgroup.as_dict().get('sensortype') or ""
if not (datatype and sensortype):
raise wsme.exc.ClientSideError(_("sensorgroup-add: Cannot "
"add a sensorgroup "
"without a valid datatype "
"and sensortype."))
if datatype not in constants.SENSOR_DATATYPE_VALID_LIST:
raise wsme.exc.ClientSideError(
_("sensorgroup datatype must be one of %s.") %
constants.SENSOR_DATATYPE_VALID_LIST)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(SensorGroup, body=SensorGroup)
def post(self, sensorgroup):
"""Create a new sensorgroup."""
if self._from_hosts:
raise exception.OperationNotPermitted
self._new_sensorgroup_semantic_checks(sensorgroup)
try:
sensorgroup_dict = sensorgroup.as_dict()
new_sensorgroup = _create(sensorgroup_dict)
except exception.InventoryException as e:
LOG.exception(e)
raise wsme.exc.ClientSideError(_("Invalid data"))
return sensorgroup.convert_with_links(new_sensorgroup)
def _get_host_uuid(self, body):
host_uuid = body.get('host_uuid') or ""
try:
host = pecan.request.dbapi.host_get(host_uuid)
except exception.NotFound:
raise wsme.exc.ClientSideError("_get_host_uuid lookup failed")
return host.uuid
@wsme_pecan.wsexpose('json', body=unicode)
def relearn(self, body):
"""Handle Sensor Model Relearn Request."""
host_uuid = self._get_host_uuid(body)
# LOG.info("Host UUID: %s - BM_TYPE: %s" % (host_uuid, bm_type ))
# hwmon_sensorgroup = {'ihost_uuid': host_uuid}
request_body = {'host_uuid': host_uuid}
hwmon_response = hwmon_api.sensorgroup_relearn(
self._api_token, self._hwmon_address, self._hwmon_port,
request_body,
constants.HWMON_DEFAULT_TIMEOUT_IN_SECS)
if not hwmon_response:
hwmon_response = {'status': 'fail',
'reason': 'no response',
'action': 'retry'}
elif hwmon_response['status'] != 'pass':
msg = _("HWMON has returned with "
"a status of {}, reason: {}, "
"recommended action: {}").format(
hwmon_response.get('status'),
hwmon_response.get('reason'),
hwmon_response.get('action'))
raise wsme.exc.ClientSideError(msg)
@cutils.synchronized(LOCK_NAME)
@wsme.validate(types.uuid, [SensorGroupPatchType])
@wsme_pecan.wsexpose(SensorGroup, types.uuid,
body=[SensorGroupPatchType])
def patch(self, sensorgroup_uuid, patch):
"""Update an existing sensorgroup."""
if self._from_hosts:
raise exception.OperationNotPermitted
rsensorgroup = objects.SensorGroup.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
if rsensorgroup.datatype == 'discrete':
rsensorgroup = objects.SensorGroupDiscrete.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
elif rsensorgroup.datatype == 'analog':
rsensorgroup = objects.SensorGroupAnalog.get_by_uuid(
pecan.request.context, sensorgroup_uuid)
else:
raise wsme.exc.ClientSideError(_("Invalid datatype={}").format(
rsensorgroup.datatype))
rsensorgroup_orig = copy.deepcopy(rsensorgroup)
host = pecan.request.dbapi.host_get(
rsensorgroup['host_id']).as_dict()
utils.validate_patch(patch)
patch_obj = jsonpatch.JsonPatch(patch)
my_host_uuid = None
for p in patch_obj:
# For Profile replace host_uuid with corresponding id
if p['path'] == '/host_uuid':
p['path'] = '/host_id'
host = objects.Host.get_by_uuid(pecan.request.context,
p['value'])
p['value'] = host.id
my_host_uuid = host.uuid
# update sensors if set
sensors = None
for s in patch:
if '/sensors' in s['path']:
sensors = s['value']
patch.remove(s)
break
if sensors:
_update_sensors("modify", rsensorgroup, host, sensors)
try:
sensorgroup = SensorGroup(**jsonpatch.apply_patch(
rsensorgroup.as_dict(),
patch_obj))
except utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
# Update only the fields that have changed
if rsensorgroup.datatype == 'discrete':
fields = objects.SensorGroupDiscrete.fields
else:
fields = objects.SensorGroupAnalog.fields
for field in fields:
if rsensorgroup[field] != getattr(sensorgroup, field):
rsensorgroup[field] = getattr(sensorgroup, field)
delta = rsensorgroup.obj_what_changed()
sensorgroup_suppress_attrs = ['suppress']
force_action = False
if any(x in delta for x in sensorgroup_suppress_attrs):
valid_suppress = ['True', 'False', 'true', 'false', 'force_action']
if rsensorgroup.suppress.lower() not in valid_suppress:
raise wsme.exc.ClientSideError(_("Invalid suppress value, "
"select 'True' or 'False'"))
elif rsensorgroup.suppress.lower() == 'force_action':
LOG.info("suppress=%s" % rsensorgroup.suppress.lower())
rsensorgroup.suppress = rsensorgroup_orig.suppress
force_action = True
self._semantic_modifiable_fields(patch_obj, force_action)
if not pecan.request.user_agent.startswith('hwmon'):
hwmon_sensorgroup = cutils.removekeys_nonhwmon(
rsensorgroup.as_dict())
if not my_host_uuid:
host = objects.Host.get_by_uuid(pecan.request.context,
rsensorgroup.host_id)
my_host_uuid = host.uuid
hwmon_sensorgroup.update({'host_uuid': my_host_uuid})
hwmon_response = hwmon_api.sensorgroup_modify(
self._api_token, self._hwmon_address, self._hwmon_port,
hwmon_sensorgroup,
constants.HWMON_DEFAULT_TIMEOUT_IN_SECS)
if not hwmon_response:
hwmon_response = {'status': 'fail',
'reason': 'no response',
'action': 'retry'}
if hwmon_response['status'] != 'pass':
msg = _("HWMON has returned with a status of {}, reason: {}, "
"recommended action: {}").format(
hwmon_response.get('status'),
hwmon_response.get('reason'),
hwmon_response.get('action'))
if force_action:
LOG.error(msg)
else:
raise wsme.exc.ClientSideError(msg)
sensorgroup_prop_attrs = ['audit_interval_group',
'actions_minor_group',
'actions_major_group',
'actions_critical_group',
'suppress']
if any(x in delta for x in sensorgroup_prop_attrs):
# propagate to Sensors within this SensorGroup
sensor_val = {'audit_interval': rsensorgroup.audit_interval_group,
'actions_minor': rsensorgroup.actions_minor_group,
'actions_major': rsensorgroup.actions_major_group,
'actions_critical':
rsensorgroup.actions_critical_group}
if 'suppress' in delta:
sensor_val.update({'suppress': rsensorgroup.suppress})
pecan.request.dbapi.sensorgroup_propagate(
rsensorgroup.uuid, sensor_val)
rsensorgroup.save()
return SensorGroup.convert_with_links(rsensorgroup)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, sensorgroup_uuid):
"""Delete a sensorgroup."""
if self._from_hosts:
raise exception.OperationNotPermitted
pecan.request.dbapi.sensorgroup_destroy(sensorgroup_uuid)
@staticmethod
def _semantic_modifiable_fields(patch_obj, force_action=False):
# Prevent auto populated fields from being updated
state_rel_path = ['/uuid', '/id', '/host_id', '/datatype',
'/sensortype']
if any(p['path'] in state_rel_path for p in patch_obj):
raise wsme.exc.ClientSideError(_("The following fields can not be "
"modified: %s ") % state_rel_path)
if not (pecan.request.user_agent.startswith('hwmon') or force_action):
state_rel_path = ['/sensorgroupname', '/path',
'/state', '/possible_states',
'/actions_critical_choices',
'/actions_major_choices',
'/actions_minor_choices',
'/unit_base_group',
'/unit_modifier_group',
'/unit_rate_group',
'/t_minor_lower_group',
'/t_minor_upper_group',
'/t_major_lower_group',
'/t_major_upper_group',
'/t_critical_lower_group',
'/t_critical_upper_group',
]
if any(p['path'] in state_rel_path for p in patch_obj):
raise wsme.exc.ClientSideError(
_("The following fields are not remote-modifiable: %s") %
state_rel_path)
def _create(sensorgroup, from_profile=False):
"""Create a sensorgroup through a non-HTTP request e.g. via profile.py
while still passing through sensorgroup semantic checks.
Hence, not declared inside a class.
Param:
sensorgroup - dictionary of sensorgroup values
from_profile - Boolean whether from profile
"""
if 'host_id' in sensorgroup and sensorgroup['host_id']:
ihostid = sensorgroup['host_id']
else:
ihostid = sensorgroup['host_uuid']
ihost = pecan.request.dbapi.host_get(ihostid)
if uuidutils.is_uuid_like(ihostid):
host_id = ihost['id']
else:
host_id = ihostid
sensorgroup.update({'host_id': host_id})
LOG.info("sensorgroup post sensorgroups ihostid: %s" % host_id)
sensorgroup['host_uuid'] = ihost['uuid']
# Assign UUID if not already done.
if not sensorgroup.get('uuid'):
sensorgroup['uuid'] = str(uuid.uuid4())
# Get sensors
sensors = None
if 'sensors' in sensorgroup:
sensors = sensorgroup['sensors']
# Set defaults - before checks to allow for optional attributes
# if not from_profile:
# sensorgroup = _set_defaults(sensorgroup)
# Semantic checks
# sensorgroup = _check("add",
# sensorgroup,
# sensors=sensors,
# ifaces=uses_if,
# from_profile=from_profile)
if sensorgroup.get('datatype'):
if sensorgroup['datatype'] == 'discrete':
new_sensorgroup = pecan.request.dbapi.sensorgroup_discrete_create(
ihost.id, sensorgroup)
elif sensorgroup['datatype'] == 'analog':
new_sensorgroup = pecan.request.dbapi.sensorgroup_analog_create(
ihost.id, sensorgroup)
else:
raise wsme.exc.ClientSideError(_("Invalid datatype. %s") %
sensorgroup.datatype)
else:
raise wsme.exc.ClientSideError(_("Unspecified datatype."))
# Update sensors
if sensors:
try:
_update_sensors("modify",
new_sensorgroup.as_dict(),
ihost,
sensors)
except Exception as e:
pecan.request.dbapi.sensorgroup_destroy(
new_sensorgroup.as_dict()['uuid'])
raise e
# Update sensors
# return new_sensorgroup
return SensorGroup.convert_with_links(new_sensorgroup)
def _update_sensors(op, sensorgroup, ihost, sensors):
sensors = sensors.split(',')
this_sensorgroup_datatype = None
this_sensorgroup_sensortype = None
if op == "add":
this_sensorgroup_id = 0
else:
this_sensorgroup_id = sensorgroup['id']
this_sensorgroup_datatype = sensorgroup['datatype']
this_sensorgroup_sensortype = sensorgroup['sensortype']
if sensors:
# Update Sensors' sensorgroup_uuid attribute
sensors_list = pecan.request.dbapi.sensor_get_all(
host_id=ihost['id'])
for p in sensors_list:
# if new sensor associated
if (p.uuid in sensors or p.sensorname in sensors) \
and not p.sensorgroup_id:
values = {'sensorgroup_id': sensorgroup['id']}
# else if old sensor disassociated
elif ((p.uuid not in sensors and p.sensorname not in sensors) and
p.sensorgroup_id and
p.sensorgroup_id == this_sensorgroup_id):
values = {'sensorgroup_id': None}
else:
continue
if p.datatype != this_sensorgroup_datatype:
msg = _("Invalid datatype: host {} sensor {}: Expected: {} "
"Received: {}.").format(
(ihost['hostname'], p.sensorname,
this_sensorgroup_datatype, p.datatype))
raise wsme.exc.ClientSideError(msg)
if p.sensortype != this_sensorgroup_sensortype:
msg = _("Invalid sensortype: host {} sensor {}: Expected: {} "
"Received: {}.").format(
ihost['hostname'], p.sensorname,
this_sensorgroup_sensortype, p.sensortype)
raise wsme.exc.ClientSideError(msg)
try:
pecan.request.dbapi.sensor_update(p.uuid, values)
except exception.HTTPNotFound:
msg = _("Sensor update of sensorgroup_uuid failed: host {} "
"sensor {}").format(ihost['hostname'], p.sensorname)
raise wsme.exc.ClientSideError(msg)