You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

530 lines
20 KiB

# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved.
import json
import re
import sys
from splunk.clilib.bundle_paths import make_splunkhome_path
from splunk.util import normalizeBoolean
import splunk.search as search
sys.path.append(make_splunkhome_path(['etc', 'apps', 'SA-ITOA', 'lib']))
sys.path.append(make_splunkhome_path(['etc', 'apps', 'SA-ITOA', 'lib', 'SA_ITOA_app_common']))
from ITOA.setup_logging import logger
from ITOA.event_management.event_management_object_manifest import object_manifest
from ITOA.event_management.notable_event_utils import ActionDispatchConfiguration
from ITOA.itoa_common import get_conf
from ITOA.event_management.notable_event_utils import NotableEventConfiguration, token_replacement
# FIXME - this class can't be used along because it has reference of some instance variable like
# self._session etc, which is initialized by another imported class.
# this is really very bad pattern. We should address this
class EventManagementService(object):
def __init__(self, session_key=None):
self._session_key = session_key
def _get_instance(self, object_type, owner, current_user_name=None):
"""
Get instance for given object_ type
@type: string
@param object_type: type of object to instantiate
@type: string
@param owner: owner of the object
@rtype: ItsiNotableEvent
@return: object_ instance of given type
"""
object_class = object_manifest.get(object_type)
# We want to perform all operation at nobody context
return object_class(self._session_key, current_user_name=current_user_name)
def get_all_notable_event_configuration(self, session_key):
"""
Get all notable event configuration info like severities, statuses, owners and email formats too
@param session_key: basestring
@param session_key: session key
@rtype: dict
@return: dict for all notable event information
Return a dictionary which hold information about severities, status and owners
{
severities: [
{
name: <name>,
value: <name>,
default: 0|1,
color: <name>
}..],
statuses: [
{
name: <name>,
value: <name>,
default: 0|1,
end: 0|1 (optional, only included if it exists)
} ...],
owners: [{
name: <name>,
value: <name>,
default: 0|1
}..],
email_formats: [{
name: <name>,
value: <name>,
default: 0|1
}..]
"""
ns_config = NotableEventConfiguration(session_key, logger)
data = {}
# Get Severity
data['severities'] = []
for severity_value, content in ns_config.severity_contents.items():
data['severities'].append({
'label': content.get('label'),
'value': severity_value,
'default': ns_config.get_default_severity() == severity_value,
'color': content.get('color')
})
# Get Status
data['statuses'] = []
for status_value, content in ns_config.status_contents.items():
data['statuses'].append({
'label': content.get('label'),
'value': status_value,
'default': ns_config.get_default_status() == status_value
})
if int(content.get('end', 0)) == 1:
data['statuses'][-1]['end'] = 1
# Get Owner
data['owners'] = []
for key, owner in ns_config.owner_contents.items():
data['owners'].append({
'label': owner.get('realname') if owner.get('realname') else owner.get('_key'),
'value': owner.get('_key'),
'default': ns_config.get_default_owner() == owner.get('_key')
})
data['email_formats'] = []
for eformat in ['table', 'raw', 'csv']:
data['email_formats'].append({
'label': eformat,
'value': eformat,
'default': eformat == 'table'
})
logger.debug('Return notable event configuration "%s"', data)
return data
def _get_actions_conf(self, session_key, search='', host_base_uri=''):
"""
Fetches the action conf.
:param session_key: session_key for host
:param host_base_uri: host uri to fetch actions conf
:return: dict of actions configuration
"""
rval = get_conf(
session_key,
'notable_event_actions',
search='',
count=-1,
app='SA-ITOA',
host_base_uri=host_base_uri
)
response = rval.get('response')
configuration = {}
if int(response.status) != 200:
logger.error("Unable to fetch actions conf")
else:
actions_conf = rval.get('content')
content = json.loads(actions_conf)
for entry in content.get('entry', []):
configuration[entry.get('name')] = entry.get('content', {})
return configuration
def check_remote_host_notable_event_actions(self, session_key):
"""
Checks if remote host's actions are available on local host.
:param session_key: local session_key
:return: dictionary of missing_remote_actions and disabled_local_actions
"""
action_dispatch_config = ActionDispatchConfiguration(session_key, logger)
missing_remote_actions = []
disabled_local_actions = []
if action_dispatch_config.ea_role == 'executor':
remote_actions_conf = self._get_actions_conf(
action_dispatch_config.get_master_host_session_key(),
host_base_uri=action_dispatch_config.remote_ea_mgmt_uri
)
local_actions_conf = self._get_actions_conf(session_key)
for remote_action in remote_actions_conf.keys():
if remote_action not in local_actions_conf.keys() and not remote_actions_conf[remote_action].get(
'disabled'):
missing_remote_actions.append(remote_action)
if remote_action in local_actions_conf.keys() and local_actions_conf[remote_action].get('disabled') and \
not remote_actions_conf[remote_action].get('disabled'):
disabled_local_actions.append(remote_action)
return {
'missing_remote_actions': missing_remote_actions,
'disabled_local_actions': disabled_local_actions
}
def _update_comment_with_token_replacement(self, data, owner):
prepend_command = f'search `itsi_event_management_group_index` {data["event_id"]} | dedup itsi_group_id | fields *'
job = search.dispatch(prepend_command, sessionKey=self._session_key, owner=owner, rf='*')
search.waitForJob(job)
if job.isFailed:
job.cancel()
raise search.SearchException(('Search failed to clean up event(s), messages="%s".'), job.messages)
results = job.results
if not results:
raise ValueError('No results found for the search job.')
# Create a dictionary from the first result's fields
result_fields = results[0].fields.data
res = {key: str(result_fields[key]) for key in result_fields}
job.cancel()
# Update the comment token from the result's fields
return token_replacement(data['comment'], res)
# CRUD and BULK Notable event CRUD Operation
def upsert(self, owner, object_type, identifier, kwargs, current_user_name=None, object_instance=None):
"""
Perform create or update
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object_type type
@type identifier: basestring
@param identifier: id. Id can be null. When id is not specified then create operation is being
called otherwise update
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: current user who initiated the request
@type object_instance: object instance
@param object_instance: based upon type instance was created
@rtype: json or Exception
@return: json object
"""
if not object_instance:
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
# add user who is performing this operation
data = kwargs.get('data') or kwargs
self._add_user(data, owner)
self._delete_data(kwargs)
# get earliest or latest time from data itself
if 'earliest_time' not in kwargs:
kwargs['earliest_time'] = data.get('earliest_time')
if 'latest_time' not in kwargs:
kwargs['latest_time'] = data.get('latest_time')
# check if the key already exists
create = True
if identifier:
try:
object_instance.get(identifier)
create = False
except Exception:
logger.debug('Object %s does not exist.' % identifier)
if identifier and not create:
logger.debug('Performing update for id=%s', identifier)
is_partial_update = normalizeBoolean(kwargs.pop('is_partial_update', True))
# Enforcing RBAC checks on update require the current user for correlation search.
# Other objects are not enforcing rbac check for now
result = object_instance.update(identifier, data, is_partial_update=is_partial_update, **kwargs)
else:
logger.debug('Creating new %s', object_type)
# Add user who create this object_type in _owner field
self._add_user(data, owner, '_owner')
if normalizeBoolean(data.get('_is_group', False)) is True:
result = object_instance.create_for_group(data, **kwargs)
else:
if object_type == 'notable_event_comment' and 'comment' in data:
regex = re.compile(r'\%([\w.\s]+)\%')
dynamic_fields = regex.findall(data['comment'])
if dynamic_fields:
data['comment'] = self._update_comment_with_token_replacement(data, owner)
result = object_instance.create(data, **kwargs)
logger.debug('Returning values=%s', result)
return result
def get(self, owner, object_type, identifier, kwargs, current_user_name=None):
"""
Perform get operations
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object_type type
@type identifier: basestring
@param identifier: id. Id can be null. When id is not specified then create operation is being
called otherwise update
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: the user making the request.
@rtype: json or Exception
@return: json object
"""
logger.debug('Getting _key=%s of object=%s ,and user=%s', identifier, object_type, owner)
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
self._delete_data(kwargs)
result = object_instance.get(identifier, **kwargs)
logger.debug('Returning values=%s', result)
return result
def delete(self, owner, object_type, identifier, kwargs, current_user_name=None):
"""
Perform delete operation
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object_type type
@type identifier: basestring
@param identifier: id. Id can be null. When id is not specified then create operation is being
called otherwise update
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: the user making the request.
@rtype: None or Exception
@return: None if delete operation is successful otherwise exception
"""
logger.debug('Deleting _key=%s of object=%s', identifier, object_type)
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
self._delete_data(kwargs)
object_instance.delete(identifier, **kwargs)
def get_bulk(self, owner, object_type, kwargs, current_user_name=None):
"""
Perform get bulk operations
if data is not specify in the kwargs then all objects are return based
upon count and offset values
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object_type type
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: the user making the request.
@rtype: json or Exception
@return: json object
"""
logger.debug('Getting %s in bulk for user=%s', object_type, owner)
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
ids = kwargs.get('ids')
if ids:
object_ids_list = json.loads(ids)
else:
object_ids_list = []
logger.info('Getting %s with ids=%s', object_type, ids)
object_ids = object_ids_list if object_ids_list else []
self._delete_data(kwargs)
results = object_instance.get_bulk(object_ids, **kwargs)
logger.debug('Returned objects=%s for object=%s', len(results), object_type)
return results
def delete_bulk(self, owner, object_type, kwargs, current_user_name=None):
"""
Perform delete operations in bulk. If data is not specify then all objects is delete
for provided type
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object type
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: the user making the request.
@rtype: None or Exception
@return: None if operation is successful otherwise exception
"""
logger.debug('Deleting objects for object=%s user=%s', object_type, owner)
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
ids = kwargs.get('ids')
if ids:
object_ids = json.loads(ids)
else:
object_ids = None
logger.info('Deleting %s with ids=%s', object_type, object_ids)
self._delete_data(kwargs)
object_instance.delete_bulk(object_ids, **kwargs)
logger.debug('Successfully deleted object(s) of object=%s', object_type)
def upsert_bulk(self, owner, object_type, kwargs, current_user_name=None):
"""
Perform update/create operations in bulk, note that this method is not safe for upserts of mixed creates and
updates, it must all be create or all update.
@type owner: basestring
@param owner: owner who is performing this operation
@type object_type: basestring
@param object_type: Target object type
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type current_user_name: basestring
@param current_user_name: the user making the request.
@rtype: json or Exception
@return: json object
"""
object_instance = self._get_instance(object_type, owner, current_user_name=current_user_name)
data_list = kwargs.get('data') or kwargs
self._delete_data(kwargs)
if len(data_list) > 0:
# create a list of object ids that already exist
get_kwargs = dict(kwargs)
ids_list = list([data.get(object_instance.id_key) for data in data_list])
get_kwargs['ids'] = json.dumps(ids_list)
try:
get_result = self.get_bulk(owner, object_type, get_kwargs)
except Exception:
get_result = []
existing_ids_list = list([data.get(object_instance.id_key) for data in get_result])
# separate items that need to be created or updated
update_list = []
create_list = []
for data in data_list:
if data.get(object_instance.id_key) in existing_ids_list:
update_list.append(data)
else:
create_list.append(data)
update_results = []
if update_list:
logger.debug('Updating %s objects object=%s', len(update_list), object_type)
object_ids_list = [data.get(object_instance.id_key) for data in update_list]
object_ids = object_ids_list if object_ids_list else None
is_partial_update = normalizeBoolean(kwargs.pop('is_partial_update', True))
# Update one
update_results = object_instance.update_bulk(object_ids, update_list,
is_partial_update=is_partial_update, **kwargs)
create_results = []
if create_list:
# Create one
logger.debug('Creating bulk %s objects %s', len(create_list), object_type)
create_results = object_instance.create_bulk(create_list, **kwargs)
return update_results + create_results
# Helper functions
def _delete_data(self, kwargs):
"""
Deleting extra data from kwargs otherwise data would be passed twice
@rtype: dict
@return: updated kwargs
"""
kwargs.pop('data', None)
def _add_user(self, data, owner, field_name='_user'):
"""
Add user or owner information in data to know who is performing the operation
@type data: dict
@param data: data (inplace update to the dict)
@type field_name: basestring
@param field_name: field name for user
@rtype: dict
@return: Updated dict
"""
if isinstance(data, dict):
data[field_name] = owner
else:
raise ValueError('Data should be valid dict')
def _check_and_call_operation(self, owner, object, kwargs, single_operation, bulk_operation,
current_user_name=None):
"""
Support function to call
@type owner: basestring
@param owner: owner who is performing this operation
@type object: basestring
@param object: Target object type
@type kwargs:dict
@param kwargs: which contain addition parameters of controller
@type single_operation: function
@param single_operation: function which perform single operation
@type bulk_operation: function
@param bulk_operation: function which perform bulk operation
@return: return of either function or exception
"""
data = kwargs.get('data') or kwargs
logger.debug('data="%s" kwargs="%s"', data, kwargs)
if isinstance(data, list):
return bulk_operation(owner, object, kwargs,
current_user_name=current_user_name)
elif isinstance(data, dict):
object_instance = self._get_instance(
object, owner,
current_user_name=current_user_name)
object_id = data.get(object_instance.id_key)
return single_operation(
owner, object, object_id, kwargs,
current_user_name=current_user_name,
object_instance=object_instance)
else:
raise TypeError('Invalid data format, data can be either dict or list. {}'.format(type(data)))