Merge branch 'develop' into readme_fix

This commit is contained in:
jzsplunk
2019-12-10 09:49:10 -08:00
627 changed files with 57 additions and 179644 deletions
+2 -2
View File
@@ -90,8 +90,8 @@ jobs:
source $BASH_ENV
go get -u github.com/raviqqe/liche
cd security-content
liche -r docs/
liche README.md
liche docs/stories_categories.md -v
liche README.md -v
build-sources:
executor: content-executor
steps:
+2 -2
View File
@@ -79,7 +79,7 @@ Install project dependecies and tests that run before content is committed:
##### CI Tools
Tools that help with testing CI jobs:
1. Install CircleCI [CLI Tool](https://circleci.com/docs/2.0/local-cli/#installation).
1. Install CircleCI [CLI Tool](https://circleci.com/docs/2.0/local-cli/).
2. To test a local change to CircleCI or build, make sure you are running Docker, then enter
`circleci local execute -e GITHUB_TOKEN=$GITHUB_TOKEN --branch <your branch>`.
@@ -99,7 +99,7 @@ To automatically generate docs from schema:
* note that [requirements.txt](https://github.com/splunk/security-content/blob/develop/requirements.txt) hardcodes the versions for packages we use [dependabot](https://dependabot.com/) to make sure we safely always upgrade to the latest versions.
## Support
Please use the [GitHub Issue Tracker](https://github.com/splunk/security_content/issues) to submit bugs or request features.
Please use the [GitHub Issue Tracker](https://github.com/splunk/security-content/issues) to submit bugs or request features.
If you have questions or need support, you can:
-4
View File
@@ -149,10 +149,6 @@ def identify_next_steps(detections, investigations):
next_steps += "Based on ESCU investigate recommendations:\\n%s\"}" % (investigations_output)
if has_phantom:
detection['recommended_actions'] = 'runphantomplaybook, escu_investigate'
else:
detection['recommended_actions'] = 'escu_investigate'
detection['next_steps'] = next_steps
enriched_detections.append(detection)
return enriched_detections
-2
View File
@@ -79,8 +79,6 @@ action.notable.param.severity = {{ detection.confidence }}
action.notable.param.drilldown_name = {{ correlation_rule.notable.drilldown_name }}
action.notable.param.drilldown_search = {{ correlation_rule.notable.drilldown_search }}
{% endif %}
action.notable.param.recommended_actions = {{ detection.recommended_actions }}
action.notable.param.next_steps = {{ detection.next_steps }}
{% if correlation_rule.risk is defined %}
action.risk = 1
action.risk.param._risk_object = {{ correlation_rule.risk.risk_object }}
+1 -3
View File
@@ -30,8 +30,6 @@ action.notable.param.rule_description = Your AWS infrastructure was provisioned
action.notable.param.rule_title = AWS Provision Activity From $city$
action.notable.param.security_domain = endpoint
action.notable.param.severity = medium
action.notable.param.recommended_actions = escu_investigate
action.notable.param.next_steps = {"version": 1, "data": "Recommended following steps:\n\n1. [[action|escu_investigate]]: Based on ESCU investigate recommendations:\nESCU - Get All AWS Activity From IP Address\nESCU - Get All AWS Activity From City\n"}
action.risk = 1
action.risk.param._risk_object = dest
action.risk.param._risk_object_type = system
@@ -98,4 +96,4 @@ action.escu.known_false_positives =
disabled=true
schedule_window = auto
is_visible = false
search = | inputlookup interesting_processes_lookup | search note!=ESCU* | inputlookup append=T prohibitedProcesses_lookup | fillnull value=* dest dest_pci_domain | fillnull value=false is_required is_secure | fillnull value=true is_prohibited | outputlookup interesting_processes_lookup | stats count
search = | inputlookup interesting_processes_lookup | search note!=ESCU* | inputlookup append=T prohibitedProcesses_lookup | fillnull value=* dest dest_pci_domain | fillnull value=false is_required is_secure | fillnull value=true is_prohibited | outputlookup interesting_processes_lookup | stats count
@@ -146,12 +146,7 @@ require([
$('.run-story').unbind('click');
$('.run-story').on('click', function(evt) {
splunkUtil.redirect_to('/app/DA-ESS-ContentUpdate/search', {
q: `| runstory story="${asName}" | table name, num_search_results, description, kill_chain_phases, mitre_attack`,
earliest: "-60m",
latest: "now",
}, window.open(), true);
window.open('https://github.com/splunk/analytic_story_execution');
});
let asSearch = splunkjs.mvc.Components.getInstance(epoch);
@@ -593,4 +588,4 @@ require([
return htmlTmpl;
}
});
});
@@ -1,211 +0,0 @@
import csv
import gzip
import sys
from splunk.clilib.bundle_paths import make_splunkhome_path
sys.path.insert(0, make_splunkhome_path(["etc", "apps", "Splunk_SA_CIM", "lib"]))
import requests
from cim_actions import ModularAction
from logging_helper import get_logger
import logging
from splunk_aoblib.rest_helper import TARestHelper
from splunk_aoblib.setup_util import Setup_Util
class ModularAlertBase(ModularAction):
def __init__(self, ta_name, alert_name):
self._alert_name = alert_name
# self._logger_name = "modalert_" + alert_name
self._logger_name = alert_name + "_modalert"
self._logger = get_logger(self._logger_name)
super(ModularAlertBase, self).__init__(
sys.stdin.read(), self._logger, alert_name)
self.setup_util_module = None
self.setup_util = None
self.result_handle = None
self.ta_name = ta_name
self.splunk_uri = self.settings.get('server_uri')
self.setup_util = Setup_Util(self.splunk_uri, self.session_key, self._logger)
level = self.get_log_level()
if level:
self._logger.setLevel(level)
self.rest_helper = TARestHelper(self._logger)
def log_error(self, msg):
self.message(msg, 'failure', level=logging.ERROR)
def log_info(self, msg):
self.message(msg, 'success', level=logging.INFO)
def log_debug(self, msg):
self.message(msg, None, level=logging.DEBUG)
def log_warn(self, msg):
self.message(msg, None, level=logging.WARN)
def set_log_level(self, level):
self._logger.setLevel(level)
def get_param(self, param_name):
return self.configuration.get(param_name)
def get_global_setting(self, var_name):
return self.setup_util.get_customized_setting(var_name)
def get_user_credential(self, username):
'''
if the username exists, return
{
"username": username,
"password": credential
}
'''
return self.setup_util.get_credential_by_username(username)
@property
def log_level(self):
return self.get_log_level()
@property
def proxy(self):
return self.get_proxy()
def get_log_level(self):
return self.setup_util.get_log_level()
def get_proxy(self):
''' if the proxy setting is set. return a dict like
{
proxy_url: ... ,
proxy_port: ... ,
proxy_username: ... ,
proxy_password: ... ,
proxy_type: ... ,
proxy_rdns: ...
}
'''
return self.setup_util.get_proxy_settings()
def _get_proxy_uri(self):
uri = None
proxy = self.get_proxy()
if proxy and proxy.get('proxy_url') and proxy.get('proxy_type'):
uri = proxy['proxy_url']
if proxy.get('proxy_port'):
uri = '{0}:{1}'.format(uri, proxy.get('proxy_port'))
if proxy.get('proxy_username') and proxy.get('proxy_password'):
uri = '{0}://{1}:{2}@{3}/'.format(proxy['proxy_type'], proxy[
'proxy_username'], proxy['proxy_password'], uri)
else:
uri = '{0}://{1}'.format(proxy['proxy_type'], uri)
return uri
def send_http_request(self, url, method, parameters=None, payload=None, headers=None, cookies=None, verify=True, cert=None, timeout=None, use_proxy=True):
return self.rest_helper.send_http_request(url=url, method=method, parameters=parameters, payload=payload,
headers=headers, cookies=cookies, verify=verify, cert=cert,
timeout=timeout,
proxy_uri=self._get_proxy_uri() if use_proxy else None)
def build_http_connection(self, config, timeout=120,
disable_ssl_validation=False):
from httplib2 import (socks, ProxyInfo, Http)
"""
:config: dict like, proxy and account information are in the following
format {
"username": xx,
"password": yy,
"proxy_url": zz,
"proxy_port": aa,
"proxy_username": bb,
"proxy_password": cc,
"proxy_type": http,http_no_tunnel,sock4,sock5,
"proxy_rdns": 0 or 1,
}
:return: Http2.Http object
"""
if not config:
config = {}
proxy_type_to_code = {
"http": socks.PROXY_TYPE_HTTP,
"http_no_tunnel": socks.PROXY_TYPE_HTTP_NO_TUNNEL,
"socks4": socks.PROXY_TYPE_SOCKS4,
"socks5": socks.PROXY_TYPE_SOCKS5,
}
if config.get("proxy_type") in proxy_type_to_code:
proxy_type = proxy_type_to_code[config["proxy_type"]]
else:
proxy_type = socks.PROXY_TYPE_HTTP
rdns = config.get("proxy_rdns")
proxy_info = None
if config.get("proxy_url") and config.get("proxy_port"):
if config.get("proxy_username") and config.get("proxy_password"):
proxy_info = ProxyInfo(proxy_type=proxy_type,
proxy_host=config["proxy_url"],
proxy_port=int(config["proxy_port"]),
proxy_user=config["proxy_username"],
proxy_pass=config["proxy_password"],
proxy_rdns=rdns)
else:
proxy_info = ProxyInfo(proxy_type=proxy_type,
proxy_host=config["proxy_url"],
proxy_port=int(config["proxy_port"]),
proxy_rdns=rdns)
if proxy_info:
http = Http(proxy_info=proxy_info, timeout=timeout,
disable_ssl_certificate_validation=disable_ssl_validation)
else:
http = Http(timeout=timeout,
disable_ssl_certificate_validation=disable_ssl_validation)
if config.get("username") and config.get("password"):
http.add_credentials(config["username"], config["password"])
return http
def process_event(self, *args, **kwargs):
raise NotImplemented()
def pre_handle(self, num, result):
result.setdefault('rid', str(num))
self.update(result)
return result
def get_events(self):
self.result_handle = gzip.open(self.results_file, 'rb')
return (self.pre_handle(num, result) for num, result in enumerate(csv.DictReader(self.result_handle)))
def prepare_meta_for_cam(self):
with gzip.open(self.results_file, 'rb') as rf:
for num, result in enumerate(csv.DictReader(rf)):
result.setdefault('rid', str(num))
self.update(result)
self.invoke()
break
def run(self, argv):
status = 0
if len(argv) < 2 or argv[1] != "--execute":
msg = 'Error: argv="{}", expected="--execute"'.format(argv)
print >> sys.stderr, msg
sys.exit(1)
try:
status = self.process_event()
except Exception as e:
msg = "Unexpected error: {}."
if e.message:
self.log_error(msg.format(e.message))
else:
import traceback
self.log_error(msg.format(traceback.format_exc()))
sys.exit(2)
finally:
if self.result_handle:
self.result_handle.close()
return status
@@ -1,508 +0,0 @@
import collections
import csv
import json
import logging
import logging.handlers
import os
import random
import re
import splunk.rest as rest
import time
from splunk.clilib.bundle_paths import make_splunkhome_path
from splunk.util import mktimegm, normalizeBoolean
# set the maximum allowable CSV field size
#
# The default of the csv module is 128KB; upping to 10MB. See SPL-12117 for
# the background on issues surrounding field sizes.
# (this method is new in python 2.5)
csv.field_size_limit(10485760)
class InvalidResultID(Exception):
pass
class ModularAction(object):
DEFAULT_MSGFIELDS = ['signature',
'action_name',
'search_name',
'sid',
'orig_sid',
'rid',
'orig_rid',
'app',
'user',
'action_mode',
'action_status']
DEFAULT_MESSAGE = 'sendmodaction - ' + ' '.join(['{i}="{{d[{i}]}}"'.format(i=i) for i in DEFAULT_MSGFIELDS])
# The above yields a string.format() compatible format string:
#
# 'sendmodaction - signature="{d[signature]}" action_name="{d[action_name]}"
# search_name="{d[search_name]}" sid="{d[sid]}" orig_sid="{d[orig_sid]}"
# rid="{d[rid]}" orig_rid="{d[orig_rid]}" app="{d[app]}" user="{d[user]}"
# action_mode="{d[action_mode]}" action_status="{d[action_status]}"'
DEFAULT_DROPEXP = lambda x: ((x.startswith('_') and x not in ['_raw', '_time'])
or x.startswith('date_')
or x in ['punct', 'sid', 'rid', 'orig_sid', 'orig_rid'])
DEFAULT_MAPEXP = lambda x: (x.startswith('tag::')
or x in ['_time', '_raw', 'splunk_server', 'index',
'source', 'sourcetype', 'host', 'linecount',
'timestartpos', 'timeendpos', 'eventtype',
'tag', 'search_name', 'event_hash', 'event_id'])
DEFAULT_HEADER = '***SPLUNK*** index="%s" host="%s" source="%s"'
DEFAULT_BREAKER = '==##~~##~~ 1E8N3D4E6V5E7N2T9 ~~##~~##==\n'
DEFAULT_IDLINE = '***Common Action Model*** orig_action_name="%s" orig_sid="%s" orig_rid="%s" sourcetype="%s"\n'
DEFAULT_INDEX = 'summary'
DEFAULT_CHUNK = 50000
SHORT_FORMAT = '%(asctime)s %(levelname)s %(message)s'
def __init__(self, settings, logger, action_name='unknown'):
""" Initialize ModularAction class.
@param settings: A modular action payload in JSON format.
@param logger: A logging instance.
Recommend using ModularAction.setup_logger.
@param action_name: The action name.
action_name in payload will take precedence.
"""
self.settings = json.loads(settings)
self.logger = logger
self.session_key = self.settings.get('session_key')
self.sid = self.settings.get('sid')
self.sid_snapshot = ''
## if sid contains rt_scheduler with snapshot-sid; drop snapshot-sid
## sometimes self.sid may be an integer (1465593470.1228)
try:
rtsid = re.match('^(rt_scheduler.*)\.(\d+)$', self.sid)
if rtsid:
self.sid = rtsid.group(1)
self.sid_snapshot = rtsid.group(2)
except:
pass
## rid_ntuple is a named tuple that represents
## the three variables that change on a per-result basis
self.rid_ntuple = collections.namedtuple('ID', ['orig_sid','rid','orig_rid'])
## rids is a list of rid_ntuple values
## automatically maintained by update() calls
self.rids = []
## current orig_sid based on update()
## aka self.rids[-1].orig_sid
self.orig_sid = ''
## current rid based on update()
## aka self.rids[-1].rid
self.rid = ''
## current orig_rid based on update()
## aka self.rids[-1].orig_rid
self.orig_rid = ''
self.results_file = self.settings.get('results_file')
## info
self.info = {}
if self.results_file:
self.info_file = os.path.join(os.path.dirname(self.results_file), 'info.csv')
self.search_name = self.settings.get('search_name')
self.app = self.settings.get('app')
self.user = self.settings.get('user') or self.settings.get('owner')
self.configuration = self.settings.get('configuration', {})
## enforce configuration is a 'dict'
if not isinstance(self.configuration, dict):
self.configuration = {}
## set loglevel to DEBUG if verbose
if normalizeBoolean(self.configuration.get('verbose', 'false')):
self.logger.setLevel(logging.DEBUG)
self.logger.debug('loglevel set to DEBUG')
## use | sendalert param.action_name=$action_name$
self.action_name = self.configuration.get('action_name') or action_name
## use sid to determine action_mode
if isinstance(self.sid, basestring) and 'scheduler' in self.sid:
self.action_mode = 'saved'
else:
self.action_mode = 'adhoc'
self.action_status = ''
## Since we don't use the result object we get from settings it will be purged
try:
del self.settings['result']
except Exception:
pass
## events
self.events = []
def addinfo(self):
""" The purpose of this method is to populate the
modular action info variable with the contents of info.csv.
@raise Exception: raises Exception if self.info_file could not be opened
or if there were problems parsing the info.csv data
"""
if self.info_file:
try:
with open(self.info_file, 'rU') as fh:
self.info = csv.DictReader(fh).next()
except Exception as e:
self.message('Could not retrieve info.csv', level=logging.WARN)
def addjobinfo(self):
""" The purpose of this method is to populate the job variable
with the contents from REST (/services/search/jobs/<sid>)
SPL-112815 - sendalert - not all $job.<param>$ parameters come through
@raise Exception: raises Exception if search job information could not
be retrieved via REST (search/jobs) based on self.sid
"""
self.job = {}
if self.sid:
try:
response, content = rest.simpleRequest('search/jobs/%s' % self.sid,
sessionKey=self.session_key,
getargs={'output_mode': 'json'})
if response.status == 200:
self.job = json.loads(content)['entry'][0]['content']
self.message('Successfully retrieved search job info')
self.logger.debug(self.job)
else:
self.message('Could not retrieve search job info', level=logging.WARN)
except Exception as e:
self.message('Could not retrieve search job info', level=logging.WARN)
def message(self, signature, status=None, rids=None, level=logging.INFO, **kwargs):
""" The purpose of this method is to provide a common messaging interface.
@param signature: A string representing the message we want to log.
@param status: An optional status that we want to log.
Defaults to None.
@param rids: An optional list of rid_ntuple values in case we
want to generate the message for multiple rids.
Defaults to None (use the rid currently loaded).
@param level: The logging level to use when writing the message.
Defaults to logging.INFO (INFO)
@param kwargs: Additional keyword arguments to be included with the
message.
Defaults to "no arguments".
@return message: This method logs the message; however, for
backwards compatibility we also return the message.
"""
## status
status = status or self.action_status or ''
## rid
if not isinstance(rids, list):
rids = [self.rid_ntuple(self.orig_sid, self.rid, self.orig_rid)]
## kwargs - prune any duplicate keys based on DEFAULT_MSGFIELDS
## prune any keys with special characters [A-Za-z_]+
newargs = [x for x in kwargs\
if (x not in ModularAction.DEFAULT_MSGFIELDS) and re.match('[A-Za-z_]+', x)]
## MSG
msg = '%s %s' % (ModularAction.DEFAULT_MESSAGE, ' '.join(['{i}="{{d[{i}]}}"'.format(i=i) for i in newargs]))
# This will set the default value of any value NOT in the dictionary to the
# empty string.
argsdict = collections.defaultdict(str)
# order is important here - here we update first from kwargs, then from our
# expected arg set.
argsdict.update(kwargs)
argsdict.update({
'signature': signature or '',
'action_name': self.action_name or '',
'search_name': self.search_name or '',
'sid': self.sid or '',
'app': self.app or '',
'user': self.user or '',
'action_mode': self.action_mode or '',
'action_status': status
})
for rid_ntuple in rids:
if len(rid_ntuple)==3:
## Update the arguments dictionary
argsdict.update({
'orig_sid': rid_ntuple.orig_sid or '',
'rid': rid_ntuple.rid or '',
'orig_rid': rid_ntuple.orig_rid or ''
})
## This is where the magic happens. The format string will use the
## attributes of "argsdict"
message = msg.format(d=argsdict)
## prune empty string key-value pairs
for match in re.finditer('[A-Za-z_]+=\"\"(\s|$)', message):
message = message.replace(match.group(0),'',1)
message = message.strip()
self.logger.log(level, message)
else:
self.logger.warn('Could not unpack rid_ntuple')
message = ''
return message
def update(self, result):
""" The purpose of this method is to update the ModularAction instance
identifiers based on the current result being operated on.
This is the most important method in the library as it sets up
rid, orig_sid, and orig_rid to be used by subsequent class methods.
Not calling update() immediately for each result before doing additional
work can have adverse affects.
@param signature: A string representing the message we want to log.
@param status: An optional status that we want to log.
Defaults to None.
@param rids: An optional list of rid_ntuple values in case we
want to generate the message for multiple rids.
Defaults to None (use the rid currently loaded).
@param level: The logging level to use when writing the message.
Defaults to logging.INFO (INFO)
@param kwargs: Additional keyword arguments to be included with the
message.
Defaults to "no arguments".
@return message: This method logs the message; however, for
backwards compatiblity we also return the message.
"""
## This is for events/results that were created as the result of a previous action
self.orig_sid = result.get('orig_sid', '')
## This is for events/results that were created as the result of a previous action
self.orig_rid = result.get('orig_rid', '')
if 'rid' in result and isinstance(result['rid'], (basestring, int)):
self.rid = str(result['rid'])
if self.sid_snapshot:
self.rid = '%s.%s' % (self.rid, self.sid_snapshot)
## add result info to list of named tuples
self.rids.append(self.rid_ntuple(self.orig_sid, self.rid, self.orig_rid))
else:
raise InvalidResultID('Result must have an ID')
def invoke(self):
""" The purpose of this method is to generate per-result invocation messages.
This method is used to identify that an action is being attempted on a per-result basis.
Remember to call update() prior to invoke() to ensure that the invocation message
reflects the appropriate identifiers.
"""
self.message('Invoking modular action')
def result2stash(self, result, dropexp=DEFAULT_DROPEXP, mapexp=DEFAULT_MAPEXP, addinfo=False):
""" The purpose of this method is to formulate an event in stash format
@param result: The result dictionary to generate a stash event for.
@param dropexp: A lambda expression used to determine whether a field
should be dropped or not.
Defaults to DEFAULT_DROPEXP.
@param mapexp: A lambda expression used to determine whether a field
should be mapped (prepended with "orig_") or not.
Defaults to DEFAULT_MAPEXP.
@param addinfo: Whether or not to add search information to the event.
"info" includes search_now, info_min_time, info_max_time,
and info_search_time fields.
Requires that information was loaded into the ModularAction
instance via addinfo()
@return _raw: Returns a string which represents the result in stash format.
The following example has been broken onto multiple lines for readability:
06/21/2016 10:00:00 -0700,
search_name="Access - Brute Force Access Behavior Detected - Rule",
search_now=0.000, info_min_time=1466528400.000, info_max_time=1466532600.000, info_search_time=1465296264.179,
key1=key1val, key2=key2val, key3=key3val, key4=key4val1, key4=key4val2, ...
"""
dropexp = dropexp or (lambda x: False)
mapexp = mapexp or (lambda x: False)
orig_dropexp = lambda x: x.startswith('orig_') and x[5:] in result and mapexp(x[5:])
## addinfo
if addinfo:
result['info_min_time'] = self.info.get('_search_et', '0.000')
info_max_time = self.info.get('_search_lt')
if not info_max_time or info_max_time==0 or info_max_time=='0':
info_max_time = '+Infinity'
result['info_max_time'] = info_max_time
result['info_search_time'] = self.info.get('_timestamp', '')
## construct _raw
_raw = '%s' % result.get('_time', mktimegm(time.gmtime()))
if self.search_name:
_raw += ', search_name="%s"' % self.search_name
processed_keys = []
for key, val in sorted(result.items()):
vals = []
## if we have a proper mv field
if (key.startswith('__mv_')
and val and isinstance(val, basestring)
and val.startswith('$') and val.endswith('$')):
real_key = key[5:]
vals = val[1:-1].split('$;$')
## if proper sv field
elif val and not key.startswith('__mv_'):
real_key = key
vals = [val]
## if we have vals and key hasn't been processed
## and key is not to be dropped...
if (vals
and (real_key not in processed_keys)
and not dropexp(real_key)
and not orig_dropexp(real_key)):
## iterate vals
for val in vals:
## format literal '$'
if key.startswith('__mv'):
val = val.replace('$$', '$')
## escape quotes
if isinstance(val, basestring):
val = val.replace('"', r'\"')
## check map
if mapexp(real_key):
_raw += ', %s="%s"' % ('orig_' + real_key.lstrip('_'), val)
else:
_raw += ', %s="%s"' % (real_key, val)
processed_keys.append(real_key)
return _raw
def addevent(self, raw, sourcetype, cam_header=True):
""" The purpose of this method is to add a properly constructed event
to the events list in the ModularAction instance. This ensures events
are created with the appropriate index-time header.
The index-time header is responsible for setting sourcetype,
orig_action_name, orig_sid, and orig_rid. The index-time header will
not be present in the _raw of generated events.
Remember to call update() prior to addevent() to ensure that the events
reflect the appropriate orig_sid and orig_rid identifiers.
@param raw: The text of the event you want to generate.
@param sourcetype: The sourcetype of the event you want to generate.
@param cam_header: Optionally exclude the inclusion of the index-time header.
Defaults to True (include header).
"""
if cam_header:
if self.orig_sid:
action_idline = ModularAction.DEFAULT_IDLINE % (
self.action_name,
self.orig_sid,
self.orig_rid,
sourcetype)
else:
action_idline = ModularAction.DEFAULT_IDLINE % (
self.action_name,
self.sid,
self.rid,
sourcetype)
self.events.append(action_idline + raw)
else:
self.events.append(raw)
def writeevents(self, index='summary', host=None, source=None, fext='common_action_model'):
""" The purpose of this method is to create arbitrary splunk events
from the list of events in the ModularAction instance.
Please use addevent() for populating the list of events in
the ModularAction instance.
@param index: The index to write the events to.
Defaults to "summary".
@param host: The value of host the events should take on.
Defaults to None (auto).
@param source: The value of source the events should take on.
Defaults to None (auto).
@param fext: The extension of the file to write out.
Files are written to $SPLUNK_HOME/var/spool/splunk.
File extensions can only contain word characters,
dash, and have a 200 char max.
"stash_" is automatically prepended to all extensions.
Defaults to "common_action_model" ("stash_common_action_model").
Only override if you've set up a corresponding props.conf
stanza to handle the extension.
@return bool: Returns True if all events were successfully written
Returns False if any errors were encountered
"""
## internal makeevents method for normalizing strings
## that will be used in the various headers we write out
def get_string(input, default):
try:
return input.replace('"', '_')
except AttributeError:
return default
if self.events:
## sanitize file extension
if not fext or not re.match('^[\w-]+$', fext):
self.logger.warn('Requested file extension was ignored due to invalid characters')
fext = 'common_action_model'
elif len(fext)>200:
self.logger.warn('Requested file extension was ignored due to length')
fext = 'common_action_model'
## header
header_line = ModularAction.DEFAULT_HEADER % (
get_string(index, ModularAction.DEFAULT_INDEX),
get_string(host, ''),
get_string(source, ''))
## process event chunks
for chunk in (self.events[x:x+ModularAction.DEFAULT_CHUNK]
for x in xrange(0, len(self.events), ModularAction.DEFAULT_CHUNK)):
## initialize output string
default_breaker = '\n' + ModularAction.DEFAULT_BREAKER
fout = header_line + default_breaker + (default_breaker).join(chunk)
## write output string
try:
fn = '%s_%s.stash_%s' % (mktimegm(time.gmtime()), random.randint(0, 100000), fext)
fp = make_splunkhome_path(['var', 'spool', 'splunk', fn])
## obtain fh
with open(fp, 'w') as fh:
fh.write(fout)
except:
signature = 'Error obtaining file handle during makeevents'
self.message(signature, level=logging.ERROR, file_path=fp)
self.logger.exception(signature + ' file_path=%s' % fp)
return False
self.message('Successfully created splunk events', event_count=len(self.events))
return True
return False
def dowork(self):
""" This method serves as an illustration stub.
Serves as a container for operations which satisfy the nature of the action.
For instance, the third party API call.
For cleanliness it is recommended that you subclass ModularAction
and implement your own dowork() method.
"""
return
@staticmethod
def setup_logger(name, level=logging.INFO, maxBytes=25000000, backupCount=5, format=SHORT_FORMAT):
""" Set up a logging instance.
@param name: The log file name.
We recommend "$action_name$_modalert".
@param level: The logging level.
@param maxBytes: The maximum log file size before rollover.
@param backupCount: The number of log files to retain.
@return logger: Returns an instance of logger
"""
logfile = make_splunkhome_path(['var', 'log', 'splunk', name + '.log'])
logger = logging.getLogger(name)
logger.setLevel(level)
logger.propagate = False # Prevent the log messages from being duplicated in the python.log file
# Prevent re-adding handlers to the logger object, which can cause duplicate log lines.
handler_exists = any([True for h in logger.handlers if h.baseFilename == logfile])
if not handler_exists:
file_handler = logging.handlers.RotatingFileHandler(logfile, maxBytes=maxBytes, backupCount=backupCount)
formatter = logging.Formatter(format)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
@@ -1,10 +0,0 @@
"""
APP Cloud Connect
"""
import os
from .common.lib_util import register_cacert_locater
register_cacert_locater(os.path.join(os.path.dirname(__file__), 'core', 'cacerts'))
__version__ = '1.0.1'
@@ -1,77 +0,0 @@
import copy
import os.path
import traceback
from .common.log import get_cc_logger
from .common.util import load_json_file
from .configuration import get_loader_by_version
from .core import CloudConnectEngine
from .core.exceptions import ConfigException
_logger = get_cc_logger()
class CloudConnectClient(object):
"""The client of cloud connect used to start a cloud connect engine instance.
"""
def __init__(self, context, config_file, checkpoint_mgr):
"""
Constructs a `CloudConnectClient` with `context` which contains variables
to render template in the configuration parsed from file `config_file`.
:param context: context to render template.
:param config_file: file path for load user passed interface.
"""
self._context = context
self._config_file = config_file
self._engine = None
self._config = None
self._checkpoint_mgr = checkpoint_mgr
def _load_config(self):
"""Load a JSON based configuration definition from file.
:return: A `dict` contains user defined JSON interface.
"""
try:
conf = load_json_file(self._config_file)
except:
raise ConfigException(
'Unable to load configuration file %s: %s'
% (self._config_file, traceback.format_exc())
)
version = conf.get('meta', {'apiVersion', None}).get('apiVersion', None)
if not version:
raise ConfigException(
'Config meta or api version not present in {}'.format(
self._config_file))
config_loader, schema_file = get_loader_by_version(version)
schema_path = os.path.join(
os.path.dirname(__file__), 'configuration', schema_file)
return config_loader.load(conf, schema_path, self._context)
def start(self):
"""
Initialize a new `CloudConnectEngine` instance and start it.
"""
try:
if self._config is None:
self._config = self._load_config()
self._engine = CloudConnectEngine()
self._engine.start(
context=copy.deepcopy(self._context),
config=self._config,
checkpoint_mgr=self._checkpoint_mgr
)
except Exception as ex:
_logger.exception('Error while starting client')
raise ex
def stop(self):
"""Stop the current cloud connect engine.
"""
if self._engine:
self._engine.stop()
@@ -1,54 +0,0 @@
import os
import os.path as op
import platform
import sys
import __main__
from ..splunktacollectorlib.common import log as stulog
def get_main_file():
"""Return the running mod input file"""
return __main__.__file__
def get_app_root_dir():
"""Return the root dir of app"""
return op.dirname(op.dirname(op.abspath(get_main_file())))
def get_mod_input_script_name():
"""Return the name of running mod input"""
script_name = os.path.basename(get_main_file())
if script_name.lower().endswith('.py'):
script_name = script_name[:-3]
return script_name
def register_module(new_path):
""" register_module(new_path): adds a directory to sys.path.
Do nothing if it does not exist or if it's already in sys.path.
"""
if not os.path.exists(new_path):
return
new_path = os.path.abspath(new_path)
if platform.system() == 'Windows':
new_path = new_path.lower()
for x in sys.path:
x = os.path.abspath(x)
if platform.system() == 'Windows':
x = x.lower()
if new_path in (x, x + os.sep):
return
sys.path.insert(0, new_path)
def register_cacert_locater(cacerts_locater_path):
for x in sys.modules:
if (x == "httplib2" or x.endswith(".httplib2")) and sys.modules[x] \
is not None:
stulog.logger.warning("Httplib2 module '{}' is already installed. "
"The ca_certs_locater may not work".format(x))
register_module(cacerts_locater_path)
@@ -1,32 +0,0 @@
import logging
from solnlib.pattern import Singleton
from ..splunktacollectorlib.common import log as stulog
class CloudClientLogAdapter(logging.LoggerAdapter):
__metaclass__ = Singleton
def __init__(self, logger=None, extra=None, prefix=""):
super(CloudClientLogAdapter, self).__init__(logger, extra)
self.cc_prefix = prefix if prefix else ""
def process(self, msg, kwargs):
msg = "{} {}".format(self.cc_prefix, msg)
return super(CloudClientLogAdapter, self).process(msg, kwargs)
def set_level(self, val):
self.logger.setLevel(val)
_adapter = CloudClientLogAdapter(stulog.logger)
def set_cc_logger(logger, logger_prefix=''):
global _adapter
_adapter.logger = logger
_adapter.cc_prefix = logger_prefix or ''
def get_cc_logger():
return _adapter
@@ -1,48 +0,0 @@
import json
from ..splunktalib.common import util
from solnlib.modular_input.event import XMLEvent
def is_valid_bool(val):
"""Check whether a string can be convert to bool.
:param val: value as string.
:return: `True` if value can be convert to bool else `False`.
"""
return util.is_true(val) or util.is_false(val)
def is_true(val):
return util.is_true(val)
def is_valid_port(port):
"""Check whether a port is valid.
:param port: port to check.
:return: `True` if port is valid else `False`.
"""
try:
return 1 <= int(port) <= 65535
except ValueError:
return False
def load_json_file(file_path):
"""
Load a dict from a JSON file.
:param file_path: JSON file path.
:return: A `dict` object.
"""
with open(file_path, 'r') as file_pointer:
return json.load(file_pointer)
def format_events(raw_events, time=None,
index=None, host=None, source=None, sourcetype=None,
stanza=None, unbroken=False, done=False):
return XMLEvent.format_events(XMLEvent(data, time=time,
index=index, host=host,
source=source,
sourcetype=sourcetype,
stanza=stanza, unbroken=unbroken,
done=done) for data in
raw_events)
@@ -1 +0,0 @@
from .loader import get_loader_by_version
@@ -1,300 +0,0 @@
import logging
import re
import traceback
from abc import abstractmethod
from jsonschema import validate, ValidationError
from munch import munchify
from ..common.log import get_cc_logger
from ..common.util import (
load_json_file, is_valid_bool, is_valid_port, is_true
)
from ..core.exceptions import ConfigException
from ..core.ext import lookup_method
from ..core.models import (
BasicAuthorization, Request, Processor,
Condition, Task, Checkpoint, IterationMode,
DictToken
)
_logger = get_cc_logger()
_PROXY_TYPES = ['http', 'socks4', 'socks5', 'http_no_tunnel']
_AUTH_TYPES = {
'basic_auth': BasicAuthorization
}
_LOGGING_LEVELS = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'FATAL': logging.FATAL,
'CRITICAL': logging.CRITICAL
}
# FIXME Make this configurable
_DEFAULT_LOG_LEVEL = 'INFO'
class CloudConnectConfigLoader(object):
"""The Base cloud connect configuration loader"""
@staticmethod
def _get_schema_from_file(schema_file):
""" Load JSON based schema definition from schema file path.
:return: A `dict` contains schema.
"""
try:
return load_json_file(schema_file)
except:
raise ConfigException(
'Cannot load schema from file {}: {}'.format(
schema_file, traceback.format_exc())
)
@abstractmethod
def load(self, definition, schema_file, context):
pass
class CloudConnectConfigLoaderV1(CloudConnectConfigLoader):
@staticmethod
def _render_from_dict(source, ctx):
rendered = DictToken(source).render(ctx)
return dict((k, v.strip() if isinstance(v, basestring) else v)
for k, v in rendered.iteritems())
def _load_proxy(self, candidate, variables):
"""
Render and validate proxy setting with given variables.
:param candidate: raw proxy setting as `dict`
:param variables: variables to render template in proxy setting.
:return: A `dict` contains rendered proxy setting.
"""
if not candidate:
return {}
proxy = self._render_from_dict(candidate, variables)
enabled = proxy.get('enabled', '0')
if not is_valid_bool(enabled):
raise ValueError(
'Proxy "enabled" expect to be bool type: {}'.format(enabled)
)
proxy['enabled'] = is_true(enabled)
host, port = proxy.get('host'), proxy.get('port')
if host or port:
if not host:
raise ValueError('Proxy "host" must not be empty')
if not is_valid_port(port):
raise ValueError(
'Proxy "port" expect to be in range [1,65535]: %s' % port
)
# proxy type default to 'http'
proxy_type = proxy.get('type')
proxy_type = proxy_type.lower() if proxy_type else 'http'
if proxy_type not in _PROXY_TYPES:
raise ValueError(
'Proxy "type" expect to be one of [{}]: {}'.format(
','.join(_PROXY_TYPES), proxy_type)
)
else:
proxy['type'] = proxy_type
# proxy rdns default to '0'
proxy_rdns = proxy.get('rdns', '0')
if not is_valid_bool(proxy_rdns):
raise ValueError(
'Proxy "rdns" expect to be bool type: {}'.format(proxy_rdns)
)
else:
proxy['rdns'] = is_true(proxy_rdns)
return proxy
@staticmethod
def _get_log_level(level_name):
if level_name:
level_name = level_name.upper().strip()
for k, v in _LOGGING_LEVELS.iteritems():
if k.startswith(level_name):
return v
_logger.warning(
'The log level "%s" is invalid, set it to default: "%s"',
level_name, _DEFAULT_LOG_LEVEL
)
return _LOGGING_LEVELS[_DEFAULT_LOG_LEVEL]
def _load_logging(self, log_setting, variables):
logger = self._render_from_dict(log_setting, variables)
logger['level'] = self._get_log_level(logger.get('level'))
return logger
def _load_global_setting(self, candidate, variables):
"""
Load and render global setting with variables.
:param candidate: Global setting as a `dict`
:param variables: variables from context to render setting
:return: A `Munch` object
"""
candidate = candidate or {}
proxy_setting = self._load_proxy(candidate.get('proxy'), variables)
log_setting = self._load_logging(candidate.get('logging'), variables)
return munchify({'proxy': proxy_setting, 'logging': log_setting})
@staticmethod
def _load_authorization(candidate):
if candidate is None:
return None
auth_type = candidate['type'].lower()
if auth_type not in _AUTH_TYPES:
raise ValueError(
'Auth type expect to be one of [{}]: {}'.format(
','.join(_AUTH_TYPES.keys()), auth_type)
)
return _AUTH_TYPES[auth_type](candidate['options'])
def _load_options(self, options):
return Request(
auth=self._load_authorization(options.get('auth')),
url=options['url'],
method=options.get('method', 'GET'),
header=options.get('headers', {}),
body=options.get('body', {})
)
@staticmethod
def _validate_method(method):
if lookup_method(method) is None:
raise ValueError('Unimplemented method: {}'.format(method))
def _parse_tasks(self, raw_tasks):
tasks = []
for item in raw_tasks:
self._validate_method(item['method'])
tasks.append(Task(item['input'], item['method'], item.get('output')))
return tasks
def _parse_conditions(self, raw_conditions):
conditions = []
for item in raw_conditions:
self._validate_method(item['method'])
conditions.append(Condition(item['input'], item['method']))
return conditions
@staticmethod
def _load_checkpoint(checkpoint):
if not checkpoint:
return None
return Checkpoint(
checkpoint.get('namespace', []), checkpoint['content'])
def _load_iteration_mode(self, iteration_mode):
count = iteration_mode.get('iteration_count', '0')
try:
iteration_count = int(count)
except ValueError:
raise ValueError(
'"iteration_count" must be an integer: %s' % count)
stop_conditions = self._parse_conditions(
iteration_mode['stop_conditions'])
return IterationMode(iteration_count=iteration_count,
conditions=stop_conditions)
def _load_processor(self, processor):
skip_conditions = self._parse_conditions(
processor.get('skip_conditions', [])
)
pipeline = self._parse_tasks(processor.get('pipeline', []))
return Processor(
skip_conditions=skip_conditions,
pipeline=pipeline
)
def _load_request(self, request):
options = self._load_options(request['request'])
pre_process = self._load_processor(request.get('pre_process', {}))
post_process = self._load_processor(request['post_process'])
checkpoint = self._load_checkpoint(request.get('checkpoint'))
iteration_mode = self._load_iteration_mode(request['iteration_mode'])
return munchify({
'request': options,
'pre_process': pre_process,
'post_process': post_process,
'checkpoint': checkpoint,
'iteration_mode': iteration_mode,
})
def load(self, definition, schema_file, context):
"""Load cloud connect configuration from a `dict` and validate
it with schema and global settings will be rendered.
:param schema_file: Schema file location used to validate config.
:param definition: A dictionary contains raw configs.
:param context: variables to render template in global setting.
:return: A `Munch` object.
"""
try:
validate(definition, self._get_schema_from_file(schema_file))
except ValidationError:
raise ConfigException(
'Failed to validate interface with schema: {}'.format(
traceback.format_exc()))
try:
global_settings = self._load_global_setting(
definition.get('global_settings'), context
)
requests = [self._load_request(item) for item in definition['requests']]
return munchify({
'meta': munchify(definition['meta']),
'tokens': definition['tokens'],
'global_settings': global_settings,
'requests': requests,
})
except Exception as ex:
error = 'Unable to load configuration: %s' % str(ex)
_logger.exception(error)
raise ConfigException(error)
_loader_and_schema_by_version = {
r'1\.0\.0': (CloudConnectConfigLoaderV1, 'schema_1_0_0.json'),
}
def get_loader_by_version(version):
""" Instantiate a configuration loader on basis of a given version.
A `ConfigException` will raised if the version is not supported.
:param version: Version to lookup config loader.
:return: A config loader.
"""
for support_version in _loader_and_schema_by_version:
if re.match(support_version, version):
loader_cls, schema = _loader_and_schema_by_version[support_version]
return loader_cls(), schema
raise ConfigException(
'Unsupported schema version {}, current supported'
' versions should match these regex [{}]'.format(version, ','.join(
_loader_and_schema_by_version))
)
@@ -1,344 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": false,
"definitions": {
"Authentication": {
"additionalProperties": false,
"properties": {
"options": {
"type": "object"
},
"type": {
"enum": [
"digest",
"basic_auth"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"Checkpoint": {
"additionalProperties": false,
"properties": {
"content": {
"type": "object"
},
"namespace": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"content"
],
"type": "object"
},
"GlobalSettings": {
"additionalProperties": false,
"properties": {
"logging": {
"properties": {
"level": {
"type": "string"
}
},
"type": "object"
},
"proxy": {
"$ref": "#/definitions/Proxy"
}
},
"type": "object"
},
"IterationMode": {
"additionalProperties": false,
"properties": {
"iteration_count": {
"oneOf": [
{
"pattern": "^[+-]?[1-9]\\d*|0$",
"type": "string"
},
{
"type": "integer"
}
]
},
"stop_conditions": {
"items": {
"additionalProperties": false,
"properties": {
"input": {
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"type": "string"
}
},
"required": [
"input",
"method"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"Meta": {
"additionalProperties": false,
"properties": {
"apiVersion": {
"pattern": "(?:\\d{1,3}\\.){2}[\\w\\-]{1,15}",
"type": "string"
}
},
"required": [
"apiVersion"
],
"type": "object"
},
"Proxy": {
"additionalProperties": false,
"properties": {
"enabled": {
"default": false,
"oneOf": [
{
"type": "string"
},
{
"type": "boolean"
},
{
"type": "integer"
}
]
},
"host": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"oneOf": [
{
"type": "string"
},
{
"exclusiveMaximum": true,
"exclusiveMinimum": true,
"maximum": 65535,
"minimum": 1,
"type": "integer"
}
]
},
"rdns": {
"type": "string"
},
"type": {
"type": "string"
},
"username": {
"type": "string"
}
},
"required": [
"host",
"port"
],
"type": "object"
},
"Request": {
"additionalProperties": false,
"properties": {
"checkpoint": {
"$ref": "#/definitions/Checkpoint"
},
"iteration_mode": {
"$ref": "#/definitions/IterationMode"
},
"post_process": {
"additionalProperties": false,
"properties": {
"pipeline": {
"items": {
"additionalProperties": false,
"properties": {
"input": {
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"type": "string"
},
"output": {
"type": "string"
}
},
"required": [
"input",
"method"
],
"type": "object"
},
"type": "array"
},
"skip_conditions": {
"items": {
"additionalProperties": false,
"properties": {
"input": {
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"type": "string"
}
},
"required": [
"input",
"method"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"pre_process": {
"additionalProperties": false,
"properties": {
"pipeline": {
"items": {
"additionalProperties": false,
"properties": {
"input": {
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"type": "string"
},
"output": {
"type": "string"
}
},
"required": [
"input",
"method"
],
"type": "object"
},
"type": "array"
},
"skip_conditions": {
"items": {
"additionalProperties": false,
"properties": {
"input": {
"items": {
"type": "string"
},
"type": "array"
},
"method": {
"type": "string"
}
},
"required": [
"input",
"method"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"request": {
"additionalProperties": false,
"properties": {
"auth": {
"$ref": "#/definitions/Authentication"
},
"body": {
"type": "object"
},
"headers": {
"type": "object"
},
"method": {
"default": "GET",
"enum": [
"GET",
"POST"
],
"type": "string"
},
"url": {
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
}
},
"required": [
"request",
"post_process",
"iteration_mode"
],
"type": "object"
}
},
"properties": {
"global_settings": {
"$ref": "#/definitions/GlobalSettings"
},
"meta": {
"$ref": "#/definitions/Meta"
},
"requests": {
"items": {
"$ref": "#/definitions/Request"
},
"minItems": 1,
"type": "array"
},
"tokens": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"meta",
"tokens",
"requests"
],
"type": "object"
}
@@ -1,2 +0,0 @@
from .engine import CloudConnectEngine
from .exceptions import ConfigException, HTTPError
@@ -1,130 +0,0 @@
"""
`ca_certs_locater` is a lib for extending httplib2 to allow system certificate store to be used when
verifying SSL certificates, to enable this lib, you should add it to your python import path before
initializing httplib2. As we're not trying to implement SSL certificate RFCs, parsing and validating
certificates are not included.
"""
import atexit
import os
import os.path as op
import ssl
import sys
TEMP_CERT_FILE_NAME = 'httplib2_merged_certificates_{}.crt'
LINUX_CERT_PATH_1 = '/etc/pki/tls/certs/ca-bundle.crt' # RedHat
LINUX_CERT_PATH_2 = '/etc/ssl/certs/ca-certificates.crt' # Debian
DARWIN_CERT_PATH = '/usr/local/etc/openssl/cert.pem'
HTTPLIB2_CA_CERT_FILE_NAME = 'cacerts.txt'
TEMP_CERT_FILE_PATH = None
def get():
"""
Returns: a path to generated certificate authority file
"""
try:
return _get()
except (IOError, OSError, ssl.SSLError):
_fallback() # IO and SSL relative errors should be swallowed to protect the HTTP request
def _get():
global TEMP_CERT_FILE_PATH
# also check file existence as it's possible for the temp file to be deleted
if TEMP_CERT_FILE_PATH is None or not os.path.exists(TEMP_CERT_FILE_PATH):
temp_cert_file_path = _generate_temp_cert_file_name()
ssl_ca_certs = _read_ssl_default_ca_certs()
if not ssl_ca_certs:
# it's possible the ca load path is not well configured, try some typical paths
ssl_ca_certs = _read_platform_pem_cert_file()
if ssl_ca_certs: # only update temp cert file when there's additional PEM certs found
cert_files = [ssl_ca_certs, _read_httplib2_default_certs()]
_update_temp_cert_file(temp_cert_file_path, cert_files)
TEMP_CERT_FILE_PATH = temp_cert_file_path
else:
_fallback()
return TEMP_CERT_FILE_PATH
def _fallback():
"""
Give up the loading process by throwing specified exception, httplib2 will then use its
bundled certificates
"""
raise ImportError('Unable to load system certificate authority files')
def _read_platform_pem_cert_file():
if sys.platform.startswith('linux'):
pem_files = [_read_pem_file(LINUX_CERT_PATH_1), _read_pem_file(LINUX_CERT_PATH_2)]
return '\n'.join(filter(None, pem_files))
elif sys.platform.startswith('darwin'):
return _read_pem_file(DARWIN_CERT_PATH)
else:
return ""
def _read_ssl_default_ca_certs():
# it's not guaranteed to return PEM formatted certs when `binary_form` is False
der_certs = ssl.create_default_context().get_ca_certs(binary_form=True)
pem_certs = [ssl.DER_cert_to_PEM_cert(der_cert_bytes) for der_cert_bytes in der_certs]
return '\n'.join(pem_certs)
def _read_httplib2_default_certs():
import httplib2 # import error should not happen here, and will be well handled by outer called
httplib_dir = os.path.dirname(os.path.abspath(httplib2.__file__))
ca_certs_path = os.path.join(httplib_dir, HTTPLIB2_CA_CERT_FILE_NAME)
return _read_pem_file(ca_certs_path)
def _read_pem_file(path):
if os.path.exists(path):
with open(path, mode='r') as pem_file:
return pem_file.read()
else:
return ""
def _update_temp_cert_file(temp_file, pem_texts):
with open(temp_file, mode='w') as temp_cert_file:
for pem_text in pem_texts:
if len(pem_text) > 0:
temp_cert_file.write(pem_text + '\n')
temp_cert_file.flush()
atexit.register(_do_safe_remove, temp_file)
def _do_safe_remove(file_path):
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
def _get_temp_cert_file_dir():
import __main__
app_root = op.dirname(op.dirname(op.abspath(__main__.__file__)))
temp_dir = op.join(app_root, 'temp_certs')
if not op.isdir(temp_dir):
try:
os.mkdir(temp_dir)
except:
pass
for candidate in ['temp_certs', 'local', 'default']:
dir_path = op.join(app_root, candidate)
if op.isdir(dir_path):
return dir_path
return app_root
def _generate_temp_cert_file_name():
file_name = TEMP_CERT_FILE_NAME.format(os.getpid())
return os.path.join(_get_temp_cert_file_dir(), file_name)
@@ -1,20 +0,0 @@
"""Default config for cloud connect"""
timeout = 120 # request timeout is two minutes
disable_ssl_cert_validation = False # default enable SSL validation
success_statuses = (200, 201) # statuses be treated as success.
# response status which need to retry.
retry_statuses = (429, 500, 501, 502, 503, 504, 505, 506, 507,
509, 510, 511)
# response status which need print a warning log.
warning_statuses = (203, 204, 205, 206, 207, 208, 226,
300, 301, 302, 303, 304, 305, 306, 307, 308)
retries = 3 # Default maximum retry times.
max_iteration_count = 100 # maximum iteration loop count
charset = 'utf-8' # Default response charset if not found in response header
@@ -1,326 +0,0 @@
import json
import threading
from . import defaults
from .exceptions import HTTPError, StopCCEIteration
from .http import HTTPRequest
from ..common.log import get_cc_logger
_logger = get_cc_logger()
class CloudConnectEngine(object):
"""The cloud connect engine to process request instantiated
from user options."""
def __init__(self):
self._stopped = False
self._running_job = None
@staticmethod
def _set_logging(log_setting):
_logger.set_level(log_setting.level)
def start(self, context, config, checkpoint_mgr):
"""Start current client instance to execute each request parsed
from config.
"""
if not config:
raise ValueError('Config must not be empty')
context = context or {}
global_setting = config.global_settings
CloudConnectEngine._set_logging(global_setting.logging)
_logger.info('Start to execute requests jobs.')
processed = 0
for request in config.requests:
job = Job(
request=request,
context=context,
checkpoint_mgr=checkpoint_mgr,
proxy=global_setting.proxy,
)
self._running_job = job
job.run()
processed += 1
_logger.info('%s job(s) process finished', processed)
if self._stopped:
_logger.info(
'Engine has been stopped, stopping to execute jobs.')
break
self._stopped = True
_logger.info('Engine executing finished')
def stop(self):
"""Stops engine and running job. Do nothing if engine already
been stopped."""
if self._stopped:
_logger.info('Engine already stopped, do nothing.')
return
_logger.info('Stopping engine')
if self._running_job:
_logger.info('Attempting to stop the running job.')
self._running_job.terminate()
_logger.info('Stopping job finished.')
self._stopped = True
class Job(object):
"""Job class represents a single request to send HTTP request until
reached it's stop condition.
"""
def __init__(self, request, context, checkpoint_mgr, proxy=None):
"""
Constructs a `Job` with properties request, context and a
optional proxy setting.
:param request: A `Request` instance which contains request settings.
:param context: A values set contains initial values for template
variables.
:param proxy: A optional `Proxy` object contains proxy related
settings.
"""
self._request = request
self._context = context
self._checkpoint_mgr = checkpoint_mgr
self._client = HTTPRequest(proxy)
self._stopped = True
self._should_stop = False
self._request_iterated_count = 0
self._iteration_mode = self._request.iteration_mode
self._max_iteration_count = self._get_max_iteration_count()
self._running_thread = None
self._terminated = threading.Event()
def _get_max_iteration_count(self):
mode_max_count = self._iteration_mode.iteration_count
default_max_count = defaults.max_iteration_count
return min(default_max_count, mode_max_count) \
if mode_max_count > 0 else default_max_count
def terminate(self, block=True, timeout=30):
"""Terminate this job, the current thread will blocked util
the job is terminate finished if block is True """
if self.is_stopped():
_logger.info('Job already been stopped.')
return
if self._running_thread == threading.current_thread():
_logger.warning('Job cannot terminate itself.')
return
_logger.info('Stopping job')
self._should_stop = True
if not block:
return
if not self._terminated.wait(timeout):
_logger.warning('Terminating job timeout.')
def _set_context(self, key, value):
self._context[key] = value
def _execute_tasks(self, tasks):
if not tasks:
return
for task in tasks:
if self._check_should_stop():
return
self._context.update(task.execute(self._context))
def _on_pre_process(self):
"""
Execute tasks in pre process one by one if condition satisfied.
"""
pre_processor = self._request.pre_process
if pre_processor.should_skipped(self._context):
_logger.info('Skip pre process condition satisfied, do nothing')
return
tasks = pre_processor.pipeline
_logger.debug(
'Got %s tasks need be executed before process', len(tasks))
self._execute_tasks(tasks)
def _on_post_process(self):
"""
Execute tasks in post process one by one if condition satisfied.
"""
post_processor = self._request.post_process
if post_processor.should_skipped(self._context):
_logger.info('Skip post process condition satisfied, '
'do nothing')
return
tasks = post_processor.pipeline
_logger.debug(
'Got %s tasks need to be executed after process', len(tasks)
)
self._execute_tasks(tasks)
def _update_checkpoint(self):
"""Updates checkpoint based on checkpoint namespace and content."""
checkpoint = self._request.checkpoint
if not checkpoint:
_logger.info('Checkpoint not specified, do not update it.')
return
self._checkpoint_mgr.update_ckpt(
checkpoint.normalize_content(self._context),
namespaces=checkpoint.normalize_namespace(self._context),
)
def _get_checkpoint(self):
checkpoint = self._request.checkpoint
if not checkpoint:
_logger.info('Checkpoint not specified, do not read it.')
return
namespaces = checkpoint.normalize_namespace(self._context)
checkpoint = self._checkpoint_mgr.get_ckpt(namespaces)
if checkpoint:
self._context.update(checkpoint)
def _is_stoppable(self):
"""Check if repeat mode conditions satisfied."""
if self._request_iterated_count >= self._max_iteration_count:
_logger.info(
'Job iteration count is %s, current request count is %s,'
' stop condition satisfied.',
self._max_iteration_count, self._request_iterated_count
)
return True
if self._iteration_mode.passed(self._context):
_logger.info('Job stop condition satisfied.')
return True
return False
def is_stopped(self):
"""Return if this job is stopped."""
return self._stopped
def run(self):
"""Start job and exit util meet stop condition. """
_logger.info('Start to process job')
self._stopped = False
try:
self._running_thread = threading.current_thread()
self._run()
except Exception:
_logger.exception('Error encountered while running job.')
raise
finally:
self._terminated.set()
self._stopped = True
_logger.info('Job processing finished')
def _check_should_stop(self):
if self._should_stop:
_logger.info('Job should been stopped.')
return self._should_stop
def _run(self):
request = self._request.request
method = request.method
authorizer = request.auth
self._get_checkpoint()
while 1:
if self._check_should_stop():
return
try:
self._on_pre_process()
except StopCCEIteration:
_logger.info('Stop iteration command in pre process is received, exit job now.')
return
url = request.normalize_url(self._context)
header = request.normalize_header(self._context)
body = request.normalize_body(self._context)
body_json = json.dumps(body) if body else None
if authorizer:
authorizer(header, self._context)
if self._check_should_stop():
return
response, need_terminate = \
self._send_request(url, method, header, body=body_json)
if need_terminate:
_logger.info('This job need to be terminated.')
break
self._request_iterated_count += 1
self._set_context('__response__', response)
if self._check_should_stop():
return
try:
self._on_post_process()
except StopCCEIteration:
_logger.info('Stop iteration command in post process is received, exit job now.')
return
if self._check_should_stop():
return
self._update_checkpoint()
if self._is_stoppable():
_logger.info('Stop condition reached, exit job now')
break
def _send_request(self, url, method, header, body):
"""Do send request with a simple error handling strategy."""
try:
response = self._client.request(
url, method, headers=header, body=body
)
except HTTPError as error:
_logger.exception(
'HTTPError reason=%s when sending request to '
'url=%s method=%s', error.reason, url, method)
return None, True
status = response.status_code
if status in defaults.success_statuses:
if not (response.body or '').strip():
_logger.info(
'The response body of request which url=%s and'
' method=%s is empty, status=%s.',
url, method, status
)
return None, True
return response, False
error_log = ('The response status=%s for request which url=%s and'
' method=%s.') % (
status, url, method
)
if status in defaults.warning_statuses:
_logger.warning(error_log)
else:
_logger.error(error_log)
return None, True
@@ -1,27 +0,0 @@
"""APP Cloud Connect errors"""
class ConfigException(Exception):
"""Config exception"""
pass
class FuncException(Exception):
"""Ext function call exception"""
pass
class HTTPError(Exception):
""" HTTPError raised when HTTP request returned a error."""
def __init__(self, reason=None):
"""
Initialize HTTPError with `response` object and `status`.
"""
self.reason = reason
super(HTTPError, self).__init__(reason)
class StopCCEIteration(Exception):
"""Exception to exit from the engine iteration."""
pass
@@ -1,336 +0,0 @@
import calendar
import json
import re
import traceback
from datetime import datetime
from jsonpath_rw import parse
from .exceptions import FuncException, StopCCEIteration
from .pipemgr import PipeManager
from ..common import util, log
_logger = log.get_cc_logger()
def regex_match(pattern, source, flags=0):
"""
Determine whether a string is match a regex pattern.
:param pattern: regex pattern
:param source: candidate to match regex
:param flags: flags for regex match
:return: `True` if candidate match pattern else `False`
"""
try:
return re.match(pattern, source, flags) is not None
except Exception:
_logger.warning(
'Unable to match source with pattern=%s, cause=%s',
pattern,
traceback.format_exc()
)
return False
def regex_not_match(pattern, source, flags=0):
"""
Determine whether a string is not match a regex pattern.
:param pattern: regex expression
:param source: candidate to match regex
:param flags: flags for regex match
:return: `True` if candidate not match pattern else `False`
"""
return not regex_match(pattern, source, flags)
def json_path(source, json_path_expr):
""" Extract value from string with JSONPATH expression.
:param json_path_expr: JSONPATH expression
:param source: string to extract value
:return: A `list` contains all values extracted
"""
if not source:
_logger.debug('source to apply JSONPATH is empty, return empty.')
return ''
if isinstance(source, basestring):
_logger.debug(
'source expected is a JSON, not %s. Attempt to'
' convert it to JSON',
type(source)
)
try:
source = json.loads(source)
except Exception as ex:
_logger.warning(
'Unable to load JSON from source: %s. '
'Attempt to apply JSONPATH "%s" on source directly.',
ex.message,
json_path_expr
)
try:
expression = parse(json_path_expr)
results = [match.value for match in expression.find(source)]
_logger.debug(
'Got %s elements extracted with JSONPATH expression "%s"',
len(results), json_path_expr
)
if not results:
return ''
return results[0] or '' if len(results) == 1 else results
except Exception as ex:
_logger.warning(
'Unable to apply JSONPATH expression "%s" on source,'
' message=%s cause=%s',
json_path_expr,
ex.message,
traceback.format_exc()
)
return ''
def splunk_xml(candidates,
time=None,
index=None,
host=None,
source=None,
sourcetype=None):
""" Wrap a event with splunk xml format.
:param candidates: data used to wrap as event
:param time: timestamp which must be empty or a valid float
:param index: index name for event
:param host: host for event
:param source: source for event
:param sourcetype: sourcetype for event
:return: A wrapped event with splunk xml format.
"""
if not isinstance(candidates, (list, tuple)):
candidates = [candidates]
time = time or None
if time:
try:
time = float(time)
except ValueError:
_logger.warning(
'"time" %s is expected to be a float, set "time" to None',
time
)
time = None
return util.format_events(
candidates,
time=time,
index=index,
host=host,
source=source,
sourcetype=sourcetype
)
def std_output(candidates):
""" Output a string to stdout.
:param candidates: List of string to output to stdout or a single string.
"""
if isinstance(candidates, basestring):
candidates = [candidates]
all_str = True
for candidate in candidates:
if all_str and not isinstance(candidate, basestring):
all_str = False
_logger.warning(
'The type of data needs to print is "%s" rather than'
' basestring',
type(candidate)
)
if not PipeManager().write_events(candidate):
raise FuncException('Fail to output data to stdout. The event'
' writer is stopped or encountered exception')
_logger.debug('Writing events to stdout finished.')
return True
def _parse_json(source, json_path_expr=None):
if not source:
_logger.debug('Unable to parse JSON from empty source, return empty.')
return {}
if json_path_expr:
_logger.debug(
'Try to extract JSON from source with JSONPATH expression: %s, ',
json_path_expr
)
source = json_path(source, json_path_expr)
elif isinstance(source, basestring):
source = json.loads(source)
return source
def json_empty(source, json_path_expr=None):
"""Check whether a JSON is empty, return True only if the JSON to
check is a valid JSON and is empty.
:param json_path_expr: A optional JSONPATH expression
:param source: source to extract JSON
:return: `True` if the result JSON is empty
"""
try:
data = _parse_json(source, json_path_expr)
if isinstance(data, (list, tuple)):
return all(len(ele) == 0 for ele in data)
return len(data) == 0
except Exception as ex:
_logger.warning(
'Unable to determine whether source is json_empty, treat it as '
'not json_empty: %s', ex.message
)
return False
def json_not_empty(source, json_path_expr=None):
"""Check if a JSON object is not empty, return True only if the
source is a valid JSON object and the value leading by
json_path_expr is empty.
:param json_path_expr: A optional JSONPATH expression
:param source: source to extract JSON
:return: `True` if the result JSON is not empty
"""
try:
data = _parse_json(source, json_path_expr)
if isinstance(data, (list, tuple)):
return any(len(ele) > 0 for ele in data)
return len(data) > 0
except Exception as ex:
_logger.warning(
'Unable to determine whether source is json_not_empty, '
'treat it as not json_not_empty: %s',
ex.message
)
return False
def set_var(value):
"""Set a variable which name should be specified in `output` with value"""
return value
def _fix_microsecond_format(fmt, micros):
"""
implement %Nf so that user can control the digital number of microsecond.
If number of % is even, don't do replacement.
If N is not in [1-6], don't do replacement.
If time length m is less than N, convert it to 6 digitals and return N
digitals.
"""
micros = str(micros).zfill(6)
def do_replacement(x, micros):
if int(x.group(1)) in range(1, 7) and len(x.group()) % 2:
return x.group().replace('%' + x.group(1) + 'f',
micros[:min(int(x.group(1)), len(micros))])
return x.group()
return re.sub(r'%+([1-6])f', lambda x: do_replacement(x, micros), fmt)
def _fix_timestamp_format(fmt, timestamp):
"""Replace '%s' in time format with timestamp if the number
of '%' before 's' is odd."""
return re.sub(
r'%+s',
(
lambda x:
x.group() if len(x.group()) % 2 else x.group().replace('%s',
timestamp)
),
fmt
)
def time_str2str(date_string, from_format, to_format):
"""Convert a date string with given format to another format. Return
the original date string if it's type is not string or failed to parse or
convert it with format."""
if not isinstance(date_string, basestring):
_logger.warning(
'"date_string" must be a string type, found %s,'
' return the original date_string directly.',
type(date_string)
)
return date_string
try:
dt = datetime.strptime(date_string, from_format)
# Need to pre process '%s' in to_format here because '%s' is not
# available on all platforms. Even on supported platforms, the
# result may be different because it depends on implementation on each
# platform. Replace it with UTC timestamp here directly.
if to_format:
timestamp = calendar.timegm(dt.timetuple())
to_format = _fix_timestamp_format(to_format, str(timestamp))
to_format = _fix_microsecond_format(to_format, str(dt.microsecond))
return dt.strftime(to_format)
except Exception:
_logger.warning(
'Unable to convert date_string "%s" from format "%s" to "%s",'
' return the original date_string, cause=%s',
date_string,
from_format,
to_format,
traceback.format_exc()
)
return date_string
def is_true(value):
"""Determine whether value is True"""
return str(value).strip().lower() == 'true'
def exit_if_true(value):
"""Raise a StopCCEIteration exception if value is True"""
if is_true(value):
raise StopCCEIteration
def assert_true(value, message=None):
"""Assert value is True"""
if not is_true(value):
raise AssertionError(
message or '"{value}" is not true'.format(value=value)
)
_extension_functions = {
'assert_true': assert_true,
'exit_if_true': exit_if_true,
'is_true': is_true,
'regex_match': regex_match,
'regex_not_match': regex_not_match,
'set_var': set_var,
'splunk_xml': splunk_xml,
'std_output': std_output,
'json_path': json_path,
'json_empty': json_empty,
'json_not_empty': json_not_empty,
'time_str2str': time_str2str,
}
def lookup_method(name):
""" Find a predefined function with given function name.
:param name: function name.
:return: A function with given name.
"""
return _extension_functions.get(name)
@@ -1,234 +0,0 @@
import time
import traceback
from httplib2 import ProxyInfo, Http, socks, SSLHandshakeError
from solnlib.packages.requests import PreparedRequest, utils
from . import defaults
from .exceptions import HTTPError
from ..common.log import get_cc_logger
_logger = get_cc_logger()
class HTTPResponse(object):
"""
HTTPResponse class wraps response of HTTP request for later use.
"""
def __init__(self, response, content):
"""Construct a HTTPResponse from response and content returned
with httplib2 request"""
self._status_code = response.status
self._header = response
self._body = self._decode_content(response, content)
@staticmethod
def _decode_content(response, content):
if not content:
return ''
charset = utils.get_encoding_from_headers(response)
if charset is None:
charset = defaults.charset
_logger.info(
'Unable to find charset in response headers,'
' set it to default "%s"', charset
)
_logger.info('Decoding response content with charset=%s', charset)
try:
return content.decode(charset, errors='replace')
except Exception as ex:
_logger.warning(
'Failure decoding response content with charset=%s,'
' decode it with utf-8: %s',
charset, ex.message
)
return content.decode('utf-8', errors='replace')
@property
def header(self):
return self._header
@property
def body(self):
"""
Return response body as a `string`.
:return: A `string`
"""
return self._body
@property
def status_code(self):
"""
Return response status code.
:return: A `integer`
"""
return self._status_code
def _make_prepare_url_func():
"""Expose prepare_url in `PreparedRequest`"""
pr = PreparedRequest()
def prepare_url(url, params=None):
"""Prepare the given HTTP URL with ability provided in requests lib.
For some illegal characters in URL or parameters like space(' ') will
be escaped to make sure we can request the correct URL."""
pr.prepare_url(url, params=params)
return pr.url
return prepare_url
class HTTPRequest(object):
"""
HTTPRequest class represents a single request to send HTTP request until
reached it's stop condition.
"""
_PROXY_TYPE = {
'http': socks.PROXY_TYPE_HTTP,
'http_no_tunnel': socks.PROXY_TYPE_HTTP_NO_TUNNEL,
'socks4': socks.PROXY_TYPE_SOCKS4,
'socks5': socks.PROXY_TYPE_SOCKS5,
}
def __init__(self, proxy=None):
"""Constructs a `HTTPRequest` with a optional proxy setting.
:param proxy: A optional `Proxy` object contains proxy related
settings.
"""
self._proxy_info = self._prepare_proxy_info(proxy)
self._connection = None
self._prepare_url_func = _make_prepare_url_func()
def _send_request(self, uri, method, headers=None, body=None):
"""Do send request to target URL and validate SSL cert by default.
If validation failed, disable it and try again."""
if self._connection is None:
self._connection = self._build_http_connection(
proxy_info=self._proxy_info,
disable_ssl_cert_validation=False)
try:
return self._connection.request(
uri, body=body, method=method, headers=headers
)
except SSLHandshakeError:
_logger.warning(
"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verification failed. "
"The certificate of the https server [%s] is not trusted, "
"this add-on will proceed to connect with this certificate. "
"You may need to check the certificate and "
"refer to the documentation and add it to the trust list. %s",
uri,
traceback.format_exc()
)
self._connection = self._build_http_connection(
proxy_info=self._proxy_info,
disable_ssl_cert_validation=True
)
return self._connection.request(
uri, body=body, method=method, headers=headers
)
def request(self, url, method='GET', headers=None, body=None):
"""
Invoke a request with httplib2 and return it's response.
:param url: url address to send request to.
:param method: request method `GET` by default.
:param headers: request headers.
:param body: request body.
:return: A `HTTPResponse` object.
"""
if body and not isinstance(body, str):
raise TypeError('Request body type must be str')
if self._connection is None:
self._connection = self._build_http_connection(self._proxy_info)
try:
uri = self._prepare_url_func(url)
except Exception:
_logger.warning(
'Failed to encode url=%s: %s, use original url directly',
url, traceback.format_exc()
)
uri = url
_logger.info('Preparing to invoke request to [%s]', uri)
result = self._do_request(uri, method, headers, body)
_logger.info('Invoking request to [%s] finished', uri)
return result
def _prepare_proxy_info(self, proxy):
if not proxy or not proxy.enabled:
_logger.debug('Proxy is not enabled')
return None
username = proxy.username \
if 'username' in proxy and proxy.username else None
password = proxy.password \
if 'password' in proxy and proxy.password else None
proxy_type = self._PROXY_TYPE.get(proxy.type) or self._PROXY_TYPE['http']
return ProxyInfo(proxy_host=proxy.host,
proxy_port=int(proxy.port),
proxy_type=proxy_type,
proxy_user=username,
proxy_pass=password,
proxy_rdns=proxy.rdns)
@staticmethod
def _build_http_connection(
proxy_info=None,
timeout=defaults.timeout,
disable_ssl_cert_validation=defaults.disable_ssl_cert_validation):
return Http(
proxy_info=proxy_info,
timeout=timeout,
disable_ssl_certificate_validation=disable_ssl_cert_validation)
@staticmethod
def _is_need_retry(status, retried, maximum_retries):
return retried < maximum_retries \
and status in defaults.retry_statuses
def _do_request(self, uri, method='GET', headers=None, body=None):
"""Invokes request and auto retry with an exponential backoff
if the response status is configured in defaults.retry_statuses."""
retries = max(defaults.retries, 0)
for i in xrange(retries + 1):
try:
response, content = self._send_request(
uri, body=body, method=method, headers=headers
)
except Exception as err:
_logger.exception(
'Could not send request url=%s method=%s', uri, method)
raise HTTPError('HTTP Error %s' % str(err))
status = response.status
if self._is_need_retry(status, i, retries):
delay = 2 ** i
_logger.warning(
'The response status=%s of request which url=%s and'
' method=%s. Retry after %s seconds.',
status, uri, method, delay,
)
time.sleep(delay)
continue
return HTTPResponse(response, content)
@@ -1,277 +0,0 @@
import base64
import traceback
from .ext import lookup_method
from .template import compile_template
from ..common.log import get_cc_logger
_logger = get_cc_logger()
class _Token(object):
"""Token class wraps a template expression"""
def __init__(self, source):
"""Constructs _Token from source. A rendered template
will be created if source is string type because Jinja
template must be a string."""
self._source = source
self._value_for = compile_template(source) \
if isinstance(source, basestring) else None
def render(self, variables):
"""Render value with variables if source is a string.
Otherwise return source directly."""
if self._value_for is None:
return self._source
try:
return self._value_for(variables)
except Exception as ex:
_logger.warning(
'Unable to render template "%s". Please make sure template is'
' a valid Jinja2 template and token is exist in variables. '
'message=%s cause=%s',
self._source,
ex.message,
traceback.format_exc()
)
return self._source
class DictToken(object):
"""DictToken wraps a dict which value is template expression"""
def __init__(self, template_expr):
self._tokens = {k: _Token(v)
for k, v in (template_expr or {}).iteritems()}
def render(self, variables):
return {k: v.render(variables) for k, v in self._tokens.iteritems()}
class BaseAuth(object):
"""A base class for all authorization classes"""
def __call__(self, headers, context):
raise NotImplementedError('Auth must be callable.')
class BasicAuthorization(BaseAuth):
"""BasicAuthorization class implements basic auth"""
def __init__(self, options):
if not options:
raise ValueError('Options for basic auth unexpected to be empty')
username = options.get('username')
if not username:
raise ValueError('Username is mandatory for basic auth')
password = options.get('password')
if not password:
raise ValueError('Password is mandatory for basic auth')
self._username = _Token(username)
self._password = _Token(password)
def __call__(self, headers, context):
username = self._username.render(context)
password = self._password.render(context)
headers['Authorization'] = 'Basic %s' % base64.encodestring(
username + ':' + password
).strip()
class Request(object):
def __init__(self, url, method, header=None, auth=None, body=None):
self._header = DictToken(header)
self._url = _Token(url)
self._method = method.upper()
self._auth = auth
self._body = DictToken(body)
@property
def header(self):
return self._header
@property
def url(self):
return self._url
@property
def method(self):
return self._method
@property
def auth(self):
return self._auth
@property
def body(self):
return self._body
def normalize_url(self, context):
"""Normalize url"""
return self._url.render(context)
def normalize_header(self, context):
"""Normalize headers which must be a dict which keys and values are
string."""
header = self.header.render(context)
return {k: str(v) for k, v in header.iteritems()}
def normalize_body(self, context):
"""Normalize body"""
return self.body.render(context)
class _Function(object):
def __init__(self, inputs, function):
self._inputs = tuple(_Token(expr) for expr in inputs or [])
self._function = function
@property
def inputs(self):
return self._inputs
def inputs_values(self, context):
"""
Get rendered input values.
"""
for arg in self._inputs:
yield arg.render(context)
@property
def function(self):
return self._function
class Task(_Function):
"""Task class wraps a task in processor pipeline"""
def __init__(self, inputs, function, output=None):
super(Task, self).__init__(inputs, function)
self._output = output
@property
def output(self):
return self._output
def execute(self, context):
"""Execute task with arguments which rendered from context """
args = [arg for arg in self.inputs_values(context)]
caller = lookup_method(self.function)
output = self._output
_logger.info(
'Executing task method: [%s], input size: [%s], output: [%s]',
self.function, len(args), output
)
if output is None:
caller(*args)
return {}
return {output: caller(*args)}
class Condition(_Function):
"""A condition return the value calculated from input and function"""
def calculate(self, context):
"""Calculate condition with input arguments rendered from context
and method which expected return a bool result.
:param context: context contains key value pairs
:return A bool value returned from the corresponding method
"""
args = [arg for arg in self.inputs_values(context)]
callable_method = lookup_method(self.function)
_logger.debug(
'Calculating condition with method: [%s], input size: [%s]',
self.function, len(args)
)
result = callable_method(*args)
_logger.debug("Calculated result: %s", result)
return result
class _Conditional(object):
"""A base class for all conditional action"""
def __init__(self, conditions):
self._conditions = conditions or []
@property
def conditions(self):
return self._conditions
def passed(self, context):
"""Determine if any condition is satisfied.
:param context: variables to render template
:return: `True` if all passed else `False`
"""
return any(
condition.calculate(context) for condition in self._conditions
)
class Processor(_Conditional):
"""Processor class contains a conditional data process pipeline"""
def __init__(self, skip_conditions, pipeline):
super(Processor, self).__init__(skip_conditions)
self._pipeline = pipeline or []
@property
def pipeline(self):
return self._pipeline
def should_skipped(self, context):
"""Determine processor if should skip process"""
return self.passed(context)
class IterationMode(_Conditional):
def __init__(self, iteration_count, conditions):
super(IterationMode, self).__init__(conditions)
self._iteration_count = iteration_count
@property
def iteration_count(self):
return self._iteration_count
@property
def conditions(self):
return self._conditions
class Checkpoint(object):
"""A checkpoint includes a namespace to determine the checkpoint location
and a content defined the format of content stored in checkpoint."""
def __init__(self, namespace, content):
"""Constructs checkpoint with given namespace and content template. """
if not content:
raise ValueError('Checkpoint content must not be empty')
self._namespace = tuple(_Token(expr) for expr in namespace or ())
self._content = DictToken(content)
@property
def namespace(self):
return self._namespace
def normalize_namespace(self, ctx):
"""Normalize namespace with context used to render template."""
return [token.render(ctx) for token in self._namespace]
@property
def content(self):
return self._content
def normalize_content(self, ctx):
"""Normalize checkpoint with context used to render template."""
return self._content.render(ctx)
@@ -1,14 +0,0 @@
from solnlib.pattern import Singleton
class PipeManager(object):
__metaclass__ = Singleton
def __init__(self, event_writer=None):
self._event_writer = event_writer
def write_events(self, events):
if not self._event_writer:
print events
return True
return self._event_writer.write_events(events)
@@ -1,20 +0,0 @@
from jinja2 import Template
import re
# This pattern matches the template with only one token inside like "{{
# token1}}", "{{ token2 }"
PATTERN = re.compile(r"^\{\{\s*(\w+)\s*\}\}$")
def compile_template(template):
_origin_template = template
_template = Template(template)
def translate_internal(context):
match = re.match(PATTERN, _origin_template)
if match:
context_var = context.get(match.groups()[0])
return context_var if context_var else ''
return _template.render(context)
return translate_internal
@@ -1,71 +0,0 @@
import ConfigParser
import os.path as op
from .data_collection import ta_mod_input as ta_input
from .ta_cloud_connect_client import TACloudConnectClient as CollectorCls
from ..common.lib_util import (
get_main_file, get_app_root_dir, get_mod_input_script_name
)
def _load_options_from_inputs_spec(app_root, stanza_name):
input_spec_file = 'inputs.conf.spec'
file_path = op.join(app_root, 'README', input_spec_file)
if not op.isfile(file_path):
raise RuntimeError("README/%s doesn't exist" % input_spec_file)
parser = ConfigParser.RawConfigParser(allow_no_value=True)
parser.read(file_path)
options = parser.defaults().keys()
stanza_prefix = '%s://' % stanza_name
stanza_exist = False
for section in parser.sections():
if section == stanza_name or section.startswith(stanza_prefix):
options.extend(parser.options(section))
stanza_exist = True
if not stanza_exist:
raise RuntimeError("Stanza %s doesn't exist" % stanza_name)
return set(options)
def _find_ucc_global_config_json(app_root, ucc_config_filename):
"""Find UCC config file from all possible directories"""
candidates = ['local', 'default', 'bin',
op.join('appserver', 'static', 'js', 'build')]
for candidate in candidates:
file_path = op.join(app_root, candidate, ucc_config_filename)
if op.isfile(file_path):
return file_path
raise RuntimeError(
'Unable to load %s from [%s]'
% (ucc_config_filename, ','.join(candidates))
)
def _get_cloud_connect_config_json(script_name):
config_file_name = '.'.join([script_name, 'cc.json'])
return op.join(op.dirname(get_main_file()), config_file_name)
def run(single_instance=False):
script_name = get_mod_input_script_name()
cce_config_file = _get_cloud_connect_config_json(script_name)
app_root = get_app_root_dir()
ucc_config_path = _find_ucc_global_config_json(
app_root, 'globalConfig.json'
)
schema_params = _load_options_from_inputs_spec(app_root, script_name)
ta_input.main(
CollectorCls,
schema_file_path=ucc_config_path,
log_suffix=script_name,
cc_json_file=cce_config_file,
schema_para_list=schema_params,
single_instance=single_instance
)
@@ -1,52 +0,0 @@
import json
import hashlib
def load_schema_file(schema_file):
"""
Load schema file.
"""
with open(schema_file) as f:
ret = json.load(f)
common = ret.get("_common_", dict())
if common:
for k, v in ret.items():
if k == "_common_" or not isinstance(v, dict):
continue
# merge common into other values
for _k, _v in common.items():
if _k not in v:
v[_k] = _v
ret[k] = v
return ret
def md5_of_dict(data):
"""
MD5 of dict data.
"""
md5 = hashlib.sha256()
if isinstance(data, dict):
for key in sorted(data.keys()):
md5.update(repr(key))
md5.update(md5_of_dict(data[key]))
elif isinstance(data, list):
for item in sorted(data):
md5.update(md5_of_dict(item))
else:
md5.update(repr(data))
return md5.hexdigest()
class UCCException(Exception):
"""
Dispatch engine exception.
"""
pass
@@ -1,49 +0,0 @@
import logging
from ...splunktalib.common import log as stclog
def set_log_level(log_level):
"""
Set log level.
"""
if isinstance(log_level, basestring):
if log_level.upper() == "DEBUG":
stclog.Logs().set_level(logging.DEBUG)
elif log_level.upper() == "INFO":
stclog.Logs().set_level(logging.INFO)
elif log_level.upper() == "WARN":
stclog.Logs().set_level(logging.WARN)
elif log_level.upper() == "ERROR":
stclog.Logs().set_level(logging.ERROR)
elif log_level.upper() == "WARNING":
stclog.Logs().set_level(logging.WARNING)
elif log_level.upper() == "CRITICAL":
stclog.Logs().set_level(logging.CRITICAL)
else:
stclog.Logs().set_level(logging.INFO)
elif isinstance(log_level, int):
if log_level in [logging.DEBUG, logging.INFO, logging.ERROR,
logging.WARN, logging.WARNING, logging.CRITICAL]:
stclog.Logs().set_level(log_level)
else:
stclog.Logs().set_level(logging.INFO)
else:
stclog.Logs().set_level(logging.INFO)
# Global logger
logger = stclog.Logs().get_logger("cloud_connect_engine")
def reset_logger(name):
"""
Reset logger.
"""
stclog.reset_logger(name)
global logger
logger = stclog.Logs().get_logger(name)
@@ -1,71 +0,0 @@
"""
This module provides Read-Write lock.
"""
import threading
class _ReadLocker(object):
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.acquire_read()
def __exit__(self, exc_type, exc_val, exc_tb):
self.lock.release_read()
return False
class _WriteLocker(object):
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.acquire_write()
def __exit__(self, exc_type, exc_val, exc_tb):
self.lock.release_write()
return False
class RWLock(object):
""" Simple Read-Write lock.
Allow multiple read but only one writing concurrently.
"""
def __init__(self):
self._condition = threading.Condition(threading.Lock())
self._readers = 0
def acquire_read(self):
self._condition.acquire()
self._readers += 1
self._condition.release()
def release_read(self):
self._condition.acquire()
try:
self._readers -= 1
if not self._readers:
self._condition.notifyAll()
finally:
self._condition.release()
def acquire_write(self):
self._condition.acquire()
while self._readers > 0:
self._condition.wait()
def release_write(self):
self._condition.release()
@property
def reader_lock(self):
return _ReadLocker(self)
@property
def writer_lock(self):
return _WriteLocker(self)
@@ -1,7 +0,0 @@
FIELD_PRODUCT = '_product'
FIELD_REST_NAMESPACE = '_rest_namespace'
FIELD_REST_PREFIX = '_rest_prefix'
FIELD_PROTOCOL_VERSION = '_protocol_version'
FIELD_VERSION = '_version'
FIELD_ENCRYPTION_FORMATTER = '_encryption_formatter'
@@ -1,367 +0,0 @@
"""UCC Config Module
This is for load/save configuration in UCC server or TA.
The load/save action is based on specified schema.
"""
from __future__ import absolute_import
import json
import logging
import traceback
import time
from ..splunktalib.rest import splunkd_request, code_to_msg
from ..splunktalib.common import util as sc_util
from .common import log as stulog
from .common import UCCException
from urllib import quote
LOGGING_STOPPED = False
def stop_logging():
"""
Stop Config Logging. This is for not showing REST request error
while splunkd shutting down.
:return:
"""
global LOGGING_STOPPED
LOGGING_STOPPED = True
def log(msg, msgx='', level=logging.INFO, need_tb=False):
"""
Logging in UCC Config Module.
:param msg: message content
:param msgx: detail info.
:param level: logging level
:param need_tb: if need logging traceback
:return:
"""
global LOGGING_STOPPED
if LOGGING_STOPPED:
return
msgx = ' - ' + msgx if msgx else ''
content = 'UCC Config Module: %s%s' % (msg, msgx)
if need_tb:
stack = ''.join(traceback.format_stack())
content = '%s\r\n%s' % (content, stack)
stulog.logger.log(level, content, exc_info=1)
class ConfigException(UCCException):
"""Exception for UCC Config Exception
"""
pass
class Config(object):
"""UCC Config Module
"""
# Placeholder stands for any field
FIELD_PLACEHOLDER = '*'
# Head of non-processing endpoint
NON_PROC_ENDPOINT = '#'
# Some meta fields in UCC Config schema
META_FIELDS = ('_product', '_rest_namespace', '_rest_prefix',
'_protocol_version', '_version',
'_encryption_formatter')
# Default Values for Meta fields
META_FIELDS_DEFAULT = {
'_encryption_formatter': '',
}
def __init__(self, splunkd_uri, session_key, schema,
user='nobody', app='-'):
"""
:param splunkd_uri: the root uri of Splunk server,
like https://127.0.0.1:8089
:param session_key: session key for Splunk server
:param schema:
:param user: owner of the resources requested
:param app: namespace of the resources requested
:return:
"""
self.splunkd_uri = splunkd_uri.strip('/')
self.session_key = session_key
self.user, self.app = user, app
self._parse_schema(schema)
self._check_protocol_version()
def load(self):
"""Load Configurations in UCC according to the schema
It will raise exception if failing to load any endpoint,
because it make no sense with not complete configuration info.
"""
log('"load" method in', level=logging.DEBUG)
ret = {meta_field: getattr(self, meta_field)
for meta_field in Config.META_FIELDS}
for ep_id, ep in self._endpoints.iteritems():
data = {'output_mode': 'json', '--cred--': '1'}
retries = 4
waiting_time = [1, 2, 2]
for retry in xrange(retries):
resp, cont = splunkd_request(
splunkd_uri=self.make_uri(ep_id),
session_key=self.session_key,
data=data,
retry=3
)
if resp is None or resp.status != 200:
msg = 'Fail to load endpoint "{ep_id}" - {err}' \
''.format(ep_id=ep_id,
err=code_to_msg(resp, cont)
if resp else cont)
log(msg, level=logging.ERROR, need_tb=True)
raise ConfigException(msg)
try:
ret[ep_id] = self._parse_content(ep_id, cont)
except ConfigException, exc:
log(exc, level=logging.WARNING, need_tb=True)
if retry < retries-1:
time.sleep(waiting_time[retry])
else:
break
else:
log(exc, level=logging.ERROR, need_tb=True)
raise exc
log('"load" method out', level=logging.DEBUG)
return ret
def update_items(self, endpoint_id, item_names, field_names, data,
raise_if_failed=False):
"""Update items in specified endpoint with given fields in data
:param endpoint_id: endpoint id in schema, the key name in schema
:param item_names: a list of item name
:param field_names: a list of updated fields
:param data: a dict of content for items, for example:
{
"item_name_1": {
"field_name_1": "value_1",
"field_name_2": "value_2",
},
"item_name_2": {
"field_name_1": "value_1x",
"field_name_2": "value_2x",
}
}
:raise_if_failed: raise an exception if updating failed.
:return: a list of endpoint ids, which are failed to be updated.
If raise_if_failed is True, it will exist with an exception
on any updating failed.
"""
log('"update_items" method in',
msgx='endpoint_id=%s, item_names=%s, field_names=%s'
% (endpoint_id, item_names, field_names),
level=logging.DEBUG)
assert endpoint_id in self._endpoints, \
'Unexpected endpoint id in given schema - {ep_id}' \
''.format(ep_id=endpoint_id)
item_names_failed = []
for item_name in item_names:
item_data = data.get(item_name, {})
item_data = {field_name: self.dump_value(endpoint_id,
item_name,
field_name,
item_data[field_name])
for field_name in field_names
if field_name in item_data}
if not item_data:
continue
item_uri = self.make_uri(endpoint_id, item_name=item_name)
resp, cont = splunkd_request(splunkd_uri=item_uri,
session_key=self.session_key,
data=item_data,
method="POST",
retry=3
)
if resp is None or resp.status not in (200, 201):
msg = 'Fail to update item "{item}" in endpoint "{ep_id}"' \
' - {err}'.format(ep_id=endpoint_id,
item=item_name,
err=code_to_msg(resp, cont)
if resp else cont)
log(msg, level=logging.ERROR)
if raise_if_failed:
raise ConfigException(msg)
item_names_failed.append(item_name)
log('"update_items" method out', level=logging.DEBUG)
return item_names_failed
def make_uri(self, endpoint_id, item_name=None):
"""Make uri for REST endpoint in TA according to given schema
:param endpoint_id: endpoint id in schema
:param item_name: item name for given endpoint. None for listing all
:return:
"""
endpoint = self._endpoints[endpoint_id]['endpoint']
ep_full = endpoint[1:].strip('/') \
if endpoint.startswith(Config.NON_PROC_ENDPOINT) else \
'{admin_match}/{protocol_version}/{endpoint}' \
''.format(admin_match=self._rest_namespace,
protocol_version=self._protocol_version,
endpoint=(self._rest_prefix +
self._endpoints[endpoint_id]['endpoint']))
ep_uri = None if endpoint_id not in self._endpoints else \
'{splunkd_uri}/servicesNS/{user}/{app}/{endpoint_full}' \
''.format(splunkd_uri=self.splunkd_uri,
user=self.user,
app=self.app,
endpoint_full=ep_full
)
url = ep_uri if item_name is None else "{ep_uri}/{item_name}"\
.format(ep_uri=ep_uri, item_name=quote(item_name))
if item_name is None:
url += '?count=-1'
log('"make_uri" method', msgx='url=%s' % url,
level=logging.DEBUG)
return url
def _parse_content(self, endpoint_id, content):
"""Parse content returned from REST
:param content: a JSON string returned from REST.
"""
try:
content = json.loads(content)['entry']
ret = {ent['name']: ent['content'] for ent in content}
except Exception as exc:
msg = 'Fail to parse content from endpoint_id=%s' \
' - %s' % (endpoint_id, exc)
log(msg, level=logging.ERROR, need_tb=True)
raise ConfigException(msg)
ret = {name: {key: self.load_value(endpoint_id, name, key, val)
for key, val in ent.iteritems()
if not key.startswith('eai:')}
for name, ent in ret.iteritems()}
return ret
def _parse_schema(self, ucc_config_schema):
try:
ucc_config_schema = json.loads(ucc_config_schema)
except ValueError:
msg = 'Invalid JSON content of schema'
log(msg, level=logging.ERROR, need_tb=True)
raise ConfigException(msg)
except Exception as exc:
log(exc, level=logging.ERROR, need_tb=True)
raise ConfigException(exc)
ucc_config_schema.update({key: val for key, val in
Config.META_FIELDS_DEFAULT.iteritems()
if key not in ucc_config_schema})
for field in Config.META_FIELDS:
assert field in ucc_config_schema and \
isinstance(ucc_config_schema[field], basestring), \
'Missing or invalid field "%s" in given schema' % field
setattr(self, field, ucc_config_schema[field])
self._endpoints = {}
for key, val in ucc_config_schema.iteritems():
if key.startswith('_'):
continue
assert isinstance(val, dict), \
'The schema of endpoint "%s" should be dict' % key
assert 'endpoint' in val, \
'The endpoint "%s" has no endpoint entry' % key
self._endpoints[key] = val
def _check_protocol_version(self):
"""
Check if the protocol version in given schema is supported.
:return:
"""
if not self._protocol_version:
return
if not self._protocol_version.startswith('1.'):
raise ConfigException('Unsupported protocol version "%s" '
'in given schema' % self._protocol_version)
def load_value(self, endpoint_id, item_name, fname, fval):
field_type = self._get_field_type(endpoint_id, item_name, fname)
if field_type == '':
return fval
try:
field_type = field_type.lower()
if field_type == 'bool':
return True if sc_util.is_true(fval) else False
elif field_type == 'int':
return int(fval)
elif field_type == 'json':
return json.loads(fval)
except Exception as exc:
msg = 'Fail to load value of "{type_name}" - ' \
'endpoint={endpoint}, item={item}, field={field}' \
''.format(type_name=field_type,
endpoint=endpoint_id,
item=item_name,
field=fname)
log(msg, msgx=str(exc), level=logging.WARNING, need_tb=True)
raise ConfigException(msg)
def dump_value(self, endpoint_id, item_name, fname, fval):
field_type = self._get_field_type(endpoint_id, item_name, fname)
if field_type == '':
return fval
try:
field_type = field_type.lower()
if field_type == 'bool':
return str(fval).lower()
elif field_type == 'json':
return json.dumps(fval)
else:
return fval
except Exception, exc:
msg = 'Fail to dump value of "{type_name}" - ' \
'endpoint={endpoint}, item={item}, field={field}' \
''.format(type_name=field_type,
endpoint=endpoint_id,
item=item_name,
field=fname)
log(msg, msgx=str(exc), level=logging.ERROR, need_tb=True)
raise ConfigException(msg)
def _get_field_type(self, endpoint_id, item_name, fname):
field_types = self._endpoints[endpoint_id].get('field_types', {})
if item_name in field_types:
fields = field_types[item_name]
elif Config.FIELD_PLACEHOLDER in field_types:
fields = field_types[Config.FIELD_PLACEHOLDER]
else:
fields = {}
field_type = fields.get(fname, '')
if field_type not in ('', 'bool', 'int', 'json'):
msg = 'Unsupported type "{type_name}" for value in schema - ' \
'endpoint={endpoint}, item={item}, field={field}' \
''.format(type_name=field_type,
endpoint=endpoint_id,
item=item_name,
field=fname)
log(msg, level=logging.ERROR, need_tb=True)
raise ConfigException(msg)
return field_type
def get_endpoints(self):
return self._endpoints
@@ -1,147 +0,0 @@
import json
import re
from . import ta_consts as c
from . import ta_helper as th
from ..common import log as stulog
from ...splunktalib import state_store as ss
from ...splunktalib.common.util import is_true
class TACheckPointMgr(object):
SEPARATOR = "_" * 3
# FIXME We'd better move all default values together
_DEFAULT_MAX_CACHE_SECONDS = 5
_MAXIMUM_MAX_CACHE_SECONDS = 3600
def __init__(self, meta_config, task_config):
self._task_config = task_config
self._store = self._create_state_store(
meta_config,
task_config.get(c.checkpoint_storage_type),
task_config[c.appname]
)
def _create_state_store(self, meta_config, storage_type, app_name):
stulog.logger.debug('Got checkpoint storage type=%s', storage_type)
if storage_type == c.checkpoint_kv_storage:
collection_name = self._get_collection_name()
stulog.logger.debug(
'Creating KV state store, collection name=%s', collection_name
)
return ss.get_state_store(
meta_config,
appname=app_name,
collection_name=collection_name,
use_kv_store=True
)
use_cache_file = self._use_cache_file()
max_cache_seconds = \
self._get_max_cache_seconds() if use_cache_file else None
stulog.logger.debug(
'Creating file state store, use_cache_file=%s, max_cache_seconds=%s',
use_cache_file, max_cache_seconds
)
return ss.get_state_store(
meta_config,
app_name,
use_cache_file=use_cache_file,
max_cache_seconds=max_cache_seconds
)
def _get_collection_name(self):
collection = self._task_config.get(c.collection_name)
collection = collection.strip() if collection else ''
if not collection:
input_name = self._task_config[c.mod_input_name]
stulog.logger.info(
'Collection name="%s" is empty, set it to "%s"',
collection, input_name
)
collection = input_name
return re.sub(r'[^\w]+', '_', collection)
def _use_cache_file(self):
# TODO Move the default value outside code
use_cache_file = is_true(self._task_config.get(c.use_cache_file, True))
if use_cache_file:
stulog.logger.info(
"Stanza=%s using cached file store to create checkpoint",
self._task_config[c.stanza_name]
)
return use_cache_file
def _get_max_cache_seconds(self):
default = self._DEFAULT_MAX_CACHE_SECONDS
seconds = self._task_config.get(
c.max_cache_seconds, default
)
try:
seconds = int(seconds)
except ValueError:
stulog.logger.warning(
"The max_cache_seconds '%s' is not a valid integer,"
" so set this variable to default value %s",
seconds, default
)
seconds = default
else:
maximum = self._MAXIMUM_MAX_CACHE_SECONDS
if not (1 <= seconds <= maximum):
# for seconds>3600 set it to 3600. for seconds <=0 set it to default.
adjusted = max(min(seconds, maximum), default)
stulog.logger.warning(
"The max_cache_seconds (%s) is expected in range[1,%s],"
" set it to %s",
seconds, maximum, adjusted
)
seconds = adjusted
return seconds
def get_ckpt_key(self, namespaces=None):
return self._key_formatter(namespaces)
def get_ckpt(self, namespaces=None, show_namespaces=False):
key, namespaces = self.get_ckpt_key(namespaces)
raw_checkpoint = self._store.get_state(key)
stulog.logger.info("Get checkpoint key='%s' value='%s'",
key, json.dumps(raw_checkpoint))
if not show_namespaces and raw_checkpoint:
return raw_checkpoint.get("data")
return raw_checkpoint
def update_ckpt(self, ckpt, namespaces=None):
if not ckpt:
stulog.logger.warning("Checkpoint expect to be not empty.")
return
key, namespaces = self.get_ckpt_key(namespaces)
value = {"namespaces": namespaces, "data": ckpt}
stulog.logger.info("Update checkpoint key='%s' value='%s'",
key, json.dumps(value))
self._store.update_state(key, value)
def remove_ckpt(self, namespaces=None):
key, namespaces = self.get_ckpt_key(namespaces)
self._store.delete_state(key)
def _key_formatter(self, namespaces=None):
if not namespaces:
stulog.logger.info('Namespaces is empty, using stanza name instead.')
namespaces = [self._task_config[c.stanza_name]]
key_str = TACheckPointMgr.SEPARATOR.join(namespaces)
hashed_file = th.format_name_for_file(key_str)
stulog.logger.info("raw_file='%s' hashed_file='%s'", key_str, hashed_file)
return hashed_file, namespaces
def close(self, key=None):
try:
self._store.close(key)
stulog.logger.info('Closed state store successfully. key=%s', key)
except Exception:
stulog.logger.exception('Error closing state store. key=%s', key)
@@ -1,155 +0,0 @@
import os.path as op
import socket
import ta_consts as c
import ta_helper as th
from ..common import log as stulog
from ...splunktalib import modinput as modinput
from ...splunktalib import splunk_cluster as sc
from ...splunktalib.common import util
# methods can be overrided by subclass : process_task_configs
class TaConfig(object):
_current_hostname = socket.gethostname()
_appname = util.get_appname_from_path(op.abspath(__file__))
def __init__(self, meta_config, client_schema, log_suffix=None,
stanza_name=None, input_type=None,
single_instance=True):
self._meta_config = meta_config
self._stanza_name = stanza_name
self._input_type = input_type
self._log_suffix = log_suffix
self._single_instance = single_instance
self._task_configs = []
self._client_schema = client_schema
self._server_info = sc.ServerInfo(meta_config[c.server_uri],
meta_config[c.session_key])
self._all_conf_contents = {}
self._get_division_settings = {}
self.set_logging()
self._load_task_configs()
def is_shc_member(self):
return self._server_info.is_shc_member()
def is_search_head(self):
return self._server_info.is_search_head()
def is_single_instance(self):
return self._single_instance
def get_meta_config(self):
return self._meta_config
def get_task_configs(self):
return self._task_configs
def get_all_conf_contents(self):
if self._all_conf_contents:
return self._all_conf_contents.get(c.inputs), \
self._all_conf_contents.get(c.all_configs), \
self._all_conf_contents.get(c.global_settings)
inputs, configs, global_settings = th.get_all_conf_contents(
self._meta_config[c.server_uri],
self._meta_config[c.session_key],
self._client_schema, self._input_type)
self._all_conf_contents[c.inputs] = inputs
self._all_conf_contents[c.all_configs] = configs
self._all_conf_contents[c.global_settings] = global_settings
return inputs, configs, global_settings
def set_logging(self):
# The default logger name is "cloud_connect_engine"
if self._stanza_name and self._log_suffix:
logger_name = self._log_suffix + "_" + th.format_name_for_file(
self._stanza_name)
stulog.reset_logger(logger_name)
inputs, configs, global_settings = self.get_all_conf_contents()
log_level = "INFO"
for item in global_settings.get("settings"):
if item.get(c.name) == "logging" and item.get("loglevel"):
log_level = item["loglevel"]
break
stulog.set_log_level(log_level)
stulog.logger.info("Set log_level={}".format(log_level))
stulog.logger.info("Start {} task".format(self._stanza_name))
def get_input_type(self):
return self._input_type
def _get_checkpoint_storage_type(self, config):
cs_type = config.get(c.checkpoint_storage_type)
stulog.logger.debug("Checkpoint storage type=%s", cs_type)
cs_type = cs_type.strip() if cs_type else c.checkpoint_auto
# Allow user configure 'auto' and 'file' only.
if cs_type not in (c.checkpoint_auto, c.checkpoint_file):
stulog.logger.warning(
"Checkpoint storage type='%s' is invalid, change it to '%s'",
cs_type, c.checkpoint_auto
)
cs_type = c.checkpoint_auto
if cs_type == c.checkpoint_auto and self.is_search_head():
stulog.logger.info(
"Checkpoint storage type is '%s' and instance is "
"search head, set checkpoint storage type to '%s'.",
c.checkpoint_auto,
c.checkpoint_kv_storage
)
cs_type = c.checkpoint_kv_storage
return cs_type
def _load_task_configs(self):
inputs, configs, global_settings = self.get_all_conf_contents()
if self._input_type:
inputs = inputs.get(self._input_type)
if not self._single_instance:
inputs = [input for input in inputs if
input[c.name] == self._stanza_name]
all_task_configs = []
for input in inputs:
task_config = {}
task_config.update(input)
task_config[c.configs] = configs
task_config[c.settings] = \
{item[c.name]: item for item in global_settings["settings"]}
if self.is_single_instance():
collection_interval = "collection_interval"
task_config[c.interval] = task_config.get(collection_interval)
task_config[c.interval] = int(task_config[c.interval])
if task_config[c.interval] <= 0:
raise ValueError(
"The interval value {} is invalid."
" It should be a positive integer".format(
task_config[c.interval]))
task_config[c.checkpoint_storage_type] = \
self._get_checkpoint_storage_type(task_config)
task_config[c.appname] = TaConfig._appname
task_config[c.mod_input_name] = self._input_type
task_config[c.stanza_name] = task_config[c.name]
all_task_configs.append(task_config)
self._task_configs = all_task_configs
# Override this method if some transforms or validations needs to be done
# before task_configs is exposed
def process_task_configs(self, task_configs):
pass
def create_ta_config(settings, config_cls=TaConfig, log_suffix=None,
single_instance=True):
meta_config, configs = modinput.get_modinput_configs_from_stdin()
stanza_name = None
input_type = None
if configs and "://" in configs[0].get("name", ""):
input_type, stanza_name = configs[0].get("name").split("://", 1)
return config_cls(meta_config, settings, log_suffix, stanza_name,
input_type, single_instance=single_instance)
@@ -1,54 +0,0 @@
server_uri = "server_uri"
session_key = "session_key"
version = "version"
appname = "appname"
event_writer = "event_writer"
index = "index"
default_index = "default"
source = "source"
sourcetype = "sourcetype"
data_loader = "data_loader"
meta_configs = "meta_configs"
disabled = "disabled"
resource = "resource"
events = "events"
scope = "scope"
checkpoint_dir = "checkpoint_dir"
ckpt_dict = "ckpt_dict"
inputs = "inputs"
input_name = "input_name"
input_data = "input_data"
interval = "interval"
data = "data"
batch_size = 'batch_size'
time_fmt = "%Y-%m-%dT%H:%M:%S"
utc_time_fmt = "%Y-%m-%dT%H:%M:%S.%fZ"
# system setting keys
checkpoint_storage_type = "builtin_system_checkpoint_storage_type"
# Possible values for checkpoint storage type
checkpoint_auto = 'auto'
checkpoint_kv_storage = 'kv_store'
checkpoint_file = 'file'
# For cache file
use_cache_file = "builtin_system_use_cache_file"
max_cache_seconds = "builtin_system_max_cache_seconds"
# For kv store
collection_name = "builtin_system_kvstore_collection_name"
settings = "__settings__"
configs = "__configs__"
name = "name"
config = "config"
division = "division"
stanza_name = "stanza_name"
divide_key = "_divide_key"
divide_endpoint = "_divide_endpoint"
mod_input_name = "mod_input_name"
global_settings = "global_settings"
all_configs = "all_configs"
@@ -1,84 +0,0 @@
#!/usr/bin/python
from . import ta_checkpoint_manager as cp
from . import ta_data_collector as tdc
def build_event(host=None,
source=None,
sourcetype=None,
time=None,
index=None,
raw_data="",
is_unbroken=False,
is_done=False):
if is_unbroken is False and is_done is True:
raise Exception('is_unbroken=False is_done=True is invalid')
return tdc.event_tuple._make([host, source, sourcetype, time, index,
raw_data, is_unbroken, is_done])
class TaDataClient(object):
def __init__(self,
meta_config,
task_config,
checkpoint_mgr=None,
event_writer=None):
self._meta_config = meta_config
self._task_config = task_config
self._checkpoint_mgr = checkpoint_mgr
self._event_writer = event_writer
self._stop = False
def is_stopped(self):
return self._stop
def stop(self):
self._stop = True
def get(self):
raise StopIteration
def create_data_collector(dataloader,
tconfig,
meta_configs,
task_config,
data_client_cls,
checkpoint_cls=None):
checkpoint_manager_cls = checkpoint_cls or cp.TACheckPointMgr
return tdc.TADataCollector(tconfig, meta_configs, task_config,
checkpoint_manager_cls, data_client_cls,
dataloader)
def client_adatper(job_func):
class TaDataClientAdapter(TaDataClient):
def __init__(self, all_conf_contents, meta_config, task_config,
chp_mgr):
super(TaDataClientAdapter, self).__init__(meta_config, task_config,
chp_mgr)
self._execute_times = 0
self._gen = job_func(self._task_config, chp_mgr)
def stop(self):
"""
overwrite to handle stop control command
"""
# normaly base class just set self._stop as True
super(TaDataClientAdapter, self).stop()
def get(self):
"""
overwrite to get events
"""
self._execute_times += 1
if self.is_stopped():
# send stop signal
self._gen.send(self.is_stopped())
raise StopIteration
if self._execute_times == 1:
return self._gen.next()
return self._gen.send(self.is_stopped())
return TaDataClientAdapter
@@ -1,144 +0,0 @@
#!/usr/bin/python
import threading
import time
from collections import namedtuple
import ta_consts as c
from ..common import log as stulog
from ...splunktalib.common import util as scu
evt_fmt = ("<stream><event><host>{0}</host>"
"<source><![CDATA[{1}]]></source>"
"<sourcetype><![CDATA[{2}]]></sourcetype>"
"<time>{3}</time>"
"<index>{4}</index><data>"
"<![CDATA[{5}]]></data></event></stream>")
unbroken_evt_fmt = ("<stream>"
"<event unbroken=\"1\">"
"<host>{0}</host>"
"<source><![CDATA[{1}]]></source>"
"<sourcetype><![CDATA[{2}]]></sourcetype>"
"<time>{3}</time>"
"<index>{4}</index>"
"<data><![CDATA[{5}]]></data>"
"{6}"
"</event>"
"</stream>")
event_tuple = namedtuple('Event',
['host', 'source', 'sourcetype', 'time', 'index',
'raw_data', 'is_unbroken', 'is_done'])
class TADataCollector(object):
def __init__(self, tconfig, meta_config, task_config,
checkpoint_manager_cls, data_client_cls, data_loader):
self._lock = threading.Lock()
self._ta_config = tconfig
self._meta_config = meta_config
self._task_config = task_config
self._stopped = False
self._p = self._get_logger_prefix()
self._checkpoint_manager = checkpoint_manager_cls(meta_config,
task_config)
self.data_client_cls = data_client_cls
self._data_loader = data_loader
self._client = None
def get_meta_configs(self):
return self._meta_config
def get_task_config(self):
return self._task_config
def get_interval(self):
return self._task_config[c.interval]
def _get_logger_prefix(self):
pairs = ['{}="{}"'.format(c.stanza_name, self._task_config[
c.stanza_name])]
return "[{}]".format(" ".join(pairs))
def stop(self):
self._stopped = True
if self._client:
self._client.stop()
def __call__(self):
self.index_data()
def _build_event(self, events):
if not events:
return None
if not isinstance(events, list):
events = [events]
evts = []
for event in events:
assert event.raw_data, "the raw data of events is empty"
if event.is_unbroken:
evt = unbroken_evt_fmt.format(
event.host or "", event.source or "", event.sourcetype or
"", event.time or "", event.index or "",
scu.escape_cdata(event.raw_data), "<done/>" if
event.is_done else "")
else:
evt = evt_fmt.format(event.host or "", event.source or "",
event.sourcetype or "", event.time or "",
event.index or "",
scu.escape_cdata(event.raw_data))
evts.append(evt)
return evts
def _create_data_client(self):
return self.data_client_cls(self._meta_config,
self._task_config,
self._checkpoint_manager,
self._data_loader.get_event_writer())
def index_data(self):
if self._lock.locked():
stulog.logger.debug(
"Last round of stanza={} is not done yet".format(
self._task_config[c.stanza_name]))
return
with self._lock:
try:
self._do_safe_index()
self._checkpoint_manager.close()
except Exception:
stulog.logger.exception("{} Failed to index data"
.format(self._p))
stulog.logger.info("{} End of indexing data".format(self._p))
if not self._ta_config.is_single_instance():
self._data_loader.tear_down()
def _write_events(self, events):
evts = self._build_event(events)
if evts:
if not self._data_loader.write_events(evts):
stulog.logger.info("{} the event queue is closed and the "
"received data will be discarded".format(
self._p))
return False
return True
def _do_safe_index(self):
self._client = self._create_data_client()
while not self._stopped:
try:
events = self._client.get()
if not events:
continue
else:
if not self._write_events(events):
break
except StopIteration:
stulog.logger.info("{} Finished this round".format(self._p))
return
except Exception:
stulog.logger.exception("{} Failed to get msg".format(self._p))
break
# in case encounter exception or fail to write events
if not self._stopped:
self.stop()
@@ -1,168 +0,0 @@
"""
Data Loader main entry point
"""
import Queue
import os.path as op
import ConfigParser
from ...splunktalib.concurrent import concurrent_executor as ce
from ...splunktalib import timer_queue as tq
from ...splunktalib.schedule import job as sjob
from ...splunktalib.common import log
class TADataLoader(object):
"""
Data Loader boots all underlying facilities to handle data collection
"""
def __init__(self, job_scheduler, event_writer):
"""
@configs: a list like object containing a list of dict
like object. Each element shall implement dict.get/[] like interfaces
to get the value for a key.
@job_scheduler: schedulering the jobs. shall implement get_ready_jobs
@event_writer: write_events
"""
self._settings = self._read_default_settings()
self._settings["daemonize_thread"] = False
self._event_writer = event_writer
self._wakeup_queue = Queue.Queue()
self._scheduler = job_scheduler
self._timer_queue = tq.TimerQueue()
self._executor = ce.ConcurrentExecutor(self._settings)
self._started = False
def run(self, jobs):
if self._started:
return
self._started = True
self._event_writer.start()
self._executor.start()
self._timer_queue.start()
self._scheduler.start()
log.logger.info("TADataLoader started.")
def _enqueue_io_job(job):
job_props = job.get_props()
real_job = job_props["real_job"]
self.run_io_jobs((real_job,))
for job in jobs:
j = sjob.Job(_enqueue_io_job, {"real_job": job},
job.get_interval())
self._scheduler.add_jobs((j,))
self._wait_for_tear_down()
for job in jobs:
job.stop()
self._scheduler.tear_down()
self._timer_queue.tear_down()
self._executor.tear_down()
self._event_writer.tear_down()
log.logger.info("DataLoader stopped.")
def _wait_for_tear_down(self):
wakeup_q = self._wakeup_queue
while 1:
try:
go_exit = wakeup_q.get(timeout=1)
except Queue.Empty:
pass
else:
if go_exit:
log.logger.info("DataLoader got stop signal")
self._stopped = True
break
def tear_down(self):
self._wakeup_queue.put(True)
log.logger.info("DataLoader is going to stop.")
def stopped(self):
return self._stopped
def run_io_jobs(self, jobs, block=True):
self._executor.enqueue_io_funcs(jobs, block)
def run_compute_job(self, func, args=(), kwargs={}):
self._executor.run_compute_func_sync(func, args, kwargs)
def run_compute_job_async(self, func, args=(), kwargs={}, callback=None):
"""
@return: AsyncResult
"""
return self._executor.run_compute_func_async(func, args,
kwargs, callback)
def add_timer(self, callback, when, interval):
return self._timer_queue.add_timer(callback, when, interval)
def remove_timer(self, timer):
self._timer_queue.remove_timer(timer)
def write_events(self, events):
return self._event_writer.write_events(events)
def get_event_writer(self):
return self._event_writer
@staticmethod
def _read_default_settings():
cur_dir = op.dirname(op.abspath(__file__))
setting_file = op.join(cur_dir,"../../","splunktalib", "setting.conf")
parser = ConfigParser.ConfigParser()
parser.read(setting_file)
settings = {}
keys = ("process_size", "thread_min_size", "thread_max_size",
"task_queue_size")
for option in keys:
try:
settings[option] = parser.get("global", option)
except ConfigParser.NoOptionError:
settings[option] = -1
try:
settings[option] = int(settings[option])
except ValueError:
settings[option] = -1
log.logger.debug("settings: %s", settings)
return settings
class GlobalDataLoader(object):
""" Singleton, inited when started"""
__instance = None
@staticmethod
def get_data_loader(scheduler, writer):
if GlobalDataLoader.__instance is None:
GlobalDataLoader.__instance = TADataLoader(
scheduler, writer)
return GlobalDataLoader.__instance
@staticmethod
def reset():
GlobalDataLoader.__instance = None
def create_data_loader():
"""
create a data loader with default event_writer, job_scheudler
"""
from ...splunktalib import event_writer as ew
from ...splunktalib.schedule import scheduler as sched
writer = ew.EventWriter()
scheduler = sched.Scheduler()
loader = GlobalDataLoader.get_data_loader(scheduler, writer)
return loader
@@ -1,154 +0,0 @@
import hashlib
import json
import os.path as op
import re
from calendar import timegm
from datetime import datetime
import functools32
from splunktaucclib.global_config import GlobalConfig, GlobalConfigSchema
from . import ta_consts as c
from ...splunktacollectorlib import config as sc
from ...splunktalib.common import util
def utc2timestamp(human_time):
regex1 = ur"\d{4}-\d{2}-\d{2}.\d{2}:\d{2}:\d{2}"
match = re.search(regex1, human_time)
if match:
formated = match.group()
else:
return None
strped_time = datetime.strptime(formated, c.time_fmt)
timestamp = timegm(strped_time.utctimetuple())
regex2 = "\d{4}-\d{2}-\d{2}.\d{2}:\d{2}:\d{2}(\.\d+)"
match = re.search(regex2, human_time)
if match:
timestamp += float(match.group(1))
else:
timestamp += float("0.000000")
return timestamp
def get_md5(data):
"""
function name is not change, actually use sha1 instead
:param data:
:return:
"""
assert data is not None, "The input cannot be None"
if isinstance(data, (unicode, str)):
return hashlib.sha256(data.encode('utf-8')).hexdigest()
elif isinstance(data, (list, tuple, dict)):
return hashlib.sha256(json.dumps(data).encode('utf-8')).hexdigest()
def get_all_conf_contents(server_uri, sessionkey, settings, input_type=None):
schema = GlobalConfigSchema(settings)
global_config = GlobalConfig(
server_uri, sessionkey, schema
)
inputs = global_config.inputs.load(input_type=input_type)
configs = global_config.configs.load()
settings = global_config.settings.load()
return inputs, configs, settings
@functools32.lru_cache(maxsize=64)
def format_name_for_file(name):
return hashlib.sha256(name).hexdigest()
class ConfigSchemaHandler(object):
_app_name = util.get_appname_from_path(op.abspath(__file__))
# Division schema keys.
TYPE = "type"
TYPE_SINGLE = "single"
TYPE_MULTI = "multi"
REFER = "refer"
SEPARATOR = "separator"
def __init__(self, meta_configs, client_schema):
self._config = sc.Config(splunkd_uri=meta_configs[c.server_uri],
session_key=meta_configs[c.session_key],
schema=json.dumps(client_schema[
c.config]),
user="nobody",
app=ConfigSchemaHandler._app_name)
self._client_schema = client_schema
self._all_conf_contents = {}
self._load_conf_contents()
self._division_settings = self._divide_settings()
def get_endpoints(self):
return self._config.get_endpoints()
def get_all_conf_contents(self):
return self._all_conf_contents
def get_single_conf_contents(self, endpoint):
return self._all_conf_contents.get(endpoint)
def get_division_settings(self):
return self._division_settings
def _divide_settings(self):
division_schema = self._client_schema[c.division]
division_settings = dict()
for division_endpoint, division_contents in division_schema.iteritems():
division_settings[division_endpoint] = self._process_division(
division_endpoint, division_contents)
return division_settings
def _load_conf_contents(self):
self._all_conf_contents = self._config.load()
def _process_division(self, division_endpoint, division_contents):
division_metrics = []
assert isinstance(division_contents, dict)
for division_key, division_value in division_contents.iteritems():
try:
assert self.TYPE in division_value and \
division_value[self.TYPE] in \
[self.TYPE_SINGLE, self.TYPE_MULTI] and \
self.SEPARATOR in division_value if \
division_value[self.TYPE] == self.TYPE_MULTI else True
except Exception:
raise Exception("Invalid division schema")
division_metrics.append(DivisionRule(division_endpoint,
division_key,
division_value[self.TYPE],
division_value.get(
self.SEPARATOR,
),
division_value.get(
self.REFER,
)))
return division_metrics
class DivisionRule(object):
def __init__(self, endpoint, metric, type, separator, refer):
self._endpoint = endpoint
self._metric = metric
self._type = type
self._separator = separator
self._refer = refer
def endpoint(self):
return self._endpoint
def metric(self):
return self._metric
def type(self):
return self._type
def separator(self):
return self._separator
def refer(self):
return self._refer
@@ -1,272 +0,0 @@
#!/usr/bin/python
"""
This is the main entry point for My TA
"""
import os.path as op
import platform
import sys
import time
from . import ta_checkpoint_manager as cpmgr
from . import ta_config as tc
from . import ta_data_client as tdc
from . import ta_data_loader as dl
from ..common import load_schema_file as ld
from ..common import log as stulog
from ...common.lib_util import get_app_root_dir, get_mod_input_script_name
from ...splunktalib import file_monitor as fm
from ...splunktalib import modinput
from ...splunktalib import orphan_process_monitor as opm
from ...splunktalib.common import util as utils
utils.remove_http_proxy_env_vars()
__CHECKPOINT_DIR_MAX_LEN__ = 180
def do_scheme(
mod_input_name,
schema_para_list=None,
single_instance=True,
):
"""
Feed splunkd the TA's scheme
"""
builtin_names = {
"name", "index", "sourcetype", "host", "source",
"disabled", "interval"
}
param_string_list = []
if schema_para_list is None:
schema_para_list = ()
for param in schema_para_list:
if param in builtin_names:
continue
param_string_list.append(
"""
<arg name="{param}">
<title>{param}</title>
<required_on_create>0</required_on_create>
<required_on_edit>0</required_on_edit>
</arg>
""".format(param=param)
)
description = ("Go to the add-on's configuration UI and configure"
" modular inputs under the Inputs menu.")
print """
<scheme>
<title>{data_input_title}</title>
<description>{description}</description>
<use_external_validation>true</use_external_validation>
<streaming_mode>xml</streaming_mode>
<use_single_instance>{single_instance}</use_single_instance>
<endpoint>
<args>
<arg name="name">
<title>{data_input_title} Data Input Name</title>
</arg>
{param_str}
</args>
</endpoint>
</scheme>
""".format(
single_instance=(str(single_instance)).lower(),
data_input_title=mod_input_name,
param_str=''.join(param_string_list),
description=description,
)
def _setup_signal_handler(data_loader, ta_short_name):
"""
Setup signal handlers
:data_loader: data_loader.DataLoader instance
"""
def _handle_exit(signum, frame):
stulog.logger.info("{} receives exit signal".format(ta_short_name))
if data_loader is not None:
data_loader.tear_down()
utils.handle_tear_down_signals(_handle_exit)
def _handle_file_changes(data_loader):
"""
:reload conf files and exit
"""
def _handle_refresh(changed_files):
stulog.logger.info("Detect {} changed, reboot itself".format(
changed_files))
data_loader.tear_down()
return _handle_refresh
def _get_conf_files(settings):
rest_root = settings.get("meta").get("restRoot")
file_list = [rest_root + "_settings.conf"]
if settings.get("pages") and settings.get("pages").get("configuration"):
configs = settings.get("pages").get("configuration")
tabs = configs.get("tabs") if configs.get("tabs") else []
for tab in tabs:
if tab.get("table"):
file_list.append(rest_root + "_" + tab.get("name") + ".conf")
ta_dir = get_app_root_dir()
return [op.join(ta_dir, "local", f) for f in file_list]
def run(collector_cls, settings, checkpoint_cls=None, config_cls=None,
log_suffix=None, single_instance=True, cc_json_file=None):
"""
Main loop. Run this TA forever
"""
ta_short_name = settings["meta"]["name"].lower()
# This is for stdout flush
utils.disable_stdout_buffer()
# http://bugs.python.org/issue7980
time.strptime('2016-01-01', '%Y-%m-%d')
loader = dl.create_data_loader()
# handle signal
_setup_signal_handler(loader, ta_short_name)
# monitor files to reboot
try:
monitor = fm.FileMonitor(_handle_file_changes(loader),
_get_conf_files(settings))
loader.add_timer(monitor.check_changes, time.time(), 10)
except Exception:
stulog.logger.exception("Fail to add files for monitoring")
# add orphan process handling, which will check each 1 second
orphan_checker = opm.OrphanProcessChecker(loader.tear_down)
loader.add_timer(orphan_checker.check_orphan, time.time(), 1)
tconfig = tc.create_ta_config(settings, config_cls or tc.TaConfig,
log_suffix, single_instance=single_instance)
task_configs = tconfig.get_task_configs()
if not task_configs:
stulog.logger.debug("No task and exiting...")
return
meta_config = tconfig.get_meta_config()
meta_config["cc_json_file"] = cc_json_file
if tconfig.is_shc_member():
# Don't support SHC env
stulog.logger.error("This host is in search head cluster environment , "
"will exit.")
return
# In this case, use file for checkpoint
if _is_checkpoint_dir_length_exceed_limit(tconfig,
meta_config["checkpoint_dir"]):
stulog.logger.error("The length of the checkpoint directory path: '{}' "
"is too long. The max length we support is {}",
meta_config["checkpoint_dir"],
__CHECKPOINT_DIR_MAX_LEN__)
return
jobs = [
tdc.create_data_collector(
loader,
tconfig,
meta_config,
task_config,
collector_cls,
checkpoint_cls=checkpoint_cls or cpmgr.TACheckPointMgr
)
for task_config in task_configs
]
loader.run(jobs)
def _is_checkpoint_dir_length_exceed_limit(config, checkpoint_dir):
return platform.system() == 'Windows' \
and not config.is_search_head() \
and len(checkpoint_dir) >= __CHECKPOINT_DIR_MAX_LEN__
def validate_config():
"""
Validate inputs.conf
"""
_, configs = modinput.get_modinput_configs_from_stdin()
return 0
def usage():
"""
Print usage of this binary
"""
hlp = "%s --scheme|--validate-arguments|-h"
print >> sys.stderr, hlp % sys.argv[0]
sys.exit(1)
def main(
collector_cls,
schema_file_path,
log_suffix="modinput",
checkpoint_cls=None,
config_cls=None,
cc_json_file=None,
schema_para_list=None,
single_instance=True
):
"""
Main entry point
"""
assert collector_cls, "ucc modinput collector is None."
assert schema_file_path, "ucc modinput schema file is None"
settings = ld(schema_file_path)
mod_input_name = get_mod_input_script_name()
args = sys.argv
if len(args) > 1:
if args[1] == "--scheme":
do_scheme(
mod_input_name=mod_input_name,
schema_para_list=schema_para_list,
single_instance=single_instance
)
elif args[1] == "--validate-arguments":
sys.exit(validate_config())
elif args[1] in ("-h", "--h", "--help"):
usage()
else:
usage()
else:
try:
run(
collector_cls,
settings,
checkpoint_cls=checkpoint_cls,
config_cls=config_cls,
log_suffix=log_suffix,
single_instance=single_instance,
cc_json_file=cc_json_file
)
except Exception:
stulog.logger.exception(
"{} task encounter exception".format(mod_input_name))
stulog.logger.info("End {} task".format(mod_input_name))
sys.exit(0)
@@ -1,17 +0,0 @@
"""
This module is used to filter and reload PATH.
"""
import os
import sys
import re
ta_name = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ta_lib_name = re.sub("[^\w]+", "_", ta_name.lower())
assert ta_name or ta_name == "package", "TA name is None or package"
pattern = re.compile(r"[\\/]etc[\\/]apps[\\/][^\\/]+[\\/]bin[\\/]?$")
new_paths = [path for path in sys.path if not pattern.search(path) or ta_name in path]
new_paths.insert(0, os.path.sep.join([os.path.dirname(__file__), ta_lib_name]))
sys.path = new_paths
@@ -1,41 +0,0 @@
from .data_collection.ta_data_client import TaDataClient
from ..splunktacollectorlib.common import log as stulog
from ..splunktacollectorlib.data_collection import ta_consts as c
from ..common.log import set_cc_logger
class TACloudConnectClient(TaDataClient):
def __init__(self,
meta_config,
task_config,
checkpoint_mgr=None,
event_writer=None
):
super(TACloudConnectClient, self).__init__(meta_config,
task_config,
checkpoint_mgr,
event_writer)
self._set_log()
self._cc_config_file = self._meta_config["cc_json_file"]
from ..core.pipemgr import PipeManager
from ..client import CloudConnectClient as Client
self._pipe_mgr = PipeManager(event_writer=event_writer)
self._client = Client(self._task_config, self._cc_config_file,
checkpoint_mgr)
def _set_log(self):
pairs = ['{}="{}"'.format(c.stanza_name, self._task_config[
c.stanza_name])]
set_cc_logger(stulog.logger,
logger_prefix="[{}]".format(" ".join(pairs)))
def is_stopped(self):
return self._stop
def stop(self):
self._stop = True
self._client.stop()
def get(self):
self._client.start()
raise StopIteration
@@ -1,2 +0,0 @@
__version__ = "0.9"
__license__ = "Splunk"
@@ -1 +0,0 @@
util_log = "util"
@@ -1,137 +0,0 @@
"""
Copyright (C) 2005-2015 Splunk Inc. All Rights Reserved.
log utility for TA
"""
import logging
import logging.handlers as handlers
import os
import os.path as op
from ..splunk_platform import make_splunkhome_path
from . import util as cutil
from .pattern import singleton
import time
logging.Formatter.converter = time.gmtime
__LOG_FORMAT__ = "%(asctime)s +0000 log_level=%(levelname)s, pid=%(process)d, " \
"tid=%(threadName)s, file=%(filename)s, " \
"func_name=%(funcName)s, code_line_no=%(lineno)d | %(message)s"
def log_enter_exit(logger):
"""
Log decorator to log function enter and exit
"""
def log_decorator(func):
def wrapper(*args, **kwargs):
logger.debug("{} entered.".format(func.__name__))
result = func(*args, **kwargs)
logger.debug("{} exited.".format(func.__name__))
return result
return wrapper
return log_decorator
def check_add_stderr_handler():
env_var = os.environ.get('splunk.cloudconnect.settings.logging.type')
return env_var and env_var == "stderr"
@singleton
class Logs(object):
def __init__(self, namespace=None, default_level=logging.INFO):
self._loggers = {}
self._default_level = default_level
if namespace is None:
namespace = cutil.get_appname_from_path(op.abspath(__file__))
if namespace:
namespace = namespace.lower()
self._namespace = namespace
def get_logger(self, name, level=None,
maxBytes=25000000, backupCount=5):
"""
Set up a default logger.
:param name: The log file name.
:param level: The logging level.
:param maxBytes: The maximum log file size before rollover.
:param backupCount: The number of log files to retain.
"""
# Strip ".py" from the log file name if auto-generated by a script.
if level is None:
level = self._default_level
name = self._get_log_name(name)
if name in self._loggers:
return self._loggers[name]
logger = logging.getLogger(name)
if check_add_stderr_handler():
import sys
ch = logging.StreamHandler(sys.stderr)
ch.setLevel(logging.ERROR)
formatter = logging.Formatter(__LOG_FORMAT__)
ch.setFormatter(formatter)
logger.addHandler(ch)
else:
logfile = make_splunkhome_path(["var", "log", "splunk", name])
handler_exists = any(
[True for h in logger.handlers if h.baseFilename == logfile])
if not handler_exists:
file_handler = handlers.RotatingFileHandler(
logfile, mode="a", maxBytes=maxBytes, backupCount=backupCount)
formatter = logging.Formatter(__LOG_FORMAT__ )
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.setLevel(level)
logger.propagate = False
self._loggers[name] = logger
return logger
def set_level(self, level, name=None):
"""
Change the log level of the logging
:param level: the level of the logging to be setLevel
:param name: the name of the logging to set, in case it is not set,
all the loggers will be affected
"""
if name is not None:
name = self._get_log_name(name)
logger = self._loggers.get(name)
if logger is not None:
logger.setLevel(level)
else:
self._default_level = level
for logger in self._loggers.itervalues():
logger.setLevel(level)
def _get_log_name(self, name):
if name.endswith(".py"):
name = name.replace(".py", "")
if self._namespace:
name = "{}_{}.log".format(self._namespace, name)
else:
name = "{}.log" .format(name)
return name
# Global logger
logger = Logs().get_logger("util")
def reset_logger(name):
"""
Reset global logger.
"""
global logger
logger = Logs().get_logger(name)
@@ -1,37 +0,0 @@
"""
Copyright (C) 2005-2015 Splunk Inc. All Rights Reserved.
Commonly used design partten for python user, includes:
- singleton (Decorator function used to build singleton)
"""
from functools import wraps
def singleton(class_):
"""
Singleton decoorator function.
"""
instances = {}
@wraps(class_)
def getinstance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return getinstance
class Singleton(type):
"""
Singleton meta class
"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(
*args, **kwargs)
print cls
return cls._instances[cls]
@@ -1,108 +0,0 @@
"""
Copyright (C) 2005-2015 Splunk Inc. All Rights Reserved.
"""
import os
import os.path as op
import datetime
import sys
import gc
import urllib
def handle_tear_down_signals(callback):
import signal
signal.signal(signal.SIGTERM, callback)
signal.signal(signal.SIGINT, callback)
if os.name == "nt":
signal.signal(signal.SIGBREAK, callback)
def datetime_to_seconds(dt):
epoch_time = datetime.datetime.utcfromtimestamp(0)
return (dt - epoch_time).total_seconds()
def is_true(val):
value = str(val).strip().upper()
if value in ("1", "TRUE", "T", "Y", "YES"):
return True
return False
def is_false(val):
value = str(val).strip().upper()
if value in ("0", "FALSE", "F", "N", "NO", "NONE", ""):
return True
return False
def remove_http_proxy_env_vars():
for k in ("http_proxy", "https_proxy"):
if k in os.environ:
del os.environ[k]
elif k.upper() in os.environ:
del os.environ[k.upper()]
def get_appname_from_path(absolute_path):
absolute_path = op.normpath(absolute_path)
parts = absolute_path.split(os.path.sep)
parts.reverse()
for key in ("apps", "slave-apps", "master-apps"):
try:
idx = parts.index(key)
except ValueError:
continue
else:
try:
if parts[idx + 1] == "etc":
return parts[idx - 1]
except IndexError:
pass
continue
#return None
return "-"
def escape_cdata(data):
# FIXME: This is a workaround for JIRA [addon-10459]
data = data.decode("utf-8", errors="replace").encode("utf-8", errors="xmlcharrefreplace")
data = data.replace("]]>", "]]&gt;")
if data.endswith("]"):
data = data[:-1] + "%5D"
return data
def extract_datainput_name(stanza_name):
"""
stansa_name: string like aws_s3://my_s3_data_input
"""
sep = "://"
try:
idx = stanza_name.index(sep)
except ValueError:
return stanza_name
return stanza_name[idx + len(sep):]
def escape_json_control_chars(json_str):
control_chars = ((r"\n", "\\\\n"), (r"\r", "\\\\r"),
(r"\r\n", "\\\\r\\\\n"))
for ch, replace in control_chars:
json_str = json_str.replace(ch, replace)
return json_str
def disable_stdout_buffer():
os.environ["PYTHONUNBUFFERED"] = "1"
sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 0)
gc.garbage.append(sys.stdout)
def format_stanza_name(name):
return urllib.quote(name.encode("utf-8"), "")
@@ -1,47 +0,0 @@
import re
from xml.etree import cElementTree as et
def parse_conf_xml_dom(xml_content):
"""
@xml_content: XML DOM from splunkd
"""
m = re.search(r'xmlns="([^"]+)"', xml_content)
ns = m.group(1)
m = re.search(r'xmlns:s="([^"]+)"', xml_content)
sub_ns = m.group(1)
entry_path = "./{%s}entry" % ns
stanza_path = "./{%s}title" % ns
key_path = "./{%s}content/{%s}dict/{%s}key" % (ns, sub_ns, sub_ns)
meta_path = "./{%s}dict/{%s}key" % (sub_ns, sub_ns)
list_path = "./{%s}list/{%s}item" % (sub_ns, sub_ns)
xml_conf = et.fromstring(xml_content)
stanza_objs = []
for entry in xml_conf.iterfind(entry_path):
for stanza in entry.iterfind(stanza_path):
stanza_obj = {"name": stanza.text,"stanza": stanza.text}
break
else:
continue
for key in entry.iterfind(key_path):
if key.get("name") == "eai:acl":
meta = {}
for k in key.iterfind(meta_path):
meta[k.get("name")] = k.text
stanza_obj[key.get("name")] = meta
elif key.get("name") != "eai:attributes":
name = key.get("name")
if name.startswith("eai:"):
name = name[4:]
list_vals = [k.text for k in key.iterfind(list_path)]
if list_vals:
stanza_obj[name] = list_vals
else:
stanza_obj[name] = key.text
if key.text == "None":
stanza_obj[name] = None
stanza_objs.append(stanza_obj)
return stanza_objs
@@ -1,88 +0,0 @@
"""
Concurrent executor provides concurrent executing function either in
a thread pool or a process pool
"""
from ..concurrent import thread_pool as tp
from ..concurrent import process_pool as pp
class ConcurrentExecutor(object):
def __init__(self, config):
"""
:param config: dict like object, contains thread_min_size (int),
thread_max_size (int), daemonize_thread (bool),
process_size (int)
"""
self._io_executor = tp.ThreadPool(config.get("thread_min_size", 0),
config.get("thread_max_size", 0),
config.get("task_queue_size", 1024),
config.get("daemonize_thread", True))
self._compute_executor = None
if config.get("process_size", 0):
self._compute_executor = pp.ProcessPool(
config.get("process_size", 0))
def start(self):
self._io_executor.start()
def tear_down(self):
self._io_executor.tear_down()
if self._compute_executor is not None:
self._compute_executor.tear_down()
def run_io_func_sync(self, func, args=(), kwargs=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:return whatever the func returns
"""
return self._io_executor.apply(func, args, kwargs)
def run_io_func_async(self, func, args=(), kwargs=None, callback=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:calllback: when func is done and without exception, call the callback
:return whatever the func returns
"""
return self._io_executor.apply_async(func, args, kwargs, callback)
def enqueue_io_funcs(self, funcs, block=True):
"""
run jobs in a fire and forget way, no result will be handled
over to clients
:param funcs: tuple/list-like or generator like object, func shall be
callable
"""
return self._io_executor.enqueue_funcs(funcs, block)
def run_compute_func_sync(self, func, args=(), kwargs={}):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:return whatever the func returns
"""
assert self._compute_executor is not None
return self._compute_executor.apply(func, args, kwargs)
def run_compute_func_async(self, func, args=(), kwargs={}, callback=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:calllback: when func is done and without exception, call the callback
:return whatever the func returns
"""
assert self._compute_executor is not None
return self._compute_executor.apply_async(func, args, kwargs, callback)
@@ -1,63 +0,0 @@
"""
A wrapper of multiprocessing.pool
"""
import multiprocessing
from ..common import log
class ProcessPool(object):
"""
A simple wrapper of multiprocessing.pool
"""
def __init__(self, size=0, maxtasksperchild=10000):
if size <= 0:
size = multiprocessing.cpu_count()
self.size = size
self._pool = multiprocessing.Pool(processes=size,
maxtasksperchild=maxtasksperchild)
self._stopped = False
def tear_down(self):
"""
Tear down the pool
"""
if self._stopped:
log.logger.info("ProcessPool has already stopped.")
return
self._stopped = True
self._pool.close()
self._pool.join()
log.logger.info("ProcessPool stopped.")
def apply(self, func, args=(), kwargs={}):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:return whatever the func returns
"""
if self._stopped:
log.logger.info("ProcessPool has already stopped.")
return None
return self._pool.apply(func, args, kwargs)
def apply_async(self, func, args=(), kwargs={}, callback=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:callback: when func is done without exception, call this callack
:return whatever the func returns
"""
if self._stopped:
log.logger.info("ProcessPool has already stopped.")
return None
return self._pool.apply_async(func, args, kwargs, callback)
@@ -1,344 +0,0 @@
"""
A simple thread pool implementation
"""
import threading
import Queue
import multiprocessing
import traceback
import exceptions
from time import time
from ..common import log
class ThreadPool(object):
"""
A simple thread pool implementation
"""
_high_watermark = 0.2
_resize_window = 10
def __init__(self, min_size=1, max_size=128,
task_queue_size=1024, daemon=True):
assert task_queue_size
if not min_size or min_size <= 0:
min_size = multiprocessing.cpu_count()
if not max_size or max_size <= 0:
max_size = multiprocessing.cpu_count() * 8
self._min_size = min_size
self._max_size = max_size
self._daemon = daemon
self._work_queue = Queue.Queue(task_queue_size)
self._thrs = []
for _ in range(min_size):
thr = threading.Thread(target=self._run)
self._thrs.append(thr)
self._admin_queue = Queue.Queue()
self._admin_thr = threading.Thread(target=self._do_admin)
self._last_resize_time = time()
self._last_size = min_size
self._lock = threading.Lock()
self._occupied_threads = 0
self._count_lock = threading.Lock()
self._started = False
def start(self):
"""
Start threads in the pool
"""
with self._lock:
if self._started:
return
self._started = True
for thr in self._thrs:
thr.daemon = self._daemon
thr.start()
self._admin_thr.start()
log.logger.info("ThreadPool started.")
def tear_down(self):
"""
Tear down thread pool
"""
with self._lock:
if not self._started:
return
self._started = False
for thr in self._thrs:
self._work_queue.put(None, block=True)
self._admin_queue.put(None)
if not self._daemon:
log.logger.info("Wait for threads to stop.")
for thr in self._thrs:
thr.join()
self._admin_thr.join()
log.logger.info("ThreadPool stopped.")
def enqueue_funcs(self, funcs, block=True):
"""
run jobs in a fire and forget way, no result will be handled
over to clients
:param funcs: tuple/list-like or generator like object, func shall be
callable
"""
if not self._started:
log.logger.info("ThreadPool has already stopped.")
return
for func in funcs:
self._work_queue.put(func, block)
def apply_async(self, func, args=(), kwargs=None, callback=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:callback: when func is done and without exception, call the callback
:return AsyncResult, clients can poll or wait the result through it
"""
if not self._started:
log.logger.info("ThreadPool has already stopped.")
return None
res = AsyncResult(func, args, kwargs, callback)
self._work_queue.put(res)
return res
def apply(self, func, args=(), kwargs=None):
"""
:param func: callable
:param args: free params
:param kwargs: named params
:return whatever the func returns
"""
if not self._started:
log.logger.info("ThreadPool has already stopped.")
return None
res = self.apply_async(func, args, kwargs)
return res.get()
def size(self):
return self._last_size
def resize(self, new_size):
"""
Resize the pool size, spawn or destroy threads if necessary
"""
if new_size <= 0:
return
if self._lock.locked() or not self._started:
log.logger.info("Try to resize thread pool during the tear "
"down process, do nothing")
return
with self._lock:
self._remove_exited_threads_with_lock()
size = self._last_size
self._last_size = new_size
if new_size > size:
for _ in xrange(new_size - size):
thr = threading.Thread(target=self._run)
thr.daemon = self._daemon
thr.start()
self._thrs.append(thr)
elif new_size < size:
for _ in xrange(size - new_size):
self._work_queue.put(None)
log.logger.info("Finished ThreadPool resizing. New size=%d", new_size)
def _remove_exited_threads_with_lock(self):
"""
Join the exited threads last time when resize was called
"""
joined_thrs = set()
for thr in self._thrs:
if not thr.is_alive():
try:
if not thr.daemon:
thr.join(timeout=0.5)
joined_thrs.add(thr.ident)
except RuntimeError:
pass
if joined_thrs:
live_thrs = []
for thr in self._thrs:
if thr.ident not in joined_thrs:
live_thrs.append(thr)
self._thrs = live_thrs
def _do_resize_according_to_loads(self):
if (self._last_resize_time and
time() - self._last_resize_time < self._resize_window):
return
thr_size = self._last_size
free_thrs = thr_size - self._occupied_threads
work_size = self._work_queue.qsize()
log.logger.debug("current_thr_size=%s, free_thrs=%s, work_size=%s",
thr_size, free_thrs, work_size)
if work_size and work_size > free_thrs:
if thr_size < self._max_size:
thr_size = min(thr_size * 2, self._max_size)
self.resize(thr_size)
elif free_thrs > 0:
free = free_thrs * 1.0
if free / thr_size >= self._high_watermark and free_thrs >= 2:
# 20 % thrs are idle, tear down half of the idle ones
thr_size = thr_size - free_thrs / 2
if thr_size > self._min_size:
self.resize(thr_size)
self._last_resize_time = time()
def _do_admin(self):
admin_q = self._admin_queue
resize_win = self._resize_window
while 1:
try:
wakup = admin_q.get(timeout=resize_win + 1)
except Queue.Empty:
self._do_resize_according_to_loads()
continue
if wakup is None:
break
else:
self._do_resize_according_to_loads()
log.logger.info("ThreadPool admin thread=%s stopped.",
threading.current_thread().getName())
def _run(self):
"""
Threads callback func, run forever to handle jobs from the job queue
"""
work_queue = self._work_queue
count_lock = self._count_lock
while 1:
log.logger.debug("Going to get job")
func = work_queue.get()
if func is None:
break
if not self._started:
break
log.logger.debug("Going to exec job")
with count_lock:
self._occupied_threads += 1
try:
func()
except Exception:
log.logger.error(traceback.format_exc())
with count_lock:
self._occupied_threads -= 1
log.logger.debug("Done with exec job")
log.logger.info("Thread work_queue_size=%d", work_queue.qsize())
log.logger.debug("Worker thread %s stopped.",
threading.current_thread().getName())
class AsyncResult(object):
def __init__(self, func, args, kwargs, callback):
self._func = func
self._args = args
self._kwargs = kwargs
self._callback = callback
self._q = Queue.Queue()
def __call__(self):
try:
if self._args and self._kwargs:
res = self._func(*self._args, **self._kwargs)
elif self._args:
res = self._func(*self._args)
elif self._kwargs:
res = self._func(**self._kwargs)
else:
res = self._func()
except Exception as e:
self._q.put(e)
return
else:
self._q.put(res)
if self._callback is not None:
self._callback()
def get(self, timeout=None):
"""
Return the result when it arrives. If timeout is not None and the
result does not arrive within timeout seconds then
multiprocessing.TimeoutError is raised. If the remote call raised an
exception then that exception will be reraised by get().
"""
try:
res = self._q.get(timeout=timeout)
except Queue.Empty:
raise multiprocessing.TimeoutError("Timed out")
if isinstance(res, Exception):
raise res
return res
def wait(self, timeout=None):
"""
Wait until the result is available or until timeout seconds pass.
"""
try:
res = self._q.get(timeout=timeout)
except Queue.Empty:
pass
else:
self._q.put(res)
def ready(self):
"""
Return whether the call has completed.
"""
return len(self._q)
def successful(self):
"""
Return whether the call completed without raising an exception.
Will raise AssertionError if the result is not ready.
"""
if not self.ready():
raise exceptions.AssertionError("Function is not ready")
res = self._q.get()
self._q.put(res)
if isinstance(res, Exception):
return False
return True
@@ -1,142 +0,0 @@
from .request import content_request
from ..common import util
from ..common import xml_dom_parser as xdp
CONF_ENDPOINT = "%s/servicesNS/%s/%s/configs/conf-%s"
def _conf_endpoint_ns(uri, owner, app, conf_name):
return CONF_ENDPOINT % (uri, owner, app, conf_name)
def reload_conf(splunkd_uri, session_key, app_name, conf_name, throw=False):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param conf_names: a list of the name of the conf file, e.g. ["props"]
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
"""
uri = _conf_endpoint_ns(splunkd_uri, "nobody", app_name, conf_name)
uri += "/_reload"
msg = "Failed to reload conf in app=%s: %s" % (app_name, conf_name)
try:
content_request(uri, session_key, "GET", None, msg)
except Exception:
if throw:
raise
def create_stanza(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, key_values):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:param key_values: the key-value dict of the stanza
:return: None on success otherwise throw exception
"""
uri = _conf_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
msg = "Failed to create stanza=%s in conf=%s" % (stanza, conf_name)
payload = {"name": unicode(stanza).encode('utf-8')}
for key in key_values:
if key != "name":
payload[key] = str(key_values[key])
content_request(uri, session_key, "POST", payload, msg)
def get_conf(splunkd_uri, session_key, owner, app_name, conf_name,
stanza=None):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:return: a list of stanzas in the conf file, including metadata
"""
uri = _conf_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
if stanza:
uri += "/" + util.format_stanza_name(stanza)
# get all the stanzas at one time
uri += "?count=0&offset=0"
msg = "Failed to get stanza=%s in conf=%s" % (stanza if stanza else stanza, conf_name)
content = content_request(uri, session_key, "GET", None, msg)
return xdp.parse_conf_xml_dom(content)
def update_stanza(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, key_values):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:param key_values: the key-value dict of the stanza
:return: None on success otherwise raise exception
"""
uri = _conf_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
uri += "/" + util.format_stanza_name(stanza)
msg = "Failed to update stanza=%s in conf=%s" % (stanza, conf_name)
return content_request(uri, session_key, "POST", key_values, msg)
def delete_stanza(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, throw=False):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:return: None on success otherwise raise exception
"""
uri = _conf_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
uri += "/" + util.format_stanza_name(stanza)
msg = "Failed to delete stanza=%s in conf=%s" % (stanza, conf_name)
content_request(uri, session_key, "DELETE", None, msg)
def stanza_exist(splunkd_uri, session_key, owner, app_name, conf_name,
stanza):
try:
res = get_conf(splunkd_uri, session_key, owner, app_name, conf_name,
stanza)
return len(res) > 0
except Exception:
return False
def operate_conf(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, operation):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:param operation: must be "disable" or "enable"
"""
assert operation in ("disable", "enable")
uri = _conf_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
uri += "/%s/%s" % (util.format_stanza_name(stanza), operation)
msg = "Failed to disable/enable stanza=%s in conf=%s" % (stanza, conf_name)
content_request(uri, session_key, "POST", None, msg)
@@ -1,225 +0,0 @@
"""
This module hanles configuration related stuff
"""
import os.path as op
from . import conf_endpoints as scmc
from . import data_input_endpoints as scmdi
from . import property_endpoints as scmp
from . import request as req
def conf_file2name(conf_file):
conf_name = op.basename(conf_file)
if conf_name.endswith(".conf"):
conf_name = conf_name[:-5]
return conf_name
class ConfManager(object):
def __init__(self, splunkd_uri, session_key, owner="nobody", app_name="-"):
"""
:app_name: when creating conf stanza, app_name is required to set not
to "-"
:owner: when creating conf stanza, app_name is required to set not
to "-"
"""
self.splunkd_uri = splunkd_uri
self.session_key = session_key
self.owner = owner
self.app_name = app_name
def set_appname(self, appname):
"""
This are cases we need edit/remove/create confs in different app
context. call this interface to switch app context before manipulate
the confs in different app context
"""
self.app_name = appname
def all_stanzas(self, conf_name, do_reload=False, ret_metadata=False):
"""
:return: a list of dict stanza objects if successful.
Otherwise raise exception
"""
if do_reload:
self.reload_conf(conf_name)
stanzas = scmc.get_conf(self.splunkd_uri, self.session_key,
"-", "-", conf_name)
return self._delete_metadata(stanzas, ret_metadata)
def all_stanzas_as_dicts(self, conf_name, do_reload=False,
ret_metadata=False):
"""
:return: a dict of dict stanza objects if successful.
otherwise raise exception
"""
stanzas = self.all_stanzas(conf_name, do_reload, ret_metadata)
return {stanza["name"]: stanza for stanza in stanzas}
def get_stanza(self, conf_name, stanza,
do_reload=False, ret_metadata=False):
"""
@return dict if success otherwise raise exception
"""
if do_reload:
self.reload_conf(conf_name)
stanzas = scmc.get_conf(self.splunkd_uri, self.session_key,
"-", "-", conf_name, stanza)
stanzas = self._delete_metadata(stanzas, ret_metadata)
return stanzas[0]
def reload_conf(self, conf_name):
scmc.reload_conf(self.splunkd_uri, self.session_key, "-", conf_name)
def enable_conf(self, conf_name, stanza):
scmc.operate_conf(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza, "enable")
def disable_conf(self, conf_name, stanza):
scmc.operate_conf(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza, "disable")
def get_property(self, conf_name, stanza, key, do_reload=False):
if do_reload:
self.reload_conf(conf_name)
return scmp.get_property(self.splunkd_uri, self.session_key,
"-", "-", conf_name, stanza, key)
def stanza_exist(self, conf_name, stanza):
return scmc.stanza_exist(self.splunkd_uri, self.session_key,
"-", "-", conf_name, stanza)
def create_stanza(self, conf_name, stanza, key_values):
scmc.create_stanza(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza, key_values)
def update_stanza(self, conf_name, stanza, key_values):
scmc.update_stanza(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza, key_values)
def delete_stanza(self, conf_name, stanza):
scmc.delete_stanza(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza)
def create_properties(self, conf_name, stanza):
scmp.create_properties(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza)
def update_properties(self, conf_name, stanza, key_values):
scmp.update_properties(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
conf_name, stanza, key_values)
def delete_stanzas(self, conf_name, stanzas):
"""
:param stanzas: list of stanzas
:return: list of failed stanzas
"""
failed_stanzas = []
for stanza in stanzas:
try:
self.delete_stanza(conf_name, stanza)
except Exception:
failed_stanzas.append(stanza)
return failed_stanzas
# data input management
def create_data_input(self, input_type, name, key_values=None):
scmdi.create_data_input(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
input_type, name, key_values)
def update_data_input(self, input_type, name, key_values):
scmdi.update_data_input(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
input_type, name, key_values)
def delete_data_input(self, input_type, name):
scmdi.delete_data_input(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
input_type, name)
def get_data_input(self, input_type, name=None, do_reload=False):
if do_reload:
self.reload_data_input(input_type)
return scmdi.get_data_input(self.splunkd_uri, self.session_key,
"-", "-", input_type, name)
def reload_data_input(self, input_type):
scmdi.reload_data_input(self.splunkd_uri, self.session_key,
"-", "-", input_type)
def enable_data_input(self, input_type, name):
scmdi.operate_data_input(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
input_type, name, "enable")
def disable_data_input(self, input_type, name):
scmdi.operate_data_input(self.splunkd_uri, self.session_key,
self.owner, self.app_name,
input_type, name, "disable")
def data_input_exist(self, input_type, name):
try:
result = self.get_data_input(input_type, name)
except req.ConfNotExistsException:
return False
return result is not None
def all_data_input_stanzas(self, input_type, do_reload=False,
ret_metadata=False):
stanzas = self.get_data_input(input_type, do_reload=do_reload)
for stanza in stanzas:
if "eai:acl" in stanza and "app" in stanza["eai:acl"]:
stanza["appName"] = stanza["eai:acl"]["app"]
stanza["userName"] = stanza["eai:acl"].get("owner", "nobody")
return self._delete_metadata(stanzas, ret_metadata)
def get_data_input_stanza(self, input_type, name, do_reload=False,
ret_metadata=False):
stanzas = self.get_data_input(input_type, name, do_reload)
stanzas = self._delete_metadata(stanzas, ret_metadata)
return stanzas[0]
def delete_data_input_stanzas(self, input_type, names):
"""
:param stanzas: list of stanzas
:return: list of failed stanzas
"""
failed_names = []
for name in names:
try:
self.delete_data_input(input_type, name)
except Exception:
failed_names.append(name)
return failed_names
def _delete_metadata(self, stanzas, ret_metadata):
if stanzas and not ret_metadata:
for stanza in stanzas:
for key in stanza.keys():
if key.startswith("eai:"):
del stanza[key]
return stanzas
@@ -1,152 +0,0 @@
from .request import content_request
from ..common import util
from ..common import xml_dom_parser as xdp
INPUT_ENDPOINT = "%s/servicesNS/%s/%s/data/inputs/%s"
def _input_endpoint_ns(uri, owner, app, input_type):
return INPUT_ENDPOINT % (uri, owner, app, input_type)
def reload_data_input(splunkd_uri, session_key, owner, app_name,
input_type, throw=False):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
"""
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
uri += "/_reload"
msg = "Failed to reload data input in app=%s: %s" % (app_name, input_type)
try:
content_request(uri, session_key, "GET", None, msg)
except Exception:
if throw:
raise
def create_data_input(splunkd_uri, session_key, owner, app_name, input_type,
name, key_values):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
:param name: The name of the input stanza to create.
i.e. stanza [<input_type>://<name>] will be created.
:param key_values: a K-V dict of details in the data input stanza.
:return: None on success else raise exception
"""
key_values["name"] = unicode(name).encode('utf-8')
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
msg = "Failed to create data input in app=%s: %s://%s" % (
app_name, input_type, name)
content_request(uri, session_key, "POST", key_values, msg)
def get_data_input(splunkd_uri, session_key, owner, app_name, input_type,
name=None):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
:param name: The name of the input stanza to create.
i.e. stanza [<input_type>://<name>] will be deleted.
:return: a list of stanzas in the input type, including metadata
"""
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
if name:
uri += "/" + util.format_stanza_name(name)
# get all the stanzas at one time
uri += "?count=0&offset=0"
msg = "Failed to get data input in app=%s: %s://%s" % (
app_name, input_type, name if name else name)
content = content_request(uri, session_key, "GET", None, msg)
return xdp.parse_conf_xml_dom(content)
def update_data_input(splunkd_uri, session_key, owner, app_name, input_type,
name, key_values):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
:param name: The name of the input stanza to create.
i.e. stanza [<input_type>://<name>] will be updated.
:param key_values: a K-V dict of details in the data input stanza.
:return: raise exception when failure
"""
if "name" in key_values:
del key_values["name"]
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
uri += "/" + util.format_stanza_name(name)
msg = "Failed to update data input in app=%s: %s://%s" % (
app_name, input_type, name)
content_request(uri, session_key, "POST", key_values, msg)
def delete_data_input(splunkd_uri, session_key, owner, app_name, input_type,
name):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
:param name: The name of the input stanza to create.
i.e. stanza [<input_type>://<name>] will be deleted.
:return raise exception when failed
"""
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
uri += "/" + util.format_stanza_name(name)
msg = "Failed to delete data input in app=%s: %s://%s" % (
app_name, input_type, name)
content_request(uri, session_key, "DELETE", None, msg)
def operate_data_input(splunkd_uri, session_key, owner, app_name,
input_type, name, operation):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param input_type: name of the input type.
if it is a script input, the input is "script",
for modinput, say snow, the input is "snow"
:param name: The name of the input stanza to create.
i.e. stanza [<input_type>://<name>] will be operated.
:param operation: must be "disable" or "enable"
"""
assert operation in ("disable", "enable")
uri = _input_endpoint_ns(splunkd_uri, owner, app_name, input_type)
uri += "/%s/%s" % (util.format_stanza_name(name), operation)
msg = "Failed to %s data input in app=%s: %s://%s" % (
operation, app_name, input_type, name)
content_request(uri, session_key, "POST", None, msg)
@@ -1,36 +0,0 @@
from . import request as req
from ..common import xml_dom_parser as xdp
class KnowledgeObjectManager(object):
def __init__(self, splunkd_uri, session_key):
self.splunkd_uri = splunkd_uri
self.session_key = session_key
def apps(self):
"""
@return: a list of dict containing apps if successfuly otherwise
otherwise raise exceptions
"""
uri = "{}/services/apps/local?count=0&offset=0".format(
self.splunkd_uri)
apps = self._do_request(uri, "GET", None, "Failed to get apps")
return apps
def indexes(self):
"""
@return: a list of dict containing indexes if successfuly
otherwise raise exceptions
"""
uri = "{}/services/data/indexes/?count=0&offset=0".format(
self.splunkd_uri)
indexes = self._do_request(uri, "GET", None, "Failed to get indexes")
return indexes
def _do_request(self, uri, method, payload, err_msg):
_, content = req.content_request(uri, self.session_key, method,
payload, err_msg)
return xdp.parse_conf_xml_dom(content)
@@ -1,78 +0,0 @@
from .request import content_request
from ..common import util
PROPERTY_ENDPOINT = "%s/servicesNS/%s/%s/properties/%s"
def _property_endpoint_ns(uri, owner, app, conf_name):
return PROPERTY_ENDPOINT % (uri, owner, app, conf_name)
def create_properties(splunkd_uri, session_key, owner, app_name, conf_name,
stanza):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:return: None on success else raise exception
"""
uri = _property_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
msg = "Properties: failed to create stanza=%s in conf=%s" % \
(stanza, conf_name)
payload = {"__stanza": stanza}
content_request(uri, session_key, "POST", payload, msg)
def get_property(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, key):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:param key: the property name
:return: the property value
"""
uri = _property_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
uri += "/%s/%s" % (util.format_stanza_name(stanza), key)
msg = "Properties: failed to get conf=%s, stanza=%s, key=%s" % \
(conf_name, stanza, key)
return content_request(uri, session_key, "GET", None, msg)
def update_properties(splunkd_uri, session_key, owner, app_name, conf_name,
stanza, key_values):
"""
:param splunkd_uri: splunkd uri, e.g. https://127.0.0.1:8089
:param session_key: splunkd session key
:param owner: the owner (ACL user), e.g. "-", "nobody"
:param app_name: the app"s name, e.g. "Splunk_TA_aws"
:param conf_name: the name of the conf file, e.g. "props"
:param stanza: stanza name, e.g. "aws:cloudtrail"
:param key_values: the key-value dict of the stanza
:return: raise exception when failed
"""
uri = _property_endpoint_ns(splunkd_uri, owner, app_name, conf_name)
uri += "/" + util.format_stanza_name(stanza)
msg = "Properties: failed to update conf=%s, stanza=%s" % \
(conf_name, stanza)
has_name = False
if "name" in key_values:
has_name = True
name = key_values["name"]
del key_values["name"]
content_request(uri, session_key, "POST", key_values, msg)
if has_name:
key_values["name"] = name
@@ -1,44 +0,0 @@
from .. import rest
from ..common import log
class ConfRequestException(Exception):
pass
class ConfNotExistsException(ConfRequestException):
pass
class ConfExistsException(ConfRequestException):
pass
def content_request(uri, session_key, method, payload, err_msg):
"""
:return: response content if successful otherwise raise
ConfRequestException
"""
resp, content = rest.splunkd_request(uri, session_key, method,
data=payload, retry=3)
if resp is None and content is None:
return None
if resp.status >= 200 and resp.status <= 204:
return content
else:
msg = "%s, status=%s, reason=%s, detail=%s" % (
err_msg, resp.status, resp.reason, content.decode('utf-8'))
if not (method == "GET" and resp.status == 404):
log.logger.error(msg)
if resp.status == 404:
raise ConfNotExistsException(msg)
if resp.status == 409:
raise ConfExistsException(msg)
else:
if content and "already exists" in content:
raise ConfExistsException(msg)
raise ConfRequestException(msg)
@@ -1,211 +0,0 @@
"""
This module hanles high level TA configuration related stuff
"""
import copy
import os.path as op
from . import conf_manager as conf
from . import request as conf_req
from .. import credentials as cred
from ..common import util as utils
class TAConfManager(object):
encrypted_token = "******"
reserved_keys = ("userName", "appName")
def __init__(self, conf_file, splunkd_uri, session_key, appname=None):
if appname is None:
appname = utils.get_appname_from_path(op.abspath(__file__))
self._conf_file = conf.conf_file2name(conf_file)
self._conf_mgr = conf.ConfManager(splunkd_uri, session_key,
app_name=appname)
self._cred_mgr = cred.CredentialManager(
splunkd_uri, session_key, app=appname,
owner="nobody", realm=appname)
self._keys = None
def set_appname(self, appname):
"""
This are cases we need edit/remove/create confs in different app
context. call this interface to switch app context before manipulate
the confs in different app context
"""
self._conf_mgr.set_appname(appname)
self._cred_mgr.set_appname(appname)
def _delete_reserved_keys(self, stanza):
new_stanza = copy.deepcopy(stanza)
for k in self.reserved_keys:
if k in new_stanza:
del new_stanza[k]
return new_stanza
def create(self, stanza):
"""
@stanza: dick like object
{
"name": xxx,
"k1": v1,
"k2": v2,
...
}
@return exception if failure
"""
stanza = self._delete_reserved_keys(stanza)
encrypted_stanza = self._encrypt(stanza)
self._conf_mgr.create_stanza(self._conf_file,
encrypted_stanza["name"],
encrypted_stanza)
def update(self, stanza):
"""
@stanza: dick like object
{
"name": xxx,
"k1": v1,
"k2": v2,
...
}
@return: exception if failure
"""
if not self._conf_mgr.stanza_exist(self._conf_file, stanza["name"]):
self.create(stanza)
else:
stanza = self._delete_reserved_keys(stanza)
encrypted_stanza = self._encrypt(stanza)
self._conf_mgr.update_properties(
self._conf_file, encrypted_stanza["name"], encrypted_stanza)
def delete(self, stanza_name):
"""
@return: exception if failure
"""
try:
stanza = self._conf_mgr.get_stanza(self._conf_file, stanza_name)
except conf_req.ConfNotExistsException:
return
self._delete_creds(stanza)
self._conf_mgr.delete_stanza(self._conf_file, stanza_name)
def get(self, stanza_name, return_acl=False):
"""
@return: dict object if sucess otherwise raise exception
"""
stanza = self._conf_mgr.get_stanza(self._conf_file, stanza_name,
ret_metadata=return_acl)
stanza = self._decrypt(stanza)
stanza["disabled"] = utils.is_true(stanza.get("disabled"))
return stanza
def all(self, filter_disabled=False, return_acl=True):
"""
@return: a dict of dict objects if success
otherwise exception
"""
results = {}
stanzas = self._conf_mgr.all_stanzas(self._conf_file,
ret_metadata=return_acl)
for stanza in stanzas:
stanza = self._decrypt(stanza)
stanza["disabled"] = utils.is_true(stanza.get("disabled"))
if filter_disabled and stanza["disabled"]:
continue
results[stanza["name"]] = stanza
return results
def reload(self):
self._conf_mgr.reload_conf(self._conf_file)
def set_encrypt_keys(self, keys):
"""
:keys: a list keys of a stanza which need to be encrypted
for example: ["username", "password"]
"""
self._keys = keys
def is_encrypted(self, stanza):
"""
:stanza: dict object
return True if the values of encrypt keys equals self.encrypted_token
otherwise return False
"""
if self._keys is None:
return False
for k in stanza.iterkeys():
if k in self._keys:
if stanza.get(k) == self.encrypted_token:
return True
return False
def _encrypt(self, stanza):
"""
:stanza: if self._keys are in stanza, encrypt the values of the key
and then mask the value to self.encrypted_token
"""
if self._keys is None:
return stanza
stanza_to_be_encrypted = {}
for key in self._keys:
if key in stanza:
stanza_to_be_encrypted[key] = stanza[key]
if stanza_to_be_encrypted:
self._cred_mgr.update({stanza["name"]: stanza_to_be_encrypted})
encrypted_stanza = copy.deepcopy(stanza)
for key in stanza_to_be_encrypted.iterkeys():
encrypted_stanza[key] = self.encrypted_token
return encrypted_stanza
return stanza
def _decrypt(self, stanza):
"""
:stanza: if there are keys in self._keys in stanza and if the values of
the keys are self.encrypted_token, decrypt the value
"""
if self._keys is None:
return stanza
stanza_name = stanza["name"]
clear_password = None
for key in self._keys:
if key in stanza and stanza[key] == self.encrypted_token:
clear_password = self._cred_mgr.get_clear_password(
stanza_name)
break
if clear_password:
for key in self._keys:
if key in clear_password[stanza_name]:
stanza[key] = clear_password[stanza_name][key]
return stanza
def _delete_creds(self, stanza):
"""
:stanza: if there are keys of self._keys and the keys are in stanza,
delete the encrypted creds
"""
if self._keys is None:
return
for key in self._keys:
if key in stanza:
self._cred_mgr.delete(stanza["name"])
break
@@ -1,337 +0,0 @@
"""
Handles credentials related stuff
"""
import re
import xml.dom.minidom as xdm
from . import rest
from .common import util
from .common import xml_dom_parser as xdp
# Splunk can only encrypt string when length <=255
SPLUNK_CRED_LEN_LIMIT = 255
class CredException(Exception):
pass
class CredNotFound(CredException):
"""
Credential information not exists
"""
pass
def create_credential_manager(username, password, splunkd_uri,
app, owner, realm):
session_key = CredentialManager.get_session_key(
username, password, splunkd_uri)
return CredentialManager(splunkd_uri, session_key, app, owner, realm)
class CredentialManager(object):
"""
Credential related interfaces
"""
def __init__(self, splunkd_uri, session_key,
app="-", owner="nobody", realm=None):
"""
:app: when creating/upating/deleting app is required
"""
self._app = app
self._splunkd_uri = splunkd_uri
self._owner = owner
self._sep = "``splunk_cred_sep``"
if realm:
self._realm = realm
else:
self._realm = app
self._session_key = session_key
def set_appname(self, app):
"""
This are cases we need edit/remove/create confs in different app
context. call this interface to switch app context before manipulate
the confs in different app context
"""
self._app = app
@staticmethod
def get_session_key(username, password,
splunkd_uri="https://localhost:8089"):
"""
Get session key by using login username and passwrod
:return: session_key if successful, None if failed
"""
eid = "".join((splunkd_uri, "/services/auth/login"))
postargs = {
"username": username,
"password": password,
}
response, content = rest.splunkd_request(
eid, None, method="POST", data=postargs)
if response is None and content is None:
raise CredException("Get session key failed.")
xml_obj = xdm.parseString(content)
session_nodes = xml_obj.getElementsByTagName("sessionKey")
if not session_nodes:
raise CredException("Invalid username or password.")
session_key = session_nodes[0].firstChild.nodeValue
if not session_key:
raise CredException("Get session key failed.")
return session_key
def update(self, stanza):
"""
Update or Create credentials based on the stanza
:stanza: nested dict object. The outlayer keys are stanza name, and
inner dict is user/pass key/value pair to be encrypted
{
"stanza_name": {"tommy": "tommypasswod", "jerry": "jerrypassword"}
}
:return: raise on failure
"""
for name, encr_dict in stanza.items():
encrypts = []
for key, val in encr_dict.items():
encrypts.append(key)
encrypts.append(val)
self._update(name, self._sep.join(encrypts))
def _update(self, name, str_to_encrypt):
"""
Update the string for the name.
:return: raise on failure
"""
self.delete(name)
if len(str_to_encrypt) <= SPLUNK_CRED_LEN_LIMIT:
self._create(name, str_to_encrypt)
return
# split the str_to_encrypt when len > 255
length = SPLUNK_CRED_LEN_LIMIT
i = 0
while length < len(str_to_encrypt) + SPLUNK_CRED_LEN_LIMIT:
curr_str = str_to_encrypt[length - SPLUNK_CRED_LEN_LIMIT:length]
length += SPLUNK_CRED_LEN_LIMIT
stanza_name = self._sep.join((name, str(i)))
self._create(stanza_name, curr_str)
i += 1
def _create(self, name, str_to_encrypt):
"""
Create a new stored credential.
:return: raise on failure
"""
payload = {
"name": name,
"password": str_to_encrypt,
"realm": self._realm,
}
endpoint = self._get_endpoint(name)
resp, content = rest.splunkd_request(endpoint, self._session_key,
method="POST", data=payload)
if not resp or resp.status not in (200, 201, "200", "201"):
raise CredException("Failed to encrypt username {}".format(name))
def delete(self, name, throw=False):
"""
Delete the encrypted entry
"""
try:
self._delete(name, throw=True)
except CredNotFound:
# try to delete the split stanzas
try:
stanzas = self._get_all_passwords()
except Exception:
raise
ent_regx = "%s:(%s%s\d+):" % (self._realm, name, self._sep)
ent_pattern = re.compile(ent_regx)
for stanza in stanzas:
stanza_name = stanza.get("name")
match = ent_pattern.match(stanza_name)
if match:
try:
delete_name = match.group(1)
self._delete(delete_name, throw=True)
except CredNotFound:
pass
except CredException:
raise
except CredException:
raise
def _delete(self, name, throw=False):
"""
Delete the encrypted entry
"""
endpoint = self._get_endpoint(name)
response, content = rest.splunkd_request(
endpoint, self._session_key, method="DELETE")
if response is not None and response.status in (404, "404"):
if throw:
raise CredNotFound(
"Credential stanza not exits - {}".format(name))
elif not response or response.status not in (200, 201, "200", "201"):
if throw:
raise CredException(
"Failed to delete credential stanza {}".format(name))
def get_all_passwords(self):
results = {}
all_stanzas = self._get_all_passwords()
for stanza in all_stanzas:
name = stanza.get("name")
match = re.match(r"(.+){}(\d+)".format(self._sep), name)
if match:
actual_name = match.group(1) + ":"
index = int(match.group(2))
if results.get(actual_name):
exist_stanza = results.get(actual_name)
else:
exist_stanza = stanza
exist_stanza['name'] = actual_name
exist_stanza['username'] = \
exist_stanza['username'].split(self._sep)[0]
exist_stanza['clears'] = {}
exist_stanza['encrs'] = {}
try:
exist_stanza['clears'][index] = stanza.get('clear_password')
exist_stanza['encrs'][index] = stanza.get('encr_password')
except KeyError:
exist_stanza['clears'] = {}
exist_stanza['encrs'] = {}
exist_stanza['clears'][index] = stanza.get('clear_password')
exist_stanza['encrs'][index] = stanza.get('encr_password')
results[actual_name] = exist_stanza
else:
results[name] = stanza
# merge the stanzas by index
for name, stanza in results.items():
field_clear = stanza.get('clears')
field_encr = stanza.get('encrs')
if isinstance(field_clear, dict):
clear_password = ""
encr_password = ""
for index in sorted(field_clear.keys()):
clear_password += field_clear.get(index)
encr_password += field_encr.get(index)
stanza['clear_password'] = clear_password
stanza['encr_password'] = encr_password
del stanza['clears']
del stanza['encrs']
return results.values()
def _get_all_passwords(self):
"""
:return: a list of dict when successful, None when failed.
the dict at least contains
{
"realm": xxx,
"username": yyy,
"clear_password": zzz,
}
"""
endpoint = self._get_endpoint()
response, content = rest.splunkd_request(
endpoint, self._session_key, method="GET")
if response and response.status in (200, 201, "200", "201") and content:
return xdp.parse_conf_xml_dom(content)
raise CredException("Failed to get credentials")
def get_clear_password(self, name=None):
"""
:return: clear password(s)
{
stanza_name: {"user": pass}
}
"""
return self._get_credentials("clear_password", name)
def get_encrypted_password(self, name=None):
"""
:return: encyrpted password(s)
"""
return self._get_credentials("encr_password", name)
def _get_credentials(self, prop, name=None):
"""
:return: clear or encrypted password for specified realm, user
"""
all_stanzas = self.get_all_passwords()
results = {}
for stanza in all_stanzas:
if name and not stanza.get("name").endswith(":" + name + ":"):
continue
if stanza.get("realm") == self._realm:
values = stanza[prop].split(self._sep)
if len(values) % 2 == 1:
continue
result = {values[i]: values[i + 1]
for i in range(0, len(values), 2)}
results[stanza.get("username")] = result
return results
@staticmethod
def _build_name(realm, name):
return util.format_stanza_name(
"".join((CredentialManager._escape_string(realm), ":",
CredentialManager._escape_string(name), ":")))
@staticmethod
def _escape_string(string_to_escape):
r"""
Splunk secure credential storage actually requires a custom style of
escaped string where all the :'s are escaped by a single \.
But don't escape the control : in the stanza name.
"""
return string_to_escape.replace(":", "\\:")
def _get_endpoint(self, name=None, query=False):
app = self._app
owner = self._owner
if query:
app = "-"
owner = "-"
if name:
realm_user = self._build_name(self._realm, name)
rest_endpoint = "{}/servicesNS/{}/{}/storage/passwords/{}".format(
self._splunkd_uri, owner, app, realm_user)
else:
rest_endpoint = "{}/servicesNS/{}/{}/storage/passwords?count=-1" \
"".format(self._splunkd_uri, owner, app)
return rest_endpoint
@@ -1,84 +0,0 @@
import Queue
import multiprocessing
import threading
import sys
from collections import Iterable
from .common import log
class EventWriter(object):
def __init__(self, process_safe=False):
if process_safe:
self._mgr = multiprocessing.Manager()
self._event_queue = self._mgr.Queue(1000)
else:
self._event_queue = Queue.Queue(1000)
self._event_writer = threading.Thread(target=self._do_write_events)
self._event_writer.daemon = True
self._started = False
self._exception = False
def start(self):
if self._started:
return
self._started = True
self._event_writer.start()
log.logger.info("Event writer started.")
def tear_down(self):
if not self._started:
return
self._started = False
self._event_queue.put(None)
self._event_writer.join()
log.logger.info("Event writer stopped.")
def isopen(self):
return self._started and (not self._exception)
def write_events(self, events):
if not self.isopen():
return False
if events is None:
return True
self._event_queue.put(events)
return True
def _do_write_events(self):
event_queue = self._event_queue
write = sys.stdout.write
got_shutdown_signal = False
while 1:
try:
event = event_queue.get(timeout=3)
if event is not None:
if isinstance(event, basestring):
write(event)
elif isinstance(event, Iterable):
for evt in event:
write(evt)
else:
log.logger.info("Event writer got tear down signal")
got_shutdown_signal = True
except Queue.Empty:
# We need drain the queue before shutdown
# timeout means empty for now
if got_shutdown_signal:
log.logger.info("Event writer is going to exit...")
break
else:
continue
except Exception:
log.logger.exception("EventWriter encounter exception which may"
"cause data loss, queue leftsize={"
"}".format(
event_queue.qsize()))
self._exception = True
break
log.logger.info("Event writer stopped, queue leftsize={}".format(
event_queue.qsize()))
@@ -1,56 +0,0 @@
import os.path as op
import traceback
from .common import log
class FileMonitor(object):
def __init__(self, callback, files):
"""
:files: files to be monidtored with full path
"""
self._callback = callback
self._files = files
self.file_mtimes = {
file_name: None for file_name in self._files
}
for k in self.file_mtimes:
if not op.exists(k):
continue
try:
if not op.exists(k):
continue
self.file_mtimes[k] = op.getmtime(k)
except OSError:
log.logger.error("Getmtime for %s, failed: %s",
k, traceback.format_exc())
def __call__(self):
return self.check_changes()
def check_changes(self):
log.logger.debug("Checking files=%s", self._files)
file_mtimes = self.file_mtimes
changed_files = []
for f, last_mtime in file_mtimes.iteritems():
try:
if not op.exists(f):
continue
current_mtime = op.getmtime(f)
if current_mtime != last_mtime:
file_mtimes[f] = current_mtime
changed_files.append(f)
log.logger.info("Detect %s has changed", f)
except OSError:
pass
if changed_files:
if self._callback:
self._callback(changed_files)
return True
return False
@@ -1,202 +0,0 @@
import re
import json
from xml.etree import cElementTree as et
from . import rest as rest
class KVException(Exception):
pass
class KVAlreadyExists(KVException):
pass
class KVNotExists(KVException):
pass
class KVClient(object):
def __init__(self, splunkd_host, session_key):
self._splunkd_host = splunkd_host
self._session_key = session_key
def create_collection(self, collection, app, owner="nobody"):
"""
:collection: collection name
:return: None if successful otherwise KV exception thrown
"""
assert collection
assert app
uri = self._get_config_endpoint(app, owner)
data = {
"name": collection
}
self._do_request(uri, "POST", data)
def list_collection(self, collection=None, app=None, owner="nobody"):
"""
:collection: collection name. When euqals "None", return all
collections in the system.
:return: a list containing the connection names if successful, throws
KVNotExists if no such colection or other exception if other error
happened
"""
uri = self._get_config_endpoint(app, owner, collection)
content = self._do_request(uri, method="GET")
m = re.search(r'xmlns="([^"]+)"', content)
path = "./entry/title"
if m:
ns = m.group(1)
path = "./{%s}entry/{%s}title" % (ns, ns)
collections = et.fromstring(content)
return [node.text for node in collections.iterfind(path)]
def delete_collection(self, collection, app, owner="nobody"):
"""
:collection: collection name to be deleted
:return: None if successful otherwise throw KVNotExists exception if
the collection doesn't exist in the system or other exception if other
error happened
"""
assert collection
uri = self._get_config_endpoint(app, owner, collection)
self._do_request(uri, method="DELETE")
def insert_collection_data(self, collection, data, app, owner="nobody"):
"""
:collection: collection name
:data: dict like key values to be inserted and attached to
this collection
:return: {"_key": "key_id"} when successful, clients can use this
key to do query/delete/update, throws KV exceptions when failed
"""
assert collection
assert data is not None
assert app
uri = self._get_data_endpoint(app, owner, collection)
key = self._do_request(uri, "POST", data,
content_type="application/json")
return json.loads(key)
def delete_collection_data(self, collection, key_id, app, owner="nobody"):
"""
:collection: collection name
:key_id: key id returned when creation. If None, delete all data
associated with this collection
:return: None if successful otherwise throws KV exception
"""
assert collection
uri = self._get_data_endpoint(app, owner, collection, key_id)
self._do_request(uri, "DELETE", content_type="application/json")
def update_collection_data(self, collection, key_id, data,
app, owner="nobody"):
"""
:collection: collection name
:key_id: key id returned when creation
:return: key id if successful otherwise throws KV exception
"""
assert collection
assert key_id
assert app
uri = self._get_data_endpoint(app, owner, collection, key_id)
k = self._do_request(uri, "POST", data,
content_type="application/json")
return json.loads(k)
def get_collection_data(self, collection, key_id, app, owner="nobody"):
"""
:collection: collection name
:key_id: key id returned when creation. If None, get all data
associated with this collection
:return: when key_id is not None, return key values if
successful. when key_id is None, return a list of key values if
sucessful. Throws KV exception if failure
"""
assert collection
uri = self._get_data_endpoint(app, owner, collection, key_id)
k = self._do_request(uri, "GET")
return json.loads(k)
def _do_request(self, uri, method, data=None,
content_type="application/x-www-form-urlencoded"):
headers = {"Content-Type": content_type}
resp, content = rest.splunkd_request(uri, self._session_key,
method, headers, data)
if resp is None and content is None:
raise KVException("Failed uri={0}, data={1}".format(uri, data))
if resp.status in (200, 201):
return content
elif resp.status == 409:
raise KVAlreadyExists("{0}-{1} already exists".format(uri, data))
elif resp.status == 404:
raise KVNotExists("{0}-{1} not exists".format(uri, data))
else:
raise KVException("Failed to {0} {1}, reason={2}".format(
method, uri, resp.reason))
def _get_config_endpoint(self, app, owner, collection=None):
uri = "{0}/servicesNS/{1}/{2}/storage/collections/config"
return self._do_get_endpoint(app, owner, collection, None, uri)
def _get_data_endpoint(self, app, owner, collection, key_id=None):
uri = "{0}/servicesNS/{1}/{2}/storage/collections/data"
return self._do_get_endpoint(app, owner, collection, key_id, uri)
def _do_get_endpoint(self, app, owner, collection, key_id, uri_template):
if not app:
app = "-"
if not owner:
owner = "-"
uri = uri_template.format(self._splunkd_host, owner, app)
if collection is not None:
uri += "/{0}".format(collection)
if key_id is not None:
uri += "/{0}".format(key_id)
return uri
def create_collection(kv_client, collection, appname):
not_exists = False
try:
res = kv_client.list_collection(collection, appname)
except KVNotExists:
not_exists = True
except Exception:
not_exists = True
if not_exists or not res:
for i in xrange(3):
try:
kv_client.create_collection(collection, appname)
except KVAlreadyExists:
return
except Exception as e:
ex = e
else:
return
else:
raise ex
@@ -1,147 +0,0 @@
import sys
import subprocess
import traceback
from . import splunk_platform as sp
from .common import log
def _parse_modinput_configs(root, outer_block, inner_block):
"""
When user splunkd spawns modinput script to do config check or run
<?xml version="1.0" encoding="UTF-8"?>
<input>
<server_host>localhost.localdomain</server_host>
<server_uri>https://127.0.0.1:8089</server_uri>
<session_key>xxxyyyzzz</session_key>
<checkpoint_dir>ckpt_dir</checkpoint_dir>
<configuration>
<stanza name="snow://alm_asset">
<param name="duration">60</param>
<param name="host">localhost.localdomain</param>
<param name="index">snow</param>
<param name="priority">10</param>
</stanza>
...
</configuration>
</input>
When user create an stanza through data input on WebUI
<?xml version="1.0" encoding="UTF-8"?>
<items>
<server_host>localhost.localdomain</server_host>
<server_uri>https://127.0.0.1:8089</server_uri>
<session_key>xxxyyyzzz</session_key>
<checkpoint_dir>ckpt_dir</checkpoint_dir>
<item name="abc">
<param name="duration">60</param>
<param name="exclude"></param>
<param name="host">localhost.localdomain</param>
<param name="index">snow</param>
<param name="priority">10</param>
</item>
</items>
"""
confs = root.getElementsByTagName(outer_block)
if not confs:
log.logger.error("Invalid config, missing %s section", outer_block)
raise Exception("Invalid config, missing %s section".format(
outer_block
))
configs = []
stanzas = confs[0].getElementsByTagName(inner_block)
for stanza in stanzas:
config = {}
stanza_name = stanza.getAttribute("name")
if not stanza_name:
log.logger.error("Invalid config, missing name")
raise Exception("Invalid config, missing name")
config["name"] = stanza_name
params = stanza.getElementsByTagName("param")
for param in params:
name = param.getAttribute("name")
if (name and param.firstChild and
param.firstChild.nodeType == param.firstChild.TEXT_NODE):
config[name] = param.firstChild.data
configs.append(config)
return configs
def parse_modinput_configs(config_str):
"""
@config_str: modinput XML configuration feed by splunkd
@return: meta_config and stanza_config
"""
import xml.dom.minidom as xdm
meta_configs = {
"server_host": None,
"server_uri": None,
"session_key": None,
"checkpoint_dir": None,
}
root = xdm.parseString(config_str)
doc = root.documentElement
for tag in meta_configs.iterkeys():
nodes = doc.getElementsByTagName(tag)
if not nodes:
log.logger.error("Invalid config, missing %s section", tag)
raise Exception("Invalid config, missing %s section", tag)
if (nodes[0].firstChild and
nodes[0].firstChild.nodeType == nodes[0].TEXT_NODE):
meta_configs[tag] = nodes[0].firstChild.data
else:
log.logger.error("Invalid config, expect text ndoe")
raise Exception("Invalid config, expect text ndoe")
if doc.nodeName == "input":
configs = _parse_modinput_configs(doc, "configuration", "stanza")
else:
configs = _parse_modinput_configs(root, "items", "item")
return meta_configs, configs
def get_modinput_configs_from_cli(modinput, modinput_stanza=None):
"""
@modinput: modinput name
@modinput_stanza: modinput stanza name, for multiple instance only
"""
assert modinput
splunkbin = sp.get_splunk_bin()
cli = [splunkbin, "cmd", "splunkd", "print-modinput-config", modinput]
if modinput_stanza:
cli.append(modinput_stanza)
out, err = subprocess.Popen(cli, stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()
if err:
log.logger.error("Failed to get modinput configs with error: %s", err)
return None, None
else:
return parse_modinput_configs(out)
def get_modinput_config_str_from_stdin():
"""
Get modinput from stdin which is feed by splunkd
"""
try:
return sys.stdin.read(5000)
except Exception:
log.logger.error(traceback.format_exc())
raise
def get_modinput_configs_from_stdin():
config_str = get_modinput_config_str_from_stdin()
return parse_modinput_configs(config_str)
@@ -1,65 +0,0 @@
import os
import threading
import time
import traceback
from ..splunktalib.common import log
class OrphanProcessChecker(object):
def __init__(self, callback=None):
"""
Only work for Linux platform. On Windows platform, is_orphan is always
False
"""
if os.name == "nt":
self._ppid = 0
else:
self._ppid = os.getppid()
self._callback = callback
def is_orphan(self):
if os.name == "nt":
return False
res = self._ppid != os.getppid()
if res:
log.logger.warn("Process=%s has become orphan", os.getpid())
return res
def check_orphan(self):
res = self.is_orphan()
if res and self._callback:
self._callback()
return res
class OrphanProcessMonitor(object):
def __init__(self, callback):
self._checker = OrphanProcessChecker(callback)
self._thr = threading.Thread(target=self._do_monitor)
self._thr.daemon = True
self._started = False
def start(self):
if self._started:
return
self._started = True
self._thr.start()
def stop(self):
self._started = False
def _do_monitor(self):
while self._started:
try:
res = self._checker.check_orphan()
if res:
break
time.sleep(1)
except Exception:
log.logger.error("Failed to monitor orphan process, reason=%s",
traceback.format_exc())
@@ -1,122 +0,0 @@
import urllib
import json
from traceback import format_exc
from .common import util as scu
from .common import log as log
from httplib2 import (socks, ProxyInfo, Http)
def splunkd_request(splunkd_uri, session_key, method="GET",
headers=None, data=None, timeout=30, retry=1):
"""
:return: httplib2.Response and content
"""
headers = headers if headers is not None else {}
headers["Authorization"] = "Splunk {0}".format(session_key)
content_type = headers.get("Content-Type")
if not content_type:
content_type = headers.get("content-type")
if not content_type:
content_type = "application/x-www-form-urlencoded"
headers["Content-Type"] = content_type
if data is not None:
if content_type == "application/json":
data = json.dumps(data)
else:
data = urllib.urlencode(data)
http = Http(timeout=timeout, disable_ssl_certificate_validation=True)
msg_temp = "Failed to send rest request=%s, errcode=%s, reason=%s"
resp, content = None, None
for _ in range(retry):
try:
resp, content = http.request(splunkd_uri, method=method,
headers=headers, body=data)
except Exception:
log.logger.error(msg_temp, splunkd_uri, "unknown", format_exc())
else:
if resp.status not in (200, 201):
if not (method == "GET" and resp.status == 404):
log.logger.debug(msg_temp, splunkd_uri, resp.status,
code_to_msg(resp, content))
else:
return resp, content
else:
return resp, content
def code_to_msg(resp, content):
code_msg_tbl = {
400: "Request error. reason={}".format(content),
401: "Authentication failure, invalid access credentials.",
402: "In-use license disables this feature.",
403: "Insufficient permission.",
404: "Requested endpoint does not exist.",
409: "Invalid operation for this endpoint. reason={}".format(content),
500: "Unspecified internal server error. reason={}".format(content),
503: ("Feature is disabled in the configuration file. "
"reason={}".format(content)),
}
return code_msg_tbl.get(resp.status, content)
def build_http_connection(config, timeout=120, disable_ssl_validation=False):
"""
:config: dict like, proxy and account information are in the following
format {
"username": xx,
"password": yy,
"proxy_url": zz,
"proxy_port": aa,
"proxy_username": bb,
"proxy_password": cc,
"proxy_type": http,http_no_tunnel,sock4,sock5,
"proxy_rdns": 0 or 1,
}
:return: Http2.Http object
"""
proxy_type_to_code = {
"http": socks.PROXY_TYPE_HTTP,
"http_no_tunnel": socks.PROXY_TYPE_HTTP_NO_TUNNEL,
"socks4": socks.PROXY_TYPE_SOCKS4,
"socks5": socks.PROXY_TYPE_SOCKS5,
}
if config.get("proxy_type") in proxy_type_to_code:
proxy_type = proxy_type_to_code[config["proxy_type"]]
else:
proxy_type = socks.PROXY_TYPE_HTTP
rdns = scu.is_true(config.get("proxy_rdns"))
proxy_info = None
if config.get("proxy_url") and config.get("proxy_port"):
if config.get("proxy_username") and config.get("proxy_password"):
proxy_info = ProxyInfo(proxy_type=proxy_type,
proxy_host=config["proxy_url"],
proxy_port=int(config["proxy_port"]),
proxy_user=config["proxy_username"],
proxy_pass=config["proxy_password"],
proxy_rdns=rdns)
else:
proxy_info = ProxyInfo(proxy_type=proxy_type,
proxy_host=config["proxy_url"],
proxy_port=int(config["proxy_port"]),
proxy_rdns=rdns)
if proxy_info:
http = Http(proxy_info=proxy_info, timeout=timeout,
disable_ssl_certificate_validation=disable_ssl_validation)
else:
http = Http(timeout=timeout,
disable_ssl_certificate_validation=disable_ssl_validation)
if config.get("username") and config.get("password"):
http.add_credentials(config["username"], config["password"])
return http
@@ -1,90 +0,0 @@
import threading
import time
class Job(object):
"""
Timer wraps the callback and timestamp related stuff
"""
_ident = 0
_lock = threading.Lock()
def __init__(self, func, job_props, interval, when=None, job_id=None):
"""
@job_props: dict like object
@func: execution function
@interval: execution interval
@when: seconds from epoch
@job_id: a unique id for the job
"""
self._props = job_props
self._func = func
if when is None:
self._when = time.time()
else:
self._when = when
self._interval = interval
if job_id is not None:
self._id = job_id
else:
with Job._lock:
self._id = Job._ident + 1
Job._ident = Job._ident + 1
self._stopped = False
def ident(self):
return self._id
def get_interval(self):
return self._interval
def set_interval(self, interval):
self._interval = interval
def get_expiration(self):
return self._when
def set_initial_due_time(self, when):
if self._when is None:
self._when = when
def update_expiration(self):
self._when += self._interval
def get(self, key, default):
return self._props.get(key, default)
def get_props(self):
return self._props
def set_props(self, props):
self._props = props
def __cmp__(self, other):
if other is None:
return 1
self_k = (self.get_expiration(), self.ident())
other_k = (other.get_expiration(), other.ident())
if self_k == other_k:
return 0
elif self_k < other_k:
return -1
else:
return 1
def __eq__(self, other):
return isinstance(other, Job) and (self.ident() == other.ident())
def __call__(self):
self._func(self)
def stop(self):
self._stopped = True
def stopped(self):
return self._stopped
@@ -1,143 +0,0 @@
import threading
from time import time
import random
import Queue
from ..common import log
class Scheduler(object):
"""
A simple scheduler which schedules the periodic or once event
"""
import sortedcontainers as sc
max_delay_time = 60
def __init__(self):
self._jobs = Scheduler.sc.SortedSet()
self._wakeup_q = Queue.Queue()
self._lock = threading.Lock()
self._thr = threading.Thread(target=self._do_jobs)
self._thr.deamon = True
self._started = False
def start(self):
"""
Start the schduler which will start the internal thread for scheduling
jobs. Please do tear_down when doing cleanup
"""
if self._started:
log.logger.info("Scheduler already started.")
return
self._started = True
self._thr.start()
def tear_down(self):
"""
Stop the schduler which will stop the internal thread for scheduling
jobs.
"""
if not self._started:
log.logger.info("Scheduler already tear down.")
return
self._wakeup_q.put(True)
def _do_jobs(self):
while 1:
(sleep_time, jobs) = self.get_ready_jobs()
self._do_execution(jobs)
try:
done = self._wakeup_q.get(timeout=sleep_time)
except Queue.Empty:
pass
else:
if done:
break
self._started = False
log.logger.info("Scheduler exited.")
def get_ready_jobs(self):
"""
@return: a 2 element tuple. The first element is the next ready
duration. The second element is ready jobs list
"""
now = time()
ready_jobs = []
sleep_time = 1
with self._lock:
job_set = self._jobs
total_jobs = len(job_set)
for job in job_set:
if job.get_expiration() <= now:
ready_jobs.append(job)
if ready_jobs:
del job_set[:len(ready_jobs)]
for job in ready_jobs:
if job.get_interval() != 0 and not job.stopped():
# repeated job, calculate next due time and enqueue
job.update_expiration()
job_set.add(job)
if job_set:
sleep_time = job_set[0].get_expiration() - now
if sleep_time < 0:
log.logger.warn("Scheduler satuation, sleep_time=%s",
sleep_time)
sleep_time = 0.1
if ready_jobs:
log.logger.info("Get %d ready jobs, next duration is %f, "
"and there are %s jobs scheduling",
len(ready_jobs), sleep_time, total_jobs)
ready_jobs.sort(key=lambda job: job.get("priority", 0), reverse=True)
return (sleep_time, ready_jobs)
def add_jobs(self, jobs):
with self._lock:
now = time()
job_set = self._jobs
for job in jobs:
delay_time = random.randrange(0, self.max_delay_time)
job.set_initial_due_time(now + delay_time)
job_set.add(job)
self._wakeup()
def update_jobs(self, jobs):
with self._lock:
job_set = self._jobs
for njob in jobs:
job_set.discard(njob)
job_set.add(njob)
self._wakeup()
def remove_jobs(self, jobs):
with self._lock:
job_set = self._jobs
for njob in jobs:
njob.stop()
job_set.discard(njob)
self._wakeup()
def number_of_jobs(self):
with self._lock:
return len(self._jobs)
def disable_randomization(self):
self.max_delay_time = 1
def _wakeup(self):
self._wakeup_q.put(None)
def _do_execution(self, jobs):
for job in jobs:
job()
@@ -1,5 +0,0 @@
[global]
process_size = 0
thread_min_size = 4
thread_max_size = 128
task_queue_size = 1024
@@ -1,53 +0,0 @@
from ..splunktalib import rest
from ..splunktalib.common import xml_dom_parser as xdp
def _do_rest(uri, session_key):
resp, content = rest.splunkd_request(uri, session_key)
if resp is None:
return None
if resp.status not in (200, 201):
return None
stanza_objs = xdp.parse_conf_xml_dom(content)
if not stanza_objs:
return None
return stanza_objs[0]
class ServerInfo(object):
def __init__(self, splunkd_uri, session_key):
uri = "{}/services/server/info".format(splunkd_uri)
server_info = _do_rest(uri, session_key)
if server_info is None:
raise Exception("Failed to init ServerInfo")
self._server_info = server_info
def is_captain(self):
"""
:return: True if splunkd_uri is captain otherwise False
"""
return "shc_captain" in self._server_info["server_roles"]
def is_search_head(self):
for sh in ("search_head", "cluster_search_head"):
if sh in self._server_info["server_roles"]:
return True
return False
def is_shc_member(self):
server_roles = self._server_info['server_roles']
return any(
role in server_roles for role in ('shc_member', 'shc_captain')
)
def version(self):
return self._server_info["version"]
def to_dict(self):
return self._server_info
@@ -1,104 +0,0 @@
import os
import os.path as op
import subprocess
from ConfigParser import ConfigParser
from cStringIO import StringIO
from .common import util as scu
def make_splunkhome_path(parts):
"""
create a path string by the several parts of the path
"""
relpath = os.path.normpath(os.path.join(*parts))
basepath = os.environ["SPLUNK_HOME"] # Assume SPLUNK_HOME env has been set
fullpath = os.path.normpath(os.path.join(basepath, relpath))
# Check that we haven't escaped from intended parent directories.
if os.path.relpath(fullpath, basepath)[0:2] == '..':
raise ValueError('Illegal escape from parent directory "%s": %s' %
(basepath, fullpath))
return fullpath
def get_splunk_bin():
if os.name == "nt":
splunk_bin = "splunk.exe"
else:
splunk_bin = "splunk"
return make_splunkhome_path(("bin", splunk_bin))
def get_appname_from_path(absolute_path):
return scu.get_appname_from_path(absolute_path)
def _get_merged_conf_raw(conf_name):
"""
:conf_name: configure file name
:return: raw output of all contents for the same conf file
Note: it depends on SPLUNK_HOME env variable
"""
assert conf_name
if conf_name.endswith(".conf"):
conf_name = conf_name[:-5]
# FIXME dynamically caculate SPLUNK_HOME
btool_cli = [op.join(os.environ["SPLUNK_HOME"], "bin", "btool"),
conf_name, "list"]
try:
p = subprocess.Popen(btool_cli, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = p.communicate()
except OSError:
raise
return out
def _get_conf_stanzas(conf_name):
"""
:return: {stanza_name: stanza_configs}, dict of dict
"""
res = _get_merged_conf_raw(conf_name)
res = StringIO(res)
parser = ConfigParser()
parser.optionxform = str
parser.readfp(res)
res = {}
for section in parser.sections():
res[section] = {item[0]: item[1] for item in parser.items(section)}
return res
def get_splunkd_uri():
if "SPLUNKD_URI" in os.environ:
return os.environ["SPLUNKD_URI"]
else:
server_conf = _get_conf_stanzas("server")
if server_conf["sslConfig"]["enableSplunkdSSL"].lower() == "true":
http = "https://"
else:
http = "http://"
web_conf = _get_conf_stanzas("web")
host_port = web_conf["settings"]["mgmtHostPort"]
splunkd_uri = "{}{}".format(http, host_port)
if os.environ.get("SPLUNK_BINDIP"):
bip = os.environ["SPLUNK_BINDIP"]
port_idx = bip.rfind(":")
if port_idx > 0:
bip = bip[:port_idx]
port = host_port[host_port.rfind(":"):]
splunkd_uri = "{}{}{}".format(http, bip, port)
return splunkd_uri
@@ -1,264 +0,0 @@
import json
import os
import os.path as op
import time
from ..splunktacollectorlib.common import log as stulog
from ..splunktalib import kv_client as kvc
from ..splunktalib.common import util
def get_state_store(meta_configs,
appname,
collection_name="talib_states",
use_kv_store=False,
use_cache_file=True,
max_cache_seconds=5):
# FIXME refactor this
if util.is_true(use_kv_store):
return StateStore(meta_configs, appname, collection_name)
if util.is_true(use_cache_file):
return CachedFileStateStore(meta_configs, appname, max_cache_seconds)
return FileStateStore(meta_configs, appname)
class BaseStateStore(object):
def __init__(self, meta_configs, appname):
self._meta_configs = meta_configs
self._appname = appname
def update_state(self, key, states):
pass
def get_state(self, key):
pass
def delete_state(self, key):
pass
def close(self, key=None):
pass
class StateStore(BaseStateStore):
def __init__(self, meta_configs, appname, collection_name="talib_states"):
"""
:meta_configs: dict like and contains checkpoint_dir, session_key,
server_uri etc
:app_name: the name of the app
:collection_name: the collection name to be used.
Don"t use other method to visit the collection if you are using
StateStore to visit it.
"""
super(StateStore, self).__init__(meta_configs, appname)
# State cache is a dict from _key to value
self._states_cache = {}
self._kv_client = None
self._collection = collection_name
self._kv_client = kvc.KVClient(meta_configs["server_uri"],
meta_configs["session_key"])
kvc.create_collection(self._kv_client, self._collection, self._appname)
self._load_states_cache()
def update_state(self, key, states):
"""
:state: Any JSON serializable
:return: None if successful, otherwise throws exception
"""
if key not in self._states_cache:
self._kv_client.insert_collection_data(
self._collection, {"_key": key, "value": json.dumps(states)},
self._appname)
else:
self._kv_client.update_collection_data(
self._collection, key, {"value": json.dumps(states)},
self._appname)
self._states_cache[key] = states
def get_state(self, key=None):
if key:
return self._states_cache.get(key, None)
return self._states_cache
def delete_state(self, key=None):
if key:
self._delete_state(key)
else:
[self._delete_state(_key) for _key in self._states_cache.keys()]
def _delete_state(self, key):
if key not in self._states_cache:
return
self._kv_client.delete_collection_data(
self._collection, key, self._appname)
del self._states_cache[key]
def _load_states_cache(self):
states = self._kv_client.get_collection_data(
self._collection, None, self._appname)
if not states:
return
for state in states:
if "value" in state:
value = state["value"]
else:
value = state
try:
value = json.loads(value)
except Exception:
pass
self._states_cache[state["_key"]] = value
def _create_checkpoint_dir_if_needed(checkpoint_dir):
if os.path.isdir(checkpoint_dir):
return
stulog.logger.info(
"Checkpoint dir '%s' doesn't exist, try to create it",
checkpoint_dir)
try:
os.mkdir(checkpoint_dir)
except OSError:
stulog.logger.exception(
"Failure creating checkpoint dir '%s'", checkpoint_dir
)
raise Exception(
"Unable to create checkpoint dir '{}'".format(checkpoint_dir)
)
class FileStateStore(BaseStateStore):
def __init__(self, meta_configs, appname):
"""
:meta_configs: dict like and contains checkpoint_dir, session_key,
server_uri etc
"""
super(FileStateStore, self).__init__(meta_configs, appname)
def update_state(self, key, states):
"""
:state: Any JSON serializable
:return: None if successful, otherwise throws exception
"""
checkpoint_dir = self._meta_configs["checkpoint_dir"]
_create_checkpoint_dir_if_needed(checkpoint_dir)
fname = op.join(checkpoint_dir, key)
with open(fname + ".new", "w") as jsonfile:
json.dump(states, jsonfile)
if op.exists(fname):
os.remove(fname)
os.rename(fname + ".new", fname)
# commented this to disable state cache for local file
# if key not in self._states_cache:
# self._states_cache[key] = {}
# self._states_cache[key] = states
def get_state(self, key):
fname = op.join(self._meta_configs["checkpoint_dir"], key)
if op.exists(fname):
with open(fname) as jsonfile:
state = json.load(jsonfile)
# commented this to disable state cache for local file
# self._states_cache[key] = state
return state
else:
return None
def delete_state(self, key):
fname = op.join(self._meta_configs["checkpoint_dir"], key)
if op.exists(fname):
os.remove(fname)
class CachedFileStateStore(BaseStateStore):
def __init__(self, meta_configs, appname, max_cache_seconds=5):
"""
:meta_configs: dict like and contains checkpoint_dir, session_key,
server_uri etc
"""
super(CachedFileStateStore, self).__init__(meta_configs, appname)
self._states_cache = {} # item: time, dict
self._states_cache_lmd = {} #item: time, dict
self.max_cache_seconds = max_cache_seconds
def update_state(self, key, states):
now = time.time()
if key in self._states_cache:
last = self._states_cache_lmd[key][0]
if now - last >= self.max_cache_seconds:
self.update_state_flush(now, key, states)
else:
self.update_state_flush(now, key, states)
self._states_cache[key] = (now, states)
def update_state_flush(self, now, key, states):
"""
:state: Any JSON serializable
:return: None if successful, otherwise throws exception
"""
self._states_cache_lmd[key] = (now, states)
checkpoint_dir = self._meta_configs["checkpoint_dir"]
_create_checkpoint_dir_if_needed(checkpoint_dir)
fname = op.join(checkpoint_dir, key)
with open(fname + ".new", "w") as jsonfile:
json.dump(states, jsonfile)
if op.exists(fname):
os.remove(fname)
os.rename(fname + ".new", fname)
def get_state(self, key):
if key in self._states_cache:
return self._states_cache[key][1]
fname = op.join(self._meta_configs["checkpoint_dir"], key)
if op.exists(fname):
with open(fname) as jsonfile:
state = json.load(jsonfile)
now = time.time()
self._states_cache[key] = now, state
self._states_cache_lmd[key] = now, state
return state
else:
return None
def delete_state(self, key):
fname = op.join(self._meta_configs["checkpoint_dir"], key)
if op.exists(fname):
os.remove(fname)
if self._states_cache.get(key):
del self._states_cache[key]
if self._states_cache_lmd.get(key):
del self._states_cache_lmd[key]
def close(self, key=None):
if not key:
for k, (t, s) in self._states_cache.iteritems():
self.update_state_flush(t, k, s)
self._states_cache.clear()
self._states_cache_lmd.clear()
elif key in self._states_cache:
self.update_state_flush(self._states_cache[key][0], key,
self._states_cache[key][1])
del self._states_cache[key]
del self._states_cache_lmd[key]
@@ -1,60 +0,0 @@
import threading
class Timer(object):
"""
Timer wraps the callback and timestamp related stuff
"""
_ident = 0
_lock = threading.Lock()
def __init__(self, callback, when, interval, ident=None):
self._callback = callback
self._when = when
self._interval = interval
if ident is not None:
self._id = ident
else:
with Timer._lock:
self._id = Timer._ident + 1
Timer._ident = Timer._ident + 1
def get_interval(self):
return self._interval
def set_interval(self, interval):
self._interval = interval
def get_expiration(self):
return self._when
def set_initial_due_time(self, when):
self._when = when
def update_expiration(self):
self._when += self._interval
def __cmp__(self, other):
if other is None:
return 1
self_k = (self.get_expiration(), self.ident())
other_k = (other.get_expiration(), other.ident())
if self_k == other_k:
return 0
elif self_k < other_k:
return -1
else:
return 1
def __eq__(self, other):
return isinstance(other, Timer) and (self.ident() == other.ident())
def __call__(self):
self._callback()
def ident(self):
return self._id
@@ -1,138 +0,0 @@
"""
A timer queue implementation
"""
import threading
import Queue
from time import time
import traceback
from .timer import Timer
from .common import log
class TimerQueue(object):
"""
A timer queue implementation, runs a separate thread to handle timers
"""
import sortedcontainers as sc
def __init__(self):
self._timers = TimerQueue.sc.SortedSet()
self._cancelling_timers = {}
self._lock = threading.Lock()
self._wakeup_queue = Queue.Queue()
self._thr = threading.Thread(target=self._check_and_execute)
self._started = False
def start(self):
"""
Start the timer queue to make it start function
"""
if self._started:
return
self._started = True
self._thr.start()
log.logger.info("TimerQueue started.")
def tear_down(self):
if not self._started:
return
self._started = True
self._wakeup(None)
self._thr.join()
def add_timer(self, callback, when, interval):
"""
Add timer to the queue
"""
timer = Timer(callback, when, interval)
with self._lock:
self._timers.add(timer)
self._wakeup()
return timer
def remove_timer(self, timer):
"""
Remove timer from the queue.
"""
with self._lock:
try:
self._timers.remove(timer)
except ValueError:
log.logger.info("Timer=%s is not in queue, move it to cancelling "
"list", timer.ident())
else:
self._cancelling_timers[timer.ident()] = timer
def _check_and_execute(self):
wakeup_queue = self._wakeup_queue
while 1:
(next_expired_time, expired_timers) = self._get_expired_timers()
for timer in expired_timers:
try:
timer()
except Exception:
log.logger.error(traceback.format_exc())
self._reset_timers(expired_timers)
# Calc sleep time
if next_expired_time:
now = time()
if now < next_expired_time:
sleep_time = next_expired_time - now
else:
sleep_time = 0.1
else:
sleep_time = 1
try:
wakeup = wakeup_queue.get(timeout=sleep_time)
if wakeup is None:
break
except Queue.Empty:
pass
log.logger.info("TimerQueue stopped.")
def _get_expired_timers(self):
next_expired_time = 0
now = time()
expired_timers = []
with self._lock:
for timer in self._timers:
if timer.get_expiration() <= now:
expired_timers.append(timer)
if expired_timers:
del self._timers[:len(expired_timers)]
if self._timers:
next_expired_time = self._timers[0].get_expiration()
return (next_expired_time, expired_timers)
def _reset_timers(self, expired_timers):
has_new_timer = False
with self._lock:
cancelling_timers = self._cancelling_timers
for timer in expired_timers:
if timer.ident() in cancelling_timers:
log.logger.INFO("Timer=%s has been cancelled", timer.ident())
continue
elif timer.get_interval():
# Repeated timer
timer.update_expiration()
self._timers.add(timer)
has_new_timer = True
cancelling_timers.clear()
if has_new_timer:
self._wakeup()
def _wakeup(self, something="not_None"):
self._wakeup_queue.put(something)
@@ -1,414 +0,0 @@
# ######################### LICENSE ############################ #
# Copyright (c) 2005-2017, Michele Simionato
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# Redistributions in bytecode form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
"""
Decorator module, see http://pypi.python.org/pypi/decorator
for the documentation.
"""
from __future__ import print_function
import re
import sys
import inspect
import operator
import itertools
import collections
__version__ = '4.0.11'
if sys.version >= '3':
from inspect import getfullargspec
def get_init(cls):
return cls.__init__
else:
FullArgSpec = collections.namedtuple(
'FullArgSpec', 'args varargs varkw defaults '
'kwonlyargs kwonlydefaults')
def getfullargspec(f):
"A quick and dirty replacement for getfullargspec for Python 2.X"
return FullArgSpec._make(inspect.getargspec(f) + ([], None))
def get_init(cls):
return cls.__init__.__func__
# getargspec has been deprecated in Python 3.5
ArgSpec = collections.namedtuple(
'ArgSpec', 'args varargs varkw defaults')
def getargspec(f):
"""A replacement for inspect.getargspec"""
spec = getfullargspec(f)
return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults)
DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
# basic functionality
class FunctionMaker(object):
"""
An object with the ability to create functions with a given signature.
It has attributes name, doc, module, signature, defaults, dict and
methods update and make.
"""
# Atomic get-and-increment provided by the GIL
_compile_count = itertools.count()
# make pylint happy
args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = ()
def __init__(self, func=None, name=None, signature=None,
defaults=None, doc=None, module=None, funcdict=None):
self.shortsignature = signature
if func:
# func can be a class or a callable, but not an instance method
self.name = func.__name__
if self.name == '<lambda>': # small hack for lambda functions
self.name = '_lambda_'
self.doc = func.__doc__
self.module = func.__module__
if inspect.isfunction(func):
argspec = getfullargspec(func)
self.annotations = getattr(func, '__annotations__', {})
for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
'kwonlydefaults'):
setattr(self, a, getattr(argspec, a))
for i, arg in enumerate(self.args):
setattr(self, 'arg%d' % i, arg)
if sys.version < '3': # easy way
self.shortsignature = self.signature = (
inspect.formatargspec(
formatvalue=lambda val: "", *argspec[:-2])[1:-1])
else: # Python 3 way
allargs = list(self.args)
allshortargs = list(self.args)
if self.varargs:
allargs.append('*' + self.varargs)
allshortargs.append('*' + self.varargs)
elif self.kwonlyargs:
allargs.append('*') # single star syntax
for a in self.kwonlyargs:
allargs.append('%s=None' % a)
allshortargs.append('%s=%s' % (a, a))
if self.varkw:
allargs.append('**' + self.varkw)
allshortargs.append('**' + self.varkw)
self.signature = ', '.join(allargs)
self.shortsignature = ', '.join(allshortargs)
self.dict = func.__dict__.copy()
# func=None happens when decorating a caller
if name:
self.name = name
if signature is not None:
self.signature = signature
if defaults:
self.defaults = defaults
if doc:
self.doc = doc
if module:
self.module = module
if funcdict:
self.dict = funcdict
# check existence required attributes
assert hasattr(self, 'name')
if not hasattr(self, 'signature'):
raise TypeError('You are decorating a non function: %s' % func)
def update(self, func, **kw):
"Update the signature of func with the data in self"
func.__name__ = self.name
func.__doc__ = getattr(self, 'doc', None)
func.__dict__ = getattr(self, 'dict', {})
func.__defaults__ = self.defaults
func.__kwdefaults__ = self.kwonlydefaults or None
func.__annotations__ = getattr(self, 'annotations', None)
try:
frame = sys._getframe(3)
except AttributeError: # for IronPython and similar implementations
callermodule = '?'
else:
callermodule = frame.f_globals.get('__name__', '?')
func.__module__ = getattr(self, 'module', callermodule)
func.__dict__.update(kw)
def make(self, src_templ, evaldict=None, addsource=False, **attrs):
"Make a new function from a given template and update the signature"
src = src_templ % vars(self) # expand name and signature
evaldict = evaldict or {}
mo = DEF.match(src)
if mo is None:
raise SyntaxError('not a valid function template\n%s' % src)
name = mo.group(1) # extract the function name
names = set([name] + [arg.strip(' *') for arg in
self.shortsignature.split(',')])
for n in names:
if n in ('_func_', '_call_'):
raise NameError('%s is overridden in\n%s' % (n, src))
if not src.endswith('\n'): # add a newline for old Pythons
src += '\n'
# Ensure each generated function has a unique filename for profilers
# (such as cProfile) that depend on the tuple of (<filename>,
# <definition line>, <function name>) being unique.
filename = '<decorator-gen-%d>' % (next(self._compile_count),)
try:
code = compile(src, filename, 'single')
exec(code, evaldict)
except:
print('Error in generated code:', file=sys.stderr)
print(src, file=sys.stderr)
raise
func = evaldict[name]
if addsource:
attrs['__source__'] = src
self.update(func, **attrs)
return func
@classmethod
def create(cls, obj, body, evaldict, defaults=None,
doc=None, module=None, addsource=True, **attrs):
"""
Create a function from the strings name, signature and body.
evaldict is the evaluation dictionary. If addsource is true an
attribute __source__ is added to the result. The attributes attrs
are added, if any.
"""
if isinstance(obj, str): # "name(signature)"
name, rest = obj.strip().split('(', 1)
signature = rest[:-1] # strip a right parens
func = None
else: # a function
name = None
signature = None
func = obj
self = cls(func, name, signature, defaults, doc, module)
ibody = '\n'.join(' ' + line for line in body.splitlines())
return self.make('def %(name)s(%(signature)s):\n' + ibody,
evaldict, addsource, **attrs)
def decorate(func, caller):
"""
decorate(func, caller) decorates a function using a caller.
"""
evaldict = dict(_call_=caller, _func_=func)
fun = FunctionMaker.create(
func, "return _call_(_func_, %(shortsignature)s)",
evaldict, __wrapped__=func)
if hasattr(func, '__qualname__'):
fun.__qualname__ = func.__qualname__
return fun
def decorator(caller, _func=None):
"""decorator(caller) converts a caller function into a decorator"""
if _func is not None: # return a decorated function
# this is obsolete behavior; you should use decorate instead
return decorate(_func, caller)
# else return a decorator function
if inspect.isclass(caller):
name = caller.__name__.lower()
doc = 'decorator(%s) converts functions/generators into ' \
'factories of %s objects' % (caller.__name__, caller.__name__)
elif inspect.isfunction(caller):
if caller.__name__ == '<lambda>':
name = '_lambda_'
else:
name = caller.__name__
doc = caller.__doc__
else: # assume caller is an object with a __call__ method
name = caller.__class__.__name__.lower()
doc = caller.__call__.__doc__
evaldict = dict(_call_=caller, _decorate_=decorate)
return FunctionMaker.create(
'%s(func)' % name, 'return _decorate_(func, _call_)',
evaldict, doc=doc, module=caller.__module__,
__wrapped__=caller)
# ####################### contextmanager ####################### #
try: # Python >= 3.2
from contextlib import _GeneratorContextManager
except ImportError: # Python >= 2.5
from contextlib import GeneratorContextManager as _GeneratorContextManager
class ContextManager(_GeneratorContextManager):
def __call__(self, func):
"""Context manager decorator"""
return FunctionMaker.create(
func, "with _self_: return _func_(%(shortsignature)s)",
dict(_self_=self, _func_=func), __wrapped__=func)
init = getfullargspec(_GeneratorContextManager.__init__)
n_args = len(init.args)
if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7
def __init__(self, g, *a, **k):
return _GeneratorContextManager.__init__(self, g(*a, **k))
ContextManager.__init__ = __init__
elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4
pass
elif n_args == 4: # (self, gen, args, kwds) Python 3.5
def __init__(self, g, *a, **k):
return _GeneratorContextManager.__init__(self, g, a, k)
ContextManager.__init__ = __init__
contextmanager = decorator(ContextManager)
# ############################ dispatch_on ############################ #
def append(a, vancestors):
"""
Append ``a`` to the list of the virtual ancestors, unless it is already
included.
"""
add = True
for j, va in enumerate(vancestors):
if issubclass(va, a):
add = False
break
if issubclass(a, va):
vancestors[j] = a
add = False
if add:
vancestors.append(a)
# inspired from simplegeneric by P.J. Eby and functools.singledispatch
def dispatch_on(*dispatch_args):
"""
Factory of decorators turning a function into a generic function
dispatching on the given arguments.
"""
assert dispatch_args, 'No dispatch args passed'
dispatch_str = '(%s,)' % ', '.join(dispatch_args)
def check(arguments, wrong=operator.ne, msg=''):
"""Make sure one passes the expected number of arguments"""
if wrong(len(arguments), len(dispatch_args)):
raise TypeError('Expected %d arguments, got %d%s' %
(len(dispatch_args), len(arguments), msg))
def gen_func_dec(func):
"""Decorator turning a function into a generic function"""
# first check the dispatch arguments
argset = set(getfullargspec(func).args)
if not set(dispatch_args) <= argset:
raise NameError('Unknown dispatch arguments %s' % dispatch_str)
typemap = {}
def vancestors(*types):
"""
Get a list of sets of virtual ancestors for the given types
"""
check(types)
ras = [[] for _ in range(len(dispatch_args))]
for types_ in typemap:
for t, type_, ra in zip(types, types_, ras):
if issubclass(t, type_) and type_ not in t.mro():
append(type_, ra)
return [set(ra) for ra in ras]
def ancestors(*types):
"""
Get a list of virtual MROs, one for each type
"""
check(types)
lists = []
for t, vas in zip(types, vancestors(*types)):
n_vas = len(vas)
if n_vas > 1:
raise RuntimeError(
'Ambiguous dispatch for %s: %s' % (t, vas))
elif n_vas == 1:
va, = vas
mro = type('t', (t, va), {}).mro()[1:]
else:
mro = t.mro()
lists.append(mro[:-1]) # discard t and object
return lists
def register(*types):
"""
Decorator to register an implementation for the given types
"""
check(types)
def dec(f):
check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
typemap[types] = f
return f
return dec
def dispatch_info(*types):
"""
An utility to introspect the dispatch algorithm
"""
check(types)
lst = []
for anc in itertools.product(*ancestors(*types)):
lst.append(tuple(a.__name__ for a in anc))
return lst
def _dispatch(dispatch_args, *args, **kw):
types = tuple(type(arg) for arg in dispatch_args)
try: # fast path
f = typemap[types]
except KeyError:
pass
else:
return f(*args, **kw)
combinations = itertools.product(*ancestors(*types))
next(combinations) # the first one has been already tried
for types_ in combinations:
f = typemap.get(types_)
if f is not None:
return f(*args, **kw)
# else call the default implementation
return func(*args, **kw)
return FunctionMaker.create(
func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
dict(_f_=_dispatch), register=register, default=func,
typemap=typemap, vancestors=vancestors, ancestors=ancestors,
dispatch_info=dispatch_info, __wrapped__=func)
gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
return gen_func_dec
@@ -1 +0,0 @@
from .functools32 import *
@@ -1,158 +0,0 @@
"""Drop-in replacement for the thread module.
Meant to be used as a brain-dead substitute so that threaded code does
not need to be rewritten for when the thread module is not present.
Suggested usage is::
try:
try:
import _thread # Python >= 3
except:
import thread as _thread # Python < 3
except ImportError:
import _dummy_thread as _thread
"""
# Exports only things specified by thread documentation;
# skipping obsolete synonyms allocate(), start_new(), exit_thread().
__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock',
'interrupt_main', 'LockType']
# A dummy value
TIMEOUT_MAX = 2**31
# NOTE: this module can be imported early in the extension building process,
# and so top level imports of other modules should be avoided. Instead, all
# imports are done when needed on a function-by-function basis. Since threads
# are disabled, the import lock should not be an issue anyway (??).
class error(Exception):
"""Dummy implementation of _thread.error."""
def __init__(self, *args):
self.args = args
def start_new_thread(function, args, kwargs={}):
"""Dummy implementation of _thread.start_new_thread().
Compatibility is maintained by making sure that ``args`` is a
tuple and ``kwargs`` is a dictionary. If an exception is raised
and it is SystemExit (which can be done by _thread.exit()) it is
caught and nothing is done; all other exceptions are printed out
by using traceback.print_exc().
If the executed function calls interrupt_main the KeyboardInterrupt will be
raised when the function returns.
"""
if type(args) != type(tuple()):
raise TypeError("2nd arg must be a tuple")
if type(kwargs) != type(dict()):
raise TypeError("3rd arg must be a dict")
global _main
_main = False
try:
function(*args, **kwargs)
except SystemExit:
pass
except:
import traceback
traceback.print_exc()
_main = True
global _interrupt
if _interrupt:
_interrupt = False
raise KeyboardInterrupt
def exit():
"""Dummy implementation of _thread.exit()."""
raise SystemExit
def get_ident():
"""Dummy implementation of _thread.get_ident().
Since this module should only be used when _threadmodule is not
available, it is safe to assume that the current process is the
only thread. Thus a constant can be safely returned.
"""
return -1
def allocate_lock():
"""Dummy implementation of _thread.allocate_lock()."""
return LockType()
def stack_size(size=None):
"""Dummy implementation of _thread.stack_size()."""
if size is not None:
raise error("setting thread stack size not supported")
return 0
class LockType(object):
"""Class implementing dummy implementation of _thread.LockType.
Compatibility is maintained by maintaining self.locked_status
which is a boolean that stores the state of the lock. Pickling of
the lock, though, should not be done since if the _thread module is
then used with an unpickled ``lock()`` from here problems could
occur from this class not having atomic methods.
"""
def __init__(self):
self.locked_status = False
def acquire(self, waitflag=None, timeout=-1):
"""Dummy implementation of acquire().
For blocking calls, self.locked_status is automatically set to
True and returned appropriately based on value of
``waitflag``. If it is non-blocking, then the value is
actually checked and not set if it is already acquired. This
is all done so that threading.Condition's assert statements
aren't triggered and throw a little fit.
"""
if waitflag is None or waitflag:
self.locked_status = True
return True
else:
if not self.locked_status:
self.locked_status = True
return True
else:
if timeout > 0:
import time
time.sleep(timeout)
return False
__enter__ = acquire
def __exit__(self, typ, val, tb):
self.release()
def release(self):
"""Release the dummy lock."""
# XXX Perhaps shouldn't actually bother to test? Could lead
# to problems for complex, threaded code.
if not self.locked_status:
raise error
self.locked_status = False
return True
def locked(self):
return self.locked_status
# Used to signal that interrupt_main was called in a "thread"
_interrupt = False
# True when not executing in a "thread"
_main = True
def interrupt_main():
"""Set _interrupt flag to True to have start_new_thread raise
KeyboardInterrupt upon exiting."""
if _main:
raise KeyboardInterrupt
else:
global _interrupt
_interrupt = True
@@ -1,423 +0,0 @@
"""functools.py - Tools for working with functions and callable objects
"""
# Python module wrapper for _functools C module
# to allow utilities written in Python to be added
# to the functools module.
# Written by Nick Coghlan <ncoghlan at gmail.com>
# and Raymond Hettinger <python at rcn.com>
# Copyright (C) 2006-2010 Python Software Foundation.
# See C source code for _functools credits/copyright
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial']
from _functools import partial, reduce
from collections import MutableMapping, namedtuple
from .reprlib32 import recursive_repr as _recursive_repr
from weakref import proxy as _proxy
import sys as _sys
try:
from thread import allocate_lock as Lock
except ImportError:
from ._dummy_thread32 import allocate_lock as Lock
################################################################################
### OrderedDict
################################################################################
class _Link(object):
__slots__ = 'prev', 'next', 'key', '__weakref__'
class OrderedDict(dict):
'Dictionary that remembers insertion order'
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as regular dictionaries.
# The internal self.__map dict maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# The sentinel is in self.__hardroot with a weakref proxy in self.__root.
# The prev links are weakref proxies (to prevent circular references).
# Individual links are kept alive by the hard reference in self.__map.
# Those hard references disappear when a key is deleted from an OrderedDict.
def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. The signature is the same as
regular dictionaries, but keyword arguments are not recommended because
their insertion order is arbitrary.
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__hardroot = _Link()
self.__root = root = _proxy(self.__hardroot)
root.prev = root.next = root
self.__map = {}
self.__update(*args, **kwds)
def __setitem__(self, key, value,
dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
'od.__setitem__(i, y) <==> od[i]=y'
# Setting a new item creates a new link at the end of the linked list,
# and the inherited dictionary is updated with the new key/value pair.
if key not in self:
self.__map[key] = link = Link()
root = self.__root
last = root.prev
link.prev, link.next, link.key = last, root, key
last.next = link
root.prev = proxy(link)
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
'od.__delitem__(y) <==> del od[y]'
# Deleting an existing item uses self.__map to find the link which gets
# removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link = self.__map.pop(key)
link_prev = link.prev
link_next = link.next
link_prev.next = link_next
link_next.prev = link_prev
def __iter__(self):
'od.__iter__() <==> iter(od)'
# Traverse the linked list in order.
root = self.__root
curr = root.next
while curr is not root:
yield curr.key
curr = curr.next
def __reversed__(self):
'od.__reversed__() <==> reversed(od)'
# Traverse the linked list in reverse order.
root = self.__root
curr = root.prev
while curr is not root:
yield curr.key
curr = curr.prev
def clear(self):
'od.clear() -> None. Remove all items from od.'
root = self.__root
root.prev = root.next = root
self.__map.clear()
dict.clear(self)
def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
if last:
link = root.prev
link_prev = link.prev
link_prev.next = root
root.prev = link_prev
else:
link = root.next
link_next = link.next
root.next = link_next
link_next.prev = root
key = link.key
del self.__map[key]
value = dict.pop(self, key)
return key, value
def move_to_end(self, key, last=True):
'''Move an existing element to the end (or beginning if last==False).
Raises KeyError if the element does not exist.
When last=True, acts like a fast version of self[key]=self.pop(key).
'''
link = self.__map[key]
link_prev = link.prev
link_next = link.next
link_prev.next = link_next
link_next.prev = link_prev
root = self.__root
if last:
last = root.prev
link.prev = last
link.next = root
last.next = root.prev = link
else:
first = root.next
link.prev = root
link.next = first
root.next = first.prev = link
def __sizeof__(self):
sizeof = _sys.getsizeof
n = len(self) + 1 # number of links including root
size = sizeof(self.__dict__) # instance dictionary
size += sizeof(self.__map) * 2 # internal dict and inherited dict
size += sizeof(self.__hardroot) * n # link objects
size += sizeof(self.__root) * n # proxy objects
return size
update = __update = MutableMapping.update
keys = MutableMapping.keys
values = MutableMapping.values
items = MutableMapping.items
__ne__ = MutableMapping.__ne__
__marker = object()
def pop(self, key, default=__marker):
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding
value. If key is not found, d is returned if given, otherwise KeyError
is raised.
'''
if key in self:
result = self[key]
del self[key]
return result
if default is self.__marker:
raise KeyError(key)
return default
def setdefault(self, key, default=None):
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
if key in self:
return self[key]
self[key] = default
return default
@_recursive_repr()
def __repr__(self):
'od.__repr__() <==> repr(od)'
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self.items()))
def __reduce__(self):
'Return state information for pickling'
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def copy(self):
'od.copy() -> a shallow copy of od'
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S.
If not specified, the value defaults to None.
'''
self = cls()
for key in iterable:
self[key] = value
return self
def __eq__(self, other):
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
while comparison to a regular mapping is order-insensitive.
'''
if isinstance(other, OrderedDict):
return len(self)==len(other) and \
all(p==q for p, q in zip(self.items(), other.items()))
return dict.__eq__(self, other)
# update_wrapper() and wraps() are tools to help write
# wrapper functions that can handle naive introspection
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
wrapper.__wrapped__ = wrapped
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
def total_ordering(cls):
"""Class decorator that fills in missing ordering methods"""
convert = {
'__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),
('__le__', lambda self, other: self < other or self == other),
('__ge__', lambda self, other: not self < other)],
'__le__': [('__ge__', lambda self, other: not self <= other or self == other),
('__lt__', lambda self, other: self <= other and not self == other),
('__gt__', lambda self, other: not self <= other)],
'__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),
('__ge__', lambda self, other: self > other or self == other),
('__le__', lambda self, other: not self > other)],
'__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),
('__gt__', lambda self, other: self >= other and not self == other),
('__lt__', lambda self, other: not self >= other)]
}
roots = set(dir(cls)) & set(convert)
if not roots:
raise ValueError('must define at least one ordering operation: < > <= >=')
root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
for opname, opfunc in convert[root]:
if opname not in roots:
opfunc.__name__ = opname
opfunc.__doc__ = getattr(int, opname).__doc__
setattr(cls, opname, opfunc)
return cls
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
class K(object):
__slots__ = ['obj']
def __init__(self, obj):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
__hash__ = None
return K
_CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize")
def lru_cache(maxsize=100):
"""Least-recently-used cache decorator.
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
Arguments to the cached function must be hashable.
View the cache statistics named tuple (hits, misses, maxsize, currsize) with
f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
"""
# Users should only access the lru_cache through its public API:
# cache_info, cache_clear, and f.__wrapped__
# The internals of the lru_cache are encapsulated for thread safety and
# to allow the implementation to change (including a possible C version).
def decorating_function(user_function,
tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):
hits, misses = [0], [0]
kwd_mark = (object(),) # separates positional and keyword args
lock = Lock() # needed because OrderedDict isn't threadsafe
if maxsize is None:
cache = dict() # simple cache without ordering or size limit
@wraps(user_function)
def wrapper(*args, **kwds):
key = args
if kwds:
key += kwd_mark + tuple(sorted(kwds.items()))
try:
result = cache[key]
hits[0] += 1
return result
except KeyError:
pass
result = user_function(*args, **kwds)
cache[key] = result
misses[0] += 1
return result
else:
cache = OrderedDict() # ordered least recent to most recent
cache_popitem = cache.popitem
cache_renew = cache.move_to_end
@wraps(user_function)
def wrapper(*args, **kwds):
key = args
if kwds:
key += kwd_mark + tuple(sorted(kwds.items()))
with lock:
try:
result = cache[key]
cache_renew(key) # record recent use of this key
hits[0] += 1
return result
except KeyError:
pass
result = user_function(*args, **kwds)
with lock:
cache[key] = result # record recent use of this key
misses[0] += 1
if len(cache) > maxsize:
cache_popitem(0) # purge least recently used cache entry
return result
def cache_info():
"""Report cache statistics"""
with lock:
return _CacheInfo(hits[0], misses[0], maxsize, len(cache))
def cache_clear():
"""Clear the cache and cache statistics"""
with lock:
cache.clear()
hits[0] = misses[0] = 0
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return wrapper
return decorating_function
@@ -1,157 +0,0 @@
"""Redo the builtin repr() (representation) but with limits on most sizes."""
__all__ = ["Repr", "repr", "recursive_repr"]
import __builtin__ as builtins
from itertools import islice
try:
from thread import get_ident
except ImportError:
from _dummy_thread32 import get_ident
def recursive_repr(fillvalue='...'):
'Decorator to make a repr function return fillvalue for a recursive call'
def decorating_function(user_function):
repr_running = set()
def wrapper(self):
key = id(self), get_ident()
if key in repr_running:
return fillvalue
repr_running.add(key)
try:
result = user_function(self)
finally:
repr_running.discard(key)
return result
# Can't use functools.wraps() here because of bootstrap issues
wrapper.__module__ = getattr(user_function, '__module__')
wrapper.__doc__ = getattr(user_function, '__doc__')
wrapper.__name__ = getattr(user_function, '__name__')
wrapper.__annotations__ = getattr(user_function, '__annotations__', {})
return wrapper
return decorating_function
class Repr:
def __init__(self):
self.maxlevel = 6
self.maxtuple = 6
self.maxlist = 6
self.maxarray = 5
self.maxdict = 4
self.maxset = 6
self.maxfrozenset = 6
self.maxdeque = 6
self.maxstring = 30
self.maxlong = 40
self.maxother = 30
def repr(self, x):
return self.repr1(x, self.maxlevel)
def repr1(self, x, level):
typename = type(x).__name__
if ' ' in typename:
parts = typename.split()
typename = '_'.join(parts)
if hasattr(self, 'repr_' + typename):
return getattr(self, 'repr_' + typename)(x, level)
else:
return self.repr_instance(x, level)
def _repr_iterable(self, x, level, left, right, maxiter, trail=''):
n = len(x)
if level <= 0 and n:
s = '...'
else:
newlevel = level - 1
repr1 = self.repr1
pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]
if n > maxiter: pieces.append('...')
s = ', '.join(pieces)
if n == 1 and trail: right = trail + right
return '%s%s%s' % (left, s, right)
def repr_tuple(self, x, level):
return self._repr_iterable(x, level, '(', ')', self.maxtuple, ',')
def repr_list(self, x, level):
return self._repr_iterable(x, level, '[', ']', self.maxlist)
def repr_array(self, x, level):
header = "array('%s', [" % x.typecode
return self._repr_iterable(x, level, header, '])', self.maxarray)
def repr_set(self, x, level):
x = _possibly_sorted(x)
return self._repr_iterable(x, level, 'set([', '])', self.maxset)
def repr_frozenset(self, x, level):
x = _possibly_sorted(x)
return self._repr_iterable(x, level, 'frozenset([', '])',
self.maxfrozenset)
def repr_deque(self, x, level):
return self._repr_iterable(x, level, 'deque([', '])', self.maxdeque)
def repr_dict(self, x, level):
n = len(x)
if n == 0: return '{}'
if level <= 0: return '{...}'
newlevel = level - 1
repr1 = self.repr1
pieces = []
for key in islice(_possibly_sorted(x), self.maxdict):
keyrepr = repr1(key, newlevel)
valrepr = repr1(x[key], newlevel)
pieces.append('%s: %s' % (keyrepr, valrepr))
if n > self.maxdict: pieces.append('...')
s = ', '.join(pieces)
return '{%s}' % (s,)
def repr_str(self, x, level):
s = builtins.repr(x[:self.maxstring])
if len(s) > self.maxstring:
i = max(0, (self.maxstring-3)//2)
j = max(0, self.maxstring-3-i)
s = builtins.repr(x[:i] + x[len(x)-j:])
s = s[:i] + '...' + s[len(s)-j:]
return s
def repr_int(self, x, level):
s = builtins.repr(x) # XXX Hope this isn't too slow...
if len(s) > self.maxlong:
i = max(0, (self.maxlong-3)//2)
j = max(0, self.maxlong-3-i)
s = s[:i] + '...' + s[len(s)-j:]
return s
def repr_instance(self, x, level):
try:
s = builtins.repr(x)
# Bugs in x.__repr__() can cause arbitrary
# exceptions -- then make up something
except Exception:
return '<%s instance at %x>' % (x.__class__.__name__, id(x))
if len(s) > self.maxother:
i = max(0, (self.maxother-3)//2)
j = max(0, self.maxother-3-i)
s = s[:i] + '...' + s[len(s)-j:]
return s
def _possibly_sorted(x):
# Since not all sequences of items can be sorted and comparison
# functions may raise arbitrary exceptions, return an unsorted
# sequence in that case.
try:
return sorted(x)
except Exception:
return list(x)
aRepr = Repr()
repr = aRepr.repr
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,110 +0,0 @@
"""
iri2uri
Converts an IRI to a URI.
"""
__author__ = "Joe Gregorio (joe@bitworking.org)"
__copyright__ = "Copyright 2006, Joe Gregorio"
__contributors__ = []
__version__ = "1.0.0"
__license__ = "MIT"
__history__ = """
"""
import urlparse
# Convert an IRI to a URI following the rules in RFC 3987
#
# The characters we need to enocde and escape are defined in the spec:
#
# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
# / %xD0000-DFFFD / %xE1000-EFFFD
escape_range = [
(0xA0, 0xD7FF),
(0xE000, 0xF8FF),
(0xF900, 0xFDCF),
(0xFDF0, 0xFFEF),
(0x10000, 0x1FFFD),
(0x20000, 0x2FFFD),
(0x30000, 0x3FFFD),
(0x40000, 0x4FFFD),
(0x50000, 0x5FFFD),
(0x60000, 0x6FFFD),
(0x70000, 0x7FFFD),
(0x80000, 0x8FFFD),
(0x90000, 0x9FFFD),
(0xA0000, 0xAFFFD),
(0xB0000, 0xBFFFD),
(0xC0000, 0xCFFFD),
(0xD0000, 0xDFFFD),
(0xE1000, 0xEFFFD),
(0xF0000, 0xFFFFD),
(0x100000, 0x10FFFD),
]
def encode(c):
retval = c
i = ord(c)
for low, high in escape_range:
if i < low:
break
if i >= low and i <= high:
retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
break
return retval
def iri2uri(uri):
"""Convert an IRI to a URI. Note that IRIs must be
passed in a unicode strings. That is, do not utf-8 encode
the IRI before passing it into the function."""
if isinstance(uri ,unicode):
(scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
authority = authority.encode('idna')
# For each character in 'ucschar' or 'iprivate'
# 1. encode as utf-8
# 2. then %-encode each octet of that utf-8
uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
uri = "".join([encode(c) for c in uri])
return uri
if __name__ == "__main__":
import unittest
class Test(unittest.TestCase):
def test_uris(self):
"""Test that URIs are invariant under the transformation."""
invariant = [
u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
u"http://www.ietf.org/rfc/rfc2396.txt",
u"ldap://[2001:db8::7]/c=GB?objectClass?one",
u"mailto:John.Doe@example.com",
u"news:comp.infosystems.www.servers.unix",
u"tel:+1-816-555-1212",
u"telnet://192.0.2.16:80/",
u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
for uri in invariant:
self.assertEqual(uri, iri2uri(uri))
def test_iri(self):
""" Test that the right type of escaping is done for each part of the URI."""
self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}"))
self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
unittest.main()
@@ -1,438 +0,0 @@
"""SocksiPy - Python SOCKS module.
Version 1.00
Copyright 2006 Dan-Haim. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Dan Haim nor the names of his contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
This module provides a standard socket-like interface for Python
for tunneling connections through SOCKS proxies.
"""
"""
Minor modifications made by Christopher Gilbert (http://motomastyle.com/)
for use in PyLoris (http://pyloris.sourceforge.net/)
Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
mainly to merge bug fixes found in Sourceforge
"""
import base64
import socket
import struct
import sys
if getattr(socket, 'socket', None) is None:
raise ImportError('socket.socket missing, proxy support unusable')
PROXY_TYPE_SOCKS4 = 1
PROXY_TYPE_SOCKS5 = 2
PROXY_TYPE_HTTP = 3
PROXY_TYPE_HTTP_NO_TUNNEL = 4
_defaultproxy = None
_orgsocket = socket.socket
class ProxyError(Exception): pass
class GeneralProxyError(ProxyError): pass
class Socks5AuthError(ProxyError): pass
class Socks5Error(ProxyError): pass
class Socks4Error(ProxyError): pass
class HTTPError(ProxyError): pass
_generalerrors = ("success",
"invalid data",
"not connected",
"not available",
"bad proxy type",
"bad input")
_socks5errors = ("succeeded",
"general SOCKS server failure",
"connection not allowed by ruleset",
"Network unreachable",
"Host unreachable",
"Connection refused",
"TTL expired",
"Command not supported",
"Address type not supported",
"Unknown error")
_socks5autherrors = ("succeeded",
"authentication is required",
"all offered authentication methods were rejected",
"unknown username or invalid password",
"unknown error")
_socks4errors = ("request granted",
"request rejected or failed",
"request rejected because SOCKS server cannot connect to identd on the client",
"request rejected because the client program and identd report different user-ids",
"unknown error")
def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
"""setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
Sets a default proxy which all further socksocket objects will use,
unless explicitly changed.
"""
global _defaultproxy
_defaultproxy = (proxytype, addr, port, rdns, username, password)
def wrapmodule(module):
"""wrapmodule(module)
Attempts to replace a module's socket library with a SOCKS socket. Must set
a default proxy using setdefaultproxy(...) first.
This will only work on modules that import socket directly into the namespace;
most of the Python Standard Library falls into this category.
"""
if _defaultproxy != None:
module.socket.socket = socksocket
else:
raise GeneralProxyError((4, "no proxy specified"))
class socksocket(socket.socket):
"""socksocket([family[, type[, proto]]]) -> socket object
Open a SOCKS enabled socket. The parameters are the same as
those of the standard socket init. In order for SOCKS to work,
you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
"""
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
_orgsocket.__init__(self, family, type, proto, _sock)
if _defaultproxy != None:
self.__proxy = _defaultproxy
else:
self.__proxy = (None, None, None, None, None, None)
self.__proxysockname = None
self.__proxypeername = None
self.__httptunnel = True
def __recvall(self, count):
"""__recvall(count) -> data
Receive EXACTLY the number of bytes requested from the socket.
Blocks until the required number of bytes have been received.
"""
data = self.recv(count)
while len(data) < count:
d = self.recv(count-len(data))
if not d: raise GeneralProxyError((0, "connection closed unexpectedly"))
data = data + d
return data
def sendall(self, content, *args):
""" override socket.socket.sendall method to rewrite the header
for non-tunneling proxies if needed
"""
if not self.__httptunnel:
content = self.__rewriteproxy(content)
return super(socksocket, self).sendall(content, *args)
def __rewriteproxy(self, header):
""" rewrite HTTP request headers to support non-tunneling proxies
(i.e. those which do not support the CONNECT method).
This only works for HTTP (not HTTPS) since HTTPS requires tunneling.
"""
host, endpt = None, None
hdrs = header.split("\r\n")
for hdr in hdrs:
if hdr.lower().startswith("host:"):
host = hdr
elif hdr.lower().startswith("get") or hdr.lower().startswith("post"):
endpt = hdr
if host and endpt:
hdrs.remove(host)
hdrs.remove(endpt)
host = host.split(" ")[1]
endpt = endpt.split(" ")
if (self.__proxy[4] != None and self.__proxy[5] != None):
hdrs.insert(0, self.__getauthheader())
hdrs.insert(0, "Host: %s" % host)
hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2]))
return "\r\n".join(hdrs)
def __getauthheader(self):
auth = self.__proxy[4] + ":" + self.__proxy[5]
return "Proxy-Authorization: Basic " + base64.b64encode(auth)
def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
"""setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
Sets the proxy to be used.
proxytype - The type of the proxy to be used. Three types
are supported: PROXY_TYPE_SOCKS4 (including socks4a),
PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
addr - The address of the server (IP or DNS).
port - The port of the server. Defaults to 1080 for SOCKS
servers and 8080 for HTTP proxy servers.
rdns - Should DNS queries be preformed on the remote side
(rather than the local side). The default is True.
Note: This has no effect with SOCKS4 servers.
username - Username to authenticate with to the server.
The default is no authentication.
password - Password to authenticate with to the server.
Only relevant when username is also provided.
"""
self.__proxy = (proxytype, addr, port, rdns, username, password)
def __negotiatesocks5(self, destaddr, destport):
"""__negotiatesocks5(self,destaddr,destport)
Negotiates a connection through a SOCKS5 server.
"""
# First we'll send the authentication packages we support.
if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
# The username/password details were supplied to the
# setproxy method so we support the USERNAME/PASSWORD
# authentication (in addition to the standard none).
self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
else:
# No username/password were entered, therefore we
# only support connections with no authentication.
self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00))
# We'll receive the server's response to determine which
# method was selected
chosenauth = self.__recvall(2)
if chosenauth[0:1] != chr(0x05).encode():
self.close()
raise GeneralProxyError((1, _generalerrors[1]))
# Check the chosen authentication method
if chosenauth[1:2] == chr(0x00).encode():
# No authentication is required
pass
elif chosenauth[1:2] == chr(0x02).encode():
# Okay, we need to perform a basic username/password
# authentication.
self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5])
authstat = self.__recvall(2)
if authstat[0:1] != chr(0x01).encode():
# Bad response
self.close()
raise GeneralProxyError((1, _generalerrors[1]))
if authstat[1:2] != chr(0x00).encode():
# Authentication failed
self.close()
raise Socks5AuthError((3, _socks5autherrors[3]))
# Authentication succeeded
else:
# Reaching here is always bad
self.close()
if chosenauth[1] == chr(0xFF).encode():
raise Socks5AuthError((2, _socks5autherrors[2]))
else:
raise GeneralProxyError((1, _generalerrors[1]))
# Now we can request the actual connection
req = struct.pack('BBB', 0x05, 0x01, 0x00)
# If the given destination address is an IP address, we'll
# use the IPv4 address request even if remote resolving was specified.
try:
ipaddr = socket.inet_aton(destaddr)
req = req + chr(0x01).encode() + ipaddr
except socket.error:
# Well it's not an IP number, so it's probably a DNS name.
if self.__proxy[3]:
# Resolve remotely
ipaddr = None
req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr
else:
# Resolve locally
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
req = req + chr(0x01).encode() + ipaddr
req = req + struct.pack(">H", destport)
self.sendall(req)
# Get the response
resp = self.__recvall(4)
if resp[0:1] != chr(0x05).encode():
self.close()
raise GeneralProxyError((1, _generalerrors[1]))
elif resp[1:2] != chr(0x00).encode():
# Connection failed
self.close()
if ord(resp[1:2])<=8:
raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
else:
raise Socks5Error((9, _socks5errors[9]))
# Get the bound address/port
elif resp[3:4] == chr(0x01).encode():
boundaddr = self.__recvall(4)
elif resp[3:4] == chr(0x03).encode():
resp = resp + self.recv(1)
boundaddr = self.__recvall(ord(resp[4:5]))
else:
self.close()
raise GeneralProxyError((1,_generalerrors[1]))
boundport = struct.unpack(">H", self.__recvall(2))[0]
self.__proxysockname = (boundaddr, boundport)
if ipaddr != None:
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
else:
self.__proxypeername = (destaddr, destport)
def getproxysockname(self):
"""getsockname() -> address info
Returns the bound IP address and port number at the proxy.
"""
return self.__proxysockname
def getproxypeername(self):
"""getproxypeername() -> address info
Returns the IP and port number of the proxy.
"""
return _orgsocket.getpeername(self)
def getpeername(self):
"""getpeername() -> address info
Returns the IP address and port number of the destination
machine (note: getproxypeername returns the proxy)
"""
return self.__proxypeername
def __negotiatesocks4(self,destaddr,destport):
"""__negotiatesocks4(self,destaddr,destport)
Negotiates a connection through a SOCKS4 server.
"""
# Check if the destination address provided is an IP address
rmtrslv = False
try:
ipaddr = socket.inet_aton(destaddr)
except socket.error:
# It's a DNS name. Check where it should be resolved.
if self.__proxy[3]:
ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
rmtrslv = True
else:
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
# Construct the request packet
req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
# The username parameter is considered userid for SOCKS4
if self.__proxy[4] != None:
req = req + self.__proxy[4]
req = req + chr(0x00).encode()
# DNS name if remote resolving is required
# NOTE: This is actually an extension to the SOCKS4 protocol
# called SOCKS4A and may not be supported in all cases.
if rmtrslv:
req = req + destaddr + chr(0x00).encode()
self.sendall(req)
# Get the response from the server
resp = self.__recvall(8)
if resp[0:1] != chr(0x00).encode():
# Bad data
self.close()
raise GeneralProxyError((1,_generalerrors[1]))
if resp[1:2] != chr(0x5A).encode():
# Server returned an error
self.close()
if ord(resp[1:2]) in (91, 92, 93):
self.close()
raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
else:
raise Socks4Error((94, _socks4errors[4]))
# Get the bound address/port
self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0])
if rmtrslv != None:
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
else:
self.__proxypeername = (destaddr, destport)
def __negotiatehttp(self, destaddr, destport):
"""__negotiatehttp(self,destaddr,destport)
Negotiates a connection through an HTTP server.
"""
# If we need to resolve locally, we do this now
if not self.__proxy[3]:
addr = socket.gethostbyname(destaddr)
else:
addr = destaddr
headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"]
headers += ["Host: ", destaddr, "\r\n"]
if (self.__proxy[4] != None and self.__proxy[5] != None):
headers += [self.__getauthheader(), "\r\n"]
headers.append("\r\n")
self.sendall("".join(headers).encode())
# We read the response until we get the string "\r\n\r\n"
resp = self.recv(1)
while resp.find("\r\n\r\n".encode()) == -1:
resp = resp + self.recv(1)
# We just need the first line to check if the connection
# was successful
statusline = resp.splitlines()[0].split(" ".encode(), 2)
if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
self.close()
raise GeneralProxyError((1, _generalerrors[1]))
try:
statuscode = int(statusline[1])
except ValueError:
self.close()
raise GeneralProxyError((1, _generalerrors[1]))
if statuscode != 200:
self.close()
raise HTTPError((statuscode, statusline[2]))
self.__proxysockname = ("0.0.0.0", 0)
self.__proxypeername = (addr, destport)
def connect(self, destpair):
"""connect(self, despair)
Connects to the specified destination through a proxy.
destpar - A tuple of the IP/DNS address and the port number.
(identical to socket's connect).
To select the proxy server use setproxy().
"""
# Do a minimal input check first
if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int):
raise GeneralProxyError((5, _generalerrors[5]))
if self.__proxy[0] == PROXY_TYPE_SOCKS5:
if self.__proxy[2] != None:
portnum = self.__proxy[2]
else:
portnum = 1080
_orgsocket.connect(self, (self.__proxy[1], portnum))
self.__negotiatesocks5(destpair[0], destpair[1])
elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
if self.__proxy[2] != None:
portnum = self.__proxy[2]
else:
portnum = 1080
_orgsocket.connect(self,(self.__proxy[1], portnum))
self.__negotiatesocks4(destpair[0], destpair[1])
elif self.__proxy[0] == PROXY_TYPE_HTTP:
if self.__proxy[2] != None:
portnum = self.__proxy[2]
else:
portnum = 8080
_orgsocket.connect(self,(self.__proxy[1], portnum))
self.__negotiatehttp(destpair[0], destpair[1])
elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL:
if self.__proxy[2] != None:
portnum = self.__proxy[2]
else:
portnum = 8080
_orgsocket.connect(self,(self.__proxy[1],portnum))
if destpair[1] == 443:
self.__negotiatehttp(destpair[0],destpair[1])
else:
self.__httptunnel = False
elif self.__proxy[0] == None:
_orgsocket.connect(self, (destpair[0], destpair[1]))
else:
raise GeneralProxyError((4, _generalerrors[4]))
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2
~~~~~~
Jinja2 is a template engine written in pure Python. It provides a
Django inspired non-XML syntax but supports inline expressions and
an optional sandboxed environment.
Nutshell
--------
Here a small example of a Jinja2 template::
{% extends 'base.html' %}
{% block title %}Memberlist{% endblock %}
{% block content %}
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
{% endblock %}
:copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
__docformat__ = 'restructuredtext en'
__version__ = "2.10.3"
# high level interface
from jinja2.environment import Environment, Template
# loaders
from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader, \
ModuleLoader
# bytecode caches
from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \
MemcachedBytecodeCache
# undefined types
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined, \
make_logging_undefined
# exceptions
from jinja2.exceptions import TemplateError, UndefinedError, \
TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \
TemplateAssertionError, TemplateRuntimeError
# decorators and public utilities
from jinja2.filters import environmentfilter, contextfilter, \
evalcontextfilter
from jinja2.utils import Markup, escape, clear_caches, \
environmentfunction, evalcontextfunction, contextfunction, \
is_undefined, select_autoescape
__all__ = [
'Environment', 'Template', 'BaseLoader', 'FileSystemLoader',
'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader',
'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache',
'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined',
'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError',
'TemplateRuntimeError',
'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape',
'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
'evalcontextfilter', 'evalcontextfunction', 'make_logging_undefined',
'select_autoescape',
]
def _patch_async():
from jinja2.utils import have_async_gen
if have_async_gen:
from jinja2.asyncsupport import patch_all
patch_all()
_patch_async()
del _patch_async
@@ -1,105 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2._compat
~~~~~~~~~~~~~~
Some py2/py3 compatibility support based on a stripped down
version of six so we don't have to depend on a specific version
of it.
:copyright: Copyright 2013 by the Jinja team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import sys
PY2 = sys.version_info[0] == 2
PYPY = hasattr(sys, 'pypy_translation_info')
_identity = lambda x: x
if not PY2:
unichr = chr
range_type = range
text_type = str
string_types = (str,)
integer_types = (int,)
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
import pickle
from io import BytesIO, StringIO
NativeStringIO = StringIO
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
ifilter = filter
imap = map
izip = zip
intern = sys.intern
implements_iterator = _identity
implements_to_string = _identity
encode_filename = _identity
else:
unichr = unichr
text_type = unicode
range_type = xrange
string_types = (str, unicode)
integer_types = (int, long)
iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems()
import cPickle as pickle
from cStringIO import StringIO as BytesIO, StringIO
NativeStringIO = BytesIO
exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')
from itertools import imap, izip, ifilter
intern = intern
def implements_iterator(cls):
cls.next = cls.__next__
del cls.__next__
return cls
def implements_to_string(cls):
cls.__unicode__ = cls.__str__
cls.__str__ = lambda x: x.__unicode__().encode('utf-8')
return cls
def encode_filename(filename):
if isinstance(filename, unicode):
return filename.encode('utf-8')
return filename
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a
# dummy metaclass for one level of class instantiation that replaces
# itself with the actual metaclass.
class metaclass(type):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
try:
from urllib.parse import quote_from_bytes as url_quote
except ImportError:
from urllib import quote as url_quote
try:
from collections import abc
except ImportError:
import collections as abc
@@ -1,2 +0,0 @@
# generated by scripts/generate_identifier_pattern.py
pattern = '·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯'
@@ -1,146 +0,0 @@
from functools import wraps
from jinja2.asyncsupport import auto_aiter
from jinja2 import filters
async def auto_to_seq(value):
seq = []
if hasattr(value, '__aiter__'):
async for item in value:
seq.append(item)
else:
for item in value:
seq.append(item)
return seq
async def async_select_or_reject(args, kwargs, modfunc, lookup_attr):
seq, func = filters.prepare_select_or_reject(
args, kwargs, modfunc, lookup_attr)
if seq:
async for item in auto_aiter(seq):
if func(item):
yield item
def dualfilter(normal_filter, async_filter):
wrap_evalctx = False
if getattr(normal_filter, 'environmentfilter', False):
is_async = lambda args: args[0].is_async
wrap_evalctx = False
else:
if not getattr(normal_filter, 'evalcontextfilter', False) and \
not getattr(normal_filter, 'contextfilter', False):
wrap_evalctx = True
is_async = lambda args: args[0].environment.is_async
@wraps(normal_filter)
def wrapper(*args, **kwargs):
b = is_async(args)
if wrap_evalctx:
args = args[1:]
if b:
return async_filter(*args, **kwargs)
return normal_filter(*args, **kwargs)
if wrap_evalctx:
wrapper.evalcontextfilter = True
wrapper.asyncfiltervariant = True
return wrapper
def asyncfiltervariant(original):
def decorator(f):
return dualfilter(original, f)
return decorator
@asyncfiltervariant(filters.do_first)
async def do_first(environment, seq):
try:
return await auto_aiter(seq).__anext__()
except StopAsyncIteration:
return environment.undefined('No first item, sequence was empty.')
@asyncfiltervariant(filters.do_groupby)
async def do_groupby(environment, value, attribute):
expr = filters.make_attrgetter(environment, attribute)
return [filters._GroupTuple(key, await auto_to_seq(values))
for key, values in filters.groupby(sorted(
await auto_to_seq(value), key=expr), expr)]
@asyncfiltervariant(filters.do_join)
async def do_join(eval_ctx, value, d=u'', attribute=None):
return filters.do_join(eval_ctx, await auto_to_seq(value), d, attribute)
@asyncfiltervariant(filters.do_list)
async def do_list(value):
return await auto_to_seq(value)
@asyncfiltervariant(filters.do_reject)
async def do_reject(*args, **kwargs):
return async_select_or_reject(args, kwargs, lambda x: not x, False)
@asyncfiltervariant(filters.do_rejectattr)
async def do_rejectattr(*args, **kwargs):
return async_select_or_reject(args, kwargs, lambda x: not x, True)
@asyncfiltervariant(filters.do_select)
async def do_select(*args, **kwargs):
return async_select_or_reject(args, kwargs, lambda x: x, False)
@asyncfiltervariant(filters.do_selectattr)
async def do_selectattr(*args, **kwargs):
return async_select_or_reject(args, kwargs, lambda x: x, True)
@asyncfiltervariant(filters.do_map)
async def do_map(*args, **kwargs):
seq, func = filters.prepare_map(args, kwargs)
if seq:
async for item in auto_aiter(seq):
yield func(item)
@asyncfiltervariant(filters.do_sum)
async def do_sum(environment, iterable, attribute=None, start=0):
rv = start
if attribute is not None:
func = filters.make_attrgetter(environment, attribute)
else:
func = lambda x: x
async for item in auto_aiter(iterable):
rv += func(item)
return rv
@asyncfiltervariant(filters.do_slice)
async def do_slice(value, slices, fill_with=None):
return filters.do_slice(await auto_to_seq(value), slices, fill_with)
ASYNC_FILTERS = {
'first': do_first,
'groupby': do_groupby,
'join': do_join,
'list': do_list,
# we intentionally do not support do_last because that would be
# ridiculous
'reject': do_reject,
'rejectattr': do_rejectattr,
'map': do_map,
'select': do_select,
'selectattr': do_selectattr,
'sum': do_sum,
'slice': do_slice,
}
@@ -1,256 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2.asyncsupport
~~~~~~~~~~~~~~~~~~~
Has all the code for async support which is implemented as a patch
for supported Python versions.
:copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
import sys
import asyncio
import inspect
from functools import update_wrapper
from jinja2.utils import concat, internalcode, Markup
from jinja2.environment import TemplateModule
from jinja2.runtime import LoopContextBase, _last_iteration
async def concat_async(async_gen):
rv = []
async def collect():
async for event in async_gen:
rv.append(event)
await collect()
return concat(rv)
async def generate_async(self, *args, **kwargs):
vars = dict(*args, **kwargs)
try:
async for event in self.root_render_func(self.new_context(vars)):
yield event
except Exception:
exc_info = sys.exc_info()
else:
return
yield self.environment.handle_exception(exc_info, True)
def wrap_generate_func(original_generate):
def _convert_generator(self, loop, args, kwargs):
async_gen = self.generate_async(*args, **kwargs)
try:
while 1:
yield loop.run_until_complete(async_gen.__anext__())
except StopAsyncIteration:
pass
def generate(self, *args, **kwargs):
if not self.environment.is_async:
return original_generate(self, *args, **kwargs)
return _convert_generator(self, asyncio.get_event_loop(), args, kwargs)
return update_wrapper(generate, original_generate)
async def render_async(self, *args, **kwargs):
if not self.environment.is_async:
raise RuntimeError('The environment was not created with async mode '
'enabled.')
vars = dict(*args, **kwargs)
ctx = self.new_context(vars)
try:
return await concat_async(self.root_render_func(ctx))
except Exception:
exc_info = sys.exc_info()
return self.environment.handle_exception(exc_info, True)
def wrap_render_func(original_render):
def render(self, *args, **kwargs):
if not self.environment.is_async:
return original_render(self, *args, **kwargs)
loop = asyncio.get_event_loop()
return loop.run_until_complete(self.render_async(*args, **kwargs))
return update_wrapper(render, original_render)
def wrap_block_reference_call(original_call):
@internalcode
async def async_call(self):
rv = await concat_async(self._stack[self._depth](self._context))
if self._context.eval_ctx.autoescape:
rv = Markup(rv)
return rv
@internalcode
def __call__(self):
if not self._context.environment.is_async:
return original_call(self)
return async_call(self)
return update_wrapper(__call__, original_call)
def wrap_macro_invoke(original_invoke):
@internalcode
async def async_invoke(self, arguments, autoescape):
rv = await self._func(*arguments)
if autoescape:
rv = Markup(rv)
return rv
@internalcode
def _invoke(self, arguments, autoescape):
if not self._environment.is_async:
return original_invoke(self, arguments, autoescape)
return async_invoke(self, arguments, autoescape)
return update_wrapper(_invoke, original_invoke)
@internalcode
async def get_default_module_async(self):
if self._module is not None:
return self._module
self._module = rv = await self.make_module_async()
return rv
def wrap_default_module(original_default_module):
@internalcode
def _get_default_module(self):
if self.environment.is_async:
raise RuntimeError('Template module attribute is unavailable '
'in async mode')
return original_default_module(self)
return _get_default_module
async def make_module_async(self, vars=None, shared=False, locals=None):
context = self.new_context(vars, shared, locals)
body_stream = []
async for item in self.root_render_func(context):
body_stream.append(item)
return TemplateModule(self, context, body_stream)
def patch_template():
from jinja2 import Template
Template.generate = wrap_generate_func(Template.generate)
Template.generate_async = update_wrapper(
generate_async, Template.generate_async)
Template.render_async = update_wrapper(
render_async, Template.render_async)
Template.render = wrap_render_func(Template.render)
Template._get_default_module = wrap_default_module(
Template._get_default_module)
Template._get_default_module_async = get_default_module_async
Template.make_module_async = update_wrapper(
make_module_async, Template.make_module_async)
def patch_runtime():
from jinja2.runtime import BlockReference, Macro
BlockReference.__call__ = wrap_block_reference_call(
BlockReference.__call__)
Macro._invoke = wrap_macro_invoke(Macro._invoke)
def patch_filters():
from jinja2.filters import FILTERS
from jinja2.asyncfilters import ASYNC_FILTERS
FILTERS.update(ASYNC_FILTERS)
def patch_all():
patch_template()
patch_runtime()
patch_filters()
async def auto_await(value):
if inspect.isawaitable(value):
return await value
return value
async def auto_aiter(iterable):
if hasattr(iterable, '__aiter__'):
async for item in iterable:
yield item
return
for item in iterable:
yield item
class AsyncLoopContext(LoopContextBase):
def __init__(self, async_iterator, undefined, after, length, recurse=None,
depth0=0):
LoopContextBase.__init__(self, undefined, recurse, depth0)
self._async_iterator = async_iterator
self._after = after
self._length = length
@property
def length(self):
if self._length is None:
raise TypeError('Loop length for some iterators cannot be '
'lazily calculated in async mode')
return self._length
def __aiter__(self):
return AsyncLoopContextIterator(self)
class AsyncLoopContextIterator(object):
__slots__ = ('context',)
def __init__(self, context):
self.context = context
def __aiter__(self):
return self
async def __anext__(self):
ctx = self.context
ctx.index0 += 1
if ctx._after is _last_iteration:
raise StopAsyncIteration()
ctx._before = ctx._current
ctx._current = ctx._after
try:
ctx._after = await ctx._async_iterator.__anext__()
except StopAsyncIteration:
ctx._after = _last_iteration
return ctx._current, ctx
async def make_async_loop_context(iterable, undefined, recurse=None, depth0=0):
# Length is more complicated and less efficient in async mode. The
# reason for this is that we cannot know if length will be used
# upfront but because length is a property we cannot lazily execute it
# later. This means that we need to buffer it up and measure :(
#
# We however only do this for actual iterators, not for async
# iterators as blocking here does not seem like the best idea in the
# world.
try:
length = len(iterable)
except (TypeError, AttributeError):
if not hasattr(iterable, '__aiter__'):
iterable = tuple(iterable)
length = len(iterable)
else:
length = None
async_iterator = auto_aiter(iterable)
try:
after = await async_iterator.__anext__()
except StopAsyncIteration:
after = _last_iteration
return AsyncLoopContext(async_iterator, undefined, after, length, recurse,
depth0)
@@ -1,361 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2.bccache
~~~~~~~~~~~~~~
This module implements the bytecode cache system Jinja is optionally
using. This is useful if you have very complex template situations and
the compiliation of all those templates slow down your application too
much.
Situations where this is useful are often forking web applications that
are initialized on the first request.
:copyright: (c) 2017 by the Jinja Team.
:license: BSD.
"""
from os import path, listdir
import os
import sys
import stat
import errno
import marshal
import tempfile
import fnmatch
from hashlib import sha1
from jinja2.utils import open_if_exists
from jinja2._compat import BytesIO, pickle, PY2, text_type
# marshal works better on 3.x, one hack less required
if not PY2:
marshal_dump = marshal.dump
marshal_load = marshal.load
else:
def marshal_dump(code, f):
if isinstance(f, file):
marshal.dump(code, f)
else:
f.write(marshal.dumps(code))
def marshal_load(f):
if isinstance(f, file):
return marshal.load(f)
return marshal.loads(f.read())
bc_version = 3
# magic version used to only change with new jinja versions. With 2.6
# we change this to also take Python version changes into account. The
# reason for this is that Python tends to segfault if fed earlier bytecode
# versions because someone thought it would be a good idea to reuse opcodes
# or make Python incompatible with earlier versions.
bc_magic = 'j2'.encode('ascii') + \
pickle.dumps(bc_version, 2) + \
pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1])
class Bucket(object):
"""Buckets are used to store the bytecode for one template. It's created
and initialized by the bytecode cache and passed to the loading functions.
The buckets get an internal checksum from the cache assigned and use this
to automatically reject outdated cache material. Individual bytecode
cache subclasses don't have to care about cache invalidation.
"""
def __init__(self, environment, key, checksum):
self.environment = environment
self.key = key
self.checksum = checksum
self.reset()
def reset(self):
"""Resets the bucket (unloads the bytecode)."""
self.code = None
def load_bytecode(self, f):
"""Loads bytecode from a file or file like object."""
# make sure the magic header is correct
magic = f.read(len(bc_magic))
if magic != bc_magic:
self.reset()
return
# the source code of the file changed, we need to reload
checksum = pickle.load(f)
if self.checksum != checksum:
self.reset()
return
# if marshal_load fails then we need to reload
try:
self.code = marshal_load(f)
except (EOFError, ValueError, TypeError):
self.reset()
return
def write_bytecode(self, f):
"""Dump the bytecode into the file or file like object passed."""
if self.code is None:
raise TypeError('can\'t write empty bucket')
f.write(bc_magic)
pickle.dump(self.checksum, f, 2)
marshal_dump(self.code, f)
def bytecode_from_string(self, string):
"""Load bytecode from a string."""
self.load_bytecode(BytesIO(string))
def bytecode_to_string(self):
"""Return the bytecode as string."""
out = BytesIO()
self.write_bytecode(out)
return out.getvalue()
class BytecodeCache(object):
"""To implement your own bytecode cache you have to subclass this class
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
these methods are passed a :class:`~jinja2.bccache.Bucket`.
A very basic bytecode cache that saves the bytecode on the file system::
from os import path
class MyCache(BytecodeCache):
def __init__(self, directory):
self.directory = directory
def load_bytecode(self, bucket):
filename = path.join(self.directory, bucket.key)
if path.exists(filename):
with open(filename, 'rb') as f:
bucket.load_bytecode(f)
def dump_bytecode(self, bucket):
filename = path.join(self.directory, bucket.key)
with open(filename, 'wb') as f:
bucket.write_bytecode(f)
A more advanced version of a filesystem based bytecode cache is part of
Jinja2.
"""
def load_bytecode(self, bucket):
"""Subclasses have to override this method to load bytecode into a
bucket. If they are not able to find code in the cache for the
bucket, it must not do anything.
"""
raise NotImplementedError()
def dump_bytecode(self, bucket):
"""Subclasses have to override this method to write the bytecode
from a bucket back to the cache. If it unable to do so it must not
fail silently but raise an exception.
"""
raise NotImplementedError()
def clear(self):
"""Clears the cache. This method is not used by Jinja2 but should be
implemented to allow applications to clear the bytecode cache used
by a particular environment.
"""
def get_cache_key(self, name, filename=None):
"""Returns the unique hash key for this template name."""
hash = sha1(name.encode('utf-8'))
if filename is not None:
filename = '|' + filename
if isinstance(filename, text_type):
filename = filename.encode('utf-8')
hash.update(filename)
return hash.hexdigest()
def get_source_checksum(self, source):
"""Returns a checksum for the source."""
return sha1(source.encode('utf-8')).hexdigest()
def get_bucket(self, environment, name, filename, source):
"""Return a cache bucket for the given template. All arguments are
mandatory but filename may be `None`.
"""
key = self.get_cache_key(name, filename)
checksum = self.get_source_checksum(source)
bucket = Bucket(environment, key, checksum)
self.load_bytecode(bucket)
return bucket
def set_bucket(self, bucket):
"""Put the bucket into the cache."""
self.dump_bytecode(bucket)
class FileSystemBytecodeCache(BytecodeCache):
"""A bytecode cache that stores bytecode on the filesystem. It accepts
two arguments: The directory where the cache items are stored and a
pattern string that is used to build the filename.
If no directory is specified a default cache directory is selected. On
Windows the user's temp directory is used, on UNIX systems a directory
is created for the user in the system temp directory.
The pattern can be used to have multiple separate caches operate on the
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
is replaced with the cache key.
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
This bytecode cache supports clearing of the cache using the clear method.
"""
def __init__(self, directory=None, pattern='__jinja2_%s.cache'):
if directory is None:
directory = self._get_default_cache_dir()
self.directory = directory
self.pattern = pattern
def _get_default_cache_dir(self):
def _unsafe_dir():
raise RuntimeError('Cannot determine safe temp directory. You '
'need to explicitly provide one.')
tmpdir = tempfile.gettempdir()
# On windows the temporary directory is used specific unless
# explicitly forced otherwise. We can just use that.
if os.name == 'nt':
return tmpdir
if not hasattr(os, 'getuid'):
_unsafe_dir()
dirname = '_jinja2-cache-%d' % os.getuid()
actual_dir = os.path.join(tmpdir, dirname)
try:
os.mkdir(actual_dir, stat.S_IRWXU)
except OSError as e:
if e.errno != errno.EEXIST:
raise
try:
os.chmod(actual_dir, stat.S_IRWXU)
actual_dir_stat = os.lstat(actual_dir)
if actual_dir_stat.st_uid != os.getuid() \
or not stat.S_ISDIR(actual_dir_stat.st_mode) \
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU:
_unsafe_dir()
except OSError as e:
if e.errno != errno.EEXIST:
raise
actual_dir_stat = os.lstat(actual_dir)
if actual_dir_stat.st_uid != os.getuid() \
or not stat.S_ISDIR(actual_dir_stat.st_mode) \
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU:
_unsafe_dir()
return actual_dir
def _get_cache_filename(self, bucket):
return path.join(self.directory, self.pattern % bucket.key)
def load_bytecode(self, bucket):
f = open_if_exists(self._get_cache_filename(bucket), 'rb')
if f is not None:
try:
bucket.load_bytecode(f)
finally:
f.close()
def dump_bytecode(self, bucket):
f = open(self._get_cache_filename(bucket), 'wb')
try:
bucket.write_bytecode(f)
finally:
f.close()
def clear(self):
# imported lazily here because google app-engine doesn't support
# write access on the file system and the function does not exist
# normally.
from os import remove
files = fnmatch.filter(listdir(self.directory), self.pattern % '*')
for filename in files:
try:
remove(path.join(self.directory, filename))
except OSError:
pass
class MemcachedBytecodeCache(BytecodeCache):
"""This class implements a bytecode cache that uses a memcache cache for
storing the information. It does not enforce a specific memcache library
(tummy's memcache or cmemcache) but will accept any class that provides
the minimal interface required.
Libraries compatible with this class:
- `cachelib <https://github.com/pallets/cachelib>`_
- `python-memcached <https://pypi.org/project/python-memcached/>`_
(Unfortunately the django cache interface is not compatible because it
does not support storing binary data, only unicode. You can however pass
the underlying cache client to the bytecode cache which is available
as `django.core.cache.cache._client`.)
The minimal interface for the client passed to the constructor is this:
.. class:: MinimalClientInterface
.. method:: set(key, value[, timeout])
Stores the bytecode in the cache. `value` is a string and
`timeout` the timeout of the key. If timeout is not provided
a default timeout or no timeout should be assumed, if it's
provided it's an integer with the number of seconds the cache
item should exist.
.. method:: get(key)
Returns the value for the cache key. If the item does not
exist in the cache the return value must be `None`.
The other arguments to the constructor are the prefix for all keys that
is added before the actual cache key and the timeout for the bytecode in
the cache system. We recommend a high (or no) timeout.
This bytecode cache does not support clearing of used items in the cache.
The clear method is a no-operation function.
.. versionadded:: 2.7
Added support for ignoring memcache errors through the
`ignore_memcache_errors` parameter.
"""
def __init__(self, client, prefix='jinja2/bytecode/', timeout=None,
ignore_memcache_errors=True):
self.client = client
self.prefix = prefix
self.timeout = timeout
self.ignore_memcache_errors = ignore_memcache_errors
def load_bytecode(self, bucket):
try:
code = self.client.get(self.prefix + bucket.key)
except Exception:
if not self.ignore_memcache_errors:
raise
code = None
if code is not None:
bucket.bytecode_from_string(code)
def dump_bytecode(self, bucket):
args = (self.prefix + bucket.key, bucket.bytecode_to_string())
if self.timeout is not None:
args += (self.timeout,)
try:
self.client.set(*args)
except Exception:
if not self.ignore_memcache_errors:
raise
File diff suppressed because it is too large Load Diff
@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja.constants
~~~~~~~~~~~~~~~
Various constants.
:copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
#: list of lorem ipsum words used by the lipsum() helper function
LOREM_IPSUM_WORDS = u'''\
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
auctor augue bibendum blandit class commodo condimentum congue consectetuer
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
penatibus per pharetra phasellus placerat platea porta porttitor posuere
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
viverra volutpat vulputate'''
@@ -1,378 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2.debug
~~~~~~~~~~~~
Implements the debug interface for Jinja. This module does some pretty
ugly stuff with the Python traceback system in order to achieve tracebacks
with correct line numbers, locals and contents.
:copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
import sys
import traceback
from types import TracebackType, CodeType
from jinja2.utils import missing, internal_code
from jinja2.exceptions import TemplateSyntaxError
from jinja2._compat import iteritems, reraise, PY2
# on pypy we can take advantage of transparent proxies
try:
from __pypy__ import tproxy
except ImportError:
tproxy = None
# how does the raise helper look like?
try:
exec("raise TypeError, 'foo'")
except SyntaxError:
raise_helper = 'raise __jinja_exception__[1]'
except TypeError:
raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
class TracebackFrameProxy(object):
"""Proxies a traceback frame."""
def __init__(self, tb):
self.tb = tb
self._tb_next = None
@property
def tb_next(self):
return self._tb_next
def set_next(self, next):
if tb_set_next is not None:
try:
tb_set_next(self.tb, next and next.tb or None)
except Exception:
# this function can fail due to all the hackery it does
# on various python implementations. We just catch errors
# down and ignore them if necessary.
pass
self._tb_next = next
@property
def is_jinja_frame(self):
return '__jinja_template__' in self.tb.tb_frame.f_globals
def __getattr__(self, name):
return getattr(self.tb, name)
def make_frame_proxy(frame):
proxy = TracebackFrameProxy(frame)
if tproxy is None:
return proxy
def operation_handler(operation, *args, **kwargs):
if operation in ('__getattribute__', '__getattr__'):
return getattr(proxy, args[0])
elif operation == '__setattr__':
proxy.__setattr__(*args, **kwargs)
else:
return getattr(proxy, operation)(*args, **kwargs)
return tproxy(TracebackType, operation_handler)
class ProcessedTraceback(object):
"""Holds a Jinja preprocessed traceback for printing or reraising."""
def __init__(self, exc_type, exc_value, frames):
assert frames, 'no frames for this traceback?'
self.exc_type = exc_type
self.exc_value = exc_value
self.frames = frames
# newly concatenate the frames (which are proxies)
prev_tb = None
for tb in self.frames:
if prev_tb is not None:
prev_tb.set_next(tb)
prev_tb = tb
prev_tb.set_next(None)
def render_as_text(self, limit=None):
"""Return a string with the traceback."""
lines = traceback.format_exception(self.exc_type, self.exc_value,
self.frames[0], limit=limit)
return ''.join(lines).rstrip()
def render_as_html(self, full=False):
"""Return a unicode string with the traceback as rendered HTML."""
from jinja2.debugrenderer import render_traceback
return u'%s\n\n<!--\n%s\n-->' % (
render_traceback(self, full=full),
self.render_as_text().decode('utf-8', 'replace')
)
@property
def is_template_syntax_error(self):
"""`True` if this is a template syntax error."""
return isinstance(self.exc_value, TemplateSyntaxError)
@property
def exc_info(self):
"""Exception info tuple with a proxy around the frame objects."""
return self.exc_type, self.exc_value, self.frames[0]
@property
def standard_exc_info(self):
"""Standard python exc_info for re-raising"""
tb = self.frames[0]
# the frame will be an actual traceback (or transparent proxy) if
# we are on pypy or a python implementation with support for tproxy
if type(tb) is not TracebackType:
tb = tb.tb
return self.exc_type, self.exc_value, tb
def make_traceback(exc_info, source_hint=None):
"""Creates a processed traceback object from the exc_info."""
exc_type, exc_value, tb = exc_info
if isinstance(exc_value, TemplateSyntaxError):
exc_info = translate_syntax_error(exc_value, source_hint)
initial_skip = 0
else:
initial_skip = 1
return translate_exception(exc_info, initial_skip)
def translate_syntax_error(error, source=None):
"""Rewrites a syntax error to please traceback systems."""
error.source = source
error.translated = True
exc_info = (error.__class__, error, None)
filename = error.filename
if filename is None:
filename = '<unknown>'
return fake_exc_info(exc_info, filename, error.lineno)
def translate_exception(exc_info, initial_skip=0):
"""If passed an exc_info it will automatically rewrite the exceptions
all the way down to the correct line numbers and frames.
"""
tb = exc_info[2]
frames = []
# skip some internal frames if wanted
for x in range(initial_skip):
if tb is not None:
tb = tb.tb_next
initial_tb = tb
while tb is not None:
# skip frames decorated with @internalcode. These are internal
# calls we can't avoid and that are useless in template debugging
# output.
if tb.tb_frame.f_code in internal_code:
tb = tb.tb_next
continue
# save a reference to the next frame if we override the current
# one with a faked one.
next = tb.tb_next
# fake template exceptions
template = tb.tb_frame.f_globals.get('__jinja_template__')
if template is not None:
lineno = template.get_corresponding_lineno(tb.tb_lineno)
tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
lineno)[2]
frames.append(make_frame_proxy(tb))
tb = next
# if we don't have any exceptions in the frames left, we have to
# reraise it unchanged.
# XXX: can we backup here? when could this happen?
if not frames:
reraise(exc_info[0], exc_info[1], exc_info[2])
return ProcessedTraceback(exc_info[0], exc_info[1], frames)
def get_jinja_locals(real_locals):
ctx = real_locals.get('context')
if ctx:
locals = ctx.get_all().copy()
else:
locals = {}
local_overrides = {}
for name, value in iteritems(real_locals):
if not name.startswith('l_') or value is missing:
continue
try:
_, depth, name = name.split('_', 2)
depth = int(depth)
except ValueError:
continue
cur_depth = local_overrides.get(name, (-1,))[0]
if cur_depth < depth:
local_overrides[name] = (depth, value)
for name, (_, value) in iteritems(local_overrides):
if value is missing:
locals.pop(name, None)
else:
locals[name] = value
return locals
def fake_exc_info(exc_info, filename, lineno):
"""Helper for `translate_exception`."""
exc_type, exc_value, tb = exc_info
# figure the real context out
if tb is not None:
locals = get_jinja_locals(tb.tb_frame.f_locals)
# if there is a local called __jinja_exception__, we get
# rid of it to not break the debug functionality.
locals.pop('__jinja_exception__', None)
else:
locals = {}
# assamble fake globals we need
globals = {
'__name__': filename,
'__file__': filename,
'__jinja_exception__': exc_info[:2],
# we don't want to keep the reference to the template around
# to not cause circular dependencies, but we mark it as Jinja
# frame for the ProcessedTraceback
'__jinja_template__': None
}
# and fake the exception
code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec')
# if it's possible, change the name of the code. This won't work
# on some python environments such as google appengine
try:
if tb is None:
location = 'template'
else:
function = tb.tb_frame.f_code.co_name
if function == 'root':
location = 'top-level template code'
elif function.startswith('block_'):
location = 'block "%s"' % function[6:]
else:
location = 'template'
if PY2:
code = CodeType(0, code.co_nlocals, code.co_stacksize,
code.co_flags, code.co_code, code.co_consts,
code.co_names, code.co_varnames, filename,
location, code.co_firstlineno,
code.co_lnotab, (), ())
else:
code = CodeType(0, code.co_kwonlyargcount,
code.co_nlocals, code.co_stacksize,
code.co_flags, code.co_code, code.co_consts,
code.co_names, code.co_varnames, filename,
location, code.co_firstlineno,
code.co_lnotab, (), ())
except Exception as e:
pass
# execute the code and catch the new traceback
try:
exec(code, globals, locals)
except:
exc_info = sys.exc_info()
new_tb = exc_info[2].tb_next
# return without this frame
return exc_info[:2] + (new_tb,)
def _init_ugly_crap():
"""This function implements a few ugly things so that we can patch the
traceback objects. The function returned allows resetting `tb_next` on
any python traceback object. Do not attempt to use this on non cpython
interpreters
"""
import ctypes
from types import TracebackType
if PY2:
# figure out size of _Py_ssize_t for Python 2:
if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
_Py_ssize_t = ctypes.c_int64
else:
_Py_ssize_t = ctypes.c_int
else:
# platform ssize_t on Python 3
_Py_ssize_t = ctypes.c_ssize_t
# regular python
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
# python with trace
if hasattr(sys, 'getobjects'):
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('_ob_next', ctypes.POINTER(_PyObject)),
('_ob_prev', ctypes.POINTER(_PyObject)),
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
class _Traceback(_PyObject):
pass
_Traceback._fields_ = [
('tb_next', ctypes.POINTER(_Traceback)),
('tb_frame', ctypes.POINTER(_PyObject)),
('tb_lasti', ctypes.c_int),
('tb_lineno', ctypes.c_int)
]
def tb_set_next(tb, next):
"""Set the tb_next attribute of a traceback object."""
if not (isinstance(tb, TracebackType) and
(next is None or isinstance(next, TracebackType))):
raise TypeError('tb_set_next arguments must be traceback objects')
obj = _Traceback.from_address(id(tb))
if tb.tb_next is not None:
old = _Traceback.from_address(id(tb.tb_next))
old.ob_refcnt -= 1
if next is None:
obj.tb_next = ctypes.POINTER(_Traceback)()
else:
next = _Traceback.from_address(id(next))
next.ob_refcnt += 1
obj.tb_next = ctypes.pointer(next)
return tb_set_next
# try to get a tb_set_next implementation if we don't have transparent
# proxies.
tb_set_next = None
if tproxy is None:
# traceback.tb_next can be modified since CPython 3.7
if sys.version_info >= (3, 7):
def tb_set_next(tb, next):
tb.tb_next = next
else:
# On Python 3.6 and older, use ctypes
try:
tb_set_next = _init_ugly_crap()
except Exception:
pass
del _init_ugly_crap
@@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
"""
jinja2.defaults
~~~~~~~~~~~~~~~
Jinja default filters and tags.
:copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
from jinja2._compat import range_type
from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner, Namespace
# defaults for the parser / lexer
BLOCK_START_STRING = '{%'
BLOCK_END_STRING = '%}'
VARIABLE_START_STRING = '{{'
VARIABLE_END_STRING = '}}'
COMMENT_START_STRING = '{#'
COMMENT_END_STRING = '#}'
LINE_STATEMENT_PREFIX = None
LINE_COMMENT_PREFIX = None
TRIM_BLOCKS = False
LSTRIP_BLOCKS = False
NEWLINE_SEQUENCE = '\n'
KEEP_TRAILING_NEWLINE = False
# default filters, tests and namespace
from jinja2.filters import FILTERS as DEFAULT_FILTERS
from jinja2.tests import TESTS as DEFAULT_TESTS
DEFAULT_NAMESPACE = {
'range': range_type,
'dict': dict,
'lipsum': generate_lorem_ipsum,
'cycler': Cycler,
'joiner': Joiner,
'namespace': Namespace
}
# default policies
DEFAULT_POLICIES = {
'compiler.ascii_str': True,
'urlize.rel': 'noopener',
'urlize.target': None,
'truncate.leeway': 5,
'json.dumps_function': None,
'json.dumps_kwargs': {'sort_keys': True},
'ext.i18n.trimmed': False,
}
# export all constants
__all__ = tuple(x for x in locals().keys() if x.isupper())
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More