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.
204 lines
6.8 KiB
204 lines
6.8 KiB
# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
|
|
'''
|
|
This module handles backfill requests
|
|
'''
|
|
import copy
|
|
import sys
|
|
from itsi_py3 import _
|
|
|
|
from splunk.clilib.bundle_paths import make_splunkhome_path
|
|
|
|
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 SA_ITOA_app_common.splunklib import results
|
|
|
|
from itsi.itsi_utils import ITOAInterfaceUtils
|
|
|
|
from ITOA.storage.itoa_generic_persistables import ItoaGenericCrudException, ItoaGenericModel, ItoaGenericCollection
|
|
from ITOA.setup_logging import logger as LOGGER
|
|
|
|
|
|
class BackfillRequestModel(ItoaGenericModel):
|
|
"""
|
|
Backfill request model class. Can be instantiated from data, or from an object
|
|
_key (in which case it is auto-fetched from the server). Supports basic CRUD
|
|
operations.
|
|
|
|
The following fields are defined:
|
|
|
|
- status: one of ['new', 'pending', 'running', 'done', 'failed', 'cancelled']
|
|
- search
|
|
- earliest
|
|
- latest
|
|
- kpi_id
|
|
- kpi_title
|
|
- job_progress: array of <nchunk> job_metadata objects.
|
|
This field created when job is added to the queue.
|
|
job_metadata objects are dicts with keys:
|
|
('et', 'lt', 'num', 'tot', 'sid', 'job_status', 'retries_left')
|
|
- t_start [not present in new/pending state]
|
|
- t_finish [not present before completion]
|
|
|
|
'status', 'search', 'earliest', 'latest', and 'kpi_id' fields are required on request creation.
|
|
"""
|
|
|
|
backing_collection = "itsi_backfill"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.set_logger(LOGGER)
|
|
super(BackfillRequestModel, self).__init__(*args, **kwargs)
|
|
|
|
def __getitem__(self, key):
|
|
return self.data.get(key, None)
|
|
|
|
def __setitem__(self, key, item):
|
|
self.data[key] = item
|
|
|
|
def get(self, key, default=None):
|
|
'''
|
|
Get the attribute from the data dict
|
|
Inherited from ItoaGenericModel
|
|
@param key: String key for the attribute
|
|
@param default: Default value if the value is not in the dict
|
|
'''
|
|
return self.data.get(key, default)
|
|
|
|
@property
|
|
def id_(self):
|
|
'''
|
|
Get the identifying attribute
|
|
or None if not present.
|
|
'''
|
|
return self.data.get("_key", None)
|
|
|
|
@property
|
|
def earliest(self):
|
|
'''
|
|
Return the value of the earliest attribute
|
|
case as an int
|
|
Throws an exception if none
|
|
'''
|
|
return int(self.get("earliest"))
|
|
|
|
@property
|
|
def latest(self):
|
|
'''
|
|
Return the value of the earliest attribute
|
|
case as an int
|
|
Throws an exception if none
|
|
'''
|
|
return int(self.get("latest"))
|
|
|
|
@property
|
|
def job_progress(self):
|
|
"""
|
|
Get job progress array. We need to ensure that index fields (such as `num` or `tot)
|
|
and epoch fields (`et`, `lt`) get cast to integers. Note that we return a copy of the
|
|
job_progress array so that non-JSON serializable objects can be added to this
|
|
data structure by the clients without affecting this model.
|
|
"""
|
|
jobs = self.get("job_progress", [])
|
|
for j in jobs:
|
|
j['et'] = int(j['et'])
|
|
j['lt'] = int(j['lt'])
|
|
j['num'] = int(j['num'])
|
|
j['tot'] = int(j['tot'])
|
|
j['retries_left'] = int(j['retries_left'])
|
|
return [copy.copy(j) for j in jobs]
|
|
|
|
def get_job_chunk(self, num):
|
|
"""
|
|
Get job chunk from job process array; this method helps avoid 0/1 indexing confusion
|
|
"""
|
|
return self.job_progress[num - 1]
|
|
|
|
def update_job_progress(self, job_num, data):
|
|
"""
|
|
Update progress for a job chunk
|
|
|
|
@param job_num: job chunk number [1..total_jobs]
|
|
@type: int
|
|
|
|
@param data: fields to update
|
|
@type: dict
|
|
"""
|
|
if job_num > len(self.job_progress):
|
|
raise ItoaGenericCrudException("Cannot update job progress")
|
|
self.get('job_progress')[job_num - 1].update(data)
|
|
self.save()
|
|
|
|
def validate_data(self):
|
|
"""
|
|
Model validation method. Checks that the model has the minimal set of required keys.
|
|
"""
|
|
data = self.data
|
|
required_keys = set(['status', 'search', 'earliest', 'latest', 'kpi_id'])
|
|
valid = required_keys.issubset(set(data.keys()))
|
|
if not valid:
|
|
message = _("Required keys %s are missing from model data") % (required_keys - set(data.keys()))
|
|
message += _("Got the following for data: %s") % data
|
|
self.logger.error(message)
|
|
raise ItoaGenericCrudException(message)
|
|
return valid
|
|
|
|
def is_backfillable(self):
|
|
"""
|
|
Runs a search to determine whether this model should be backfilled or not.
|
|
|
|
@returns: True if the model can be backfilled, False otherwise
|
|
@rtype bool
|
|
"""
|
|
kpi_id = self.get('kpi_id')
|
|
search = 'search `get_full_itsi_summary_kpi(%s)` | tail 1 | eval earliestTime=_time' % kpi_id
|
|
|
|
try:
|
|
service = ITOAInterfaceUtils.service_connection(self.interface._session_key, app_name='SA-ITOA')
|
|
search_job = service.jobs.oneshot(search, output_mode='json')
|
|
reader = results.JSONResultsReader(search_job)
|
|
search_results = [result for result in reader if isinstance(result, dict)]
|
|
except Exception as e:
|
|
self.logger.error('Error while backfilling KPI %s: %s', kpi_id, e)
|
|
return False
|
|
if len(search_results) == 0:
|
|
return True
|
|
|
|
result = search_results[0]
|
|
if result is None:
|
|
return True
|
|
|
|
earliest_kpi_time = int(float(result.get('earliestTime'))) # epoch_time could be in decimal
|
|
earliest_epoch_time = self.get('earliest')
|
|
if earliest_kpi_time < earliest_epoch_time:
|
|
# no need to backfill
|
|
return False
|
|
try:
|
|
alert_period = int(self.get('alert_period'))
|
|
except (TypeError, ValueError):
|
|
# assume that the latest time has alert_period already accounted for
|
|
return True
|
|
|
|
# set latest time for backfill to earliest time in summary index
|
|
latest_epoch_time = earliest_kpi_time - (alert_period * 60) - 1
|
|
self.update({
|
|
'latest': latest_epoch_time
|
|
})
|
|
|
|
return True
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
class BackfillRequestCollection(ItoaGenericCollection):
|
|
"""
|
|
Backfill request collection class. Supports bulk fetch, save, and delete
|
|
operations on request objects. Implements an iterable interface over
|
|
`Request` objects.
|
|
"""
|
|
|
|
backing_collection = "itsi_backfill"
|
|
model_class = BackfillRequestModel
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.set_logger(LOGGER)
|
|
super(BackfillRequestCollection, self).__init__(*args, **kwargs)
|