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.
433 lines
17 KiB
433 lines
17 KiB
# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved.
|
|
|
|
from itsi.content_packs.acl import AccessConfigurator
|
|
from itsi.content_packs.backfiller import ContentObjectsBackfiller
|
|
from itsi.content_packs.conflicts import ConflictResolver
|
|
from itsi.content_packs.preview import ContentPackPreviewer
|
|
from itsi.content_packs.constants import (
|
|
CONTENT_PACK_SOURCE_FIELD,
|
|
CONTENT_PACK_SOURCE_ID_FIELD,
|
|
CONTENT_PACK_SOURCE_VERSION_FIELD,
|
|
ContentType,
|
|
ContentPackInstallOptions
|
|
)
|
|
from itsi.content_packs.itoa import get_itoa_identifier_fields
|
|
from itsi.content_packs.enabler import ContentObjectsEnabler
|
|
from itsi.content_packs.entitlements_processor import ContentPackEntitlementsProcessor
|
|
from itsi.content_packs.journal import TransactionJournal
|
|
from itsi.content_packs.readers import ContentPackContentReader
|
|
from itsi.content_packs.prefixer import ContentObjectsPrefixer
|
|
from itsi.content_packs.retriever import retrieve_one
|
|
from itsi.content_packs.writers import ItoaContentWriter
|
|
from itsi.itsi_utils import ITOAInterfaceUtils
|
|
from itsi.objects.itsi_content_pack_saved_search_status import ItsiContentPackSavedSearchStatus
|
|
from itsi.objects.itsi_content_pack_status import ItsiContentPackStatus
|
|
from ITOA.saved_search_utility import APP_FILTER_PREFIX, SavedSearch
|
|
from ITOA.setup_logging import setup_logging, InstrumentCall
|
|
|
|
LOGGER = setup_logging(
|
|
logfile_name='itsi_content_packs_install.log',
|
|
logger_name='itsi.content_packs.install'
|
|
)
|
|
|
|
|
|
def install(content_pack_id, version, session_key=None, options={}):
|
|
"""
|
|
Installs a content pack based for the given content pack id and version.
|
|
|
|
:param content_pack_id: the content pack id
|
|
:type content_pack_id: str
|
|
|
|
:param version: the content pack version
|
|
:type version: str
|
|
|
|
:param session_key: the session key
|
|
:type session_key: str
|
|
|
|
:param options: various parameters to determine how content pack objects are installed
|
|
:type options: dict
|
|
|
|
:return: a journal of the install results
|
|
:rtype: TransactionJournal
|
|
"""
|
|
_instrumentation = InstrumentCall(LOGGER)
|
|
|
|
with _instrumentation.track('itsi.content_packs.installer.install') as transaction_id:
|
|
LOGGER.info(f'Installation of Content Pack with content_pack_id={content_pack_id} is started with tid={transaction_id}')
|
|
try:
|
|
content_pack_name = retrieve_one(content_pack_id, version, session_key)['title']
|
|
search_page_url = f'app/itsi/search?q=search%20index%3D_internal%20tid%3D{transaction_id}&display.page.search.mode' \
|
|
'=smart&dispatch.sample_ratio=1&workload_pool=&earliest=-24h%40h&latest=now'
|
|
status_message = f'To track installation progress of {content_pack_name}, [[{search_page_url}|Check the logs.]]'
|
|
ITOAInterfaceUtils.create_message(
|
|
session_key,
|
|
status_message,
|
|
name=transaction_id,
|
|
role='itoa_admin'
|
|
)
|
|
except Exception as e:
|
|
LOGGER.info(f'Unable to add Splunk message due to error: {e}')
|
|
|
|
if options.get(ContentPackInstallOptions.INSTALL_ALL):
|
|
previewer = ContentPackPreviewer(
|
|
logger=LOGGER,
|
|
session_key=session_key)
|
|
content_filter_data = previewer.format_preview_objects(content_pack_id, version)
|
|
else:
|
|
content_filter_data = options.get(ContentPackInstallOptions.CONTENT, {})
|
|
|
|
LOGGER.info(f'tid={transaction_id} Filtered list passed for installation is {content_filter_data}')
|
|
|
|
installer = ContentPackInstaller(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
processors=[
|
|
ContentObjectsFilter(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
content_filter=content_filter_data,
|
|
transaction_id=transaction_id
|
|
),
|
|
ContentObjectsPrefixer(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
prefix=options.get(ContentPackInstallOptions.PREFIX, ''),
|
|
transaction_id=transaction_id
|
|
),
|
|
ConflictResolver(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
transaction_id=transaction_id
|
|
),
|
|
ContentObjectsEnabler(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
enabled=options.get(ContentPackInstallOptions.ENABLED, False),
|
|
transaction_id=transaction_id
|
|
),
|
|
ContentPackEntitlementsProcessor(
|
|
content_pack_id=content_pack_id,
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
transaction_id=transaction_id
|
|
),
|
|
ContentObjectsBackfiller(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
backfill=options.get(ContentPackInstallOptions.BACKFILL, True),
|
|
transaction_id=transaction_id,
|
|
backfill_timerange=options.get(ContentPackInstallOptions.BACKFILL_TIMERANGE, '-7d')
|
|
),
|
|
AccessConfigurator(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
transaction_id=transaction_id
|
|
),
|
|
ContentSourceAnnotater(
|
|
logger=LOGGER,
|
|
session_key=session_key,
|
|
content_pack_id=content_pack_id,
|
|
version=version,
|
|
transaction_id=transaction_id
|
|
)
|
|
],
|
|
transaction_id=transaction_id
|
|
)
|
|
return installer.install(content_pack_id, version)
|
|
|
|
|
|
class ContentPackInstaller(object):
|
|
"""This class is responsible for installing content packs."""
|
|
|
|
def __init__(self, logger, session_key, processors=None, transaction_id=None):
|
|
"""
|
|
:param logger: a logger instance
|
|
:type logger: Logger
|
|
|
|
:param session_key: the session key
|
|
:type session_key: str
|
|
|
|
:param processors: a list of objects to process content pack data
|
|
:type processors: list
|
|
|
|
:param transaction_id: transaction info for tracking for debugging
|
|
:type transaction_id: basestring
|
|
"""
|
|
self.logger = logger
|
|
self.session_key = session_key
|
|
self.processors = processors if processors else []
|
|
self._instrumentation = InstrumentCall(logger)
|
|
self.transaction_id = transaction_id
|
|
|
|
self.reader = ContentPackContentReader(
|
|
logger=logger,
|
|
session_key=session_key
|
|
)
|
|
|
|
self.writer = ItoaContentWriter(
|
|
logger=logger,
|
|
session_key=session_key,
|
|
transaction_id=transaction_id
|
|
)
|
|
|
|
def install(self, content_pack_id, version):
|
|
"""
|
|
Installs a content pack based for the given content pack id and version.
|
|
|
|
:param content_pack_id: the content pack version id
|
|
:type content_pack_id: str
|
|
|
|
:param version: the content pack version
|
|
:type version: str
|
|
|
|
:return: a journal of the install results
|
|
:rtype: TransactionJournal
|
|
"""
|
|
with self._instrumentation.track("ContentPackInstaller.install", transaction_id=self.transaction_id):
|
|
journal = TransactionJournal()
|
|
|
|
content_objects = self.reader.read(content_pack_id, version, journal=journal)
|
|
|
|
processed_objects = self.process_objects(content_objects, journal=journal)
|
|
|
|
# This will contain complete entry of each object that are successfully processed.
|
|
self.logger.debug(f'tid={self.transaction_id} Processed objects passed for installation are {processed_objects}')
|
|
|
|
written_objects = self.writer.write(processed_objects, journal=journal)
|
|
|
|
# Only update status collection if objects actually get installed
|
|
if written_objects and any(itsi_object not in (ContentType.GLASS_TABLE_IMAGE, ContentType.GLASS_TABLE_ICON) for itsi_object in written_objects):
|
|
ItsiContentPackStatus(self.session_key, 'nobody').update_content_pack_status(content_pack_id, version)
|
|
|
|
journal.success(written_objects)
|
|
|
|
return journal, self.transaction_id
|
|
|
|
def process_objects(self, content_objects, journal):
|
|
"""
|
|
Processes and filters the given content objects before the objects are saved.
|
|
|
|
:param content_objects: the content objects separated by content type
|
|
:type content_objects: dict
|
|
|
|
:param journal: a journal instance
|
|
:type journal: TransactionJournal
|
|
|
|
:return: the filtered and processed content objects
|
|
:rtype: dict
|
|
"""
|
|
processed_objects = content_objects
|
|
|
|
for processor in self.processors:
|
|
processed_objects = processor.process_objects(processed_objects, journal=journal)
|
|
|
|
return processed_objects
|
|
|
|
|
|
class ContentObjectsFilter(object):
|
|
"""Includes content objects based on the provided filter."""
|
|
|
|
def __init__(self, logger, session_key, content_filter, transaction_id=None):
|
|
"""
|
|
:param logger: a logger instance
|
|
:type logger: Logger
|
|
|
|
:param session_key: the session key
|
|
:type session_key: str
|
|
|
|
:param content_filter: the objects filter
|
|
:type content_filter: dict
|
|
|
|
:param transaction_id: transaction info for tracking for debugging
|
|
:type transaction_id: basestring
|
|
"""
|
|
self.logger = logger
|
|
self.session_key = session_key
|
|
self.content_filter = content_filter or {}
|
|
self._instrumentation = InstrumentCall(logger)
|
|
self.transaction_id = transaction_id
|
|
|
|
def process_objects(self, content_objects, **kwargs):
|
|
"""
|
|
Employs the content filter on the given content objects.
|
|
|
|
:param content_objects: the content objects data
|
|
:type content_objects: dict
|
|
|
|
:return: the content objects data
|
|
:rtype: dict
|
|
"""
|
|
with self._instrumentation.track('ContentObjectsFilter.process_objects', transaction_id=self.transaction_id):
|
|
if not self.content_filter:
|
|
return content_objects
|
|
|
|
processed_objects = {}
|
|
|
|
for content_type, objects in content_objects.items():
|
|
content_type_filter = set(self.content_filter.get(content_type, []))
|
|
|
|
identifier = get_itoa_identifier_fields(content_type)[0]
|
|
|
|
filtered_objects = filter(
|
|
# Excluding Glass Table Images and Glass Table Icons from filtering as they are not available for user selection
|
|
lambda obj: obj[identifier] in content_type_filter or content_type == ContentType.GLASS_TABLE_IMAGE or content_type == ContentType.GLASS_TABLE_ICON,
|
|
objects
|
|
)
|
|
processed_objects[content_type] = list(filtered_objects)
|
|
|
|
return processed_objects
|
|
|
|
|
|
class ContentSourceAnnotater(object):
|
|
"""Annotates the objects with metadata about the content pack."""
|
|
|
|
def __init__(self, logger, session_key, content_pack_id, version, transaction_id=None):
|
|
"""
|
|
:param logger: a logger instance
|
|
:type logger: Logger
|
|
|
|
:param session_key: the session key
|
|
:type session_key: str
|
|
|
|
:param content_pack_id: the content pack id
|
|
:type content_pack_id: str
|
|
|
|
:param version: the content pack version
|
|
:type version: str
|
|
|
|
:param transaction_id: transaction info for tracking for debugging
|
|
:type transaction_id: basestring
|
|
"""
|
|
self.logger = logger
|
|
self.session_key = session_key
|
|
self.content_pack_id = content_pack_id
|
|
self.version = version
|
|
self._instrumentation = InstrumentCall(logger)
|
|
self.transaction_id = transaction_id
|
|
|
|
def process_objects(self, content_objects, **kwargs):
|
|
"""
|
|
Annotates the objects with metadata about the content pack.
|
|
|
|
:param content_objects: the content objects data
|
|
:type content_objects: dict
|
|
|
|
:return: the content objects data
|
|
:rtype: dict
|
|
"""
|
|
with self._instrumentation.track('ContentSourceAnnotater.process_objects', transaction_id=self.transaction_id):
|
|
for content_type, objects in content_objects.items():
|
|
for obj in objects:
|
|
obj[CONTENT_PACK_SOURCE_FIELD] = self.content_pack_id
|
|
obj[CONTENT_PACK_SOURCE_ID_FIELD] = obj['_key']
|
|
obj[CONTENT_PACK_SOURCE_VERSION_FIELD] = self.version
|
|
|
|
return content_objects
|
|
|
|
|
|
def update_saved_searches_status(session_key, content_pack_id, action):
|
|
"""
|
|
Enables or disables all the savedsearches of contentpack based on
|
|
saved_search_action value in request body. This function also update
|
|
the count of savedsearches in collection `itsi_content_pack_saved_search_status`
|
|
after enabling or disabling savedsearches for given content_pack_id.
|
|
If any other value besides "enable" or "disable" is provided
|
|
to the saved_search_action, then it is treated as "retain_status".
|
|
|
|
:param session_key: the session key
|
|
:type session_key: str
|
|
|
|
:param content_pack_id: the content pack id
|
|
:type content_pack_id: str
|
|
|
|
:param action: action to perform on the saved searches (enable/ disable/
|
|
retain_status). In case of no value or incorrect values,
|
|
retain_status is used
|
|
:type options: str
|
|
|
|
:return: updated savedsearch status
|
|
:rtype: dict
|
|
"""
|
|
updated_searches = []
|
|
failed_searches = []
|
|
saved_searches_obj = {
|
|
'total': 0,
|
|
'enabled': 0,
|
|
'disabled': 0
|
|
}
|
|
if not (action == ContentPackInstallOptions.SAVED_SEARCH_ENABLE
|
|
or action == ContentPackInstallOptions.SAVED_SEARCH_DISABLE):
|
|
saved_searches = {
|
|
'action_performed': ContentPackInstallOptions.SAVED_SEARCH_RETAIN_STATUS,
|
|
'success': updated_searches,
|
|
'failure': failed_searches
|
|
}
|
|
return saved_searches
|
|
|
|
try:
|
|
cp_saved_searches = SavedSearch.get_all_searches(
|
|
session_key=session_key,
|
|
namespace='-',
|
|
search=APP_FILTER_PREFIX + content_pack_id
|
|
)
|
|
except Exception as reason:
|
|
raise Exception('Failed to retrieve saved searches of the content pack {content_pack_id}.\
|
|
ERROR {reason}'.format(content_pack_id=content_pack_id, reason=reason))
|
|
|
|
if len(cp_saved_searches) > 0:
|
|
search_param = {'disabled': '1'} if action \
|
|
== ContentPackInstallOptions.SAVED_SEARCH_DISABLE else {'disabled': '0'}
|
|
for search in cp_saved_searches:
|
|
if (search.get('disabled').strip() == '1'
|
|
and action == ContentPackInstallOptions.SAVED_SEARCH_ENABLE) or \
|
|
(search.get('disabled').strip() == '0'
|
|
and action == ContentPackInstallOptions.SAVED_SEARCH_DISABLE):
|
|
try:
|
|
SavedSearch.update_search(
|
|
session_key,
|
|
search.name,
|
|
namespace=content_pack_id,
|
|
**search_param
|
|
)
|
|
if action == ContentPackInstallOptions.SAVED_SEARCH_ENABLE:
|
|
saved_searches_obj['enabled'] = saved_searches_obj['enabled'] + 1
|
|
else:
|
|
saved_searches_obj['disabled'] = saved_searches_obj['disabled'] + 1
|
|
updated_searches.append(search.name)
|
|
LOGGER.info('Operation "{action}" performed on saved search "{name}" is successful'
|
|
.format(action=action, name=search.name))
|
|
except Exception as reason:
|
|
failed_searches.append({'name': search.name, 'error_message': str(reason)})
|
|
if search.get('disabled').strip() == '0':
|
|
saved_searches_obj['enabled'] = saved_searches_obj['enabled'] + 1
|
|
else:
|
|
saved_searches_obj['disabled'] = saved_searches_obj['disabled'] + 1
|
|
LOGGER.exception(
|
|
'Operation "{action}" on saved search "{name}" has failed due to "{reason}"'
|
|
.format(action=action, name=search.name, reason=reason)
|
|
)
|
|
else:
|
|
updated_searches.append(search.name)
|
|
if action == ContentPackInstallOptions.SAVED_SEARCH_ENABLE:
|
|
saved_searches_obj['enabled'] = saved_searches_obj['enabled'] + 1
|
|
else:
|
|
saved_searches_obj['disabled'] = saved_searches_obj['disabled'] + 1
|
|
LOGGER.info('Saved search "{name}" is already on state "{action}"'
|
|
.format(name=search.name, action=action))
|
|
|
|
saved_searches_obj['total'] = saved_searches_obj['enabled'] + saved_searches_obj['disabled']
|
|
|
|
# update saved_searches count in collection itsi_content_pack_saved_search_status for content pack.
|
|
ItsiContentPackSavedSearchStatus(session_key, 'nobody').update_content_pack_saved_search_status(
|
|
content_pack_id,
|
|
saved_searches_obj['total'],
|
|
saved_searches_obj['enabled'],
|
|
saved_searches_obj['disabled']
|
|
)
|
|
saved_searches = {
|
|
'action_performed': action,
|
|
'success': updated_searches,
|
|
'failure': failed_searches
|
|
}
|
|
return saved_searches
|