Merge pull request #82 from splunk/migrate_app_es_soc
Moving our package to this repo from app_es_soc
@@ -3,6 +3,8 @@
|
||||
# Check https://circleci.com/docs/2.0/language-python/ for more details
|
||||
#
|
||||
|
||||
version: 2.1
|
||||
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- "~/.apt-cache"
|
||||
@@ -15,8 +17,6 @@ apt-run: &apt-install
|
||||
sudo apt update -qq
|
||||
sudo apt install -y enchant python-dev -qq
|
||||
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
content-executor:
|
||||
docker:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
git clone --branch ${CIRCLE_BRANCH} https://${GITHUB_TOKEN}@github.com/splunk/security-content.git
|
||||
fi
|
||||
- restore_cache:
|
||||
key: deps1-{{ .Branch }}-{{ checksum "security-content/requirements.txt" }}
|
||||
key: virtualenv
|
||||
- run: *apt-install
|
||||
- run:
|
||||
name: install python dependencies
|
||||
@@ -46,29 +46,17 @@ jobs:
|
||||
virtualenv --python=/usr/bin/python2.7 --clear venv
|
||||
source venv/bin/activate
|
||||
pip install -q -r requirements.txt
|
||||
- save_cache:
|
||||
key: virtualenv
|
||||
paths:
|
||||
- "/security-content/venv"
|
||||
- run:
|
||||
name: run validate
|
||||
command: |
|
||||
cd security-content
|
||||
source venv/bin/activate
|
||||
|
||||
python bin/validate.py --path . --verbose
|
||||
- run:
|
||||
name: run generate
|
||||
command: |
|
||||
cd security-content
|
||||
source venv/bin/activate
|
||||
python bin/generate.py --path . --output src --storiesv1 --use_case_lib -v
|
||||
- persist_to_workspace:
|
||||
root: security-content/src/default
|
||||
paths:
|
||||
- use_case_library.conf
|
||||
- analytic_stories.conf
|
||||
- savedsearches.conf
|
||||
- save_cache:
|
||||
key: deps1-{{ .Branch }}-{{ checksum "security-content/requirements.txt" }}
|
||||
paths:
|
||||
- "venv"
|
||||
|
||||
build-sources:
|
||||
executor: content-executor
|
||||
steps:
|
||||
@@ -80,27 +68,141 @@ jobs:
|
||||
else
|
||||
git clone --branch ${CIRCLE_BRANCH} https://${GITHUB_TOKEN}@github.com/splunk/security-content.git
|
||||
fi
|
||||
- attach_workspace:
|
||||
# Must be absolute path or relative path from working_directory
|
||||
at: ~/repo/updated
|
||||
- run: *apt-install
|
||||
- run:
|
||||
name: store updated analyticstories.conf
|
||||
name: install python dependencies
|
||||
command: |
|
||||
cd security-content
|
||||
cp -v ~/repo/updated/* src/default/
|
||||
git config credential.helper 'cache --timeout=120'
|
||||
git config user.email "research@splunk.com"
|
||||
git config user.name "research bot"
|
||||
git config --global push.default simple
|
||||
git add src/default/*
|
||||
git commit --allow-empty -m "updating src files [ci skip]"
|
||||
# Push quietly to prevent showing the token in log
|
||||
git push https://${GITHUB_TOKEN}@github.com/splunk/security-content.git ${CIRCLE_BRANCH}
|
||||
tar -czvf content-pack.tar.gz src/*
|
||||
rm -rf venv
|
||||
virtualenv --python=/usr/bin/python2.7 --clear venv
|
||||
source venv/bin/activate
|
||||
pip install -q -r requirements.txt
|
||||
- run:
|
||||
name: run generate
|
||||
command: |
|
||||
cd security-content
|
||||
source venv/bin/activate
|
||||
python bin/generate.py --path . --output package --storiesv1 --use_case_lib -v
|
||||
- run:
|
||||
name: update version and build number
|
||||
command: |
|
||||
cd security-content
|
||||
# check if tag is set, get build number from the tag if set
|
||||
if [ -z "${CIRCLE_TAG}" ]; then
|
||||
CONTENT_VERSION=$(grep -oP "(\d.\d.\d+$)" package/default/content-version.conf)
|
||||
echo "detected content version: $CONTENT_VERSION"
|
||||
else
|
||||
CONTENT_VERSION=$(echo $CIRCLE_TAG | grep -oP "\d.\d.\d+")
|
||||
echo "content version: $CONTENT_VERSION, set by tag: $CIRCLE_TAG"
|
||||
fi
|
||||
# update build number and version
|
||||
sed -i "s/build = .*$/build = $CIRCLE_BUILD_NUM/g" package/default/app.conf
|
||||
sed -i "s/version = .*$/version = $CONTENT_VERSION/g" package/default/app.conf
|
||||
sed -i "s/version = .*$/version = $CONTENT_VERSION/g" package/default/content-version.conf
|
||||
tar -czf content-pack-build.tar.gz package/*
|
||||
- persist_to_workspace:
|
||||
root: security-content/
|
||||
paths:
|
||||
- content-pack.tar.gz
|
||||
- content-pack-build.tar.gz
|
||||
build-package:
|
||||
executor: content-executor
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/dist
|
||||
- run:
|
||||
name: grab splunk packaging toolkit
|
||||
command: |
|
||||
curl -Ls https://download.splunk.com/misc/packaging-toolkit/splunk-packaging-toolkit-1.0.0.tar.gz -o ~/splunk-packaging-toolkit-latest.tar.gz
|
||||
mkdir ~/slim-latest
|
||||
tar -zxf ~/splunk-packaging-toolkit-latest.tar.gz -C ~/slim-latest --strip-components=1
|
||||
- run:
|
||||
name: install splunk packaging toolkit (slim)
|
||||
command: |
|
||||
cd ~/slim-latest
|
||||
sudo pip install --upgrade pip setuptools
|
||||
sudo pip install virtualenv
|
||||
virtualenv --python=/usr/bin/python2.7 --clear venv
|
||||
source venv/bin/activate
|
||||
pip install .
|
||||
- run:
|
||||
name: create a .spl for this build using slim
|
||||
command: |
|
||||
source ~/slim-latest/venv/bin/activate
|
||||
cd ~/dist
|
||||
tar -zxf content-pack-build.tar.gz
|
||||
CONTENT_VERSION=$(grep -oP "(\d.\d.\d+$)" package/default/content-version.conf)
|
||||
mv package DA-ESS-ContentUpdate
|
||||
mkdir upload
|
||||
slim package -o upload DA-ESS-ContentUpdate
|
||||
cp upload/*.tar.gz DA-ESS-ContentUpdate-latest.tar.gz
|
||||
- store_artifacts:
|
||||
path: ~/dist/upload
|
||||
destination: package/
|
||||
- persist_to_workspace:
|
||||
root: ~/dist
|
||||
paths:
|
||||
- DA-ESS-ContentUpdate-latest.tar.gz
|
||||
run-appinspect:
|
||||
executor: content-executor
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run: *apt-install
|
||||
- run:
|
||||
name: grab appinspect
|
||||
command: |
|
||||
curl -Ls http://dev.splunk.com/goto/appinspectdownload -o appinspect-lastest.tar.gz
|
||||
mkdir appinspect-latest
|
||||
tar -zxf appinspect-lastest.tar.gz -C appinspect-latest --strip-components=1
|
||||
- run:
|
||||
name: install app inspect
|
||||
command: |
|
||||
cd appinspect-latest
|
||||
rm -rf venv
|
||||
sudo pip install --upgrade pip setuptools
|
||||
sudo pip install virtualenv
|
||||
virtualenv --python=/usr/bin/python2.7 --clear venv
|
||||
source venv/bin/activate
|
||||
pip install .
|
||||
- run:
|
||||
name: run app inspect
|
||||
command: |
|
||||
cd appinspect-latest
|
||||
source venv/bin/activate
|
||||
splunk-appinspect inspect ~/DA-ESS-ContentUpdate-latest.tar.gz --included-tags=cloud --max-messages=all
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- DA-ESS-ContentUpdate-latest.tar.gz
|
||||
update-sources-github:
|
||||
executor: content-executor
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run: *apt-install
|
||||
- run:
|
||||
name: checkout repo
|
||||
command: |
|
||||
if [ "${CIRCLE_BRANCH}" == "" ]; then
|
||||
git clone https://${GITHUB_TOKEN}@github.com/splunk/security-content.git
|
||||
else
|
||||
git clone --branch ${CIRCLE_BRANCH} https://${GITHUB_TOKEN}@github.com/splunk/security-content.git
|
||||
fi
|
||||
# configure git to prep for commit
|
||||
git config credential.helper 'cache --timeout=120'
|
||||
git config user.email "research@splunk.com"
|
||||
git config user.name "research bot"
|
||||
it config --global push.default simple
|
||||
git add package/default/*
|
||||
git commit --allow-empty -m "updating package files [ci skip]"
|
||||
# Push quietly to prevent showing the token in log
|
||||
git push https://${GITHUB_TOKEN}@github.com/splunk/security-content.git ${CIRCLE_BRANCH}
|
||||
CONTENT_VERSION=$(echo $CIRCLE_TAG | grep -oP "\d.\d.\d+")
|
||||
tar -czf DA-ESS-ContentUpdate-${CONTENT_VERSION}.tar.gz package/*
|
||||
- persist_to_workspace:
|
||||
root: security-content/
|
||||
paths:
|
||||
- DA-ESS-ContentUpdate-${CONTENT_VERSION}.tar.gz
|
||||
publish-github-release:
|
||||
docker:
|
||||
- image: cibuilds/github:0.10
|
||||
@@ -110,18 +212,48 @@ jobs:
|
||||
- run:
|
||||
name: publish release on github
|
||||
command: |
|
||||
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} ~/repo/updated/content-pack.tar.gz
|
||||
CONTENT_VERSION=$(echo $CIRCLE_TAG | grep -oP "\d.\d.\d+")
|
||||
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} ~/repo/updated/DA-ESS-ContentUpdate-${CONTENT_VERSION}.tar.gz
|
||||
workflows:
|
||||
version: 2.1
|
||||
validate-and-build:
|
||||
jobs:
|
||||
- validate-content:
|
||||
# build always
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-sources:
|
||||
requires:
|
||||
- validate-content
|
||||
# build always
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-package:
|
||||
requires:
|
||||
- validate-content
|
||||
- build-sources
|
||||
# build always
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- run-appinspect:
|
||||
requires:
|
||||
- validate-content
|
||||
- build-sources
|
||||
- build-package
|
||||
# build always
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- update-sources-github:
|
||||
requires:
|
||||
- validate-content
|
||||
- build-sources
|
||||
- build-package
|
||||
- run-appinspect
|
||||
# only update sources in develop
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
@@ -131,6 +263,10 @@ workflows:
|
||||
requires:
|
||||
- validate-content
|
||||
- build-sources
|
||||
- build-package
|
||||
- run-appinspect
|
||||
- update-sources-github
|
||||
# only release when there is a tag
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
|
||||
@@ -4,6 +4,7 @@ repos:
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-executables-have-shebangs
|
||||
exclude: 'package/bin/da_ess_contentupdate/|package/bin/splunklib/|venv/'
|
||||
- id: check-json
|
||||
- id: check-symlinks
|
||||
- id: check-yaml
|
||||
@@ -11,4 +12,5 @@ repos:
|
||||
args: [--autofix]
|
||||
- id: flake8
|
||||
args: [--max-line-length=131]
|
||||
exclude: 'package/bin/da_ess_contentupdate/|package/bin/splunklib/|venv/|package/bin/escu_contextualize.py|package/bin/escu_investigate.py|package/bin/runstory.py'
|
||||
- id: requirements-txt-fixer
|
||||
|
||||
@@ -23,7 +23,7 @@ Can be consumed using:
|
||||
* [investigations/](investigations/) - splunk, and phantom investigation content that are used in stories
|
||||
* [responses/](responses/) - automated splunk and phantom responses that are used in stories
|
||||
* [baselines/](baselines/) - phantom and Splunk baseline needed to support detections in stories
|
||||
* [src/](src/) - splunk content app source files, includes lookups, binaries, and defaul config files
|
||||
* [package/](package/) - splunk content app source files, includes lookups, binaries, and defaul config files
|
||||
* [bin/](bin/) - where all binaries to produce, and test content lives
|
||||
* [spec/](spec/) - location of all spec files that describe ESCU content
|
||||
* [docs/](docs/) - documentation for all of the spec files
|
||||
|
||||
@@ -253,6 +253,10 @@ def generate_investigations(REPO_PATH, detections, stories):
|
||||
server = phantom['phantom_server']
|
||||
playbook = phantom['playbook_name']
|
||||
playbook_url = phantom['playbook_url']
|
||||
earliest_time = phantom['schedule']['earliest_time']
|
||||
latest_time = phantom['schedule']['latest_time']
|
||||
cron = phantom['schedule']['cron_schedule']
|
||||
search = 'CONSTRUCT DETECTION SEARCH HERE'
|
||||
except KeyError as e:
|
||||
sys.exit("ERROR: \"{1}\" missing key {0} with error:\n{1}".format(e, name, e))
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Fidelity
|
||||
|
||||
The fidelity of a narrative describes the ratio of signal (valid/positive) to noise (invalid/false positive) anticipated based on field experience.
|
||||
|
||||
* High - This indicates a relatively high signal to noise ratio, and therefore a lower likelihood of false positives, and it should not require additional searches to validate it.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
|
||||
sourcetype=WinEventLog:* EventCode=4728
|
||||
```
|
||||
|
||||
|
||||
|
||||
* Low - This indicates a relatively low signal to noise ratio, and therefore a higher likelihood of false positives. Confidence in the output can be increased through other means (i.e. cross-correlation and/or subsequent searches).
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
|
||||
url=* | eval url_length = len(url) | where url_length > 256
|
||||
```
|
||||
* Moderate - This indicates an unpredictable signal to noise ratio with a bias towards signal, and therefore a higher likelihood of false positives than high. Confidence in the output can be increased through other means (i.e. cross-correlation and/or subsequent searches).
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
http_user_agent = "*nullptr*"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright (C) 2009-2016 Splunk Inc. All Rights Reserved.
|
||||
#
|
||||
# This file contains additional options for an alert_actions.conf file.
|
||||
#
|
||||
# To learn more about configuration files (including precedence) please see the documentation
|
||||
# located at http://www.splunk.com/base/Documentation/latest/Admin/Aboutconfigurationfiles
|
||||
#
|
||||
[escu]
|
||||
|
||||
enabled = [true|false|0|1]
|
||||
* Whether or not this use-case is enabled.
|
||||
* This exists so that we are a true noop for scheduled searches.
|
||||
* action.usecase=0 action.usecase.enabled=1
|
||||
* Required.
|
||||
* Defaults to false.
|
||||
|
||||
version = [string]
|
||||
* Version of this search
|
||||
|
||||
asset_at_risk = [string]
|
||||
* The type of asset that is at risk from the behavior this search is attempting to find
|
||||
* Defaults to None
|
||||
|
||||
category = [string]
|
||||
* A description of the category that this use-case falls into
|
||||
* Defaults to None
|
||||
|
||||
channel = [string]
|
||||
* The name of the channel the search belongs to
|
||||
|
||||
confidence = [low|medium|high]
|
||||
* A description of the confidence value
|
||||
* Valid values are: low, medium, high
|
||||
* Defaults to None
|
||||
|
||||
creation_time = [datetime]
|
||||
* The date & time that the search was first created
|
||||
* The date-time should be formatted an epoch time (in GMT)
|
||||
|
||||
datamodels = [json]
|
||||
* A JSON list of the data models used by this search
|
||||
* Defaults to None
|
||||
|
||||
eli5 = [string]
|
||||
* Text explaining this search to a 5 year old
|
||||
* Defaults to None
|
||||
|
||||
full_search_name = [string]
|
||||
* The entire search name
|
||||
* Defaults to None
|
||||
|
||||
how_to_implement = [string]
|
||||
* Text discussing what needs to be done to implement this search and any local modifications that can be performed
|
||||
* Defaults to None
|
||||
|
||||
known_false_positives = [string]
|
||||
* A description of cases in which this use-case may generate false positive alerts
|
||||
* Defaults to None
|
||||
|
||||
mappings = [json]
|
||||
* A JSON list of the kill chain phases this search covers
|
||||
* Defaults to None
|
||||
|
||||
modification_time = [datetime]
|
||||
* The date that the search was last modified
|
||||
* The date-time should be formatted an epoch time (in GMT)
|
||||
|
||||
remediation = [string]
|
||||
* A high-level description of how the issue described by this use-case can be remediated.
|
||||
* Defaults to None
|
||||
|
||||
providing_technologies = [json]
|
||||
* A JSON list of the technology examples that can be used to gather data to power this search
|
||||
* Defaults to None
|
||||
|
||||
analytic_story = [json]
|
||||
* A JSON list of the use cases this search applies to
|
||||
* Defaults to None
|
||||
|
||||
earliest_time_offset = [integer]
|
||||
* Time in seconds before event time that the search should cover
|
||||
|
||||
latest_time_offset = [integer]
|
||||
* Time in seconds after event time that the search should cover
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright (C) 2009-2016 Splunk Inc. All Rights Reserved.
|
||||
#
|
||||
# This file contains all possible options for a usecases.conf file. Use this file to define a use-case.
|
||||
#
|
||||
# To learn more about configuration files (including precedence) please see the documentation
|
||||
# located at http://www.splunk.com/base/Documentation/latest/Admin/Aboutconfigurationfiles
|
||||
#
|
||||
[<analytic_story_name>]
|
||||
category = [string]
|
||||
* The category of the analytic story
|
||||
* Defaults to None
|
||||
|
||||
creation_time = [datetime]
|
||||
* The date & time that the analytic story was first created
|
||||
* The date-time should be formatted an epoch time (in GMT)
|
||||
|
||||
data_models = [json]
|
||||
* A JSON list of the data models used by the analytic story
|
||||
* Defaults to None
|
||||
|
||||
description = [string]
|
||||
* A bried description of the analytic story
|
||||
* Defaults to None
|
||||
|
||||
id = [string]
|
||||
* A description of the analytic story
|
||||
* Defaults to None
|
||||
|
||||
mappings = [json]
|
||||
* A JSON dictionary of the different mappings this story maps to
|
||||
* See appendix B for the format of this field
|
||||
* Defaults to None
|
||||
|
||||
modification_time = [datetime]
|
||||
* The date & time that the analytic story was last modified
|
||||
* The date-time should be formatted an epoch time (in GMT)
|
||||
|
||||
narrative = [string]
|
||||
* A longer narrative of the analytic story that describes the detection searches, any support searches,
|
||||
* and the corresponding contextual and investigative searches
|
||||
* Defaults to None
|
||||
|
||||
references = [json]
|
||||
* A JSON list of references for this story
|
||||
* Defaults to None
|
||||
|
||||
detection_searches = [json]
|
||||
* A JSON list of the detection searches that the analytic story applies to.
|
||||
* See appendix A for the format of this field
|
||||
* Defaults to None
|
||||
|
||||
investigative_searches = [json]
|
||||
* A JSON list of the investigative searches that the analytic story applies to.
|
||||
* See appendix A for the format of this field
|
||||
* Defaults to None
|
||||
|
||||
contextual_searches = [json]
|
||||
* A JSON list of contextual searches that the analytic story applies to.
|
||||
* See appendix A for the format of this field
|
||||
* Defaults to None
|
||||
|
||||
support_searches = [json]
|
||||
* JSON list of support searches that the analytic story applies to.
|
||||
* See appendix A for the format of this field
|
||||
* Defaults to None
|
||||
|
||||
providing_technologies = [json]
|
||||
* A JSON list of example technologies that can be used to capture the data needed for the analytic story
|
||||
* Defaults to None
|
||||
|
||||
version = [int]
|
||||
* An integer indicating which revision of the analytic story this is
|
||||
* This value should start with one and increase for each release
|
||||
|
||||
###### Appendix A: *_searches Specification #######
|
||||
# This can just be a list of saved search names. However, this also supports a hierarchical structure to denote searches that rely on other searches.
|
||||
#
|
||||
# A non-hierarchical version would look like this:
|
||||
# [ "search1", "search2" ]
|
||||
#
|
||||
# A hierarchical version would look like this:
|
||||
#[
|
||||
# "search1": [ "search1a", "search1b" ],
|
||||
# "search2": [ "search2a" ]
|
||||
#]
|
||||
|
||||
|
||||
###### Appendix B: Mappings Specification #######
|
||||
#
|
||||
# This is a dictionary of the different mappings this analytic story maps to.
|
||||
# The mapping will be the key, and the value will be an array of the labels it applies to
|
||||
#
|
||||
# Example:
|
||||
# {
|
||||
# "kill_chain_phase": ["Delivery", "Command and Control"],
|
||||
# "sans cis": ["CIS 9", "CIS 12"],
|
||||
# "att&ck": ["Command and Control"]
|
||||
# }
|
||||
#
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
The Analytic Story Details dashboard renders all the details of the content related to a specific analytic story which
|
||||
can be chose via the drop down
|
||||
|
||||
Each analytic story has attributes associated with it and the following:
|
||||
______________________________________________________________________
|
||||
|
||||
|
||||
Analytic Story: name of the analytic story
|
||||
Description ; description of the analytic story
|
||||
Search Name : The name of the searches belonging to the chosen analytic story
|
||||
Search : The search query which looks for an attack pattern corresponding to the analytic story
|
||||
Search Description: The description of the search query
|
||||
Asset Type: The analytic story specifies what asset in the infrastructure may be compromised
|
||||
Category: The category that the search belongs to (malware, vulnerabilities, best practices, abuse)
|
||||
Kill Chain Phase: The kill chain phase of the attack that the search is after.
|
||||
@@ -0,0 +1,24 @@
|
||||
The ES_SOC Summary Dashboard provides you a summarized view of the analytic story contents of the ES-SOC app.
|
||||
The dashboard has the following panels gives you following details
|
||||
|
||||
1) Analytic story Summary
|
||||
- Total Analytic Stories : The total number of Analytic stories in the ES-SOC application
|
||||
- Total Searches: The total number of searches in ES-SOC
|
||||
- Searches added last week: Number of searches added to ES-SOC in the last week.
|
||||
|
||||
2) Analytic story Category: This dashboard panel summarizes the categories of the searches that the ES-SOC app contains. The categories of the analytic stories are as follow
|
||||
-Malware: These searches detect specific malware behavior for a particular phase of the attack kill chain. E.g. a malware’s delivery method via email or a malware’s installation behavior via registry key changes
|
||||
-Vulnerability: These searches detect behavior or a signature of a vulnerable software in use. These searches are not designed to replace vulnerability management or scanning systems. The purpose of these searches is to discover a vulnerability through side effects or behaviors.
|
||||
-Abuse: Some actions can be deemed malicious because they are unexpected, violate corporate policy or are significantly different than the actions of other users. E.g. A USB disk that is seen on multiple systems or a user that uploads excessive files to a cloud service or a database query that dumps an entire table
|
||||
-Best Practices: Searches that correspond to specific guidelines from organizations like SANS or OWASP
|
||||
|
||||
3) Kill Chain phases: Every analytic story has one or more searches which look for a certain kind of attack pattern/behavior. These searches have an attribute which essentially tells you what Kill chain phase does the search correspond to.
|
||||
The numbers on the dashboard represents the number of searches correponding to each kill chain phase
|
||||
|
||||
4) Analytic story table: This table gives the user a comprehensive view of some of the details of the analytic story. Some of the listed attributes are:
|
||||
- Analytic Story : The name of the analytic story
|
||||
- Description: The description of the analyttic story
|
||||
- Search names: The name of the searches in each analytic story
|
||||
- Datamodels: The name of the datamodel that the search is querying against.
|
||||
- Technology Examples: This field represent some examples related to the technologies required to populate the datamodels(Nessues, Cisco Firewall,etc)
|
||||
- Kill chain phase: The name of the kill chain phase that the search belongs to
|
||||
@@ -0,0 +1,51 @@
|
||||
######################
|
||||
ESSOC Usage Dashboard#
|
||||
######################
|
||||
|
||||
The ESSOC Usage dashboard is designed to provide high-level insight into the usage of the ES-SOC app. It is suitable for display when providing feedback to the Splunk team or for identifying how the ES-SOC app is being used. This dashboard has two time selectors that work independently - the top time selector determines the search time range for all the single-value. And the lower time selector, determines the time range for the usage table.
|
||||
|
||||
IMPORTANT: The user loading this dashboard must have permission to search the _audit index
|
||||
|
||||
##################
|
||||
#Dashboard panels#
|
||||
##################
|
||||
|
||||
Searches Ran
|
||||
|
||||
The total number of searches in ES-SOC that were executed. This number includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax
|
||||
|
||||
Unique Searches
|
||||
|
||||
The unique/distinct searches executed on the deployment. This is equivalent to the distinct count of searches run in the ES-SOC app.
|
||||
|
||||
Most Run
|
||||
|
||||
The total number of searches in ES-SOC that were executed. This number includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Ad hoc Searches
|
||||
|
||||
The total number of searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Scheduled
|
||||
|
||||
The total number of ESSOC searches run that were scheduled.
|
||||
|
||||
Most Active User
|
||||
|
||||
The user who executed the highest number/count of searches. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Search Run Time (seconds)
|
||||
|
||||
Total run time of all searches executed in seconds. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Average Run Time (seconds)
|
||||
|
||||
Average run time of all searches executed in seconds. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Max Run Time (seconds)
|
||||
|
||||
The run time of the longest running search. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Search summary
|
||||
|
||||
This table provides details on each search that was executed in the ESSOC app.
|
||||
@@ -0,0 +1,38 @@
|
||||
[SMTP PII file sent]
|
||||
|
||||
# Alert action stuff
|
||||
action.usecase = 0
|
||||
action.usecase.enabled = 1
|
||||
action.usecase.category = pii_monitoring
|
||||
action.usecase.reference = https://en.wikipedia.org/wiki/Extended_SMTP
|
||||
action.usecase.kill_chain_phases = Weaponization,Delivery
|
||||
action.usecase.search_score = 5
|
||||
action.usecase.confidence = high
|
||||
action.usecase.known_false_positives = Some Unix SMTP servers will use enhanced SMTP commands that can trigger this event even though no malicious activity is present
|
||||
action.usecase.remediation = Block this activity at your email server using magic
|
||||
|
||||
# Normal saved search stuff
|
||||
cron_schedule = */30 * * * *
|
||||
disabled = False
|
||||
dispatch.earliest_time = -24h
|
||||
dispatch.latest_time = +0s
|
||||
enableSched = True
|
||||
is_visible = false
|
||||
search = | `tstats` values(sourcetype) from datamodel=Change_Analysis.All_Changes by All_Changes.Endpoint_Changes.Filesystem_Changes.file_hash | `tstats` append=true values(sourcetype) from datamodel=Email.All_Email by All_Email.file_hash | `tstats` append=true values(sourcetype) from datamodel=Malware.Malware_Attacks by Malware_Attacks.file_hash | `tstats` append=true values(sourcetype) from datamodel=Updates.Updates by Updates.file_hash | rename All_Changes.Endpoint_Changes.Filesystem_Changes.* as *,All_Email.* as *,Malware_Attacks.* as *,Updates.* as * | stats values(sourcetype) as orig_sourcetypes by file_hash | lookup threatintel_by_file_hash file_hash OUTPUT | search threat_collection_key=* | `zipexpand_threat_matches` | `makesv(orig_sourcetypes)`
|
||||
description = This search does PII monitoring
|
||||
|
||||
|
||||
## EXAMPLE EMPTY FIELDS
|
||||
action.usecase = 0
|
||||
action.usecase.enabled = 1
|
||||
action.usecase.category =
|
||||
action.usecase.reference =
|
||||
action.usecase.kill_chain_phases =
|
||||
action.usecase.search_score =
|
||||
action.usecase.confidence =
|
||||
action.usecase.known_false_positives =
|
||||
action.usecase.remediation =
|
||||
search =
|
||||
description =
|
||||
dispatch.earliest_time = -24h@h
|
||||
dispatch.latest_time = 0
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,186 @@
|
||||
/* .rTable {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rTableRow {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.rTableHeading {
|
||||
display: table-header-group;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.rTableCell, .rTableHead {
|
||||
display: table-cell;
|
||||
padding: 3px 10px;
|
||||
//border: 1px solid #999999;
|
||||
}
|
||||
|
||||
.rTableLeftCell {
|
||||
display: table-cell;
|
||||
padding: 3px 10px;
|
||||
//border: 1px solid #999999;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.rTableHeading {
|
||||
display: table-header-group;
|
||||
background-color: #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rTableFoot {
|
||||
display: table-footer-group;
|
||||
font-weight: bold;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.rTableBody {
|
||||
display: table-row-group;
|
||||
} */
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.as_title_attr_bar {
|
||||
padding-left: 15%;
|
||||
background-color: #eee;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.as_title_attr {
|
||||
float: left;
|
||||
margin-right: 5%;
|
||||
padding-right: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.as_search_accordion {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.as_story_details {
|
||||
display: flex;
|
||||
max-height: 500px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.as_story_details_left_col {
|
||||
float: right;
|
||||
width: 34%;
|
||||
display: block;
|
||||
overflow: scroll;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.as_story_details_right_col {
|
||||
float: left;
|
||||
width: 60%;
|
||||
overflow: scroll;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.as_left_attr{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.as_story_detail_left_attr_label {
|
||||
float: left;
|
||||
width: 30%;
|
||||
padding-top: 2%;
|
||||
}
|
||||
|
||||
.as_story_detail_left_attr {
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.value_label {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.as_story_detail_right_attr_label {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.search_content {
|
||||
display: flex;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.search_left_panel {
|
||||
float: left;
|
||||
width: 70%;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.search_right_panel {
|
||||
float: left;
|
||||
width: 25%;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.search_left_attr {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.search_right_attr {
|
||||
margin: 10px;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.search_string{
|
||||
padding: 10px;
|
||||
background-color: #ddd;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.data_model_tag {
|
||||
background-color: #11a88b;
|
||||
}
|
||||
|
||||
.kill_chain_tag {
|
||||
background-color: #ed8440;
|
||||
}
|
||||
|
||||
.attack_tag {
|
||||
background-color: #3863a0;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.heading-story {
|
||||
width: 80%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.run_story_btn {
|
||||
float: right;
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/searchmanager',
|
||||
'splunkjs/mvc/searchbarview',
|
||||
'splunkjs/mvc/tableview',
|
||||
'splunk.util',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/showdown.min',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/jquery-ui/jquery-ui',
|
||||
'css!../app/DA-ESS-ContentUpdate/js/lib/jquery-ui/jquery-ui.css',
|
||||
'css!../app/DA-ESS-ContentUpdate/analytic_story_details.css',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, SearchManager, SearchBarView, TableView, splunkUtil, showdown) {
|
||||
|
||||
let tokenModel = mvc.Components.get("default");
|
||||
let renderedComponents = [];
|
||||
|
||||
let templ = `
|
||||
<div class="as_title_attr_bar">
|
||||
<div class="as_title_attr">
|
||||
<strong>Category: </strong><span id="as_label_category"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Version: </strong><span id="as_label_version"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Created: </strong><span id="as_label_created"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Modified: </strong><span id="as_label_modified"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="headline_story">
|
||||
<div class="heading-story">
|
||||
<h1 id="story_heading"></h1>
|
||||
</div>
|
||||
<div class="run_story_btn">
|
||||
<button class="btn btn-primary run-story">Run Analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_story_details">
|
||||
<div class="as_story_details_right_col">
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<strong>Description: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<span id="description"></span>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<strong>Narrative: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label narrative_value">
|
||||
<span id="narrative"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_story_details_left_col">
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>ATT&CK: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="mitre_attack">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>Kill Chain Phases: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr kill_chain_phases" id="kill_chain_phases">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>CIS Controls: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="cis_20">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>Data Model: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="data_model">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>Technologies: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="technology">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>References: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="references">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_search_details">
|
||||
<h2>
|
||||
Analytic Story Searches
|
||||
</h2>
|
||||
<div id="accordion">
|
||||
<h3>Detection</h3>
|
||||
<div>
|
||||
<div id="search_detection">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Context</h3>
|
||||
<div>
|
||||
<div id="search_contextual">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Investigative</h3>
|
||||
<div>
|
||||
<div id="search_investigative">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Support</h3>
|
||||
<div>
|
||||
<div id="search_support">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#analytic_story_details').html(_.template(templ));
|
||||
|
||||
if (tokenModel.get('analytic_story_name')) {
|
||||
fetchAnalyticStoryDetails(tokenModel.get('analytic_story_name'));
|
||||
}
|
||||
|
||||
tokenModel.on("change:analytic_story_name", function(model, value, options) {
|
||||
fetchAnalyticStoryDetails(value);
|
||||
});
|
||||
|
||||
function fetchAnalyticStoryDetails(asName) {
|
||||
let epoch = (new Date).getTime();
|
||||
let searchGetAnalyticStoryData = new SearchManager({
|
||||
id: epoch,
|
||||
earliest_time: "-1h@h",
|
||||
latest_time: "now",
|
||||
cache: false,
|
||||
search: "| rest /services/configs/conf-analytic_stories splunk_server=local count=0 | search title=\"" + asName + "\" | spath input=providing_technologies path={} output=tex | spath input=reference path={} output=ref | spath input=data_models path={} output=dm | table title, category, description, version, mappings, creation_date, modification_date, dm, narrative, tex, ref"
|
||||
});
|
||||
|
||||
$('.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);
|
||||
});
|
||||
|
||||
let asSearch = splunkjs.mvc.Components.getInstance(epoch);
|
||||
let asResults = asSearch.data("results", {
|
||||
count: 0
|
||||
});
|
||||
asResults.on("data", function() {
|
||||
let as_attributes = {};
|
||||
let fields = asResults.data().fields;
|
||||
let rows = asResults.data().rows;
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
as_attributes[fields[i]] = rows[0][i];
|
||||
}
|
||||
renderStoryAttributes(as_attributes);
|
||||
});
|
||||
|
||||
var searchGetSearchesData = new SearchManager({
|
||||
id: "s" + epoch,
|
||||
earliest_time: "-1h@h",
|
||||
latest_time: "now",
|
||||
cache: false,
|
||||
search: "| rest /services/saved/searches splunk_server=local count=0 | spath input=action.escu.analytic_story path={} output=uc | search uc = \"" + asName + "\" | spath input=action.escu.data_models path={} output=dm | spath input=action.escu.providing_technologies path={} output=tex | table action.escu.full_search_name, search, description, action.escu.search_type, action.escu.how_to_implement, action.escu.eli5, action.escu.version, action.escu.mappings, dm, tex, action.escu.asset_at_risk, action.escu.confidence, action.escu.known_false_positives, updated, action.escu.modification_date, action.escu.creation_date "
|
||||
|
||||
});
|
||||
var searchesSearch = splunkjs.mvc.Components.getInstance("s" + epoch);
|
||||
var searchesResults = searchesSearch.data("results", {
|
||||
count: 0
|
||||
});
|
||||
|
||||
|
||||
searchesResults.on("data", function() {
|
||||
let asSearchAttr = [];
|
||||
var fields = searchesResults.data().fields;
|
||||
var rows = searchesResults.data().rows;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let searchObj = {};
|
||||
for (let j = 0; j < fields.length; j++) {
|
||||
searchObj[fields[j]] = rows[i][j];
|
||||
}
|
||||
asSearchAttr.push(searchObj);
|
||||
}
|
||||
renderSearches(asSearchAttr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStoryAttributes(asAttributes) {
|
||||
let converter = new showdown.Converter();
|
||||
let mappings = JSON.parse(asAttributes.mappings);
|
||||
$('#as_label_category').html(asAttributes.category);
|
||||
$('#as_label_version').html(asAttributes.version);
|
||||
$('#as_label_created').html(asAttributes.creation_date);
|
||||
$('#as_label_modified').html(asAttributes.modification_date);
|
||||
$('#story_heading').html(asAttributes.title);
|
||||
$('#attack').html(mappings.mitre_attack);
|
||||
$('#narrative').html(converter.makeHtml(asAttributes.narrative));
|
||||
$('#description').html(converter.makeHtml(asAttributes.description));
|
||||
$('#mitre_attack').html(getValueLabels(mappings.mitre_attack, 'attack_tag'));
|
||||
$('#data_model').html(getValueLabels(asAttributes.dm, 'data_model_tag'));
|
||||
$('#technology').html(getValueLabels(asAttributes.tex));
|
||||
$('#kill_chain_phases').html(getValueLabels(mappings.kill_chain_phases, 'kill_chain_tag'));
|
||||
$('#cis_20').html(getValueLabels(mappings.cis20));
|
||||
$('#references').html(getReferenceURLS(asAttributes.ref));
|
||||
}
|
||||
|
||||
function getReferenceURLS(refs) {
|
||||
if (refs === null) {
|
||||
return " ";
|
||||
} else {
|
||||
let refsResult = ``;
|
||||
if (Array.isArray(refs)) {
|
||||
refs.map(ref => {
|
||||
refsResult = refsResult + `<a href="${ ref }">${ ref }</a><br />`;
|
||||
});
|
||||
} else {
|
||||
refsResult = refsResult + `<a href="${ refs }">${ refs }</a><br />`
|
||||
}
|
||||
|
||||
return refsResult;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearches(asSearches) {
|
||||
clearSearchView();
|
||||
let i = 0;
|
||||
let converter = new showdown.Converter();
|
||||
asSearches.forEach(search => {
|
||||
i++;
|
||||
let epoch = (new Date).getTime();
|
||||
let searchID = `#search${ i }`;
|
||||
let resultID = `#result${ i }`;
|
||||
let searchSelector = `search${ i }`;
|
||||
let controlID = `as_search${ i }`
|
||||
let resultsControlID = `as_results_search${ i }`;
|
||||
let btnID = `btn_es_${i}`;
|
||||
|
||||
let searchPanel = `
|
||||
<h3>${ search['action.escu.full_search_name'] }</h3>
|
||||
<div class="search_content" id="${searchSelector}-content">
|
||||
<div class="search_left_panel">
|
||||
<button class="configure_in_es btn btn-primary" id="${ btnID }" data-search-type="${search['action.escu.search_type']}" data-search-name="${ search['action.escu.full_search_name'] }">Configure</button>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Description</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['description']) }
|
||||
</div>
|
||||
</div>
|
||||
<div id="${searchSelector}-eli5">
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value ${ controlID }">
|
||||
</div>
|
||||
<div class="search_left_attr_value ${ resultsControlID }">
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>How to Implement</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.how_to_implement']) }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Known False Positives</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.known_false_positives']) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_panel">
|
||||
<div class="search_right_attr data_model_srch_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Data Models</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(search['dm'], 'data_model_tag') }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Technologies</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(search['tex']) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (search['action.escu.search_type'] === "support") {
|
||||
//Process Support Search Accordion
|
||||
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_support').append(searchPanel);
|
||||
|
||||
// Adding extra params to support search
|
||||
let supportLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
$(`#${searchSelector}-eli5`).append(supportLeftAttr);
|
||||
|
||||
} else if (search['action.escu.search_type'] === "detection") {
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_detection').append(searchPanel);
|
||||
// Adding extra params to detection search
|
||||
let detectionAttrTop = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>ATT&CK</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.mitre_attack, 'attack_tag') }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Kill Chain Phases</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.kill_chain_phases, 'kill_chain_tag') }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>CIS Controls</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.cis20) }
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let detectionAttrBottom = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Asset at Risk</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.asset_at_risk'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Confidence</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.confidence'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Creation Date</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.creation_date'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Modification Date</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.modification_date'] }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let detectionLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
$(detectionAttrTop).insertBefore($(`#${searchSelector}-content`).find('.data_model_srch_attr'));
|
||||
$(`#${searchSelector}-content`).find('.search_right_panel').append(detectionAttrBottom);
|
||||
$(`#${searchSelector}-eli5`).append(detectionLeftAttr);
|
||||
} else if (search['action.escu.search_type'] === "contextual") {
|
||||
//Process contextual Search Accordion
|
||||
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_contextual').append(searchPanel);
|
||||
|
||||
|
||||
// Adding extra params to contextual search
|
||||
let contextualLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
$(`#${searchSelector}-eli5`).append(contextualLeftAttr);
|
||||
|
||||
} else if (search['action.escu.search_type'] === "investigative") {
|
||||
//Process Investigative Search Accordion
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_investigative').append(searchPanel);
|
||||
|
||||
// Adding extra params to investigative search
|
||||
let investigativeLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
$(`#${searchSelector}-eli5`).append(investigativeLeftAttr);
|
||||
}
|
||||
|
||||
/*
|
||||
let updatedAttr = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Last Updated</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['updated'] }
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$(`#${searchSelector}-content`).find('.search_right_panel').append(updatedAttr);
|
||||
*/
|
||||
|
||||
$(`#${ btnID }`).on('click', (evt) => {
|
||||
console.log($(evt.target).data("searchType"));
|
||||
if ($(evt.target).data("searchType") === "detection") {
|
||||
splunkUtil.redirect_to('app/SplunkEnterpriseSecuritySuite/correlation_search_edit', {
|
||||
search: `${$(evt.target).data("searchName")}`
|
||||
}, window.open(), true);
|
||||
} else {
|
||||
splunkUtil.redirect_to(`manager/DA-ESS-ContentUpdate/saved/searches`, {
|
||||
search: `${$(evt.target).data("searchName")}`
|
||||
}, window.open(), true);
|
||||
}
|
||||
})
|
||||
|
||||
let searchManagerID = search['action.escu.full_search_name'].split(' ').join('');
|
||||
|
||||
let searchManager = new SearchManager({
|
||||
id: searchManagerID,
|
||||
earliest_time: "-24h@h",
|
||||
latest_time: "now",
|
||||
status_buckets: 300,
|
||||
required_field_list: "*",
|
||||
preview: true,
|
||||
cache: true,
|
||||
autostart: false, // Prevent the search from running automatically
|
||||
search: search['search'],
|
||||
});
|
||||
|
||||
let searchBar = new SearchBarView({
|
||||
id: searchID,
|
||||
managerId: searchManagerID,
|
||||
timerange: true,
|
||||
el: $('.' + controlID),
|
||||
value: search['search'],
|
||||
timerange_preset: "Last 24 hours"
|
||||
}).render();
|
||||
|
||||
let tableviewer = new TableView({
|
||||
id: resultsControlID,
|
||||
managerid: searchManagerID,
|
||||
pageSize: 5,
|
||||
el: $("." + resultsControlID)
|
||||
}).render();
|
||||
|
||||
searchBar.on("change", function() {
|
||||
searchManager.settings.unset("search");
|
||||
|
||||
// Update the search query
|
||||
searchManager.settings.set("search", searchBar.val());
|
||||
|
||||
// Run the search (because autostart=false)
|
||||
searchManager.startSearch();
|
||||
});
|
||||
|
||||
searchBar.timerange.on("change", function() {
|
||||
// Update the time range of the search
|
||||
searchManager.search.set(searchBar.timerange.val());
|
||||
|
||||
// Run the search (because autostart=false)
|
||||
searchManager.startSearch();
|
||||
})
|
||||
|
||||
|
||||
renderedComponents.push(searchID, searchManagerID, resultsControlID);
|
||||
|
||||
});
|
||||
|
||||
$('#accordion').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_support').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_detection').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_contextual').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_investigative').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
}
|
||||
|
||||
function clearSearchView() {
|
||||
if ($('#accordion').hasClass('ui-accordion')) {
|
||||
$('#accordion').accordion('destroy');
|
||||
}
|
||||
|
||||
if ($('#search_support').hasClass('ui-accordion')) {
|
||||
$('#search_support').accordion('destroy');
|
||||
$('#search_support').empty();
|
||||
}
|
||||
|
||||
if ($('#search_detection').hasClass('ui-accordion')) {
|
||||
$('#search_detection').accordion('destroy');
|
||||
$('#search_detection').empty();
|
||||
}
|
||||
|
||||
if ($('#search_contextual').hasClass('ui-accordion')) {
|
||||
$('#search_contextual').accordion('destroy');
|
||||
$('#search_contextual').empty();
|
||||
}
|
||||
|
||||
if ($('#search_investigative').hasClass('ui-accordion')) {
|
||||
$('#search_investigative').accordion('destroy');
|
||||
$('#search_investigative').empty();
|
||||
}
|
||||
|
||||
$('.configure_in_es').unbind("click");
|
||||
|
||||
let len = renderedComponents.length;
|
||||
while (len--) {
|
||||
let id = renderedComponents.pop();
|
||||
mvc.Components.getInstance(id).dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function getValueLabels(values, className) {
|
||||
let cls = "";
|
||||
if (className !== undefined || className) {
|
||||
cls = className;
|
||||
}
|
||||
let valueArray = [];
|
||||
if (values) {
|
||||
if (typeof values === "string") {
|
||||
valueArray.push(values)
|
||||
} else {
|
||||
valueArray = values;
|
||||
}
|
||||
}
|
||||
let htmlTmpl = "";
|
||||
valueArray.forEach(val => {
|
||||
htmlTmpl += `<div class="value_label ${ cls }">${ val }</div> `
|
||||
});
|
||||
|
||||
return htmlTmpl;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
.btn-pill {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.killchain-phases {
|
||||
width: 100%;
|
||||
margin-left: 6%;
|
||||
}
|
||||
|
||||
.killchain_card {
|
||||
width: 14%;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.killchain {
|
||||
height: 55px;
|
||||
line-height: 55px;
|
||||
-webkit-clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
|
||||
clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
|
||||
}
|
||||
|
||||
.killchain-text {
|
||||
font-size: 300%;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
|
||||
.killchain-label {
|
||||
margin-top: 7px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-left: 10%;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.killchain-text-one-line {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-family: system-ui;
|
||||
white-space: wrap;
|
||||
width: 100%;
|
||||
padding-top: 7%;
|
||||
padding-left: 26%;
|
||||
}
|
||||
|
||||
.killchain-text-second {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-family: system-ui;
|
||||
margin-top: -10px;
|
||||
padding-left: 26%;
|
||||
}
|
||||
|
||||
.killchain-phases {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notfirst {
|
||||
margin-left: -2%;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/searchmanager',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/tabs',
|
||||
'css!../app/DA-ESS-ContentUpdate/js/lib/tabs.css',
|
||||
'css!../app/DA-ESS-ContentUpdate/escu_summary.css',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, SearchManager) {
|
||||
$('.es-soc-analytic-story-stats').html(_.template('<%- _("Analytic Story Summary").t() %>'));
|
||||
$('.es-soc-search-stats').html(_.template('<%- _("Search Summary").t() %>'));
|
||||
|
||||
const tokenModel = mvc.Components.get('default');
|
||||
const submittedTokens = mvc.Components.get('submitted');
|
||||
|
||||
$.ajax({
|
||||
url: Splunk.util.make_url('/splunkd/__raw/servicesNS/nobody/DA-ESS-ContentUpdate/apps/local'),
|
||||
type: 'GET',
|
||||
async: true,
|
||||
data: {
|
||||
output_mode: 'json',
|
||||
count: -1,
|
||||
},
|
||||
}).done(result => {
|
||||
if (result.entry) {
|
||||
const foundEss = result.entry.find(app => app.name === 'SplunkEnterpriseSecuritySuite');
|
||||
if (foundEss.content.version === "5.2.0") {
|
||||
submittedTokens.set('explore-use-case-es-show', 'true');
|
||||
const use_case_library_link = Splunk.util.make_url('app/SplunkEnterpriseSecuritySuite/ess_use_case_library');
|
||||
const template = `<div class="alert alert-info"><i class="icon-alert" />
|
||||
${ _('Εxplore ESCU content updates directly from the Use Case Library within ES. To explore it, click').t() }
|
||||
<a href="<%- use_case_library_link %>"> ${ _('here').t() }</a>.
|
||||
</div>`;
|
||||
$('#explore-use-case-es-info').html(_.template(template, { use_case_library_link: use_case_library_link }));
|
||||
}
|
||||
}
|
||||
}).fail(err => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// searchQuery -
|
||||
let kcpSearch = new SearchManager({
|
||||
id: "kcpSearch",
|
||||
preview: true,
|
||||
cache: true,
|
||||
status_buckets: 300,
|
||||
earliest_time: '-24h@h',
|
||||
latest_time: 'now',
|
||||
search: '| rest /services/configs/conf-analytic_stories splunk_server=local count=0 | spath input=mappings path=kill_chain_phases{} output=kcp | stats count by kcp',
|
||||
});
|
||||
|
||||
let results = kcpSearch.data("preview");
|
||||
|
||||
results.on("data", function() {
|
||||
results.data().rows.forEach(row => {
|
||||
let killchainID = '#' + row[0].toLowerCase().replace(/ /g,'');
|
||||
$(killchainID).html(row[1]);
|
||||
});
|
||||
});
|
||||
|
||||
$('#analytic_filter_clear').on('click', function() {
|
||||
tokenModel.set('form.as_cis', '*');
|
||||
tokenModel.set('form.as_category', '*');
|
||||
tokenModel.set('form.as_kill_chain_phase', '*');
|
||||
tokenModel.set('form.as_mitre_attack', '*');
|
||||
tokenModel.set('form.as_data_models', '*');
|
||||
});
|
||||
|
||||
$('#search_filter_clear').on('click', function() {
|
||||
tokenModel.set('form.cis', '*');
|
||||
tokenModel.set('form.searchtype', '*');
|
||||
tokenModel.set('form.kill_chain_phase', '*');
|
||||
tokenModel.set('form.mitre_attack', '*');
|
||||
tokenModel.set('form.data_models', '*');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, TableView) {
|
||||
|
||||
var defaultTokenSpace = mvc.Components.getInstance('default');
|
||||
|
||||
// This will take every textarea that has a data-token attribute and will make the given token with the value of the textarea
|
||||
$('textarea[data-token]').each(function (textarea) {
|
||||
$(this).on('input', function(input) {
|
||||
var token_to_set = $(this).data('token');
|
||||
defaultTokenSpace.set(token_to_set, $(this).val());
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 215 KiB |
@@ -0,0 +1,211 @@
|
||||
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
|
||||
@@ -0,0 +1,508 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
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'
|
||||
@@ -0,0 +1,77 @@
|
||||
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()
|
||||
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
@@ -0,0 +1,48 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
from .loader import get_loader_by_version
|
||||
@@ -0,0 +1,300 @@
|
||||
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))
|
||||
)
|
||||
@@ -0,0 +1,344 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
from .engine import CloudConnectEngine
|
||||
from .exceptions import ConfigException, HTTPError
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
`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)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""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
|
||||
@@ -0,0 +1,327 @@
|
||||
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. Refer to
|
||||
https://confluence.splunk.com/display/PROD/CC+1.0+-+Detail+Design"""
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
"""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
|
||||
@@ -0,0 +1,336 @@
|
||||
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)
|
||||
@@ -0,0 +1,234 @@
|
||||
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)
|
||||
@@ -0,0 +1,277 @@
|
||||
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)
|
||||
@@ -0,0 +1,14 @@
|
||||
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)
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
@@ -0,0 +1,71 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
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'
|
||||
@@ -0,0 +1,367 @@
|
||||
"""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
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "1.0.2"
|
||||
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
@@ -0,0 +1,54 @@
|
||||
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"
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,154 @@
|
||||
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
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
@@ -0,0 +1,2 @@
|
||||
__version__ = "0.9"
|
||||
__license__ = "Splunk"
|
||||
@@ -0,0 +1 @@
|
||||
util_log = "util"
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
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]
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
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("]]>", "]]>")
|
||||
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"), "")
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,142 @@
|
||||
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)
|
||||
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,152 @@
|
||||
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)
|
||||
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
@@ -0,0 +1,78 @@
|
||||
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
|
||||
@@ -0,0 +1,44 @@
|
||||
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)
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,84 @@
|
||||
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()))
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
@@ -0,0 +1,202 @@
|
||||
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
|
||||
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
@@ -0,0 +1,65 @@
|
||||
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())
|
||||
@@ -0,0 +1,122 @@
|
||||
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
|
||||
@@ -0,0 +1,90 @@
|
||||
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
|
||||
@@ -0,0 +1,143 @@
|
||||
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()
|
||||
@@ -0,0 +1,5 @@
|
||||
[global]
|
||||
process_size = 0
|
||||
thread_min_size = 4
|
||||
thread_max_size = 128
|
||||
task_queue_size = 1024
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
@@ -0,0 +1,104 @@
|
||||
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
|
||||
@@ -0,0 +1,264 @@
|
||||
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]
|
||||
@@ -0,0 +1,60 @@
|
||||
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
|
||||