587 lines
22 KiB
Python
587 lines
22 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 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 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
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class SensorPatchType(types.JsonPatchType):
|
|
@staticmethod
|
|
def mandatory_attrs():
|
|
return []
|
|
|
|
|
|
class Sensor(base.APIBase):
|
|
"""API representation of an Sensor
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation of an
|
|
sensor.
|
|
"""
|
|
|
|
uuid = types.uuid
|
|
"Unique UUID for this sensor"
|
|
|
|
sensorname = wtypes.text
|
|
"Represent the name of the sensor. Unique with path per host"
|
|
|
|
path = wtypes.text
|
|
"Represent the path of the sensor. Unique with sensorname per host"
|
|
|
|
sensortype = wtypes.text
|
|
"Represent the type of sensor. e.g. Temperature, WatchDog"
|
|
|
|
datatype = wtypes.text
|
|
"Represent the entity monitored. e.g. discrete, analog"
|
|
|
|
status = wtypes.text
|
|
"Represent current sensor status: ok, minor, major, critical, disabled"
|
|
|
|
state = wtypes.text
|
|
"Represent the current state of the sensor"
|
|
|
|
state_requested = wtypes.text
|
|
"Represent the requested state of the sensor"
|
|
|
|
audit_interval = int
|
|
"Represent the audit_interval of the sensor."
|
|
|
|
algorithm = wtypes.text
|
|
"Represent the algorithm of the sensor."
|
|
|
|
actions_minor = wtypes.text
|
|
"Represent the minor configured actions of the sensor. CSV."
|
|
|
|
actions_major = wtypes.text
|
|
"Represent the major configured actions of the sensor. CSV."
|
|
|
|
actions_critical = wtypes.text
|
|
"Represent the critical configured actions of the sensor. CSV."
|
|
|
|
suppress = wtypes.text
|
|
"Represent supress sensor if True, otherwise not suppress sensor"
|
|
|
|
value = wtypes.text
|
|
"Represent current value of the discrete sensor"
|
|
|
|
unit_base = wtypes.text
|
|
"Represent the unit base of the analog sensor e.g. revolutions"
|
|
|
|
unit_modifier = wtypes.text
|
|
"Represent the unit modifier of the analog sensor e.g. 10**2"
|
|
|
|
unit_rate = wtypes.text
|
|
"Represent the unit rate of the sensor e.g. /minute"
|
|
|
|
t_minor_lower = wtypes.text
|
|
"Represent the minor lower threshold of the analog sensor"
|
|
|
|
t_minor_upper = wtypes.text
|
|
"Represent the minor upper threshold of the analog sensor"
|
|
|
|
t_major_lower = wtypes.text
|
|
"Represent the major lower threshold of the analog sensor"
|
|
|
|
t_major_upper = wtypes.text
|
|
"Represent the major upper threshold of the analog sensor"
|
|
|
|
t_critical_lower = wtypes.text
|
|
"Represent the critical lower threshold of the analog sensor"
|
|
|
|
t_critical_upper = wtypes.text
|
|
"Represent the critical upper threshold of the analog sensor"
|
|
|
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
|
six.integer_types)}
|
|
"Represent meta data of the sensor"
|
|
|
|
host_id = int
|
|
"Represent the host_id the sensor belongs to"
|
|
|
|
sensorgroup_id = int
|
|
"Represent the sensorgroup_id the sensor belongs to"
|
|
|
|
host_uuid = types.uuid
|
|
"Represent the UUID of the host the sensor belongs to"
|
|
|
|
sensorgroup_uuid = types.uuid
|
|
"Represent the UUID of the sensorgroup the sensor belongs to"
|
|
|
|
links = [link.Link]
|
|
"Represent a list containing a self link and associated sensor links"
|
|
|
|
def __init__(self, **kwargs):
|
|
self.fields = objects.Sensor.fields.keys()
|
|
for k in self.fields:
|
|
setattr(self, k, kwargs.get(k))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_sensor, expand=True):
|
|
|
|
sensor = Sensor(**rpc_sensor.as_dict())
|
|
|
|
sensor_fields_common = ['uuid', 'host_id', 'sensorgroup_id',
|
|
'sensortype', 'datatype',
|
|
'sensorname', 'path',
|
|
|
|
'status',
|
|
'state', 'state_requested',
|
|
'sensor_action_requested',
|
|
'actions_minor',
|
|
'actions_major',
|
|
'actions_critical',
|
|
|
|
'suppress',
|
|
'audit_interval',
|
|
'algorithm',
|
|
'capabilities',
|
|
'host_uuid', 'sensorgroup_uuid',
|
|
'created_at', 'updated_at', ]
|
|
|
|
sensor_fields_analog = ['unit_base',
|
|
'unit_modifier',
|
|
'unit_rate',
|
|
|
|
't_minor_lower',
|
|
't_minor_upper',
|
|
't_major_lower',
|
|
't_major_upper',
|
|
't_critical_lower',
|
|
't_critical_upper', ]
|
|
|
|
if rpc_sensor.datatype == 'discrete':
|
|
sensor_fields = sensor_fields_common
|
|
elif rpc_sensor.datatype == 'analog':
|
|
sensor_fields = sensor_fields_common + sensor_fields_analog
|
|
else:
|
|
LOG.error(_("Invalid datatype={}").format(rpc_sensor.datatype))
|
|
|
|
if not expand:
|
|
sensor.unset_fields_except(sensor_fields)
|
|
|
|
# never expose the id attribute
|
|
sensor.host_id = wtypes.Unset
|
|
sensor.sensorgroup_id = wtypes.Unset
|
|
|
|
sensor.links = [link.Link.make_link('self', pecan.request.host_url,
|
|
'sensors', sensor.uuid),
|
|
link.Link.make_link('bookmark',
|
|
pecan.request.host_url,
|
|
'sensors', sensor.uuid,
|
|
bookmark=True)
|
|
]
|
|
return sensor
|
|
|
|
|
|
class SensorCollection(collection.Collection):
|
|
"""API representation of a collection of Sensor objects."""
|
|
|
|
sensors = [Sensor]
|
|
"A list containing Sensor objects"
|
|
|
|
def __init__(self, **kwargs):
|
|
self._type = 'sensors'
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_sensors, limit, url=None,
|
|
expand=False, **kwargs):
|
|
collection = SensorCollection()
|
|
collection.sensors = [Sensor.convert_with_links(p, expand)
|
|
for p in rpc_sensors]
|
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
|
return collection
|
|
|
|
|
|
LOCK_NAME = 'SensorController'
|
|
|
|
|
|
class SensorController(rest.RestController):
|
|
"""REST controller for Sensors."""
|
|
|
|
_custom_actions = {
|
|
'detail': ['GET'],
|
|
}
|
|
|
|
def __init__(self, from_hosts=False, from_sensorgroup=False):
|
|
self._from_hosts = from_hosts
|
|
self._from_sensorgroup = from_sensorgroup
|
|
self._api_token = None
|
|
self._hwmon_address = k_host.LOCALHOST_HOSTNAME
|
|
self._hwmon_port = constants.HWMON_PORT
|
|
|
|
def _get_sensors_collection(self, uuid, sensorgroup_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."))
|
|
|
|
if self._from_sensorgroup and not uuid:
|
|
raise exception.InvalidParameterValue(_(
|
|
"SensorGroup id not specified."))
|
|
|
|
limit = utils.validate_limit(limit)
|
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
|
|
|
marker_obj = None
|
|
if marker:
|
|
marker_obj = objects.Sensor.get_by_uuid(
|
|
pecan.request.context,
|
|
marker)
|
|
|
|
if self._from_hosts:
|
|
sensors = pecan.request.dbapi.sensor_get_by_host(
|
|
uuid, limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
LOG.debug("dbapi.sensor_get_by_host=%s" % sensors)
|
|
elif self._from_sensorgroup:
|
|
sensors = pecan.request.dbapi.sensor_get_by_sensorgroup(
|
|
uuid,
|
|
limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
LOG.debug("dbapi.sensor_get_by_sensorgroup=%s" % sensors)
|
|
else:
|
|
if uuid and not sensorgroup_uuid:
|
|
sensors = pecan.request.dbapi.sensor_get_by_host(
|
|
uuid, limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
LOG.debug("dbapi.sensor_get_by_host=%s" % sensors)
|
|
elif uuid and sensorgroup_uuid: # Need ihost_uuid ?
|
|
sensors = pecan.request.dbapi.sensor_get_by_host_sensorgroup(
|
|
uuid,
|
|
sensorgroup_uuid,
|
|
limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
LOG.debug("dbapi.sensor_get_by_host_sensorgroup=%s" %
|
|
sensors)
|
|
|
|
elif sensorgroup_uuid: # Need ihost_uuid ?
|
|
sensors = pecan.request.dbapi.sensor_get_by_host_sensorgroup(
|
|
uuid, # None
|
|
sensorgroup_uuid,
|
|
limit,
|
|
marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
|
|
else:
|
|
sensors = pecan.request.dbapi.sensor_get_list(
|
|
limit, marker_obj,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
|
|
return SensorCollection.convert_with_links(sensors, limit,
|
|
url=resource_url,
|
|
expand=expand,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir)
|
|
|
|
@wsme_pecan.wsexpose(SensorCollection, types.uuid, types.uuid,
|
|
types.uuid, int, wtypes.text, wtypes.text)
|
|
def get_all(self, uuid=None, sensorgroup_uuid=None,
|
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
|
"""Retrieve a list of sensors."""
|
|
|
|
return self._get_sensors_collection(uuid, sensorgroup_uuid,
|
|
marker, limit,
|
|
sort_key, sort_dir)
|
|
|
|
@wsme_pecan.wsexpose(SensorCollection, 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 sensors with detail."""
|
|
|
|
# NOTE(lucasagomes): /detail should only work against collections
|
|
parent = pecan.request.path.split('/')[:-1][-1]
|
|
if parent != "sensors":
|
|
raise exception.HTTPNotFound
|
|
|
|
expand = True
|
|
resource_url = '/'.join(['sensors', 'detail'])
|
|
return self._get_sensors_collection(uuid, marker, limit, sort_key,
|
|
sort_dir, expand, resource_url)
|
|
|
|
@wsme_pecan.wsexpose(Sensor, types.uuid)
|
|
def get_one(self, sensor_uuid):
|
|
"""Retrieve information about the given sensor."""
|
|
if self._from_hosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
rpc_sensor = objects.Sensor.get_by_uuid(
|
|
pecan.request.context, sensor_uuid)
|
|
|
|
if rpc_sensor.datatype == 'discrete':
|
|
rpc_sensor = objects.SensorDiscrete.get_by_uuid(
|
|
pecan.request.context, sensor_uuid)
|
|
elif rpc_sensor.datatype == 'analog':
|
|
rpc_sensor = objects.SensorAnalog.get_by_uuid(
|
|
pecan.request.context, sensor_uuid)
|
|
else:
|
|
LOG.error(_("Invalid datatype={}").format(rpc_sensor.datatype))
|
|
|
|
return Sensor.convert_with_links(rpc_sensor)
|
|
|
|
@staticmethod
|
|
def _new_sensor_semantic_checks(sensor):
|
|
datatype = sensor.as_dict().get('datatype') or ""
|
|
sensortype = sensor.as_dict().get('sensortype') or ""
|
|
if not (datatype and sensortype):
|
|
raise wsme.exc.ClientSideError(_("sensor-add Cannot "
|
|
"add a sensor "
|
|
"without a valid datatype "
|
|
"and sensortype."))
|
|
|
|
if datatype not in constants.SENSOR_DATATYPE_VALID_LIST:
|
|
raise wsme.exc.ClientSideError(
|
|
_("sensor datatype must be one of %s.") %
|
|
constants.SENSOR_DATATYPE_VALID_LIST)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(Sensor, body=Sensor)
|
|
def post(self, sensor):
|
|
"""Create a new sensor."""
|
|
if self._from_hosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
self._new_sensor_semantic_checks(sensor)
|
|
try:
|
|
ihost = pecan.request.dbapi.host_get(sensor.host_uuid)
|
|
|
|
if hasattr(sensor, 'datatype'):
|
|
if sensor.datatype == 'discrete':
|
|
new_sensor = pecan.request.dbapi.sensor_discrete_create(
|
|
ihost.id, sensor.as_dict())
|
|
elif sensor.datatype == 'analog':
|
|
new_sensor = pecan.request.dbapi.sensor_analog_create(
|
|
ihost.id, sensor.as_dict())
|
|
else:
|
|
raise wsme.exc.ClientSideError(
|
|
_("Invalid datatype. {}").format(sensor.datatype))
|
|
else:
|
|
raise wsme.exc.ClientSideError(_("Unspecified datatype."))
|
|
|
|
except exception.InventoryException as e:
|
|
LOG.exception(e)
|
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
|
return sensor.convert_with_links(new_sensor)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme.validate(types.uuid, [SensorPatchType])
|
|
@wsme_pecan.wsexpose(Sensor, types.uuid,
|
|
body=[SensorPatchType])
|
|
def patch(self, sensor_uuid, patch):
|
|
"""Update an existing sensor."""
|
|
if self._from_hosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
rpc_sensor = objects.Sensor.get_by_uuid(pecan.request.context,
|
|
sensor_uuid)
|
|
if rpc_sensor.datatype == 'discrete':
|
|
rpc_sensor = objects.SensorDiscrete.get_by_uuid(
|
|
pecan.request.context, sensor_uuid)
|
|
elif rpc_sensor.datatype == 'analog':
|
|
rpc_sensor = objects.SensorAnalog.get_by_uuid(
|
|
pecan.request.context, sensor_uuid)
|
|
else:
|
|
raise wsme.exc.ClientSideError(_("Invalid datatype={}").format(
|
|
rpc_sensor.datatype))
|
|
|
|
rpc_sensor_orig = copy.deepcopy(rpc_sensor)
|
|
|
|
# replace ihost_uuid and sensorgroup_uuid with corresponding
|
|
utils.validate_patch(patch)
|
|
patch_obj = jsonpatch.JsonPatch(patch)
|
|
my_host_uuid = None
|
|
for p in patch_obj:
|
|
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
|
|
|
|
if p['path'] == '/sensorgroup_uuid':
|
|
p['path'] = '/sensorgroup_id'
|
|
try:
|
|
sensorgroup = objects.sensorgroup.get_by_uuid(
|
|
pecan.request.context, p['value'])
|
|
p['value'] = sensorgroup.id
|
|
LOG.info("sensorgroup_uuid=%s id=%s" % (p['value'],
|
|
sensorgroup.id))
|
|
except exception.InventoryException:
|
|
p['value'] = None
|
|
|
|
try:
|
|
sensor = Sensor(**jsonpatch.apply_patch(rpc_sensor.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 rpc_sensor.datatype == 'discrete':
|
|
fields = objects.SensorDiscrete.fields
|
|
else:
|
|
fields = objects.SensorAnalog.fields
|
|
|
|
for field in fields:
|
|
if rpc_sensor[field] != getattr(sensor, field):
|
|
rpc_sensor[field] = getattr(sensor, field)
|
|
|
|
delta = rpc_sensor.obj_what_changed()
|
|
sensor_suppress_attrs = ['suppress']
|
|
force_action = False
|
|
if any(x in delta for x in sensor_suppress_attrs):
|
|
valid_suppress = ['True', 'False', 'true', 'false', 'force_action']
|
|
if rpc_sensor.suppress.lower() not in valid_suppress:
|
|
raise wsme.exc.ClientSideError(_("Invalid suppress value, "
|
|
"select 'True' or 'False'"))
|
|
elif rpc_sensor.suppress.lower() == 'force_action':
|
|
LOG.info("suppress=%s" % rpc_sensor.suppress.lower())
|
|
rpc_sensor.suppress = rpc_sensor_orig.suppress
|
|
force_action = True
|
|
|
|
self._semantic_modifiable_fields(patch_obj, force_action)
|
|
|
|
if not pecan.request.user_agent.startswith('hwmon'):
|
|
hwmon_sensor = cutils.removekeys_nonhwmon(
|
|
rpc_sensor.as_dict())
|
|
|
|
if not my_host_uuid:
|
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
|
rpc_sensor.host_id)
|
|
my_host_uuid = host.uuid
|
|
LOG.warn("Missing host_uuid updated=%s" % my_host_uuid)
|
|
|
|
hwmon_sensor.update({'host_uuid': my_host_uuid})
|
|
|
|
hwmon_response = hwmon_api.sensor_modify(
|
|
self._api_token, self._hwmon_address, self._hwmon_port,
|
|
hwmon_sensor,
|
|
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)
|
|
|
|
rpc_sensor.save()
|
|
|
|
return Sensor.convert_with_links(rpc_sensor)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
|
def delete(self, sensor_uuid):
|
|
"""Delete a sensor."""
|
|
if self._from_hosts:
|
|
raise exception.OperationNotPermitted
|
|
|
|
pecan.request.dbapi.sensor_destroy(sensor_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)
|
|
|
|
state_rel_path = ['/actions_critical',
|
|
'/actions_major',
|
|
'/actions_minor']
|
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
|
raise wsme.exc.ClientSideError(
|
|
_("The following fields can only be modified at the "
|
|
"sensorgroup level: %s") % state_rel_path)
|
|
|
|
if not (pecan.request.user_agent.startswith('hwmon') or force_action):
|
|
state_rel_path = ['/sensorname',
|
|
'/path',
|
|
'/status',
|
|
'/state',
|
|
'/possible_states',
|
|
'/algorithm',
|
|
'/actions_critical_choices',
|
|
'/actions_major_choices',
|
|
'/actions_minor_choices',
|
|
'/unit_base',
|
|
'/unit_modifier',
|
|
'/unit_rate',
|
|
'/t_minor_lower',
|
|
'/t_minor_upper',
|
|
'/t_major_lower',
|
|
'/t_major_upper',
|
|
'/t_critical_lower',
|
|
'/t_critical_upper',
|
|
]
|
|
|
|
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)
|