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.
912 lines
38 KiB
912 lines
38 KiB
# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved.
|
|
|
|
"""
|
|
Use this module to update information on an ITSI Notable vis-a-vis its external
|
|
tickets.
|
|
"""
|
|
|
|
import itsi_py3
|
|
import re
|
|
import sys
|
|
import json
|
|
import time
|
|
from uuid import uuid1
|
|
|
|
import splunk.rest as splunk_rest
|
|
from splunk.util import safeURLQuote, normalizeBoolean
|
|
from splunk.clilib.bundle_paths import make_splunkhome_path
|
|
|
|
|
|
from splunk import ResourceNotFound, RESTException
|
|
from ITOA.storage import itoa_storage
|
|
from .notable_event_utils import Audit
|
|
from ITOA.itoa_common import JsonPathElement
|
|
from ITOA import setup_logging
|
|
|
|
|
|
class ExternalTicket(object):
|
|
"""
|
|
An instance of this class corresponds to an external ticket
|
|
and is persisted in KV Store in the collection
|
|
'itsi_notable_event_ticketing'
|
|
An example of records is given further below. In the example below we see
|
|
two records. Each record represents tickets in a specific external ticket
|
|
system.
|
|
|
|
ticket_system is also included in tickets because lookup cannot combine a multivalue
|
|
fields with a single value
|
|
|
|
~~~~~~~~~~~~~~~~
|
|
Example:
|
|
~~~~~~~~~~~~~~~~
|
|
{
|
|
event_id: 123,
|
|
ticket_system: snow,
|
|
tickets: [{
|
|
ticket_id: <id>
|
|
ticket_url: <some url>
|
|
ticket_system: <system>
|
|
<other params for this ticket>
|
|
},{
|
|
ticket_id: <id2>
|
|
ticket_url: <some url>
|
|
ticket_system: <system>
|
|
<other params for this ticket>
|
|
}]
|
|
},
|
|
{
|
|
event_id: 124,
|
|
ticket_system: remedy,
|
|
tickets: [{
|
|
ticket_id: <id>
|
|
ticket_url: <some url>
|
|
ticket_system: <system>
|
|
<other params for this ticket>
|
|
},{
|
|
ticket_id: <id2>
|
|
ticket_url: <some url>
|
|
ticket_system: <system>
|
|
<other params for this ticket>
|
|
}]
|
|
}
|
|
"""
|
|
|
|
default_kv_collection = 'itsi_notable_event_ticketing'
|
|
default_kv_ns = 'nobody'
|
|
default_kv_app = 'SA-ITOA'
|
|
default_token_name = 'Auto Generated ITSI Notable Index Audit Token'
|
|
|
|
# keys in a record in our kv store collection
|
|
KEY_TICKET_SYSTEM = 'ticket_system' # jira, snow, siebel etc...
|
|
KEY_TICKETS = 'tickets' # all external tickets for this ticket_system
|
|
KEY_TICKETS_TICKET_ID = 'ticket_id' # external ticket id
|
|
KEY_TICKETS_TICKET_URL = 'ticket_url' # external ticket url
|
|
KEY_OBJECT_TYPE = 'object_type' # object type
|
|
KEY_POLICY_ID = 'itsi_policy_id' # policy ID
|
|
VAL_OBJECT_TYPE = 'external_ticket' # value corresponding to the key above
|
|
KEY_EVENT_ID = 'event_id' # notable event id
|
|
KEY_UID = '_key' # unique id for this object in kv store
|
|
KEY_TIME = 'mod_time' # time at which event was modified
|
|
KEY_CREATE_TIME = 'create_time' # time at which event was created
|
|
|
|
@staticmethod
|
|
def get_auditor(session_key, token):
|
|
"""
|
|
Returns an audit object
|
|
@param session_key: splunk session key
|
|
@param audit_token_name: token name for this auditor
|
|
"""
|
|
return Audit(session_key, audit_token_name=token)
|
|
|
|
def __init__(self, event_id, session_key, logger,
|
|
collection=default_kv_collection, ns=default_kv_ns,
|
|
app=default_kv_app, audit_token_name=default_token_name,
|
|
action_dispatch_config=None, current_user_name=None, **kwargs):
|
|
"""
|
|
For a given notable id, this object corresponds to an external ticket
|
|
@type event_id: basestring
|
|
@param event_id: ITSI notable event id
|
|
|
|
@type session_key: basestring
|
|
@param session_key: splunkd session key
|
|
|
|
@type logger: logger
|
|
@param logger: caller's logger
|
|
|
|
@type collection: basestring
|
|
@param collection: kv store collection
|
|
|
|
@type action_dispatch_config: ActionDispatchConfiguration
|
|
@param action_dispatch_config: the configuration of hybrid action dispatch
|
|
|
|
@type kwargs: dict
|
|
@param kwargs: other
|
|
"""
|
|
if not isinstance(event_id, itsi_py3.string_type):
|
|
raise TypeError('Invalid type "event_id".')
|
|
if not event_id.strip():
|
|
raise ValueError('Invalid value "event_id".')
|
|
if not isinstance(session_key, itsi_py3.string_type):
|
|
raise TypeError('Invalid type "session_key".')
|
|
if not session_key.strip():
|
|
raise ValueError('Invalid value "session_key".')
|
|
if not isinstance(collection, itsi_py3.string_type):
|
|
raise TypeError('Invalid type "collection".')
|
|
if not collection.strip():
|
|
raise ValueError('Invalid value "collection".')
|
|
if not isinstance(ns, itsi_py3.string_type):
|
|
raise TypeError('Invalid type "ns".')
|
|
if not ns.strip():
|
|
raise ValueError('Invalid value "ns".')
|
|
if not isinstance(app, itsi_py3.string_type):
|
|
raise TypeError('Invalid type "app".')
|
|
if not app.strip():
|
|
raise ValueError('Invalid value "app".')
|
|
|
|
self.event_id = event_id
|
|
self.session_key = session_key
|
|
self.ns = ns
|
|
self.app = app
|
|
self.collection = collection
|
|
self.object_type = ExternalTicket.VAL_OBJECT_TYPE
|
|
self.storage_interface = itoa_storage.ITOAStorage(collection=collection)
|
|
self.logger = logger
|
|
self.auditor = ExternalTicket.get_auditor(session_key, audit_token_name)
|
|
self.master_session_key = self.session_key
|
|
self.action_dispatch_config = action_dispatch_config
|
|
self.host_base_uri = ''
|
|
if self.action_dispatch_config:
|
|
self.host_base_uri = self.action_dispatch_config.remote_ea_mgmt_uri
|
|
self.master_session_key = self.action_dispatch_config.get_master_host_session_key()
|
|
self.current_user_name = current_user_name
|
|
|
|
@staticmethod
|
|
def curate_search_prepend_command(prepend_command, ids, is_group, action_name, action_config, logger):
|
|
"""
|
|
Given an input prepend command, curate it for ticketing.
|
|
For actions on a notable event associated with an external ticket,
|
|
we append the event id as a correlation id.
|
|
When working on a notable event group, we ought to pass in the group_id as the
|
|
correlation id.
|
|
All actions, read event data from the `itsi_tracked_alerts` index
|
|
regardless of whether it belongs to a group or not.
|
|
Events in this index though, have no field to indicate a group id if
|
|
applicable.
|
|
|
|
This method will append an eval field to the existing prepend_command and
|
|
make it more pertinent for a notable event group, so we assign group_id
|
|
as correlation_id.
|
|
|
|
Currently this method only does something if we are working on a group
|
|
i.e. `is_group` is True.
|
|
|
|
@type prepend_command: basestring
|
|
@param prepend_command: existing prepend command.
|
|
|
|
@type ids: list of event ids
|
|
@param ids: event ids that were passed to us as input.
|
|
|
|
@type is_group: boolean
|
|
@param is_group: Indicates if we are working on a group.
|
|
|
|
@type action_name: basestring
|
|
@param action_name: name of the action that is being executed.
|
|
|
|
@type action_config: dict
|
|
@param action_config: configuration for this action. everything under
|
|
the action's stanza in notable_event_actions.conf
|
|
|
|
@type logger: logger
|
|
@param logger: caller's logger
|
|
"""
|
|
if not logger:
|
|
logger = setup_logging.logger
|
|
|
|
if not isinstance(prepend_command, itsi_py3.string_type):
|
|
message = 'Invalid type="%s" for "%s". Expecting string.' % (type(prepend_command).__name__, prepend_command)
|
|
logger.exception(message)
|
|
raise TypeError(message)
|
|
|
|
if not isinstance(ids, list) or not ids:
|
|
message = 'Invalid ids list. Received="%s". Type="%s".' % (ids, type(ids).__name__)
|
|
logger.exception(message)
|
|
raise TypeError(message)
|
|
|
|
if not is_group:
|
|
logger.info('No special curation of prepend_command="%s" is required for regular notable events', prepend_command)
|
|
return prepend_command
|
|
|
|
# when operating on more than one group,
|
|
# we will use first id. Refresh should take care
|
|
# updating others...
|
|
group_captain_id = ids[0]
|
|
|
|
# obtain value to append to search string as an eval. we want to append this
|
|
# because sendalert should get a group_id to use as a correlation id
|
|
correlation_value = action_config.get('correlation_value_for_group', '') # `$result.itsi_group_id$`
|
|
|
|
pattern = re.compile(
|
|
r'^' # begin
|
|
r'(?:\$result.)' # ignore group. our string begins with "$result."
|
|
r'([\w-]*)' # capture group
|
|
r'(?:\$)' # ignore group. our string ends with `$`
|
|
r'$') # end
|
|
|
|
match = re.match(pattern, correlation_value)
|
|
if not match:
|
|
logger.error('Empty/missing correlation_value_for_group in your notable_event_actions.conf.')
|
|
raise KeyError('Empty/missing correlation_value_for_group in your notable_event_actions.conf.')
|
|
|
|
eval_field = match.group(1) # we are guaranteed this group.
|
|
|
|
prepend_command += ' | eval %s="%s"' % (eval_field, group_captain_id)
|
|
|
|
return prepend_command
|
|
|
|
@staticmethod
|
|
def curate_params(params, action_name, action_config, logger, **kwargs):
|
|
"""
|
|
Given params to a sendalert command, curate them. We will ensure that a
|
|
correlation id parameter is always passed on with the command to splunk
|
|
search.
|
|
|
|
@type params: basestring
|
|
@param params: already constructed params
|
|
|
|
@type action_name: basestring
|
|
@param action_name: the action that is being executed, i.e.
|
|
snow_incident etc...
|
|
|
|
@type action_config: dict
|
|
@param action_config: the configuration of the action name, essentially
|
|
everything under the action's stanza in notable_event_actions.conf
|
|
|
|
@type logger: logger
|
|
@param logger: caller's logger
|
|
|
|
@rtype params: basestring
|
|
@return newly curated params consisting of the mandatory correlation id
|
|
/ value kv pair if it doesnt already exist.
|
|
"""
|
|
if not logger:
|
|
logger = setup_logging.logger
|
|
|
|
if not isinstance(params, itsi_py3.string_type):
|
|
message = 'Expecting string type for params. Received="%s".' % type(params).__name__
|
|
logger.exception(message)
|
|
raise TypeError(message)
|
|
|
|
is_group = kwargs.get('is_group', False)
|
|
|
|
logger.debug('pre-curating params=`%s`\nis_group=`%s` action_name=`%s` action_config=`%s`', params,
|
|
is_group, action_name, json.dumps(action_config))
|
|
|
|
msg = 'Correlation id is mandated for operations pertaining to external ticket.'
|
|
|
|
correlation_key = action_config.get('correlation_key') # `correlation_id` etc..
|
|
|
|
if is_group:
|
|
correlation_value = action_config.get('correlation_value_for_group') # `$result.itsi_group_id$`
|
|
else:
|
|
correlation_value = action_config.get('correlation_value') # `$result.event_id$` etc...
|
|
|
|
if correlation_key is None or correlation_value is None:
|
|
logger.error('%s. Missing `correlation_value`/`correlation_value_for_group`/`correlation_key`. Config="%s"',
|
|
msg, action_config)
|
|
raise KeyError(msg)
|
|
|
|
# we want to capture two potential groups and check if group(2) is
|
|
# alright, i.e. not empty.
|
|
# group 1 = param.correlation_id
|
|
# group 2 = "$result.event_id$" (or $result.itsi_group_id$ for a group)
|
|
# the rest of them need not be captured
|
|
# if group 2 is empty i.e. "" then we will append event_id as correlation id,
|
|
# else we will respect what is passed in to us.
|
|
|
|
pattern = re.compile(
|
|
'^' # begin
|
|
'(?:.*)' # ignore group
|
|
'(param.' + correlation_key + '=)(\"[a-zA-Z._$-]*\")' # capture two groups
|
|
'(?:.*)$') # ignore the rest
|
|
|
|
match = re.match(pattern, params)
|
|
if match:
|
|
logger.debug('params match found. groups=`%s`', match.groups())
|
|
|
|
to_add = ' param.%s="%s"' % (correlation_key, correlation_value)
|
|
logger.debug('params to_add=%s', to_add)
|
|
|
|
# add correlation id k-v if there is no match
|
|
if not match:
|
|
logger.info('%s. None found or incorrect correlation_id kv. Will append `%s`. Config="%s"',
|
|
msg, to_add, action_config)
|
|
params += to_add
|
|
else:
|
|
# Replace incorrect correlation id kv with correct kv. Blindly appending
|
|
# with param.correlation_id=blah (say) causes unpredictable results,
|
|
# including refresh failure. TA_snow seems to randomly pick from
|
|
# `param.correlation_id=blah` vs `param.correlation_id=""` when creating
|
|
# an external ticket.
|
|
if len(match.groups()) == 2:
|
|
if match.group(2) == '""':
|
|
logger.info('%s. Empty correlation_id kv found. Will replace'
|
|
' with `%s` as correlation_id. Config=`%s`', msg, to_add, action_config)
|
|
empty_correlation_kv = 'param.%s=""' % correlation_key
|
|
params = params.replace(empty_correlation_kv, to_add)
|
|
else:
|
|
logger.info('User-given correlation_id kv found. Will replace'
|
|
' with `%s` as correlation_id. Config=`%s`', to_add, action_config)
|
|
user_given_correlation_kv = '%s%s' % (match.group(1), match.group(2))
|
|
logger.debug("user_given_correlation_kv=%s", user_given_correlation_kv)
|
|
params = params.replace(user_given_correlation_kv, to_add)
|
|
|
|
# NOTE: When operating on more than one Group, we cannot restrict the user
|
|
# from overwriting correlation_value to something else. We advise an
|
|
# end-user to not overwrite correlation_id value in the HTML modal.
|
|
|
|
logger.debug('post-curating params=`%s`', params)
|
|
return params
|
|
|
|
@staticmethod
|
|
def get_kvstore_filter(filter_string, logger):
|
|
"""
|
|
Given a filter_string, validate against allowed kvstore regex characters
|
|
and return a filter for kvstore
|
|
|
|
@type filter_string: string
|
|
@param filter_string: string sent by user
|
|
|
|
@type logger: logger
|
|
@param logger: caller's logger
|
|
|
|
@rtype kvstore_filter: dict
|
|
@return kvstore filter
|
|
"""
|
|
pattern = re.compile(r'^([\w-]+)$')
|
|
match = re.match(pattern, filter_string)
|
|
kvstore_filter = None
|
|
|
|
if not match or len(match.groups()) != 1:
|
|
logger.error('User-given filter_string has special characters other than `-` and `_`.'
|
|
'It is not clean for kvstore, filter_string=`%s`', filter_string)
|
|
else:
|
|
logger.info('User-given filter_string is correct. groups=`%s`', match.groups())
|
|
kvstore_filter = { 'tickets.ticket_id': { '$regex': '^(.*' + filter_string + '.*)$', '$options': 'i' } }
|
|
|
|
return kvstore_filter
|
|
|
|
@staticmethod
|
|
def do_refresh(session_key, logger, action_name, event_action_executed_on_ids, action_config, action_data=None,
|
|
itsi_policy_id=None, action_dispatch_config=None, current_user_name=None):
|
|
"""
|
|
Assumes that an event action has already been executed on a list of
|
|
events.
|
|
Refreshes notable events with external ticket information.
|
|
|
|
@type session_key: basestring
|
|
@param session_key: the session key for the operation
|
|
|
|
@type logger: logger
|
|
@param logger: caller's logger
|
|
|
|
@type action_name: basestring
|
|
@param action_name: the action name
|
|
|
|
@type event_action_executed_on_ids: list
|
|
@param event_action_executed_on_ids: ids on which action has been executed
|
|
|
|
@type action_data: dict
|
|
@param action_data: action data in the request
|
|
|
|
@type action_config: dict
|
|
@param action_config: configuration for our action, key'ed by action name
|
|
|
|
@type itsi_policy_id: basestring
|
|
@param itsi_policy_id: the policy ID that generated this action
|
|
|
|
@type action_dispatch_config: ActionDispatchConfiguration
|
|
@param action_dispatch_config: configuration for hybrid action dispatch
|
|
|
|
@type current_user_name: basestring
|
|
@param current_user_name: the name of the user who generated this action
|
|
|
|
@returns nothing.
|
|
@raises TypeError on invalid input parameters
|
|
"""
|
|
if action_name == "itsi_pagerduty_event":
|
|
logger.info("Skipping do_refresh for PagerDuty")
|
|
return
|
|
|
|
if not logger:
|
|
logger = setup_logging.logger
|
|
|
|
if any([not isinstance(action_name, itsi_py3.string_type),
|
|
isinstance(action_name, itsi_py3.string_type) and not action_name.strip()]):
|
|
logger.error('Invalid action_name')
|
|
raise TypeError('Invalid action_name')
|
|
|
|
if not action_config:
|
|
logger.error('No refresh config found for %s', action_name)
|
|
raise KeyError('No refresh config found for %s.' % action_name)
|
|
|
|
uri = action_config.get('relative_refresh_uri')
|
|
if not isinstance(uri, itsi_py3.string_type):
|
|
logger.error('Expecting str for uri. received="%s"', type(uri).__name__)
|
|
raise TypeError('Expecting str for uri. received="%s".' % type(uri).__name__)
|
|
|
|
# extract correlation id. w/o this our refresh is a no-go.
|
|
query_param = action_config.get('relative_refresh_correlation_key') or action_config.get('correlation_key')
|
|
|
|
getargs = {'output_mode': 'json'}
|
|
|
|
event_id = event_action_executed_on_ids[0]
|
|
if not query_param:
|
|
# means our correlation id will not go as getargs but as part of the URL
|
|
uri += '/' + event_id
|
|
else:
|
|
getargs[query_param] = event_id
|
|
|
|
if action_name == 'remedy_incident_rest':
|
|
account_name = action_data.get('action.remedy_incident_rest.param.account', None)
|
|
if account_name:
|
|
logger.info('account for remedy_incident_rest command=%s', account_name)
|
|
getargs['account'] = account_name
|
|
else:
|
|
logger.error('Unable to get account. Account is required when fetching link for '
|
|
'remedy_incident_rest action')
|
|
|
|
res, content = splunk_rest.simpleRequest(
|
|
safeURLQuote(uri),
|
|
sessionKey=session_key,
|
|
getargs=getargs)
|
|
|
|
if res.status != 200:
|
|
logger.error(
|
|
'Failed to refresh notable event=%s\n'
|
|
'uri=`%s`, getargs=`%s` response=`%s` content=`%s`',
|
|
event_id, uri, getargs, res, content
|
|
)
|
|
raise Exception('Failed to refresh notable event=%s.' % event_id)
|
|
|
|
content = json.loads(content)
|
|
logger.info('uri=`%s` getargs=`%s` \n content=`%s`', uri, getargs, json.dumps(content))
|
|
|
|
# Extract refresh data from response. For this we will first walk the
|
|
# response content which is json.
|
|
|
|
json_path = action_config.get('refresh_response_json_path')
|
|
json_path = json_path.split('.')
|
|
|
|
pertinent = content # content containing the refresh values that we care about.
|
|
for e in json_path:
|
|
path_elem = JsonPathElement(e)
|
|
if not path_elem.is_array():
|
|
pertinent = pertinent.get(str(path_elem))
|
|
else:
|
|
idx = path_elem.get_array_index()
|
|
pertinent = pertinent[idx]
|
|
logger.debug('refresh response config=`%s`', json.dumps(pertinent))
|
|
|
|
# we have `pertinent` which should be the blob we care about...
|
|
ticket_id_key = action_config.get('refresh_response_ticket_id_key')
|
|
ticket_url_key = action_config.get('refresh_response_ticket_url_key')
|
|
|
|
ticket_system = action_config.get('ticket_system_name')
|
|
|
|
ticket_id = pertinent.get(ticket_id_key)
|
|
ticket_url = pertinent.get(ticket_url_key)
|
|
|
|
# check whether the smartIT URL is configured
|
|
if len(ticket_url) > 1:
|
|
ticket_url = ticket_url[1]
|
|
|
|
# we will always work with the assumption that there can be more than
|
|
# one ticket ID/URL pair for a given Notable Event. BMC's Remedy
|
|
# supports such a config. So, bottom line is, always work with a list.
|
|
|
|
if isinstance(ticket_id, itsi_py3.string_type):
|
|
ticket_id = [ticket_id]
|
|
if isinstance(ticket_url, itsi_py3.string_type):
|
|
ticket_url = [ticket_url]
|
|
|
|
if not isinstance(ticket_id, list) or not isinstance(ticket_url, list):
|
|
raise TypeError(
|
|
'Unable to fetch the ticket ID and URL to link the ticket. '
|
|
'Check the configuration of the action, the corresponding '
|
|
'add-on, and the status of your ticketing system.')
|
|
|
|
logger.debug('Refreshing ids=%s\nticket id=%s ticket url=%s.',
|
|
event_action_executed_on_ids, ticket_id, ticket_url)
|
|
|
|
for id_, url in zip(ticket_id, ticket_url):
|
|
ExternalTicket.bulk_upsert(
|
|
event_action_executed_on_ids,
|
|
ticket_system,
|
|
id_,
|
|
url,
|
|
session_key,
|
|
logger,
|
|
itsi_policy_id=itsi_policy_id,
|
|
action_dispatch_config=action_dispatch_config,
|
|
current_user_name=current_user_name
|
|
)
|
|
|
|
def get(self, ticket_system=None):
|
|
"""
|
|
Fetch ticket details for given `ticket_system`.
|
|
Set `ticket_system` to None to get all tickets for this event.
|
|
|
|
@type ticket_system: basestring
|
|
@param ticket_system: concerned ticket system.
|
|
Could be 'remedy', 'servicenow' or 'siebel' or 'jira' or 'bugzilla'
|
|
|
|
@rtype: basestring
|
|
@return: requested ticket info
|
|
"""
|
|
query_op = "$and"
|
|
query_val = []
|
|
|
|
query_val.append({ExternalTicket.KEY_EVENT_ID: self.event_id})
|
|
if ticket_system:
|
|
self.logger.debug('Requested tickets for ticket_system=%s', ticket_system)
|
|
query_val.append({ExternalTicket.KEY_TICKET_SYSTEM: ticket_system})
|
|
|
|
query = {query_op: query_val}
|
|
|
|
self.logger.debug('query=%s', json.dumps(query))
|
|
|
|
result = self.storage_interface.get_all(
|
|
self.master_session_key,
|
|
self.ns,
|
|
self.object_type,
|
|
filter_data=query,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
self.logger.debug('Storage result get: %s', result)
|
|
return result
|
|
|
|
@staticmethod
|
|
def bulk_upsert(event_ids, ticket_system, ticket_id, ticket_url, session_key, logger, itsi_policy_id=None,
|
|
action_dispatch_config=None, current_user_name=None, **kwargs):
|
|
"""
|
|
Do bulk upsert.
|
|
@param ticket_system: external ticket system's identifier Ex: remedy
|
|
@param ticket_id: external ticket's identifier
|
|
@param ticket_url: external ticket's URL
|
|
@param itsi_policy_id: policy ID associated with this ticket.
|
|
@param action_dispatch_config: the hybrid action dispatch configuration
|
|
@param kwargs: extra key-value args to add as part of our update.
|
|
@rtype: list
|
|
@returns: result in the order of execution.
|
|
"""
|
|
if not isinstance(event_ids, list):
|
|
raise TypeError('Invalid type event_ids. received="%s".' % type(event_ids).__name__)
|
|
|
|
result = []
|
|
activities = []
|
|
activity = 'Successfully linked with external ticket system="%s", ticket ID="%s", ticket URL="%s".' % (
|
|
ticket_system, ticket_id, ticket_url)
|
|
bulk_data = []
|
|
for e in event_ids:
|
|
obj = ExternalTicket(
|
|
e,
|
|
session_key,
|
|
logger,
|
|
action_dispatch_config=action_dispatch_config,
|
|
current_user_name=current_user_name
|
|
)
|
|
r = obj.upsert(
|
|
ticket_system,
|
|
ticket_id,
|
|
ticket_url,
|
|
itsi_policy_id=itsi_policy_id,
|
|
do_audit=False,
|
|
**kwargs
|
|
)
|
|
result.extend(r)
|
|
activities.append(activity)
|
|
bulk_data.append({ExternalTicket.KEY_EVENT_ID: e,
|
|
ExternalTicket.KEY_TICKET_SYSTEM: ticket_system,
|
|
ExternalTicket.KEY_TICKETS_TICKET_ID: ticket_id,
|
|
ExternalTicket.KEY_TICKETS_TICKET_URL: ticket_url,
|
|
ExternalTicket.KEY_POLICY_ID: itsi_policy_id
|
|
})
|
|
auditor = ExternalTicket.get_auditor(
|
|
session_key,
|
|
kwargs.get('audit_token_name', ExternalTicket.default_token_name)
|
|
)
|
|
auditor.send_activity_to_audit_bulk(bulk_data, activities, 'Linked external ticket')
|
|
return result
|
|
|
|
def upsert(self, ticket_system, ticket_id, ticket_url, itsi_policy_id=None, do_audit=True, **kwargs):
|
|
"""
|
|
Update event_id with external ticket information.
|
|
|
|
@param ticket_system: external ticket system's identifier Ex: remedy
|
|
@param ticket_id: external ticket's identifier
|
|
@param ticket_url: external ticket's URL
|
|
@param itsi_policy_id: policy ID associated with this ticket.
|
|
@param do_audit: activity will be passed to audit or not. defaults to True.
|
|
@param kwargs: extra key-value args to add as part of our update.
|
|
"""
|
|
# Validations
|
|
if not isinstance(ticket_system, itsi_py3.string_type):
|
|
raise TypeError('Expecting ticket_system to be str type.')
|
|
if not ticket_system.strip():
|
|
raise ValueError('Expecting ticket_system to be non-zero length str.')
|
|
if not isinstance(ticket_id, itsi_py3.string_type):
|
|
raise TypeError('Expecting ticket_id to be str type.'
|
|
' Received={}.'.format(type(ticket_id).__name__))
|
|
if not ticket_id.strip():
|
|
raise ValueError('Expecting ticket_id to be non-zero length str.'
|
|
' Received={}.'.format(ticket_id))
|
|
|
|
# first fetch existing ticket entry for given ticket_system
|
|
existing = self.get(ticket_system)
|
|
record = json.loads(existing) if isinstance(existing, itsi_py3.string_type) else existing
|
|
|
|
if not record:
|
|
# no existing ticket(s) for ticket_system
|
|
record = {
|
|
ExternalTicket.KEY_EVENT_ID: self.event_id,
|
|
ExternalTicket.KEY_UID: str(uuid1()),
|
|
ExternalTicket.KEY_TICKET_SYSTEM: ticket_system,
|
|
ExternalTicket.KEY_TICKETS: [{
|
|
ExternalTicket.KEY_TICKET_SYSTEM: ticket_system,
|
|
ExternalTicket.KEY_TICKETS_TICKET_ID: ticket_id,
|
|
ExternalTicket.KEY_TICKETS_TICKET_URL: ticket_url
|
|
}],
|
|
ExternalTicket.KEY_POLICY_ID: itsi_policy_id,
|
|
ExternalTicket.KEY_OBJECT_TYPE: ExternalTicket.VAL_OBJECT_TYPE,
|
|
ExternalTicket.KEY_TIME: time.time(),
|
|
ExternalTicket.KEY_CREATE_TIME: time.time()
|
|
}
|
|
record[ExternalTicket.KEY_TICKETS][0].update(kwargs)
|
|
else:
|
|
if len(record) > 1:
|
|
self.logger.warning('Expecting only 1 record for an "event_id" + '
|
|
'`ticket_system` combination. Received more. Will only work '
|
|
'with the first event')
|
|
record = record[0]
|
|
record[ExternalTicket.KEY_TIME] = time.time()
|
|
record[ExternalTicket.KEY_POLICY_ID] = itsi_policy_id
|
|
# event_id has tickets for given 'ticket_system'
|
|
tickets = record.get(ExternalTicket.KEY_TICKETS, [])
|
|
ticket_exists = False
|
|
for ticket in tickets:
|
|
if ticket['ticket_id'] == ticket_id.strip():
|
|
ticket_exists = True
|
|
ticket[ExternalTicket.KEY_TICKETS_TICKET_URL] = ticket_url
|
|
ticket.update(kwargs)
|
|
|
|
if not ticket_exists:
|
|
# no such ticket exists
|
|
ticket_val = {
|
|
ExternalTicket.KEY_TICKET_SYSTEM: ticket_system,
|
|
ExternalTicket.KEY_TICKETS_TICKET_ID: ticket_id,
|
|
ExternalTicket.KEY_TICKETS_TICKET_URL: ticket_url
|
|
}
|
|
ticket_val.update(kwargs)
|
|
tickets.append(ticket_val)
|
|
|
|
results = self.storage_interface.batch_save(
|
|
self.master_session_key,
|
|
self.ns,
|
|
[record],
|
|
objecttype=self.object_type,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
if do_audit:
|
|
activity = 'Successfully linked with external ticket system="%s", ticket ID="%s", ticket URL="%s".' % (ticket_system, ticket_id, ticket_url)
|
|
data = {
|
|
ExternalTicket.KEY_EVENT_ID: self.event_id,
|
|
ExternalTicket.KEY_TICKET_SYSTEM: ticket_system,
|
|
ExternalTicket.KEY_TICKETS_TICKET_ID: ticket_id,
|
|
ExternalTicket.KEY_TICKETS_TICKET_URL: ticket_url,
|
|
ExternalTicket.KEY_POLICY_ID: itsi_policy_id
|
|
}
|
|
self.auditor.send_activity_to_audit(data, activity, 'Linked external ticket')
|
|
return results
|
|
|
|
@staticmethod
|
|
def bulk_delete(event_ids, ticket_system, ticket_id, session_key, logger, action_dispatch_config=None, current_user_name=None):
|
|
"""
|
|
Do bulk delete.
|
|
@param ticket_system: external ticket system's identifier Ex: remedy
|
|
@param ticket_id: external ticket's identifier
|
|
@param action_dispatch_config: the hybrid action dispatch configuration
|
|
@param kwargs: extra key-value args to add as part of our update.
|
|
@rtype: list
|
|
@returns: result in the order of execution.
|
|
"""
|
|
if not isinstance(event_ids, list):
|
|
raise TypeError('Invalid type event_ids. received="%s".' % type(event_ids).__name__)
|
|
|
|
for e in event_ids:
|
|
try:
|
|
obj = ExternalTicket(
|
|
e, session_key, logger,
|
|
action_dispatch_config=action_dispatch_config,
|
|
current_user_name=current_user_name)
|
|
obj.delete(ticket_system, ticket_id)
|
|
except ValueError as e:
|
|
logger.error('Bulk delete Exception: %s', e)
|
|
pass # don't break the loop if some event didn't match
|
|
|
|
def delete(self, ticket_system=None, ticket_id=None):
|
|
"""
|
|
Delete ticket corresponding to ticket system and ticket id
|
|
/snow/123 delete ticket with id 123 of ticket system snow
|
|
/snow delete all tickets of snow
|
|
/ delete all tickets for this event
|
|
|
|
@param ticket_system: external ticket system's identifier Ex: remedy
|
|
@param ticket_id: external ticket's identifier
|
|
"""
|
|
query_op = "$and"
|
|
query_val = []
|
|
query_val.append({ExternalTicket.KEY_EVENT_ID: self.event_id})
|
|
|
|
# if no ticket_system is passed delete all the tickets
|
|
if not ticket_system:
|
|
query = {query_op: query_val}
|
|
result = self.storage_interface.delete_all(
|
|
self.master_session_key,
|
|
self.ns,
|
|
self.object_type,
|
|
query,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
self.logger.debug('Successfully deleted all tickets for event_id=%s', self.event_id)
|
|
return
|
|
|
|
# retrieve list for a given ticket system
|
|
query_val.append({ExternalTicket.KEY_TICKET_SYSTEM: ticket_system})
|
|
query = {query_op: query_val}
|
|
|
|
# if ticket id is not passed delete all the tickets for the ticket system
|
|
if not ticket_id:
|
|
result = self.storage_interface.delete_all(
|
|
self.master_session_key,
|
|
self.ns,
|
|
self.object_type,
|
|
query,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
self.logger.debug('Successfully deleted all tickets for ticket_system=%s', ticket_system)
|
|
return
|
|
|
|
# if ticket_id is passed retrieve the list of tickets for a ticket system
|
|
# iterate through list for a ticket id and delete
|
|
result = self.storage_interface.get_all(
|
|
self.master_session_key,
|
|
self.ns,
|
|
self.object_type,
|
|
filter_data=query,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
if isinstance(result, itsi_py3.string_type):
|
|
result = json.loads(result)
|
|
if len(result) == 0:
|
|
self.logger.error('Could not find ticket system. ticket_system=%s', ticket_system)
|
|
raise ValueError('Failed to delete ticket. Ticketing system does not exist or has been deleted already.')
|
|
|
|
record = result[0]
|
|
tickets = record.get(ExternalTicket.KEY_TICKETS, [])
|
|
|
|
number_of_tickets = len(tickets)
|
|
if number_of_tickets == 0:
|
|
self.logger.error('Could not find ticket. ticket_id=%s ', ticket_id)
|
|
raise ValueError('Failed to delete ticket. Ticket does not exist or has been deleted already.')
|
|
|
|
# searching for the ticket in the tickets list by passed in ticket id
|
|
ticket_exists = False
|
|
for ticket in tickets:
|
|
if ticket['ticket_id'] == ticket_id.strip():
|
|
# found ticket. Need to remove ticket from tickets list
|
|
ticket_exists = True
|
|
tickets.remove(ticket)
|
|
self.logger.debug('Deleting. ticket_id=%s details=%s ',
|
|
ticket_id, json.dumps(ticket))
|
|
|
|
if not ticket_exists:
|
|
self.logger.error('Could not find ticket. ticket_id=%s ', ticket_id)
|
|
raise ValueError('Failed to delete ticket. Unable to find ticket with ID {}.'.format(ticket_id))
|
|
|
|
# if there is only one ticket for a ticket system delete the entire ticket system
|
|
if number_of_tickets == 1:
|
|
self.logger.debug(('Successfully deleted single ticket with ticket_id=%s') % ticket_id)
|
|
result = self.storage_interface.delete(
|
|
self.master_session_key,
|
|
self.ns,
|
|
self.object_type,
|
|
record.get(ExternalTicket.KEY_UID),
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
return
|
|
|
|
# if there are more than one tickets
|
|
# update the record with updated ticket array
|
|
record[ExternalTicket.KEY_TICKETS] = tickets
|
|
self.storage_interface.batch_save(
|
|
self.master_session_key,
|
|
self.ns,
|
|
result,
|
|
objecttype=self.object_type,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri
|
|
)
|
|
self.logger.debug('Successfully deleted ticket_id=%s record=%s' , ticket_id, record)
|
|
|
|
def bulk_get_info(self, **kwargs):
|
|
"""
|
|
Get relevant ticket info
|
|
@param kwargs: extra key-value args to add as part of our fetch.
|
|
@rtype: list
|
|
@returns: results unique by ticket_id
|
|
"""
|
|
def _format_results(results):
|
|
tickets = []
|
|
for result in results:
|
|
for ticket in result[ExternalTicket.KEY_TICKETS]:
|
|
tickets.append({
|
|
'text': ticket[ExternalTicket.KEY_TICKETS_TICKET_ID],
|
|
'id': ticket[ExternalTicket.KEY_TICKETS_TICKET_ID],
|
|
'ticket_system': ticket[ExternalTicket.KEY_TICKET_SYSTEM],
|
|
'group_id': result[ExternalTicket.KEY_EVENT_ID]
|
|
})
|
|
return tickets
|
|
|
|
query_op = '$and'
|
|
query_val = []
|
|
|
|
offset = kwargs.get('offset', 0)
|
|
sort_key = kwargs.get('sort_key', 'mod_time')
|
|
sort_dir = kwargs.get('sort_dir', 'desc')
|
|
filter_string = kwargs.get('filter_string', None)
|
|
is_get_summary = kwargs.get('is_get_summary', False)
|
|
count = kwargs.get('count', 20)
|
|
|
|
if filter_string and len(filter_string) != 0:
|
|
query = ExternalTicket.get_kvstore_filter(filter_string, self.logger)
|
|
else:
|
|
query_val.append({ExternalTicket.KEY_OBJECT_TYPE: ExternalTicket.VAL_OBJECT_TYPE})
|
|
query = {query_op: query_val}
|
|
|
|
if not query:
|
|
raise ValueError('Invalid query={}, Received filter_string={}.'
|
|
'Special characters other than `-` and `_` are not allowed.'.format(query, filter_string))
|
|
|
|
try:
|
|
if is_get_summary:
|
|
results = self.storage_interface.get_count(
|
|
session_key=self.master_session_key,
|
|
owner=self.ns,
|
|
object_type=self.object_type,
|
|
filter_data=query,
|
|
host_base_uri=self.host_base_uri,
|
|
)
|
|
else:
|
|
results = self.storage_interface.get_all(
|
|
session_key=self.master_session_key,
|
|
owner=self.ns,
|
|
objecttype=self.object_type,
|
|
current_user_name=self.current_user_name,
|
|
host_base_uri=self.host_base_uri,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir,
|
|
skip=offset,
|
|
limit=count,
|
|
filter_data=query
|
|
)
|
|
results = _format_results(results)
|
|
return results
|
|
except Exception as e:
|
|
self.logger.error('Bulk get info Exception: %s', e)
|