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.

1179 lines
50 KiB

import sys
import splunk.safe_lxml_etree as et
import logging
import time
import json
from io import BytesIO
from future.moves.urllib.parse import unquote
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph
import re
import base64
import splunk
import splunk.rest as rest
import splunk.entity as entity
import splunk.auth
import splunk.clilib.cli_common as cli_common
import splunk.models.dashboard as sm_dashboard
import splunk.models.dashboard_panel as sm_dashboard_panel
import splunk.models.saved_search as sm_saved_search
import splunk.search
import splunk.search.searchUtils
from splunk.util import normalizeBoolean, toDefaultStrings
import mimetypes
import splunk.pdf.pdfgen_views as pv
import splunk.pdf.pdfgen_utils as pu
import splunk.pdf.pdfgen_chart as pc
import splunk.pdf.pdfgen_table as pt
from splunk.pdf.studio.pdfgen_studio import convertDashboardToPdfContent
import splunk.pdf.image_utils as imageUtils
import splunk.pdf.pdfrenderer as pdfrenderer
from builtins import range, filter
from gettext import gettext as _
logger = pu.getLogger()
ERROR_MSG = _("Unable to render PDF.")
DEFAULT_FEED_COUNT = 1000
DEFAULT_TIMEOUT = 3600 # in seconds
DEFAULT_CHROMIUM_TIMEOUT = 30 # in seconds
DEFAULT_CHROMIUM_SCREENSHOT_DELAY = 0 # in seconds
DEFAULT_MAX_ROWS_PER_TABLE = 1000
DEFAULT_INCLUDE_SPLUNK_LOGO = True
DEFAULT_PAPER_SIZE = 'letter'
DEFAULT_PAPER_ORIENTATION = 'portrait'
def studio_catchall_error_msg(dashboard, output_format):
return f'Unable to render the dashboard {dashboard} to {output_format}. Check python.log and pdfgen.log for errors.'
# Change the default lxml parsing to not allow imported entities
import splunk.safe_lxml_etree
class ArgError(Exception):
def __init__(self, message):
super(ArgError, self).__init__(message)
def __str__(self):
return repr(self.value)
class TimeoutError(Exception):
def __str__(self):
return repr(self.value)
class PDFGenHandler(splunk.rest.BaseRestHandler):
_args = {}
_deletePdfFileAfterSettingResponse = True
_timeoutTime = None
_views = []
_inputSids = {}
_title = "Untitled"
_description = ""
_dashboardContent = None
_pdfContent = None
_dashboardName = None
_dashboardXml = None
_reportName = None
_viewType = None
_paperSize = DEFAULT_PAPER_SIZE
_namespace = None
_owner = None
_timeoutDuration = DEFAULT_TIMEOUT
_chromiumTimeoutDuration = DEFAULT_CHROMIUM_TIMEOUT
_chromiumScreenshotDelay = DEFAULT_CHROMIUM_SCREENSHOT_DELAY
_maxRowsPerTable = DEFAULT_MAX_ROWS_PER_TABLE
_includeSplunkLogo = DEFAULT_INCLUDE_SPLUNK_LOGO
_cidFontList = None
_now = None
_timestampStr = ""
_searchStr = None
_et = ''
_lt = ''
_inactiveViews = None
_fileNamePattern = None
_enableInsecurePdfgen = False
_resultId = -1
_fileFormat = 'pdf'
_touchSearchJobsLastTime = None
_pdfBuffer = None
_pdfRenderer = None
_studioErrors = []
_pdfSettings = {
pdfrenderer.SETTING_FOOTER_ENABLED : '1',
pdfrenderer.SETTING_FOOTER_ENABLED : '1',
pdfrenderer.SETTING_HEADER_CENTER : 'description',
pdfrenderer.SETTING_FOOTER_LEFT : 'logo',
pdfrenderer.SETTING_FOOTER_CENTER : 'title',
pdfrenderer.SETTING_FOOTER_RIGHT : 'timestamp,pagination',
pdfrenderer.SETTING_HTML_IMAGE_RENDERING : '1'
}
_requestSettings = {
'pdf' : {},
'server' : { 'sslRootCAPath' : ''}
}
VIEW_TYPE_DASHBOARD = 'dashboard'
VIEW_TYPE_REPORT = 'report'
VIEW_TYPE_SEARCH = 'search'
ARG_INPUT_DASHBOARD = 'input-dashboard'
ARG_INPUT_DASHBOARD_XML = 'input-dashboard-xml'
ARG_INPUT_REPORT = 'input-report'
ARG_INPUT_SEARCH = 'input-search'
ARG_INPUT_LOCALE = 'locale'
ARG_INPUT_ET = 'et'
ARG_INPUT_LT = 'lt'
ARG_INPUT_PAPERSIZE = 'paper-size'
ARG_INPUT_SID = 'sid'
ARG_INPUT_NAMESPACE = 'namespace'
ARG_INPUT_OWNER = 'owner'
ARG_INPUT_TIMEOUT = 'timeout'
ARG_INPUT_MAX_ROWS_PER_TABLE = 'max-rows-per-table'
ARG_INPUT_INCLUDE_SPLUNK_LOGO = 'include-splunk-logo'
ARG_INPUT_REPORT_FILE_NAME = 'report-file-name'
ARG_INPUT_NOW = 'now'
ARG_INPUT_TIMEZONE = 'timezone'
ARG_INPUT_RESULT_ID = 'result_id'
ARG_CACHE_BUSTER = '_'
ARG_CSRF_FORM_KEY = 'splunk_form_key'
ARG_DISPLAY_INLINE = 'inline'
ARG_FILE_FORMAT = 'type'
_validArgs = [
ARG_INPUT_DASHBOARD,
ARG_INPUT_DASHBOARD_XML,
ARG_INPUT_REPORT,
ARG_INPUT_SEARCH,
ARG_INPUT_ET,
ARG_INPUT_LT,
ARG_INPUT_PAPERSIZE,
ARG_INPUT_SID,
ARG_INPUT_NAMESPACE,
ARG_INPUT_OWNER,
ARG_INPUT_TIMEOUT,
ARG_INPUT_MAX_ROWS_PER_TABLE,
ARG_INPUT_INCLUDE_SPLUNK_LOGO,
ARG_INPUT_NOW,
ARG_CACHE_BUSTER,
ARG_CSRF_FORM_KEY,
ARG_INPUT_LOCALE,
ARG_INPUT_TIMEZONE,
ARG_INPUT_RESULT_ID,
ARG_FILE_FORMAT
]
SID_VALIDATOR_STR="sid_([0-9]+)"
sidRE=re.compile(SID_VALIDATOR_STR)
POLLING_WAIT_TIME = 0.5
TOUCH_SEARCH_INTERVAL = 10
ALERT_ACTIONS_ENTITY = '/configs/conf-alert_actions'
SERVER_ENTITY = '/configs/conf-server'
LIMITS_ENTITY = '/configs/conf-limits'
WEB_ENTITY = '/configs/conf-web'
def handle_GET(self):
self._handleRequest()
def handle_POST(self):
self._handleRequest()
def _handleRequest(self):
logger.debug("pdfgen/render request: " + str(self.request))
if not self._initialize():
return
if self.method == 'GET' and not self._enableInsecurePdfgen:
errorMsg = "pdfgen GET is deprecated. Please use POST instead."
pu.logErrorAndTrace(errorMsg)
self._outputError([errorMsg])
return
if self._dashboardContent and not self._handleStudioDashboardPdf():
return
if not self._dashboardContent and not self._render():
return
self._respond()
def _handleConverter(self):
convertedData = convertDashboardToPdfContent(self.sessionKey, self._namespace, self._owner, self._dashboardContent, self._dashboardName, self._studioErrors, self._chromiumTimeoutDuration, self._chromiumScreenshotDelay, self._fileFormat.upper())
if type(convertedData) == bool:
raise RuntimeError()
return convertedData
def _handleStudioDashboardPdf(self):
try:
self._pdfContent = self._handleConverter()
self._pdfBuffer = BytesIO(self._pdfContent.encode())
except Exception:
errorMsg = "Exception raised while trying to render Studio Dashboard \"%s\" to PDF" % self._dashboardName
pu.logErrorAndTrace(errorMsg)
self._add_generic_studio_error()
return True
def _add_generic_studio_error(self):
# Don't show generic error message if there is already a specific error message
if not self._studioErrors:
self._studioErrors.append(studio_catchall_error_msg(self._dashboardName, self._fileFormat.upper()))
def _getResponseBody(self, name):
if self._dashboardContent == None:
return self._pdfBuffer.getvalue()
if self._fileFormat == 'png':
return self._convertBytesToPng()
return self._generateStudioPDF(name)
def _initialize(self):
try:
self._initParameters()
if self._viewType == self.VIEW_TYPE_DASHBOARD:
# handle Xml vs. entity name
logger.debug("dashboardName=%s dashboardXml=%s" % (self._dashboardName, self._dashboardXml))
if self._dashboardXml != None:
(self._title, self._description, self._views) = pv.getDashboardTitleAndPanelsFromXml(
dashboardXml=self._dashboardXml,
namespace=self._namespace,
owner=self._owner,
sessionKey=self.sessionKey)
elif self._dashboardName != None:
if self._dashboardContent != None:
return True
(self._title, self._description, self._views) = pv.getDashboardTitleAndPanels(
dashboard_name=self._dashboardName,
namespace=self._namespace,
owner=self._owner,
sessionKey=self.sessionKey)
elif self._viewType == self.VIEW_TYPE_REPORT:
self._views.append(pv.Report(savedSearchName=self._reportName, namespace=self._namespace, owner=self._owner, sessionKey=self.sessionKey))
self._title = self._reportName
elif self._viewType == self.VIEW_TYPE_SEARCH:
self._title = self._reportName
self._views.append(pv.SearchReport(search=self._searchStr, et=self._et, lt=self._lt, title=self._title, namespace=self._namespace, owner=self._owner, sessionKey=self.sessionKey))
self._handlePresetSearchIDs()
# instantiate the pdfRenderer object with a file-like object
self._pdfBuffer = BytesIO()
self._pdfRenderer = pdfrenderer.PDFRenderer(namespace=self._namespace, title=self._title,
description=self._description, outputFile=self._pdfBuffer,
paperSize=self._paperSize, timestamp=self._timestampStr,
includeSplunkLogo=self._includeSplunkLogo,
cidFontList=self._cidFontList, pdfSettings=self._pdfSettings,
requestSettings=self._requestSettings, sessionKey=self.sessionKey)
return True
except ArgError as e:
self.response.setStatus(400)
self.response.write(e.args[0])
except Exception as e:
errorMsg = "Bailing out of Integrated PDF Generation. Exception raised while preparing to render \"%s\" to PDF. %s" % (self._title, e)
pu.logErrorAndTrace(errorMsg)
self._outputError([errorMsg])
return False
def _render(self):
try:
self._inactiveViews = self._views[:]
for _ in range(3):
self._startNextSearch()
except Exception as e:
errorMsg = "Exception raised while trying to prepare \"%s\" for rendering to PDF. %s" % (self._title, e)
pu.logErrorAndTrace(errorMsg)
self._outputError([errorMsg])
return False
# iterate over views and render each to the pdf
viewPrepErrors = []
for i, view in enumerate(self._views):
try:
self._keepAllSearchesAlive()
self._checkForTimeout()
isLastView = (i == len(self._views) - 1)
self._renderView(self._pdfRenderer, view, lastView = isLastView)
self._startNextSearch()
except TimeoutError:
errorMsg = "Timeout while trying to prepare \"%s\" for rendering to PDF." % self._title
self._outputTimeoutError(errorMsg)
return
except Exception as e:
errorMsg = "Exception raised while trying to prepare \"%s\" for rendering to PDF. %s" % (self._title, e)
pu.logErrorAndTrace(errorMsg)
viewPrepErrors.append(errorMsg)
if len(self._views) == 0:
# fix SPL-67268, render a empty dashboard if there's 0 views
self._pdfRenderer.renderText("")
# if we weren't able to render any views successfully, output an error as response to endpoint
if len(viewPrepErrors) == len(self._views) and len(self._views) > 0:
self._outputError(viewPrepErrors)
logger.error("No views prepared without exceptions. Bailing out of Integrated PDF Generation.")
return False
try:
self._pdfRenderer.save()
except Exception as e:
errorMsg = []
errorMsg.append("Exception raised while trying to render \"%s\" to PDF." % (self._title))
# SPL-80872 - [PDF] Cannot render PDF report of A5 size if the table is too big
if "too large on page" in e.args[0]:
errorMsg.append("Please try using a larger paper size than %s." % (self._paperSize))
errorMsg.append("%s" % (e))
pu.logErrorAndTrace(errorMsg)
self._outputError(errorMsg)
return False
return True
def _respond(self):
# save and write out the file
try:
contentType = mimetypes.types_map['.' + self._fileFormat]
name = self._generateFileName()
writeContent = self._getResponseBody(name)
self.response.setHeader('content-type', contentType)
self.response.write(writeContent, flushBuffer=True)
# override normal cache-control header to fix problem on ie6-ie8 (see SPL-50739)
self.response.setHeader('cache-control', 'max-age=0, must-revalidate')
logger.info('Generated pdf, filename = %s' % name)
disposition = 'inline' if self.ARG_DISPLAY_INLINE in self.args else 'attachment'
self.response.setHeader('content-disposition',
disposition + '; filename="' + name + '"')
self.response.setStatus(200)
if self._pdfBuffer is not None:
self._pdfBuffer.close()
except Exception as e:
errorMsg = "Exception raised while trying to respond. Bailing out of Integrated PDF Generation. Rendering \"%s\" to PDF. %s" % (self._title, e)
pu.logErrorAndTrace(errorMsg)
self._outputError([errorMsg])
return False
return True
def _generateFileName(self):
name = self._reportName
if self._viewType == self.VIEW_TYPE_DASHBOARD:
name = self._dashboardName
props = {
"owner": self._owner,
"app": self._namespace,
"type": self._viewType,
"name": name
}
return pu.makeReportName(pattern=self._fileNamePattern, type=self._fileFormat, reportProps=props)
def _outputTimeoutError(self, message):
self.response.write("PDF Generation timed out: %s" % message)
self.response.setHeader('content-type', 'text/html')
self.response.setStatus(408)
def _output_error_html(self, errorDetailsArray):
outputErrorMsg = ERROR_MSG + "<br/><ul>"
for errorDetail in errorDetailsArray:
outputErrorMsg = outputErrorMsg + "<li>" + pu.secureMessage(errorDetail) + "</li>"
outputErrorMsg = outputErrorMsg + "</ul>"
self.response.write(outputErrorMsg)
self.response.setHeader('content-type', 'text/html')
self.response.setStatus(400)
def _outputError(self, errorDetailsArray):
if self._dashboardContent is None or self._fileFormat != 'png':
self._output_error_html(errorDetailsArray)
return
self._add_generic_studio_error()
error_message = {
"status": 200,
"error_messages": self._studioErrors
}
try:
self.response.write(json.dumps(error_message))
self.response.setHeader('content-type', 'application/json')
# the Dashboard Studio team has decided for now to set it as 200
# as the export itself was successful. This will only affect Studio dasboards.
self.response.setStatus(200)
except TypeError as e:
logger.error("Exception raised while trying to respond, with error: %s" % str(e))
self._output_error_html(self._studioErrors)
def _initParameters(self):
self._owner=self.request['userName']
# SPL-107168 Need to populate namespace & owner beforehand which are needed
# to generate context specific URI while initializing conf defaults
# get namespace/owner
self._namespace = self.args.get(self.ARG_INPUT_NAMESPACE)
if self.ARG_INPUT_OWNER in self.args:
self._owner = self.args.get(self.ARG_INPUT_OWNER)
self._initArgs()
self._initWebDefaults()
self._initLimitsDefaults()
self._initAlertActionsDefaults()
# initialize view type
# the order matters, check dashboard xml first
if self.ARG_INPUT_DASHBOARD_XML in self.args:
self._dashboardXml = unquote(self.args.get(self.ARG_INPUT_DASHBOARD_XML))
self._viewType = self.VIEW_TYPE_DASHBOARD
self._dashboardName = self.args.get(self.ARG_INPUT_DASHBOARD)
logger.debug("pdfgen/render xml=%s" % self._dashboardXml)
elif self.ARG_INPUT_DASHBOARD in self.args:
self._dashboardName = self.args.get(self.ARG_INPUT_DASHBOARD)
self._viewType = self.VIEW_TYPE_DASHBOARD
elif self.ARG_INPUT_REPORT in self.args:
self._reportName = self.args.get(self.ARG_INPUT_REPORT)
self._viewType = self.VIEW_TYPE_REPORT
elif self.ARG_INPUT_SEARCH in self.args:
self._searchStr = self.args.get(self.ARG_INPUT_SEARCH, "No search query specified")
self._et = self.args.get(self.ARG_INPUT_ET, 0)
self._lt = self.args.get(self.ARG_INPUT_LT, '')
# if et or lt is 0.000 change it to 0
if float(self._et)==0.0:
logger.debug("_et was %s, updating it to '0'" % self._et)
self._et = '0'
if self._lt and float(self._lt)==0.0:
logger.debug("_lt was %s, updating it to '0'" % self._lt)
self._lt = '0'
self._reportName = 'Splunk search results'
self._viewType = self.VIEW_TYPE_SEARCH
#initialize file format
self._dashboardContent = pv.getStudioDashboardContent(self._dashboardName, self._namespace, self._owner, self.sessionKey) if self._dashboardName != None and self._dashboardXml == None else None
fileFormatArg = self.args.get(self.ARG_FILE_FORMAT)
if fileFormatArg == 'png' and self._viewType == self.VIEW_TYPE_DASHBOARD and self._dashboardContent != None:
self._fileFormat = 'png'
elif fileFormatArg and fileFormatArg!=self._fileFormat:
self._outputError([f'Unexpected {self.ARG_FILE_FORMAT}="{fileFormatArg}"'])
raise ArgError(f'{fileFormatArg} is not supported')
# initialize papersize
if self.ARG_INPUT_PAPERSIZE in self.args:
paperSizeArg = self.args.get(self.ARG_INPUT_PAPERSIZE).lower()
if paperSizeArg in pdfrenderer.PAPERSIZES:
self._paperSize = paperSizeArg
else:
logger.warn('Invalid paper size "%s"' % paperSizeArg)
raise ArgError("Paper size is not valid. Please check the pdfgen.log file for more information.")
logger.debug("pdf-init paper-size=%s" % self._paperSize)
# initialize include-splunk-logo
self._includeSplunkLogo = normalizeBoolean(self.args.get(self.ARG_INPUT_INCLUDE_SPLUNK_LOGO, self._includeSplunkLogo))
logger.debug("pdf-init include-splunk-logo=%s" % self._includeSplunkLogo)
# initialize max-row-per-table
if self.ARG_INPUT_MAX_ROWS_PER_TABLE in self.args:
maxRowsPerTableArg = self.args.get(self.ARG_INPUT_MAX_ROWS_PER_TABLE)
try:
self._maxRowsPerTable = int(maxRowsPerTableArg)
except:
logger.warn('Max-rows-per-table="%s" is invalid, must be an integer' % maxRowsPerTableArg)
raise ArgError("max-rows-per-table is invalid, must be an integer. Please check the pdfgen.log file for more information.")
logger.debug("pdf-init max-rows-per-table=%s" % (str(self._maxRowsPerTable)))
# initialize timeout
if self.ARG_INPUT_TIMEOUT in self.args:
self._timeoutDuration = int(self.args.get(self.ARG_INPUT_TIMEOUT))
logger.debug("pdf-init timeoutDuration=%s" % self._timeoutDuration)
self._startTimeoutClock()
if self.ARG_INPUT_REPORT_FILE_NAME in self.args:
self._fileNamePattern = self.args.get(self.ARG_INPUT_REPORT_FILE_NAME)
logger.debug("pdf-init report-file-name=%s" % self._fileNamePattern)
if self.ARG_INPUT_RESULT_ID in self.args:
resultIdStr = self.args.get(self.ARG_INPUT_RESULT_ID)
if resultIdStr.isdigit():
self._resultId = int(resultIdStr)
logger.debug("pdf-init per_alert_result_id=%d" % self._resultId)
else:
logger.warn("pdf-init received invalid per_alert_result_id=%s"% resultIdStr)
# initialize time of report
self._initTimeOfReport()
# check for SIDs
if self._viewType is self.VIEW_TYPE_REPORT:
if self.ARG_INPUT_SID in self.args:
self._inputSids[0] = self.args.get(self.ARG_INPUT_SID)
else:
for argK, argV in self.args.items():
if self.ARG_INPUT_SID in argK:
# we want the panel sequence number which is retrieved from "sid_<seqNum>"
match = self.sidRE.match(argK)
if match != None and len(match.groups(0)) > 0:
seqNum = match.groups(0)[0]
if len(seqNum) > 0:
self._inputSids[int(seqNum)] = argV
logger.debug("sid seqNum=%s value=%s" % (seqNum, argV))
# allow override from http arguments
for validArgs in pdfrenderer.ALL_PDF_SETTINGS:
v = self.args.get(validArgs) or self._pdfSettings.get(validArgs)
if v is not None:
v = v.strip()
# SPL-98329 convert value into lowercase except logo path
if validArgs != pdfrenderer.SETTING_LOGO_PATH:
v = v.lower()
if validArgs in pdfrenderer.PDF_BOOLEAN_SETTINGS:
self._pdfSettings[validArgs] = normalizeBoolean(v)
else:
self._pdfSettings[validArgs] = v
logger.debug("pdfSettings=%s" % (str(self._pdfSettings)))
self._validateParameters()
getargs = {'time': 'now', 'time_format': '%F %T %Z', 'now': self._now }
serverStatus, serverResp = rest.simpleRequest('/search/timeparser', getargs=getargs, sessionKey=self.sessionKey)
root = et.fromstring(serverResp)
if root.find('messages/msg'):
raise splunk.SplunkdException(root.findtext('messages/msg'))
self._timestampStr = root.xpath("//dict/key[@name='now']/text()")[0]
logger.debug("pdf-init timestampStr=%s" % self._timestampStr)
self._locale = self.args.get(self.ARG_INPUT_LOCALE)
logger.info("pdf-init locale=%s" % self._locale)
self._server_zoneinfo = self.args.get(self.ARG_INPUT_TIMEZONE)
if self._server_zoneinfo is None:
self._server_zoneinfo = toDefaultStrings(rest.simpleRequest('/services/search/timeparser/tz', sessionKey=self.sessionKey)[1])
logger.info("pdf-init server_zoneinfo=%s" % self._server_zoneinfo)
def _initTimeOfReport(self):
# 1) try to use get time-of-report from the request args
# 2) if not possible, get current UTC epoch time
if self.ARG_INPUT_NOW in self.args:
nowStr = self.args.get(self.ARG_INPUT_NOW)
try:
self._now = float(nowStr)
except:
logger.warning("invalid now argument passed to pdfgen/render arg=%s" % nowStr)
if self._now is None:
self._now = time.time()
logger.info("pdf time-of-report=%s" % self._now)
def _validateParameters(self):
# raise ArgError if there are problems with any parameters
if self._dashboardName is None and self._reportName is None and self._dashboardXml is None:
raise ArgError("PDF endpoint must be called with one of the following args: 'input-dashboard=<dashboard-id>' or 'input-report=<report-id>' or 'input-dashboard-xml=<dashboard-xml>'")
def _initArgs(self):
for arg in self.args:
logger.debug("Testing argument arg=%s value=%s" % (arg, self.args[arg]))
if arg not in self._validArgs and arg not in pdfrenderer.ALL_PDF_SETTINGS:
# check for sid_N for dashboards
if self.sidRE.match(arg) is None:
logger.warn("Invalid argument passed to pdfgen/render. arg=%s value=%s" % (arg, self.args[arg]))
def _initAlertActionsDefaults(self):
""" use alertActions entity to determine default papersize
return in form of "<size>" or "<size>-landscape"
"""
paperSize = DEFAULT_PAPER_SIZE
paperOrientation = DEFAULT_PAPER_ORIENTATION
try:
# SPL-107168 Passing namespace and owner to generate context specifc endpoint
settings = entity.getEntity(self.ALERT_ACTIONS_ENTITY, 'email', namespace=self._namespace, owner=self._owner, sessionKey=self.sessionKey)
# paperSize is 'letter', 'legal', 'A4', etc
paperSize = settings.get('reportPaperSize') or DEFAULT_PAPER_SIZE
# paperOrientation is 'portrait' or 'landscape'
paperOrientation = settings.get('reportPaperOrientation') or DEFAULT_PAPER_ORIENTATION
self._includeSplunkLogo = normalizeBoolean(settings.get('reportIncludeSplunkLogo', self._includeSplunkLogo))
cidFontListString = settings.get('reportCIDFontList', '') or ''
self._cidFontList = cidFontListString.split(' ')
self._fileNamePattern = settings.get('reportFileName')
# retrieve pdf settings
for k, v in settings.items():
if k.startswith(pdfrenderer.SETTING_PREFIX):
self._pdfSettings[k] = v
keyNoPrefix = k[len(pdfrenderer.SETTING_PREFIX):len(k)]
# SSL settings are stored in pdf namespace, but the ssl_context
# won't find these settings if they are prefixed w/ pdf
self._requestSettings['pdf'][keyNoPrefix] = v
except Exception as e:
logger.error("Could not access or parse email stanza of alert_actions.conf. Error=%s" % str(e))
if paperOrientation == 'landscape':
self._paperSize = paperSize + '-landscape'
else:
self._paperSize = paperSize
def _initLimitsDefaults(self):
""" use limits entity to determine defaults
"""
try:
# SPL-107168 Passing namespace and owner to generate context specifc endpoint
settings = entity.getEntity(self.LIMITS_ENTITY, 'pdf', namespace=self._namespace, owner=self._owner, sessionKey=self.sessionKey)
logger.debug("limitsPDFStanza=%s" % (str(settings)))
self._maxRowsPerTable = int(settings.get('max_rows_per_table', self._maxRowsPerTable))
self._timeoutDuration = int(settings.get('render_endpoint_timeout', self._timeoutDuration))
self._chromiumTimeoutDuration = int(settings.get('render_chromium_timeout', self._chromiumTimeoutDuration))
self._chromiumScreenshotDelay = int(settings.get('render_chromium_screenshot_delay', self._chromiumScreenshotDelay))
except Exception as e:
logger.error("Could not access or parse pdf stanza of limits.conf. Error=%s" % str(e))
def _initWebDefaults(self):
defaultSplunkdConnectionTimeout = 30
try:
splunkdConnectionTimeout = int(cli_common.getWebConfKeyValue('splunkdConnectionTimeout'))
self._enableInsecurePdfgen = normalizeBoolean(cli_common.getWebConfKeyValue('enable_insecure_pdfgen'))
if splunkdConnectionTimeout < defaultSplunkdConnectionTimeout:
splunkdConnectionTimeout = defaultSplunkdConnectionTimeout
splunk.rest.SPLUNKD_CONNECTION_TIMEOUT = splunkdConnectionTimeout
except Exception as e:
logger.error("Could not access or parse settings stanza of web.conf. Error=%s" % e)
splunk.rest.SPLUNKD_CONNECTION_TIMEOUT = defaultSplunkdConnectionTimeout
finally:
logger.info("splunkdConnectionTimeout=%s" % splunk.rest.SPLUNKD_CONNECTION_TIMEOUT)
#
# handling searches
#
def _handlePresetSearchIDs(self):
if len(self._inputSids) > 0:
for view in self._views:
index = view.getViewIndex()
if index in self._inputSids.keys():
try:
searchJobObj = splunk.search.SearchJob(self._inputSids[index], hostPath=None, sessionKey=self.sessionKey)
except Exception as e:
logger.warning("Nonexistent search job object for sid: " + self._inputSids[index])
searchJobObj = None
if searchJobObj != None:
if searchJobObj.isExpired():
logger.warning("Expired search job object for sid: " + self._inputSids[index])
else:
view.setSearchJobObj(searchJobObj)
def _startNextSearch(self):
if len(self._inactiveViews) == 0:
return
view = self._inactiveViews.pop(0)
if view.hasSearchError():
return
if view.getSearchJobObj() is None and view.requiresSearchJobObj():
# start the search!
logger.debug("dispatching search for view: %s" % view.getTitle())
if not view.dispatchSearch(overrideNowTime=self._now, maxRowsPerTable=self._maxRowsPerTable):
logger.error("could not dispatch search for view: %s" % view.getTitle())
def _keepAllSearchesAlive(self):
currentTime = time.time()
logger.debug("currentTime = %s self._touchSearchJobsLastTime = %s" % (currentTime, self._touchSearchJobsLastTime))
if self._touchSearchJobsLastTime is not None:
delta = currentTime - self._touchSearchJobsLastTime
if delta < self.TOUCH_SEARCH_INTERVAL:
return
self._touchSearchJobsLastTime = currentTime
for view in self._views:
if not view.hasSearchError():
view.touchSearchJob()
#
# Timeout
#
def _startTimeoutClock(self):
currentTime = int(time.time())
self._timeoutTime = currentTime + self._timeoutDuration
logger.debug("PDF timeout setup: currentTime=%s timeoutDuration=%s timeoutTime=%s" % (currentTime, self._timeoutDuration, self._timeoutTime))
def _checkForTimeout(self):
currentTime = int(time.time())
timedOut = currentTime > self._timeoutTime
if timedOut:
logger.info("PDF timeout. timeoutDuration = %s timeoutTime = %s currentTime = %s" % (self._timeoutDuration, self._timeoutTime, currentTime))
for view in self._views:
if not view.hasSearchError():
view.cancelSearch()
raise TimeoutError()
#
# Rendering
#
def _renderView(self, pdfRenderer, view, lastView=False):
""" render an individual panel """
types = view.getRenderTypes()
pdfRenderer.conditionalPageBreak(types)
# always render header first
self._renderViewHeader(pdfRenderer, view)
if view.hasError():
pdfRenderer.renderText(view.getError())
return
if view.requiresSearchJobObj():
if view.hasSearchError():
pdfRenderer.renderText(view.getSearchError())
return
while not view.isSearchComplete() and not view.isRealtime():
time.sleep(self.POLLING_WAIT_TIME)
self._keepAllSearchesAlive()
self._checkForTimeout()
if 'trellis.enabled' in view.getOptions() and normalizeBoolean(view.getOptions()['trellis.enabled']):
pdfRenderer.renderText("PDF export is not available for visualizations using trellis layout.")
else:
try:
for type in types:
if type == 'chart':
self._renderChart(pdfRenderer, view)
elif type == 'map':
self._renderMap(pdfRenderer, view)
elif type == 'table':
self._renderTable(pdfRenderer, view)
elif type == 'event':
self._renderEvents(pdfRenderer, view)
elif type == 'single':
self._renderSingle(pdfRenderer, view)
elif type == 'list':
self._renderList(pdfRenderer, view)
elif type == 'html':
self._renderHtml(pdfRenderer, view)
elif type == 'viz':
pdfRenderer.renderText("PDF export does not support custom visualizations.")
else:
pdfRenderer.renderText("No render option for type '%s'" % type)
logger.warning("PDFGenHandler::_renderView> No render option for type = '%s'" % type)
except Exception as e:
if (isinstance(e, splunk.AuthorizationFailed) and
'check_risky_command' in e.extendedMessages and
pu.isRiskyCommandCheckDashboardEnabled(self.sessionKey)):
# This is a special case for chain searches containing risky commands, which throw an AuthorizationFailed
# exception which does not contain any useful info for security reasons. Because of this, we need to return
# a more generic risky commands error message.
content = "Found potentially risky commands."
else:
content = str(e)
pu.logErrorAndTrace(e)
pdfRenderer.renderText(content)
if not lastView:
pdfRenderer.spaceBetween()
def _renderViewHeader(self, pdf_renderer, view):
title = view.getTitle()
if title is not None and len(title) > 0:
pdf_renderer.renderText("%s" % title, style = pdf_renderer.TITLE_STYLE, escapeText=True)
pdf_renderer.spaceBetween(0.1 * pdf_renderer.ONE_INCH)
subtitle = view.getSubtitle()
if subtitle is not None and len(subtitle) > 0:
pdf_renderer.renderText("%s" % subtitle, style=pdf_renderer.SUBTITLE_STYLE, escapeText=True)
pdf_renderer.spaceBetween(0.1 * pdf_renderer.ONE_INCH)
description = view.getDescription()
if description is not None and len(description) > 0:
pdf_renderer.renderText(description)
def _renderTable(self, pdfRenderer, view):
""" render a table of results """
# get options
options = view.getOptions()
if "show" in options and not normalizeBoolean(options['show']):
return
fieldFormats = view.getFieldFormats()
displayRowNumbers = False
if "displayRowNumbers" in options:
displayRowNumbers = normalizeBoolean(options['displayRowNumbers'])
overlay = None
if "dataOverlayMode" in options:
overlay = options['dataOverlayMode']
if "overlay" in options:
overlay = options['overlay']
resultsExist = False
# get results object
view.getSearchJobObj().setFetchOptions(output_mode="json_cols", time_format=pt.TIME_RAW_FORMAT)
results = view.getSearchJobResults()
# determine field set
explicitFieldList = view.getSearchFieldList()
fieldOrder = []
if len(explicitFieldList) > 0:
for field in explicitFieldList:
if field in results.fieldOrder and field not in fieldOrder:
fieldOrder.append(field)
if len(fieldOrder) == 0:
logger.warning("%s: results.fieldOrder does not contain any explicitly specified fields: %s" % (view.getTitle(), explicitFieldList))
return
else:
fieldOrder = self._renderTable_restrictFields(results.fieldOrder)
if len(fieldOrder) == 0:
pdfRenderer.renderText("No results found.")
return
tableData = pt.TableData(columnNames=fieldOrder)
if self._resultId >= len(list(results)):
logger.warn('PDFRenderTable: found result_id=%d is greater than size_of_results=%d, PDF will be generated with all of search results.'
% (self._resultId, len(list(results))))
if self._resultId == -1 or self._resultId >= len(list(results)):
for i, result in enumerate(results):
if i >= self._maxRowsPerTable:
break
if (i > 0) and ((i % 100) == 0):
self._keepAllSearchesAlive()
resultsExist = True
tableData.addRowFromSearchResult(result)
else:
logger.debug('PDFRenderTable: Generate per_result_alert for result_id=%d' % self._resultId)
resultsExist = True
tableData.addRowFromSearchResult(results[self._resultId])
if resultsExist:
pdfRenderer.renderTable(tableData, title=view.getSubtitle() or view.getTitle(), displayLineNumbers=displayRowNumbers,
fieldFormats=fieldFormats, overlayMode=overlay)
else:
logger.warning("PDFGenHandler::_renderTable> no results for table")
pdfRenderer.renderText("No results found.")
def _renderTable_restrictFields(self, inputFieldOrder):
""" restrict the fields that are output in tables
do not display any fields that start with an underscore
except for _raw and _time
position _time, if present, at the front of fieldOrder, and
position _raw, if present, at the end of fieldOrder
---
for the time being, if _time and _raw are both present,
do not show any other field
"""
timeFieldPresent = "_time" in inputFieldOrder
rawFieldPresent = "_raw" in inputFieldOrder
# do not include any fields that start with '_'
publicFields = []
for field in inputFieldOrder:
if len(field) > 0 and field[0] != '_':
publicFields.append(field)
# build up output fieldOrder
fieldOrder = []
# always show _time first
if timeFieldPresent:
fieldOrder.append("_time")
# if both _time and _raw present, do not show other fields
if timeFieldPresent == False or rawFieldPresent == False:
for field in publicFields:
fieldOrder.append(field)
# always show _raw last
if rawFieldPresent:
fieldOrder.append("_raw")
logger.debug("PDFGenHandler::_renderTable_restrictFields> inputFieldOrder=" + str(inputFieldOrder) + " fieldOrder=" + str(fieldOrder))
return fieldOrder
def _renderList(self, pdfRenderer, view):
""" render a list display """
# build 2-d list of lists
data = []
resultsExist = False
results = view.getSearchJobResults()
fieldOrder = results.fieldOrder
# get options
optionsDict = view.getOptions()
labelField = optionsDict.get("labelField", None)
valueField = optionsDict.get("valueField", None)
initialSort = optionsDict.get("initialSort", labelField)
initialSortDir = optionsDict.get("initialSortDir", "asc")
# validate options
if labelField is None or valueField is None:
logger.warning("PDFGenHandler::_renderList> missing either labelField or valueField in list")
return
# build up 2-d data set
data = []
if self._resultId >= len(list(results)):
logger.warn('PDFGenHandler::_renderList: found result_id=%d is greater than size_of_results=%d, PDF will be generated with all of search results.'
% (self._resultId, len(list(results))))
if self._resultId == -1 or self._resultId >= len(list(results)):
for result in results:
label = str(result.get(labelField, default=""))
value = str(result.get(valueField, default=""))
logger.debug("PDFGenHandler::_renderList> label, value: " + label + ", " + value)
if label != "" or value != "":
row = [label, value]
data.append(row)
else:
r = results[self._resultId]
label = str(r.get(labelField, default=""))
value = str(r.get(valueField, default=""))
logger.debug("PDFGenHandler::_renderList> for per_result_alert label, value: " + label + ", " + value)
if label != "" or value != "":
row = [label, value]
data.append(row)
# sort by initialSort and initialSortDir
sortIndex = 0
if initialSort == valueField:
sortIndex = 1
sortDescending = initialSortDir.lower() == "desc"
data = sorted(data, key=lambda row: row[sortIndex], reverse=sortDescending)
logger.debug("PDFGenHandler::_renderList> data: " +str(data))
# render the data as lines of text
if len(data) > 0:
for row in data:
pdfRenderer.renderText(row[0] + " " + row[1])
else:
pdfRenderer.renderText("No results for list")
def _renderSingle(self, pdfRenderer, view):
""" render a SingleValue display """
optionsDict = view.getOptions()
logger.debug("_renderSingle optionsDict='%s'" % optionsDict)
props = {"exportMode":"true"}
props.update(view.getSingleValueProps())
self._renderSvgBasedViz(pdfRenderer, view, props, mode="single")
def _renderChart(self, pdfRenderer, view):
""" render a chart from the results """
props = {"exportMode":"true","enableChartClick":"false","enableLegendClick":"false", "data.count":10000}
props.update(view.getChartProps())
self._renderSvgBasedViz(pdfRenderer, view, props, mode="chart")
def _renderMap(self, pdfRenderer, view):
""" render a map from the results """
mapProps = view.getMapProps()
mapFitBounds = mapProps.get('map.fitBounds')
dataFitBounds = mapProps.get('data.bounds')
fitBounds = None
fitBoundsArray = []
if dataFitBounds:
fitBounds = dataFitBounds
elif mapFitBounds:
fitBounds = mapFitBounds
else:
fitBounds = '(-90,-180,90,180)'
if fitBounds:
fitBounds = fitBounds.replace('(', '')
fitBounds = fitBounds.replace(')', '')
fitBoundsArray = [bound.strip() for bound in fitBounds.split(',')]
gFilterPostProcess = None
if mapProps.get('type', 'marker') == 'choropleth':
geomFilterArguments = []
if len(fitBoundsArray) == 4:
geomFilterArguments.append('min_y=%s' % fitBoundsArray[0])
geomFilterArguments.append('min_x=%s' % fitBoundsArray[1])
geomFilterArguments.append('max_y=%s' % fitBoundsArray[2])
geomFilterArguments.append('max_x=%s' % fitBoundsArray[3])
geomFilterPostProcess = 'geomfilter %s' % ' '.join(geomFilterArguments)
logger.debug("Rendering Map in PDF. postprocess = %s geomFilterArguments=%s" % (geomFilterPostProcess, geomFilterArguments))
gFilterPostProcess = geomFilterPostProcess
else:
geoFilterArguments = []
if len(fitBoundsArray) == 4:
geoFilterArguments.append('south=%s' % fitBoundsArray[0])
geoFilterArguments.append('west=%s' % fitBoundsArray[1])
geoFilterArguments.append('north=%s' % fitBoundsArray[2])
geoFilterArguments.append('east=%s' % fitBoundsArray[3])
if 'maxClusters' in mapProps:
geoFilterArguments.append('maxclusters=%s' % mapProps['maxClusters'])
geoFilterPostProcess = 'geofilter %s' % ' '.join(geoFilterArguments)
logger.debug("Rendering Map in PDF. postprocess = %s geoFilterArguments=%s" % (geoFilterPostProcess, geoFilterArguments))
gFilterPostProcess = geoFilterPostProcess
self._renderSvgBasedViz(pdfRenderer, view, mapProps, mode="map", postSearch=gFilterPostProcess)
def _renderSvgBasedViz(self, pdfRenderer, view, props, mode, postSearch=None):
# set the fetch option output_mode to 'json_cols'
# this will cause the data returned by getFeed to be
# pre-populated into the columnar form that the charting code wants
# set the fetch option time_format to '%FT%T.%Q%:z'
# this is the only time format that is accepted by charting
# set the show_metadata flag to true for chart mode only
# this will populate the metadata for each field, which certain chart types need to render correctly
fetchOptions = {'output_mode': 'json_cols', 'time_format': '%FT%T.%Q%:z'};
if postSearch is not None:
fetchOptions['search'] = postSearch;
if mode == "chart":
fetchOptions['show_metadata'] = 'true';
elif mode == "single":
fetchOptions['count'] = 1000;
view.getSearchJobObj().setFetchOptions(**fetchOptions)
feedCount = DEFAULT_FEED_COUNT
if 'data.count' in props:
feedCount = props['data.count']
feed = toDefaultStrings(view.getSearchJobFeed(feedCount=feedCount))
logger.debug("_renderSvgBasedViz> feed: " + str(feed) + ", " + str(type(feed)))
if feed is None or len(feed.strip()) == 0:
logger.warning("_renderSvgBasedViz> feed: " + str(feed) + ", " + str(type(feed)))
pdfRenderer.renderText("No results found.")
return
feed = json.loads(feed)
# extract columns and fields from the search results
data = feed['columns']
fields = feed['fields']
if len(data) == 0:
pdfRenderer.renderText("No results found.")
return
# set up the SVG viz object
svgBasedViz = None
if mode == "chart":
# SPL-118166 - remove 'None' values from the '_time' column.
if len(fields) and fields[0].get('name') == '_time':
data[0] = list(filter(lambda x: x is not None, data[0]))
svgBasedViz = pc.Chart(data, fields, props, self._locale, server_zoneinfo=self._server_zoneinfo)
elif mode == "map":
svgBasedViz = pc.Map(data, fields, props, self._locale, server_zoneinfo=self._server_zoneinfo)
elif mode == "single":
svgBasedViz = pc.Single(data, fields, props, self._locale, server_zoneinfo=self._server_zoneinfo)
# build the SVG viz
buildSuccess = svgBasedViz.build()
if buildSuccess:
svgString = svgBasedViz.getSvg()
pdfRenderer.renderSvgString(svgString, title=None)
else:
logger.error("SVG based viz building error")
def _renderEvents(self, pdfRenderer, view):
""" render a listing of events """
# get options -- TODO: should be refactored with _renderTable (and beyond: should have a generalized system for getting options)
options = view.getOptions()
displayRowNumbers = False
if "displayRowNumbers" in options:
displayRowNumbers = normalizeBoolean(options['displayRowNumbers'])
data = []
# fix header for events
header = ['Time', 'Event']
events = view.getSearchJobEvents()
if self._resultId >= len(list(events)):
logger.warn('PDFrenderEvents: found result_id=%d is greater than size_of_events=%d, PDF will be generated with all of search events.'
% (self._resultId, len(list(events))))
if self._resultId == -1 or self._resultId >= len(list(events)):
for i, event in enumerate(events):
if i >= self._maxRowsPerTable:
break
if (i > 0) and ((i % 100) == 0):
self._keepAllSearchesAlive()
data.append([str(event.time), event.raw])
else:
logger.debug('PDFrenderEvents: Generate per_result_alert for result_id=%d' % self._resultId)
data.append([str(events[self._resultId].time), events[self._resultId].raw])
if len(data) == 0:
pdfRenderer.renderText("No matching events found.")
else:
tableData = pt.TableData(columnNames=header, data=data)
pdfRenderer.renderTable(tableData, title=view.getTitle(), columnVAlignments=['TOP', 'TOP'],
displayLineNumbers=displayRowNumbers)
def _renderHtml(self, pdfRenderer, view):
title = view.getTitle()
if title is None:
title = "Untitled panel"
options = view.getOptions()
logger.debug("PDFGenHandler::_renderHtml> options: " + str(options))
if pu.PP_RAW_CONTENT not in options or options[pu.PP_RAW_CONTENT] is None or len(options[pu.PP_RAW_CONTENT]) == 0:
logger.warning("PDFGenHandler::_renderHtml> rawContent key not in optionsDict for view: " + title)
return
pdfRenderer.renderHtml(options[pu.PP_RAW_CONTENT])
def _convertBytesToPng(self):
bytesToStr = self._pdfBuffer.getvalue().decode()
imgData = base64.b64decode(bytesToStr.replace('data:image/png;base64,', ''))
return imgData
def _generateStudioPDF(self, name):
tmpBuffer = BytesIO()
if not self._studioErrors:
imgData = self._convertBytesToPng()
image = imageUtils.PngImageReader(imgBytes=imgData)
c = canvas.Canvas(tmpBuffer, pagesize=(image._width, image._height))
c.setTitle(name)
c.drawImage(image, 0, 0)
c.save()
else:
messages = []
doc = SimpleDocTemplate(tmpBuffer, pagesize=letter)
styles = getSampleStyleSheet()
style = styles["Normal"]
for error in reversed(self._studioErrors):
line = Paragraph(error, style)
messages.append(line)
doc.build(messages)
pdf = tmpBuffer.getvalue()
return pdf

Powered by BW's shoe-string budget.