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

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)