From 220e1432e71ffc5095bdac5513b288cbcd1cfb58 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Fri, 22 Oct 2021 13:17:17 -0700 Subject: [PATCH 01/39] Rework searchcommands_app to target only protocol v2, python3, and use docker volumes to include latest splunklib --- .gitignore | 4 +- docker-compose.yml | 6 + .../package/default/commands-scpv1.conf | 62 --------- .../{commands-scpv2.conf => commands.conf} | 6 + examples/twitted/twitted/metadata/local.meta | 129 ------------------ 5 files changed, 14 insertions(+), 193 deletions(-) delete mode 100644 examples/searchcommands_app/package/default/commands-scpv1.conf rename examples/searchcommands_app/package/default/{commands-scpv2.conf => commands.conf} (74%) delete mode 100644 examples/twitted/twitted/metadata/local.meta diff --git a/.gitignore b/.gitignore index 3df44b515..2346d353a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ MANIFEST coverage_report test.log examples/*/local -examples/*/metadata/local.meta +examples/**/local.meta +examples/**/*.log tests/searchcommands_data/log/ tests/searchcommands_data/output/ examples/searchcommands_app/searchcommand_app.log @@ -24,7 +25,6 @@ Test Results*.html tests/searchcommands/data/app/app.log splunk_sdk.egg-info/ dist/ -examples/searchcommands_app/package/default/commands.conf examples/searchcommands_app/package/lib/splunklib tests/searchcommands/apps/app_with_logging_configuration/*.log *.observed diff --git a/docker-compose.yml b/docker-compose.yml index 0527a30bd..a93a14c0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,12 @@ services: - SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113 - SPLUNK_PASSWORD=changed! - SPLUNK_APPS_URL=https://github.com/splunk/sdk-app-collection/releases/download/v1.1.0/sdkappcollection.tgz + volumes: + - ./examples/github_forks:/opt/splunk/etc/apps/github_forks + - ./examples/random_numbers:/opt/splunk/etc/apps/random_numbers + - ./examples/searchcommands_app/package:/opt/splunk/etc/apps/searchcommands_app + - ./splunklib:/opt/splunk/etc/apps/searchcommands_app/lib/splunklib + - ./examples/twitted/twitted:/opt/splunk/etc/apps/twitted ports: - 8000:8000 - 8088:8088 diff --git a/examples/searchcommands_app/package/default/commands-scpv1.conf b/examples/searchcommands_app/package/default/commands-scpv1.conf deleted file mode 100644 index 8c3408a86..000000000 --- a/examples/searchcommands_app/package/default/commands-scpv1.conf +++ /dev/null @@ -1,62 +0,0 @@ -# [commands.conf]($SPLUNK_HOME/etc/system/README/commands.conf.spec) -# Configuration for Search Commands Protocol version 1 - -[countmatches] -filename = countmatches.py -enableheader = true -outputheader = true -requires_srinfo = true -stderr_dest = message -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true - -[filter] -filename = filter.py -enableheader = true -outputheader = true -requires_srinfo = true -stderr_dest = message -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true - -[generatetext] -filename = generatetext.py -enableheader = true -outputheader = true -requires_srinfo = true -stderr_dest = message -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true - -[pypygeneratetext] -filename = pypygeneratetext.py -enableheader = true -outputheader = true -requires_srinfo = true -stderr_dest = message -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true - -[simulate] -filename = simulate.py -enableheader = true -outputheader = true -stderr_dest = message -requires_srinfo = true -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true - -[sum] -filename = sum.py -enableheader = true -outputheader = true -requires_srinfo = true -stderr_dest = message -supports_getinfo = true -supports_rawargs = true -supports_multivalues = true diff --git a/examples/searchcommands_app/package/default/commands-scpv2.conf b/examples/searchcommands_app/package/default/commands.conf similarity index 74% rename from examples/searchcommands_app/package/default/commands-scpv2.conf rename to examples/searchcommands_app/package/default/commands.conf index 7aa773abf..2a5110dc0 100644 --- a/examples/searchcommands_app/package/default/commands-scpv2.conf +++ b/examples/searchcommands_app/package/default/commands.conf @@ -4,23 +4,29 @@ [countmatches] filename = countmatches.py chunked = true +python.version = python3 [filter] filename = filter.py chunked = true +python.version = python3 [generatetext] filename = generatetext.py chunked = true +python.version = python3 [pypygeneratetext] filename = pypygeneratetext.py chunked = true +python.version = python3 [simulate] filename = simulate.py chunked = true +python.version = python3 [sum] filename = sum.py chunked = true +python.version = python3 diff --git a/examples/twitted/twitted/metadata/local.meta b/examples/twitted/twitted/metadata/local.meta deleted file mode 100644 index 489fe41a2..000000000 --- a/examples/twitted/twitted/metadata/local.meta +++ /dev/null @@ -1,129 +0,0 @@ -[app/ui] -owner = admin -version = 20110524 - -[app/launcher] -owner = admin -version = 20110524 - -[viewstates/flashtimeline%3Agog49lc6] -access = read : [ * ] -owner = nobody -version = 20110524 - -[savedsearches/Top%20Sources] -owner = admin -version = 20110524 - -[savedsearches/Top%20Words] -owner = admin -version = 20110524 - -[savedsearches/Statuses,%20verified] -owner = admin -version = 20110524 - -[savedsearches/Statuses,%20enriched] -owner = admin -version = 20110524 - -[savedsearches/Statuses] -owner = admin -version = 20110524 - -[savedsearches/Users,%20most%20followers] -owner = admin -version = 20110524 - -[savedsearches/Users,%20most%20tweets] -owner = admin -version = 20110524 - -[savedsearches/Users,%20verified,%20most%20tweets] -owner = admin -version = 20110524 - -[savedsearches/Users,%20verified,%20most%20followers] -owner = admin -version = 20110524 - -[savedsearches/Users,%20most%20seen%20tweets] -owner = admin -version = 20110524 - -[viewstates/flashtimeline%3Agopz0n46] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Statuses%2C%20most%20retweeted] -owner = admin -version = 4.3 - -[viewstates/flashtimeline%3Agot9p0bd] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Users%2C%20most%20deletes] -owner = admin -version = 4.3 - -[viewstates/flashtimeline%3Agoxlionw] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Statuses%2C%20real-time] -owner = admin -version = 4.3 - -[viewstates/flashtimeline%3Agp1rbo5g] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Top%20Words%2C%20version%202] -owner = admin -version = 4.3 - -[viewstates/flashtimeline%3Agp3htyye] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Most%20mentioned] -owner = admin -version = 4.3 - -[viewstates/flashtimeline%3Agp3hzuqr] -access = read : [ * ] -owner = nobody -version = 4.3 - -[savedsearches/Popular%20hashtags] -owner = admin -version = 4.3 - -[indexes/twitter] -owner = itay -version = 4.2.2 - -[viewstates/flashtimeline%3Agpsrhije] -access = read : [ * ] -owner = nobody -version = 4.2.2 - -[savedsearches/Top%20Tags] -owner = itay -version = 4.2.2 - -[] -access = read : [ * ], write : [ admin, power ] -export = none -version = 4.2.2 - -[inputs/tcp%3A%2F%2F9002] -owner = itay -version = 4.2.2 - From 85db94dc51d3f11b4a84bdfcd42887bf9bfe61ac Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Fri, 22 Oct 2021 15:25:58 -0700 Subject: [PATCH 02/39] Fix up searchcommands and add example searches and results --- examples/searchcommands_app/README.md | 87 ++++++++++++++----- .../searchcommands_app/package/bin/filter.py | 2 +- .../package/bin/pypygeneratetext.py | 78 ----------------- .../package/bin/simulate.py | 24 ++--- .../package/default/commands.conf | 5 -- .../package/default/searchbnf.conf | 23 +---- examples/searchcommands_app/setup.py | 1 - .../searchcommands/test_searchcommands_app.py | 20 ----- 8 files changed, 73 insertions(+), 167 deletions(-) delete mode 100755 examples/searchcommands_app/package/bin/pypygeneratetext.py diff --git a/examples/searchcommands_app/README.md b/examples/searchcommands_app/README.md index ac6d1be09..075253134 100644 --- a/examples/searchcommands_app/README.md +++ b/examples/searchcommands_app/README.md @@ -7,7 +7,6 @@ This app provides several examples of custom search commands that illustrate eac :---------------- |:-----------|:------------------------------------------------------------------------------------------- countmatches | Streaming | Counts the number of non-overlapping matches to a regular expression in a set of fields. generatetext | Generating | Generates a specified number of events containing a specified text string. - pypygeneratetext | Generating | Runs generatetext with the string 'PyPy'. simulate | Generating | Generates a sequence of events drawn from a csv file using repeated random sampling with replacement. generatehello | Generating | Generates a specified number of events containing the text string 'hello'. sum | Reporting | Adds all of the numbers in a set of fields. @@ -19,12 +18,10 @@ The app is tested on Splunk 5 and 6. Here is its manifest: ├── bin │   ├── countmatches.py .......... CountMatchesCommand implementation │ ├── generatetext.py .......... GenerateTextCommand implementation -│ ├── pypygeneratetext.py ...... Runs generatetext.py with PyPy │ ├── simulate.py .............. SimulateCommand implementation │ └── sum.py ................... SumCommand implementation ├── lib -| └── splunklib -│ └── searchcommands ....... splunklib.searchcommands module +| └── splunklib ................ splunklib module ├── default │ ├── data │ │   └── ui @@ -35,41 +32,83 @@ The app is tested on Splunk 5 and 6. Here is its manifest: │ ├── logging.conf ............. Python logging[3] configuration in ConfigParser[4] format │ └── searchbnf.conf ........... Search assistant configuration [5] └── metadata - └── local.meta ............... Permits the search assistant to use searchbnf.conf[6] + └── default.meta ............. Permits the search assistant to use searchbnf.conf[6] ``` **References** -[1] [app.conf](http://docs.splunk.com/Documentation/Splunk/latest/Admin/Appconf app.conf) -[2] [commands.conf](http://docs.splunk.com/Documentation/Splunk/latest/Admin/Commandsconf) -[3] [Python Logging HOWTO](http://docs.python.org/2/howto/logging.html) -[4] [ConfigParser—Configuration file parser](http://docs.python.org/2/library/configparser.html) -[5] [searchbnf.conf](http://docs.splunk.com/Documentation/Splunk/latest/admin/Searchbnfconf) -[6] [Set permissions in the file system](http://docs.splunk.com/Documentation/Splunk/latest/AdvancedDev/SetPermissions#Set_permissions_in_the_filesystem) +[1] [app.conf](https://docs.splunk.com/Documentation/Splunk/latest/Admin/Appconf app.conf) +[2] [commands.conf](https://docs.splunk.com/Documentation/Splunk/latest/Admin/Commandsconf) +[3] [Python Logging HOWTO](https://docs.python.org/2/howto/logging.html) +[4] [ConfigParser—Configuration file parser](https://docs.python.org/2/library/configparser.html) +[5] [searchbnf.conf](https://docs.splunk.com/Documentation/Splunk/latest/admin/Searchbnfconf) +[6] [Set permissions in the file system](https://docs.splunk.com/Documentation/Splunk/latest/AdvancedDev/SetPermissions#Set_permissions_in_the_filesystem) ## Installation -+ Link the app to $SPLUNK_HOME/etc/apps/searchcommands_app by running this command: ++ Bring up Dockerized Splunk with the app installed from the root of this repository via: ``` - ./setup.py link --scp-version {1|2} + SPLUNK_VERSION=latest docker compose up -d ``` -+ Or build a tarball to install on any Splunk instance by running this command: ++ When the `splunk` service is healthy (`health: starting` -> `healthy`) login and run test searches within the app via http://localhost:8000/en-US/app/searchcommands_app/search - ``` - ./setup.py build --scp-version {1|2} - ``` +### Example searches - The tarball is build as build/searchcommands_app-1.5.0-private.tar.gz. - -+ Then (re)start Splunk so that the app is recognized. +#### countmatches +``` +| inputlookup tweets | countmatches fieldname=word_count pattern="\\w+" text +``` +Results: +text | word_count +:----|:---| +excellent review my friend loved it yours always guppyman @GGreeny62... http://t.co/fcvq7NDHxl | 14 +Tú novia te ama mucho | 5 +... | -## Dashboards and Searches +#### filter +``` +| generatetext text="Hello world! How the heck are you?" count=6 \ +| filter predicate="(int(_serial) & 1) == 0" update="_raw = _raw.replace('world', 'Splunk')" +``` +Results: +Event | +:-----| +2. Hello Splunk! How the heck are you? | +4. Hello Splunk! How the heck are you? | +6. Hello Splunk! How the heck are you? | -+ TODO: Add saved search(es) for each example. +#### generatetext +``` +| generatetext count=3 text="Hello there" +``` +Results: +Event | +:-----| +1. Hello there | +2. Hello there | +3. Hello there | -### Searches +#### simulate +``` +| simulate csv="/opt/splunk/etc/apps/searchcommands_app/data/population.csv" rate=10 interval=00:00:01 duration=00:00:02 seed=9 +``` +Results: +Event | +:-----| +text = Margarita (8) | +text = RT @Habibies: When you were born, you cried and the world rejoiced. Live your life so that when you die, the world will cry and you will re... | +text = @dudaribeiro_13 q engraçado em. | -+ TODO: Describe saved searches. +#### sum +``` +| inputlookup tweets +| countmatches fieldname=word_count pattern="\\w+" text +| sum total=word_counts word_count +``` +Results: +word_counts | +:-----| +4497.0 | ## License diff --git a/examples/searchcommands_app/package/bin/filter.py b/examples/searchcommands_app/package/bin/filter.py index f85615c17..3a29ca9b2 100755 --- a/examples/searchcommands_app/package/bin/filter.py +++ b/examples/searchcommands_app/package/bin/filter.py @@ -49,7 +49,7 @@ class FilterCommand(EventingCommand): .. code-block:: | generatetext text="Hello world! How the heck are you?" count=6 - | filter predicate="(long(_serial) & 1) == 0" update="_raw = _raw.replace('world', 'Splunk')" + | filter predicate="(int(_serial) & 1) == 0" update="_raw = _raw.replace('world', 'Splunk')" """ predicate = Option(doc=''' diff --git a/examples/searchcommands_app/package/bin/pypygeneratetext.py b/examples/searchcommands_app/package/bin/pypygeneratetext.py deleted file mode 100755 index adf2c3b35..000000000 --- a/examples/searchcommands_app/package/bin/pypygeneratetext.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 -# -# Copyright 2011-2015 Splunk, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"): you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Requirements: -# 1. PyPy is on splunkd's path. -# Ensure this by performing these operating system-dependent tasks: -# -# CentOS -# ------ -# Create or update /etc/sysconfig/splunk with a line that looks like this: -# -# 1 PATH=$PATH:/opt/pypy/bin -# -# P1 [ ] TODO: Verify that the instructions for putting PyPy on Splunk's PATH on CentOS work -# -# OS X -# ---- -# Edit /Library/LaunchAgents/com.splunk.plist and ensure that it looks like this: -# -# 1 -# 2 -# 3 -# 4 -# 5 Label -# 6 com.splunk -# 7 ProgramArguments -# 8 -# 9 /Users/david-noble/Workspace/Splunk/bin/splunk -# 10 start -# 11 --no-prompt -# 12 --answer-yes -# 13 -# 14 RunAtLoad -# 15 -# 16 EnvironmentVariables -# 17 -# 18 PATH -# 19 /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/local/bin -# 20 -# 21 -# 22 -# -# Note lines 16-20 which extend PATH to include /opt/local/bin, the directory that the pypy executable is typically -# placed. -# -# Windows -# ------- -# Ensure that pypy.exe is on the system-wide Path environment variable. - -from __future__ import absolute_import, division, print_function, unicode_literals -import app -import sys -from os import environ, path - -splunkhome = environ['SPLUNK_HOME'] -sys.path.append(path.join(splunkhome, 'etc', 'apps', 'searchcommands_app', 'lib')) -from splunklib.searchcommands import app_root, execute - -pypy_argv = ['pypy', path.join(app_root, 'bin', 'generatetext.py')] + sys.argv[1:] -pypy_environ = dict(environ) -pypy_environ.pop('PYTHONPATH', None) # On Windows Splunk is a 64-bit service, but pypy is a 32-bit program -pypy_environ.pop('DYLD_LIBRARY_PATH', None) # On *nix Splunk includes shared objects that are incompatible with pypy - -execute('pypy', pypy_argv, pypy_environ) diff --git a/examples/searchcommands_app/package/bin/simulate.py b/examples/searchcommands_app/package/bin/simulate.py index 31d68423f..db223c71b 100755 --- a/examples/searchcommands_app/package/bin/simulate.py +++ b/examples/searchcommands_app/package/bin/simulate.py @@ -47,9 +47,7 @@ class SimulateCommand(GeneratingCommand): ##Example .. code-block:: - | simulate csv=population.csv rate=50 interval=00:00:01 - duration=00:00:05 | countmatches fieldname=word_count - pattern="\\w+" text | stats mean(word_count) stdev(word_count) + | simulate csv="/opt/splunk/etc/apps/searchcommands_app/data/population.csv" rate=10 interval=00:00:01 duration=00:00:02 seed=1 This example generates events drawn from repeated random sampling of events from :code:`tweets.csv`. Events are drawn at an average rate of 50 per second for a duration of 5 seconds. Events are piped to the example @@ -85,28 +83,20 @@ class SimulateCommand(GeneratingCommand): **Description:** Value for initializing the random number generator ''') def generate(self): - - if not self.records: - if self.seed is not None: - random.seed(self.seed) - self.records = [record for record in csv.DictReader(self.csv_file)] - self.lambda_value = 1.0 / (self.rate / float(self.interval)) + if self.seed is not None: + random.seed(self.seed) + records = [record for record in csv.DictReader(self.csv_file)] + lambda_value = 1.0 / (self.rate / float(self.interval)) duration = self.duration - while duration > 0: - count = int(round(random.expovariate(self.lambda_value))) + count = int(round(random.expovariate(lambda_value))) start_time = time.clock() - for record in random.sample(self.records, count): + for record in random.sample(records, count): yield record interval = time.clock() - start_time if interval < self.interval: time.sleep(self.interval - interval) duration -= max(interval, self.interval) - def __init__(self): - super(SimulateCommand, self).__init__() - self.lambda_value = None - self.records = None - dispatch(SimulateCommand, sys.argv, sys.stdin, sys.stdout, __name__) diff --git a/examples/searchcommands_app/package/default/commands.conf b/examples/searchcommands_app/package/default/commands.conf index 2a5110dc0..4ef41c556 100644 --- a/examples/searchcommands_app/package/default/commands.conf +++ b/examples/searchcommands_app/package/default/commands.conf @@ -16,11 +16,6 @@ filename = generatetext.py chunked = true python.version = python3 -[pypygeneratetext] -filename = pypygeneratetext.py -chunked = true -python.version = python3 - [simulate] filename = simulate.py chunked = true diff --git a/examples/searchcommands_app/package/default/searchbnf.conf b/examples/searchcommands_app/package/default/searchbnf.conf index 059815803..8254f027d 100644 --- a/examples/searchcommands_app/package/default/searchbnf.conf +++ b/examples/searchcommands_app/package/default/searchbnf.conf @@ -35,7 +35,7 @@ comment1 = \ of the records produced by the generatetext command. example1 = \ | generatetext text="Hello world! How the heck are you?" count=6 \ - | filter predicate="(long(_serial) & 1) == 0" update="_raw = _raw.replace('world', 'Splunk')" + | filter predicate="(int(_serial) & 1) == 0" update="_raw = _raw.replace('world', 'Splunk')" category = events appears-in = 1.5 maintainer = dnoble @@ -58,23 +58,6 @@ maintainer = dnoble usage = public tags = searchcommands_app -[pypygeneratetext-command] -syntax = PYPYGENERATETEXT COUNT= TEXT= -alias = -shortdesc = Generates a sequence of occurrences of a text string on the streams pipeline under control of PyPy. -description = \ - This command generates COUNT occurrences of a TEXT string under control of PyPy. Each occurrence is prefixed \ - by its _SERIAL number and stored in the _RAW field of each record. This command assumes that PyPy is on Splunkd's \ - PATH. -comment1 = \ - This example generates 10 occurrences of the string "Hello world!". -example1 = | pypygeneratetext count=10 text="Hello world!" -category = external generating -appears-in = 1.5 -maintainer = dnoble -usage = public -tags = searchcommands_app - [simulate-command] syntax = SIMULATE CSV= RATE= INTERVAL= DURATION= \ [SEED=]? @@ -90,9 +73,7 @@ comment1 = \ countmatches command which adds a word_count field containing the number of words in the text field of each event. \ The mean and standard deviation of the word_count are then computed by the builtin stats command. example1 = \ - | simulate csv=population.csv rate=50 interval=00:00:01 duration=00:00:05 \ - | countmatches fieldname=word_count pattern="\\w+" text \ - | stats mean(word_count) stdev(word_count) + | simulate csv="/opt/splunk/etc/apps/searchcommands_app/data/population.csv" rate=10 interval=00:00:01 duration=00:00:02 seed=1 category = generating appears-in = 1.2 maintainer = dnoble diff --git a/examples/searchcommands_app/setup.py b/examples/searchcommands_app/setup.py index b9dc87b78..24b124e0f 100755 --- a/examples/searchcommands_app/setup.py +++ b/examples/searchcommands_app/setup.py @@ -468,7 +468,6 @@ def run(self): os.path.join('package', 'bin', 'filter.py'), os.path.join('package', 'bin', 'generatehello.py'), os.path.join('package', 'bin', 'generatetext.py'), - os.path.join('package', 'bin', 'pypygeneratetext.py'), os.path.join('package', 'bin', 'simulate.py'), os.path.join('package', 'bin', 'sum.py') ] diff --git a/tests/searchcommands/test_searchcommands_app.py b/tests/searchcommands/test_searchcommands_app.py index 70bae6124..faf14abd8 100755 --- a/tests/searchcommands/test_searchcommands_app.py +++ b/tests/searchcommands/test_searchcommands_app.py @@ -199,26 +199,6 @@ def test_generatehello_as_unit(self): return - @skipUnless(pypy(), 'Skipping TestSearchCommandsApp.test_pypygeneratetext_as_unit because pypy is not on PATH.') - def test_pypygeneratetext_as_unit(self): - - expected, output, errors, exit_status = self._run_command('pypygeneratetext', action='getinfo', protocol=1) - self.assertEqual(0, exit_status, msg=six.text_type(errors)) - self.assertEqual('', errors, msg=six.text_type(errors)) - self._compare_csv_files_time_sensitive(expected, output) - - expected, output, errors, exit_status = self._run_command('pypygeneratetext', action='execute', protocol=1) - self.assertEqual(0, exit_status, msg=six.text_type(errors)) - self.assertEqual('', errors, msg=six.text_type(errors)) - self._compare_csv_files_time_insensitive(expected, output) - - expected, output, errors, exit_status = self._run_command('pypygeneratetext') - self.assertEqual(0, exit_status, msg=six.text_type(errors)) - self.assertEqual('', errors, msg=six.text_type(errors)) - self._compare_chunks(expected, output, time_sensitive=False) - - return - def test_sum_as_unit(self): expected, output, errors, exit_status = self._run_command('sum', action='getinfo', phase='reduce', protocol=1) From b9f902ea0e85c40ea65b469bcd61a5080ca3383f Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Fri, 22 Oct 2021 15:48:49 -0700 Subject: [PATCH 03/39] Attempt to remove build app stage and dist --- Makefile | 7 +- examples/searchcommands_app/Build-App | 31 -- examples/searchcommands_app/Build-App.ps1 | 31 -- examples/searchcommands_app/Install-App | 41 -- examples/searchcommands_app/Install-App.ps1 | 58 --- .../Select-SearchCommandsApp | 17 - examples/searchcommands_app/Test-Performance | 0 .../searchcommands_app/Test-Performance.xlsx | Bin 44875 -> 0 bytes examples/searchcommands_app/bash-prologue | 43 -- examples/searchcommands_app/setup.py | 489 ------------------ setup.py | 81 +-- 11 files changed, 2 insertions(+), 796 deletions(-) delete mode 100755 examples/searchcommands_app/Build-App delete mode 100644 examples/searchcommands_app/Build-App.ps1 delete mode 100755 examples/searchcommands_app/Install-App delete mode 100644 examples/searchcommands_app/Install-App.ps1 delete mode 100755 examples/searchcommands_app/Select-SearchCommandsApp delete mode 100644 examples/searchcommands_app/Test-Performance delete mode 100644 examples/searchcommands_app/Test-Performance.xlsx delete mode 100644 examples/searchcommands_app/bash-prologue delete mode 100755 examples/searchcommands_app/setup.py diff --git a/Makefile b/Makefile index a09da530b..233978781 100644 --- a/Makefile +++ b/Makefile @@ -18,16 +18,11 @@ DATE := `date "+%FT%T%z"` CONTAINER_NAME := 'splunk' .PHONY: all -all: build_app test +all: test init: @echo "$(ATTN_COLOR)==> init $(NO_COLOR)" -.PHONY: build_app -build_app: - @echo "$(ATTN_COLOR)==> build_app $(NO_COLOR)" - @python setup.py build dist - .PHONY: docs docs: @echo "$(ATTN_COLOR)==> docs $(NO_COLOR)" diff --git a/examples/searchcommands_app/Build-App b/examples/searchcommands_app/Build-App deleted file mode 100755 index a5cdd4a15..000000000 --- a/examples/searchcommands_app/Build-App +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash - -source "$(dirname "$0")/bash-prologue" ${BASH_SOURCE[0]} 'help,clean,debug-client:' 'hcd:' $* || exit $? - -########### -# Arguments -########### - -eval set -- $args - -while [[ $1 != '--' ]] -do - case $1 in - -h|--help) - usage; # does not return - shift 1 - ;; - -c|--clean) - declare -r clean="clean" - shift 1 - ;; - -d|--debug-client) - [[ -f "$d" ]] || error 1 "Debug client '$2' does not exist." - declare -r debugClient="--debug-client '$2'" - shift 2 - ;; - esac -done - -[[ -z ${clean:- } ]] || rm -rf "${scriptRoot}/build" -"${scriptRoot}/setup.py" build --build-number="$(git log -1 --pretty=format:%ct)" ${debugClient:-} diff --git a/examples/searchcommands_app/Build-App.ps1 b/examples/searchcommands_app/Build-App.ps1 deleted file mode 100644 index 96ce6f3ae..000000000 --- a/examples/searchcommands_app/Build-App.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -[CmdletBinding()] -param( - [parameter(Mandatory=$false)] - [switch] - $Clean, - [parameter(Mandatory=$false)] - [switch] - $DebugBuild -) - -$buildNumber = git log -1 --pretty=format:%ct - -$debugClient = if ($DebugBuild) { - "--debug-client=`"C:\Program Files (x86)\JetBrains\PyCharm\debug-eggs\pycharm-debug.egg`"" -} -else { - "" -} - -if ($Clean) { - Get-Item -ErrorAction SilentlyContinue "$PSScriptRoot\build", "${env:SPLUNK_HOME}\etc\apps\chunked_searchcommands" | Remove-Item -ErrorAction Stop -Force -Recurse -} - -$ErrorActionPreference = "Continue" ;# Because PowerShell assumes a command has failed if there's any output to stderr even if the command's exit code is zero - -python "${PSScriptRoot}\setup.py" build --build-number="${buildNumber}" $debugClient - -if ($LASTEXITCODE -ne 0) { - "Exit code = $LASTEXITCODE" - return -} diff --git a/examples/searchcommands_app/Install-App b/examples/searchcommands_app/Install-App deleted file mode 100755 index b693205a2..000000000 --- a/examples/searchcommands_app/Install-App +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -source "$(dirname "$0")/bash-prologue" ${BASH_SOURCE[0]} 'help,clean,debug-client:' 'hcd:' $* || exit $? - -########### -# Arguments -########### - -eval set -- $args - -while [[ $1 != '--' ]] -do - case $1 in - -h|--help) - usage; # does not return - shift 1 - ;; - -c|--clean) - declare -r clean="clean" - shift 1 - ;; - -d|--debug-client) - [[ -f "$d" ]] || error 1 "Debug client '$2' does not exist." - declare -r debugClient="--debug-client '$2'" - shift 2 - ;; - esac -done - -# TODO: Answer this: We like "splunk restart -f" because it's fast, but what's the right thing to do for customers? -# TODO: Do the right thing when SPLUNK_HOME is undefined -# TODO: Parameterize version number - -declare -r appName="$(basename '${scriptRoot}')" -declare -r buildNumber=$(git log -1 --pretty=format:%ct) - -[[ -z ${clean:-} ]] || rm -rf "$scriptRoot/build" "${SPLUNK_HOME}/etc/apps/${appName}" -"${scriptRoot}/setup.py" build --build-number="$buildNumber" ${debugClient:-} -splunk start ;# Because the splunk daemon might not be running -splunk install app "${scriptRoot}\build\${appName}-1.0.0-${buildNumber}.tar.gz" -auth admin:changeme -update 1 -splunk restart -f ;# Because a restart is usually required after installing an application diff --git a/examples/searchcommands_app/Install-App.ps1 b/examples/searchcommands_app/Install-App.ps1 deleted file mode 100644 index cad80403e..000000000 --- a/examples/searchcommands_app/Install-App.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -[CmdletBinding()] -param( - [parameter(Mandatory=$false)] - [switch] - $Clean, - [ValidateScript(ScriptBlock={Test-Path $_})] - [parameter(Mandatory=$false)] - [string] - $DebugClient -) - -# TODO: Answer this: We like "splunk restart -f" because it's fast, but what's the right thing to do for customers? -# TODO: Do the right thing when SPLUNK_HOME is undefined -# TODO: Parameterize version number - -$appName = Split-Path -Leaf $PSScriptRoot -$buildNumber = git log -1 --pretty=format:%ct - -$debugClient = if ($DebugClient -ne $null) { - "--debug-client=`"$DebugClient`"" -} -else { - "" -} - -if ($Clean) { - Get-Item -ErrorAction SilentlyContinue "$PSScriptRoot\build", "${env:SPLUNK_HOME}\etc\apps\${appName}" | Remove-Item -ErrorAction Stop -Force -Recurse -} - -$ErrorActionPreference = "Continue" ;# Because PowerShell assumes a command has failed if there's any output to stderr even if the command's exit code is zero - -python "${PSScriptRoot}\setup.py" build --build-number="${buildNumber}" $debugClient - -if ($LASTEXITCODE -ne 0) { - "Exit code = $LASTEXITCODE" - return -} - -splunk start ;# Because the splunk daemon might not be running - -if ($LASTEXITCODE -ne 0) { - "Exit code = $LASTEXITCODE" - return -} - -splunk install app "${PSScriptRoot}\build\${appName}-1.0.0-${buildNumber}.tar.gz" -auth admin:changeme -update 1 - -if ($LASTEXITCODE -ne 0) { - "Exit code = $LASTEXITCODE" - return -} - -splunk restart -f ;# Because a restart is usually required after installing an application - -if ($LASTEXITCODE -ne 0) { - "Exit code = $LASTEXITCODE" - return -} diff --git a/examples/searchcommands_app/Select-SearchCommandsApp b/examples/searchcommands_app/Select-SearchCommandsApp deleted file mode 100755 index 3089cef1d..000000000 --- a/examples/searchcommands_app/Select-SearchCommandsApp +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -source "$(dirname "$0")/bash-prologue" ${BASH_SOURCE[0]} 'help,clean,debug-client:' 'hcd:' $* || exit $? - -if [[ $1 == scpv1-1.3 ]]; then - rm -f "${SPLUNK_HOME}/etc/apps/searchcommands_app" - cd "${SPLUNK_HOME}/etc/apps" - ln -s ~/Workspace/splunk-sdks/splunk-sdk-python.master/examples/searchcommands_app -elif [[ $1 == scpv1-1.5 ]]; then - "${scriptRoot}/setup.py" link --scp-version 1 -elif [[ $1 == scpv2-1.5 ]]; then - "${scriptRoot}/setup.py" link --scp-version 2 -else - error 1 "Unrecognized argument: $1" -fi - -splunk restart -f diff --git a/examples/searchcommands_app/Test-Performance b/examples/searchcommands_app/Test-Performance deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/searchcommands_app/Test-Performance.xlsx b/examples/searchcommands_app/Test-Performance.xlsx deleted file mode 100644 index 1dd7cab72cc3c9cc6a61e4637ff22e8736782acd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44875 zcmeFZ1yo(lwkEo8cX!v|?(XgccMId(E?zb+US>M}ZZ@7KY<|v8lqImxj2{4Ki2nac{~PzfXx5}!4+n0{nQBmMd20;b z1fdn7@ds1pco5Jmy3A~{+yu9ABKk@R^&L$VokyeX(HJaO~C;dIcCCi9>6LqqT)?~>Tr<*t(9ip-n;5HU&D-)07wO7#c5VX=Qd zoLv3-hrYhX2q*$iBKGN-bw>_iqL|v;%>|fujAuHI9M%(F8 z37fpSclf=zk}JEkY6w$2>^Uq+*!kIQR?C%)V?#Miqmt*XLx0(`h08^Xcp!1C6!nME_9j&-xEkZpq-=gwnx3+-#h8nX?H0GHwK* z7pF)eakC2x0K@kMmp_hNSiIlY&SLwDbQ=`P6QpEyW!7e11@pH3s z+(YTOh;J;cYhJ;h4d%47V(xv*?%*UQy52W8`@MQ>`ej4R*rV!{6p49>Q)AUPB~z%l zeus^d>`;R#+4&nragF_8f4siJ70-%>@NC$oR0b(6OknCO%E3;Q0`NSajR8s!-)5=o zNnO(ivw_0*QrD81%gCaUoICp(26W+s``)=XJsd}bqfhTS#GFvawTYyJ-Ww!Ya5F?+ z1>+p^*6#AZZ4S+C{=SL7li%9?pq=0#1dn6-Ge`}u14}(&{$AuQCtl9)vo-Lt%SdM1bZjSt9^T(xnIrkG`%n`bq)Xr*|6E*vU z?{I3G?T&|WM7l72h6)=gtc-FAs`^IGEGEi~52@A+)(fT*uev|G+ioEMvTd~NhpLw8 zv?jy-PBgTk`}XopwOtx`E@^~{hmWdEBJ7?bF{O)F;;1c^;(SsO{A7c}uo;`b6O0m# zZT`Iap6}=!q^$II>l{UgJEzb{Rn&~|B)o%Q37qo@KTwm%9;IzdoY+F}6V{e{g-MkB+5FtqyQ+)Aan&Y6orkL5Q!-iLQ_}{Wzp&Y5N5ytMYC>! zl7$Wzrps%oT_hbcdvm$Q0mF#{Xc2biT?FvrohAIMPm0^1JLzn6*M=jDOxqC)o^h8= zMk0)9dlAEri}q~|_&ieTKtU0$m zGSSMz%afyQX^e&@pP>uWQGo_0k%~)<#zwA3U$H{iC&26XHp|doWLNNL4QPFm*e0M8S9~6hw}#@s_-rrz z92R^y5~i6-w`H8l1KI^QjIS?*uF^_QLmqt=)H|~)u4)6iimD7a3Np{~4{dIr1H_-k zyY%_*ygX{>&)AQGZ%kL!yK?ppQZPcFr&sK}@1Gv^vTtBTI*QIJT)l%k3dYCtTQBRy z#;dIE(9@m0g+4oiuC9f|Db#oF%Q=OP_bf2Wu6hG*#b(J})UASamQP%?*{XjH76;We zXJ~&jn-2p&wg?xpucGI3o}wR3pHU3vt9lDWB(k?Z)jqEZ6sV!+FBh4vRZriKkRLv& z_878@}Ytw%boCBKrIL@dx;YV@SQnN=P7@;n~EW&LRTokQZ3 zH%uH7%-VdPfsA*32+rH%3;iLzccT3O?xcLpy8^m$xU>moBba^eYR|bx8$hA-XKFo4 z34T((xl8f5z1Nd=Kt_u`@qV}rHYntu&;;!8Em!zv$%A^p46$VB0`_}1t-H4s?(fFZ z54M&4YxRY%c?+Dx#=O11icAVj?iO>#t`S_heVh_d=)F**-hvGwVMv3Ob{gHdg_Gt; zEa8aD1YmzS6GMminhwlE8FoPQou!Q(`@fwgeq75r3V3q!trBD4=x}o*ofoK>t*L%h zjG=jh<$WU%HYU`Y`f!i=`&U;F;Syn1g&tfxMKZqL@D#Iui;~_`x}b#fh16j307@IH zhHN<7eM9Qc>|g^KoAF=+6`Sf#76%(xG1T{8&WkKpdK$w-?V!|_0?JX_`7E1*2L>(8 zta9j}9yy}3)N?J$iaZ-@G*>#wSfkK`y9bX1 z4#jP%+ZM=KO6Wb!cUyoIYRCM+W?bK`h>%~${0}KfDE@0hQerG+Lv-ER2ND7truUN> z>>|omwiXce1&De(ts65LY$K7};i~$dpkDe7Xq`Z-I4UDz`MJ@XXzk5O-g~+)xP(#} zOk;jiUnEW^Q~`3$WHJ=A@xGzM1T0BiBuihPTI5(bFkd5(3p;Ev~j^9Pf`k3Xs*WW3_ zv^Gv&zfn2;_Ym#3db+_@Lxn;|=N8nxZ|k+yUuG|9E`KYe??p5qZL^>ld?4ZH_I@LX34ud`e@WL`urBtc*y3B$-nQ1ia*2bWWFRi@F^%7%}0opq$x4mXR^_xiCVTy7H8OYlX>q%f8TpF$KmU#`NGk_KP9e9$!UO4ICC+HCHH+lYp-tCD3ESCauvyCHRvg0X^obTWtS08D-Uavad9cF z>$SBTIxt4e*ZG}fbCT0ZOYtq@Pw5fV`~B3b3@)p_G#r}MV{|Ga?n>)3_)LVf48qaU zOwNYX2R;`}-DUh*z8ska9r@RBszE=(%I1$n1c-!DI$XD6{Jg?*VCQB?HO8RldQnoZ ze|QSn9fNe}5Z+DZ~-< z+M@UPt7R+15H>o|e&KbtdNs~03F!l?>MxBy9xPv7>_f9J?kIe>OQ)PY3LVu}ZHgSl ztnLiEDF`;e}n&Yo>L+4?zhT^H^1U3I_7fa6tVH$v3o=PmjV+LKc~Oj z)@alum$%|Znw7NSPljXC6I4wvQ@7gaSN~*m7F!pJ7S+uA!P! z7E4XKUA6TrdvO??;DMDKm-;|#e5zf!%+MVNmWP;16uQ`ZMGtKaz7aO%Nc{bQ;n-v% zv#)vbfp_)lkv`G1iod+l^G3DE?<6*Ndk03VqS@tTV@pA9Y>GOP{){Wq6l!8lP67dz zy}d2c3Wj1i+m#ar}`or0uC# zi2QTmvVP|E`Y<`BI0mne-pIqH4y;e}4xRrSv7jo%esVFVB$n+?+AKdWG{0eZ4Y5Zt zA>7+k&%cRRC^jdfjj=5@Lb4oxq^?km#U>ZPt@=vE`F^WKCsZ{5wTvS*7Av{A(K|4f zSJgSBBqC(knF_0`;d5>Q;cf50+bavpzIme9RMj-n3E-<7W}IbVkn5L}>H z^b_gS+Vp{xoE|cSF7Ko;3-OaLqpjG&zN3kY6v(={zIvO$65qgx(Nhm*6Ci6&*PfRu zs!mt)>98?Y_=?Yrf#s2)({QAjYJxEs--t}x4sZCdlsqtCQ#!Na&@{51jVhM6-;GC9 z?Ib&*5co+VRZEO-X{BTc#FBN{l&&OLTCSz$P&vbiphKz1p}E}shD2V?FpzT_B{ifY zXCXlMb*sXqDxNX6+RFS5m zSlEz?#5Ds6#S|q3C%@+|f`XasVNMK*E1(sz6P5zx7-MWE1w5u#63b2C%&3(LgxV$2 z6RJLZXK>IDr!D<+$P=WGEs9<~a!1k)fax%PuHq%#Gja-ud z5xF^b$dKD>BVRJWi2hx_;5V4Vps(9u+V27jtq2)IxD?NuiD!H~HJ;FDpIF~Z`v*98 z=6Dr4Rbf0vVAUEqSKirc6RvH_s+xxzM-+JyGp8gP1-AyZypegX)m5F3 zP#BV3UD-E-?pg&t=V>2zX$)_tt-Pz$k4!`hIwbEX@DTruY}MW!;|4K(UUgpT!g6?^ zs*i;{aQ_!uQ&#e}$kTVr9f-bv*T>!SER_6Yff-iJ} zsvir6X`SysXB;%rrynS_wbOi-D!7ws@JbgT%D`!;?0c(iwm@o;~QPA^9O zHumUjY2V9ZX)OqRP$PCGMqa$yRTU<;~bvJDhTFZVj;tjvMi>6_)=V3b5J@s^Q z1@$KEmzbbFyYDoD_f@2!_4M6SjZ3i^`s{q-6?k~3`T2f&&kF7rY=KwJ8ra~w@%-#X zTpNW<(<|NrbA;f}M`n`~x8t8+=c=rl2S4T=ug~XHX_{4cM43Jp4HSa` z%q#Th2QdC*#_1fzct%RQa&v78SX9K!%;FJf*n@0i@BE%BpYKx+&kt^p)luvPoVxP& z*7#>_Fvt+xY?EY+A9mArfTOyof zk2ject(t7MiIT~q_CH)p(;TvX5bVm4g|ymd7-W|#e$?`F|A5C>d4$?kljY`ZZ(M`k zW#nxiw&6RTZ#!?&v1sCZ+q!pu1S^NnbG{QCJ8iH63P5W^Nfa|*{5Vo<$Hvjt`J18j zik-vw&&MGveb_+athvo$Ig!N#2KCbnL5Y)dsX@4{=ms*#UaZ!{K*@$gKU;HgE^9~U z&-PFJf}|g@;W)@(PFF*}Y>psDj;Yyd^92&R{;Kx=TT45uB35jW1zaIKaw_r<4z$`I zQe7^BBL+0J-3t2Qbk!22w9M;OpI^7r(5vqt`Vg*lFC=zue1}INJ5Psl@lQ8`w1&`u`U4PK#pd(3r8YiWeN522E!*b>#gIruNM<*R2aNFM zHyqx8-(*hl+4lEE#<)Vc65%NXWZ3@S^VCRp;GFGTFD^iv-=tM z>*P@5F^&n(knCj)kxZRhE0u3Wa1O5CF(RyA_N(6>ISy7`MM`zudDPKU(Dyqv=ZQZy zJRNMbpP1z|X5Bx$$^IDHUeLu;s=8C9@G~guqeH__WCjoVD-Q4A-Gv8=darp&CdY1h zXB5P*?w?;}wX3=qWkS1|3<>}&!TjC?e`bUP-z=Cc4x-`UTRY0Bwke-}RrDoNai)}n z;Nh=|ETTQa)3qw{B0HtYK}}gHg+nw6!nRn8A72yuE210>2TP$1j-JJ4!Va>e(@2qr zV^dYM$P9gBMfSEi);ZVs&135!F6EF?Dj{r|=QtCUILA&w+a^t?P-M-S*hiD;LVbjX zQ8W2A=Y*J76QiW>sXz{2%8#4BgWQ`$qs>m)E?KUlY<}M@R^AKtF>FC7(SVYo*o)l* z+t}00ITQB!8D!8n#Sq22TiuwuPK-=4-XAf+n|Hx&Udr$Tqkq1yWRTmA^MV)mPdLLx z^MV<7zD|&%Gh>IGc_}Lej;Y?dInpz7C+9v!GHLD z3iCW%0u;`G--3c{^eR3z7P7O8r^52ERER2cZ;gv*F0Qxhm*h5;v!B`S-q@e4kRy<) zz}Ji|QVlW~)jD;bOv=HG&~W{B(HzTop=$<%QYER9`fXnvCVCKGY^{q|2>MAQOc6Ce zZ%kGt3@Q4dho{NH4#D@eoFXN~QX9maAuH%n*+B98*JaVw)MTpEPX~!eMW+|n@m}K=aH$KCj$0`<0oR~L&dVtn2s52nSdV}54QibvN z2lYEX=)5occaxtcw^Y?LVL@S-jd4hv#cpd}vLyI|kTOc|F|g^vMa*wM#>KN2yRlz( zmui;ME(I}umC>;Zs*iJT&{*I%hfk49E2vFEXPwnM|Vn{bn| zzuvYc6C$Cb7d^7Zx^YOmb;A7xLZVXewx=9;T<4dAf~>HUJfnLG&ryFZaJ+xeFJ*{% zN)3e=txD00i`;_?(j-?BR?{85+?m?ZDZFL3@{498^+|6{dM!m!f6iE8IG};VBJ#5` zn^XBMTM&Q0hMiKT69fARwz>s$4=`ximE!L3|MiGYek=$k}f|fc}f-Bb$ zQ!ps;{)6oze`vTT2{h%eb?wy-mUL>?5uI|MV5;g_u9)+~9vbVZ?1Q1Qpr*8xmdi;T ze;M;SYU&aMmSegMnW14?EDtWlsP`(^#jz@z6{ow6)n3OLJMxUj1~n5&IV!}w8@t!- z2q^@yN)w&+SHd5Nccts!X>BK>KUyoPe0{>f$u_2btr?6Q$0Nyr(%30R&t~NSD=-?mk=6?p%0oT8>`z$~9SxUFi3N%OMT))jPW<`SKlk?Q$TSWm} zTWCREbPrhX4SPs11*<3U&5p|j$f*o2l#hZ3)1)`V-o*1A$8E?b%f$~a$i!n1rleA* zxPL1j=1PNsr$zTWElesr|Mn8v3rUPtBCNPB(C~9{j!l+Eh1|-e^tp7C*y}KVXnqp5 zB1fw>(M)8pBAps0lNW6m1%2}!3XJkv?*q+EwO?SQL)ki9_kE)9sDli5(pj9Rc*yx5 z_vDl92e_C{O_K`&@om$;f~2%Fox@ixQFCc>v!7*TSd#_}^fNtqfM2tTW41L%ZRQ;X zhn+2r*w}_nP4)iyiCEh_7j4wi%9fLPyx3~jD4Vv6M4G=Vcs|N9{@byIn#*G8=yVDFNa7)xV!hVUP?G^J(oJDr{fNdEplAi&7l=>Rm1U96bB9Q#5 z%}^lZ_%MW@?iT7SRr3B?de4QKkpYYQi8`Q6`G&wM;h4tVwq}V|CN1c6n8yi0_lIeE z#7s{vv!7>H6P0kSj9#^xMO25zBARr*(oB`Bd78BxVMT9zjm8oI-js-Z+R_^yS-iNs z*ZCq^ZG7dLYNSempIW4(Lu!*ZR}WNYtnsAY`w@G7t*ZB;csFfzi`5^1fUR&ib;Cd% zsNdCmhfFxcx3AffOhDJ<6xdwSw8Dn>nVp(+&9EGe$PwLpV>}7lg&>T1rfMkM$8MM! zO6cfLP2a2xH-rw$v>i8>q}t2U?%YQHb-;VKqs%e4eCyKwd1%@zH)}a@#tgK5HrfbR zUmKPc{1m>qbh_n|85}0rSaiiDl0nrH-BVnJj|GS}@j5ajGR&X&((XU`=wKHeHh}tc zRT&#f6;{(Ns9%5iba;!QeNs&Q*{sCUAY(R1lI4|9KN^qhXA9i&jH~TaBKaC^^vF+q zy9?0>r1aYAZo27}3?kn7YlC&hM5AZgM=!^))&pem~Ok3 z9P?}?ckL$(yBhOLsc6}tw*m=)->=PUDR>VPvq~DCbn;K<8;vBeuR)aF2@3 zX=7DHG0c7}w01vZsvmdyq^BZWasP-+$~r~S6mI8gqA*CD8%?SKYGa?N3Vr2*5ezR! z(tuKBK5<_79Lxo~zQ%B+U?47YXFX(iO{(!K{q$3m8DE~LuLoK|rdIXpH_V+}Psj;8 zN@9TuJvnXIKpE8u^^{C+=}gy{88wQ?fzRVm@XnA3f^W@;({yF;d*677l7v9WQx;{3 zwvnr^hX0Vo@3Gr)?QRPKOI+4VHVXGpFeUPD6tb&H9y{B{_0l7G1>O%-H?h_~EZ?qF9GbOXRrD^~ zctnwOT+ln^vhM9xIX#7bT{aSrE~6f_4E66$=>gomrZl-#)`kS*U<5cA%ogq zjosbCru7;*-74E`6Z?6zh`ygyeM{QLOHkc-8J!^q43hF*c z$t-d1R+C8lIjaz|0K(u<=U^INJgUg?L@tH*)U$0}o3{Fgz+DRQMWHPual4bI4$0-R|{x&@~~!0lhFdP$+X8KrReW zNVypK_L%woIlHj1P}v^MV#e)!H}-6~Nx^&$NZQ%7Nkq z9h~Bzd8)qeAfjZNz}5b{h`|KWN}ss5f~U|xZGX8Ke{ z;8ut~Nr$^EapnAMaegOn)7~;#(GYBeKf?u0fj_1Jax_b-w`tsS`k}Z|muf*E_qkO# zh-Hjd&A_}DLwn(t5%ZBy9-b}7o08mak^8b2)AfhVN`Bmp+}HK*4n``Aq?8h>>WcE_ zH5jJ~3(Yh9>%{}2R$rsx23K0vzqZ@JdRPO3hf%2Hd1&1&r+T?=qs};QzZuDKSUGcg zeOEa3-;{?55QQaZjL z{fd&X*lC{v)Y_i~L?%Cl;a>UGli#)IAp4l_!;HF?D-pOA%95*mTB*J$OdgTA?LHkY z+q5^Aq;$!++s-s_{;jFh{KK_xhcYDxBOLbhV=HJKD#BbznAY2)aXS>>c*|HevE~md zr8do>X1J6WH0VBXF<*Q#I-x(n1xe`&8FnBqQfAoWi>Oyky;U80OUOhcPFW{JACH$w z=SRLWsfxC`v#7hoH1d(X3p=GPjeodat-vyia#1aTZT!oW8q`{jP4`D4bTq$tO3@mP zZ$xH%_l~Iv+dJy{ooQ&h#4_8u8yQj#7%oI;Kg>55IKVA~je6MJE88m|4>%ie{hhBef zFmtRbbPc~@u~TwM!qYfe>Hl%5NjKl;&2%A;<^p`6<4!tr))!Uk?JNo`k50&@!411O z#=VWcnT2s=e*gxyZ8BZWVPH%8X5Vyau|m?JOOfhm#VS0VOtOR_e=6xVV&VKTqDL}l znw!a1^4r$VPxxSariv!b3E%lDU8HXe=)9au+)UPz-*5z&to`3)${}BNO5tnHN*7DJ zXblCFq>@Q(Ni@f%D9J0i-X|DsYR;2nY|i2IpL|$NeWUM>H}@ol^yrJ$WvmNRWinp2 z1ZR`BY+`y+0(lCM<xKPAt0`@NqbInO__F6v-Pb? zl}X)$C^pY6rnW;F<=HFmczH6946XT$a<b{Y!<`>KR|-$6Lf#SnRlRacUn%{J z&R)56O23AIx{4%oe5JT^QRIH16K0{l7&C@dWhYWjW#*cBU^}!kl-sUk1?ev;xS@EGVZxEsYQ}Yw6;kPJxrhPVs+#z zBL)vV`#QOq@!j85ybOpJ!^#e#JtUTbrxcw0uH`l;$B1(?6F&TweVT~PZd4*6?~G55 z2>DTIWv;~(;lbdh#pE%@42i$sEb5Yg#721fENVu_1Xf5dIu-Jk7eIH`Y0;ANetX4J zi7ROUL%zbZ?!TSpLn^|#=_p4-+s^x!oYtG%VQ1ScI;ru)jVHtVJU?_WW z`R#r5ONL_o#`g$a0#yH}9?y&>?H{*fd=bC$Ik|Y++O;Y8Jfsp_uA^|vzA9(dECt5e z{jjjIPuG?-kXD#XhO|gnO(8xF+YWwvc7nDJLW5KY4XCnwel{E^*YuUT{K_wz(+I?b zh74bYhA-x_H)E#|pQlOs7#24arbg)Wf!z*U;|*~F6-<**Jw`rinI=^$kIf_wQ#sJ0 zE5?#`%F%mOXw6L-Cd^Yx)KcrrO=%y_)B1GVxR)(dkGHE=oB5utCq0P%UJdDb`~CsS z$sBHV)G@?ZEPgYu_3K5T;#I|=lpm*313^bw@^F(&R?f_d+gtgPADZ7_YwLtfPQr!d zr4n*}=F78CxLj!8%fA;dsO{fJ<)6P$6X?q|HYTp?>@E0Hg*R}<`D$bJ_NmSe#2#_D z+773@k!!Q_gb}3h6pAzg{-*zyJ0L^8xDr)`m3s1N6UX zLjfOP{tW}MFU7zofPjX*tCy>%y{j7~HyZ~aAf>1Z`%*h3{e|iNf|xU8NsB2!1MEQ< zQtmNGDg?ZV`dU$1+Dt=BT~1L&7Ge|tpem?1xVSui zM4j8(#uMTas3-__^7V3ik)J~_uBGD(hJLXT*8|c(2!1(uifi{b%<`Ac->}3Bwsv;0 zhUmPs+0EM7`UUqvaGZ~~4FtnLPD27QJ`Og%5Ih6Hv`*g64iF52U|eS#3r_%mMSYQb z*;v^_Fb4#qdgy3LL$CHfc`|7Paj zwf@`S#kYUcxYGQ~XHfW~|B(Ho>_23#B@j;CK>8;2A2Q2#0MHr@*>EiVLq_`na+Ey^ z09wZX$sYU{zSw(txe2ke`}+E_IoMdSy>#f`)Bmf&-!=bz@K5(+d%54=`;Jn^#@52y z$&2!(Q>|Q`T)jOgJ>4vmdGbg-j*5ld_7P+EC=_`G09OuSs+0A$E_8E^q2 zfE=I!7y(v*3*ZNY0SQ1BPy*Be9l!uE1*`yjz!~rWe1RY!9EbrDfm9$1$ODRiGN2Nu z1sZ@>pabXz27nP@0+kfoA&f1I2TTY|0!%hc2}~_a8_XcgG|W28G0Yt-JS+|@B`hnfFsuTs zF02)-J8TGS5^NsqC)gI)KGr4=*3@JKj1z3_d-+BEA!T3Vs9r9R5848G$5$4M99XEx{DQEg=b^ z1fdOK0%0BDEa5#7C6O$VBT*VrE71xuEHN{&Cb2JZA#p$PF$pe-Fo_jO0!ahO5-Bt( zGpQD7AZaP-DCrFuIhj0}8`(RuUb16yd~yl$H{_Y*o#Y1;I22+O4iuRbT@;6uc$AWq zPL#QneU#@^q*Mx2-c-d@V^j~+4Ai>R5!7|mD>TS70yMTXnKV5#=d={Gs2QP;cM;XU5CpM=NX9Q;(=Oq_2mlan&*9x3JJcT?9 zyg0n7ys^AJyia`md>(wYdK+1v>=qg!qKKgc^j- zgjt0hg{y@3MHobEM9M_AM5#qBM2kh&#VEzh#Xg9wi&KePh?j_OO3+AHOMH~rlVp;7 zBl%hKREk5&L#kQoR$53pSo((yjEuZYg3OpKwydsfp6sd|wVa(?wcNQpuY92V4+S^{ zWrZ|_c||fsE5%C1b0vPIV5L4~RAp`DJmpOlW)%;WHdSaJ?Z^1bD`m9$lk)seM?b*A-!ji^n!&7Q5OZMyBgov2-g z-GRNheYX9vgS11Q!^IngH$`u59n~Ex9KlZdPIb@%-wA3t3E!dNX=QdvExN`Q-b6eD!=A{jmI8{6_tm{p0=j z0^|b917QQL0(*j}g2IC~f+d5CL!d$|LwZ7KLZd==!sNp~g(HVMhX0D-h)9pPj?|BA ziz15(i`t5okFJhEkMW3^j}?wBii3@Fh#QONj(->boM4^sGm#@PJMl5eD(Po3XL4>b z_^s{Ru@t_P!c_QFm(;m5@w89rSm^=jTN$buEt!;=37OYfW?948JlRD#$T{9Q>$xhq zU*FNbOMUm4XP-BlFO^^Sp7eeE``ZHRf~i7@!rCITqQs*64-Ov|isg%2O6W_nOW{j> zN_Wfj$_C2?$}2w-e@y)NRN+#w{z>aoUnPHKWff^vN;Pz~clE(%32 z(jx*18J~cVh?tI^fsu)shnJ6EKu}0pMpjNUR~e(xdq+bzu5I+=f96tD7q)`Vt>Sa9eR-fh;I;_k3XTb5j-Qah=ck8it|K} z-E;H`3K)RHr+Bms!vM)Xa)SY>(`Yadk%;lkDS_}v{Y(S~ihZTPK(}lx7$E!r^NbaW z3I-Va#1_M0t{O6G!3{a29h91L}g&uUShOr;!CAXw~JQQl) zaCi9ldgYK`j+RS-^>fYkxA%jDyYg5v%a|U&qRC_dPqXv9<Ue{-S2ovaA z)~?w><6rl903vD(*73l*oiaymOCT+ zX$_fkc&oME9N~YaN&mz8=6`WTa~I7G)%=e%oxILfMsGmsdMpb>Jt^$jqQK4l1Oj;S zzh)vM=;~P}6yk)1%1!ns9#akY+dtU_U_ktj83+#n43K_+c~DjZ14R$)kI0c=;H?JK zQ+Y+`_*2d^oFo_+WCWh4rn~l@N5BB|2MF~tflvpBwJR`S0|vr1LN9&-;!RCwcVM6a z4E*>c4%z`ImJEF#!N6kZx(>yoM;OKltMGH^ao0IB#S5 z=Q+k5Op9O*My_@2^wT%DId$Xx-cBhXP_X4h?ARF zDQHWe!NL2_rnI2xjTPm-`<%osTV*hSZQ>{WOrU;3QWr!p+e!E+KKIP!C%)b{7Z4kh z|Ht*q1$yHJj_mn-pDM#fNaF>8e2l`Q!A2eOzRw2=r692jvq!v;E^_@-R-S z{-HpO2K`<2kr?3|Ngrbvx!_h^oZ0#7*Qbw;U;uf%TWDo9(rUecO&z9)%;>p)$u3bu z7S)iw$FV)vTet}0(Rr>312tq)Xz;Cz7`LCRR);f29~fw?h_>q-b1igv{owlVW0iT)kU(vGLvz*LG5Yb{>@8P+*?kbYVY_jFf{2He$NgYe*m! z0q~J^Aa$K~hzFB8_&3zCTT0JY)sYwdaDJ?^7xfe5n6k}vX!=5|>}?4Uj}H?!>-z5r zEDZ3}zZwkAVb)!VBDgT3MPv{~5uXP;)^CokANQd{LDE4~kO`$C1~Q+>mxF;zI>>|) z1CoA@R)?@;Jq6JILLwgQ;KUa zfa2%c-&PSG!sr*eUX*a$LAF_A*WJ=degXF<zTa1U@j9M%CvJYQWofOT zg|8W&de_UI5kxx(ekjSGRIYxpuNHDoDW=qxG4t5XZO&_WAZ52iK|w87Nj zz|4@2ob1v`C6^NLATf9RBcf5i5(nWt4RJuS4C2^AKsTN|djlmmn{9)00RPg9U6 z%C;39cWhAP5jQ(c_JEETUN-D@dy_5gFcEy4FD>(JHXL^CnRW9!*^sX8uy-Cn?Qnr6 z=Og1UQ@*3FpqVesdCUM=bV8PFu<3UvKGRy$^=S9=h_8zFh|iV59b|G>Wl6{}xAd?e zuThrY#_6!4A*X$64n_1{c16M?8AAhhr;4ftr+wj)rh5v<7rY9X38qgvbHURrwQOvQ zs36~{C|TeHU4TBu^)`uqOTI#L+g4pD%s>C<;J~3V)wg)f=M=tnX}x2ZbOsAZvzkY7 zg9^P;(FiAgOHF0}?0WgmZJWXAmc5j3pHd_9;1a>YwLiroW5dDOkc0B7^aaZ--H_H zOZEGRV*tclT9C4o{{O(m-Y1YKCvU?ph!cokGybmgLL8M$mH|UdH~5bF;CpkQ*kH^< zf2Xu0g|h+3_?^6NTzO^T9vr&wd7ih!5f|Ly6zP~rYHpeh;nI7_VT+z4SD)t^dq`$j zNN)7YhjAXn4Kz1N2_tF2y_~kA3VxEa5x!8OaDUmC3r@G)eXiKwRewzyqw5J>epF(X zLbKL;+X*MRD|@s^qW@7ln)Phb&UU&1yzky)mEwRq*&pF% zDUWQNT(Up@V$hnQHD+i!JvUwK8WK!-9?aVfCZy({cl)9BSrBh0*b$#air|c&7`YiT zO1rU?V>QNIJ@b#7qUyxB9Wep#RO}WB1xrTiy{_MqQ|fn6B^{yPTuL99hqLuN<(;aI z((EICMg?`aRJ^HihG{!|{|H8+UPOyr$_WQJW!4N4QeMnwtjs6?!o2zl`v~T=jV}`f zN1;?+N~F~M)MwX}z@A|ePTo zTRrRz4NMBFax@{Qh1&|Z2x+Q!vtfP}OO|RQdh_|q(V85w0H^C_RPZ<;Y-3k_RrT)k z&gPY$AKRlG%>I{MM2dVa`kOKS!go{*?Drk@C<;lR|Bk!i|Htd7WR~lV}VYC zPQ@1bmNsBLf;8Gl`_|7Z##H4?uJcyiTsLQ*0SmH+VfM!)?u-FIn7}37|ADp>LMPMr z1GxEn(6+}CZ&i>xTlXk}ol4eHVC44Y@@Nqd8*pewuwq|K74%a|cbwy1gITsQ|@GYLOBgoIY*Ut z?$O+LRMN5y_A@Cw6}I$e3R^dH7mWz!IaXIgn)C4>wb6KOUwGjoU1DS_s=x6ab%3}E2{UJ(Grp`pp!UD48058zx_kTv1qxfcbv;h&?7E=2 z?jHa2=+@rHX%2^h(^IUiqS(?qNAK(3gVOs|9^F2nwqL%1qdk$DN5P0lx67L0EZ5tM zZX{jtXgcN_y`bD{BAp{W92{*w=|}KAn!mU0scHT1Z+=(#lAg7=Zroi*VN+4lr#M?i zA+J$Ou}b+j1TDnOjqhGQhb zof@xmfvCVr{&Eq#a=6KNV;x+`@eLf+XP_pDsE9F(*@dh|((_RtAmilHL%O<~j5^&x zK3DZXfs3(8qCWNks@pJz;0p9Ix`4b&sSUFPQ5+_1htzH7$+Q`12RmNchxf=sa>__B zc}=~UWY}nnJx>2Mn1f9e)%4*l!AOB!MiSl>mqMkP1EYGikSRZ*a&fMYyA3tR7~)8) znFI~k`7F!;y-$Dze5o7eba`)ncb4oSQkQPA%ZO7DpDJv4J#Hh+TOOymb;0~)=WXf& z%0_)hhJj+7ws)h|Byi;ZPb#0Pe190^;-+E?(M%6{i{as%OgFjh18iR>uet+)e!)yQJ(7}%^_TkYb1Zo%qWJ!tZ3O;6kE2-Nrf zf0ZymIRfkP5`fg=eA7klTtBwbh0u6ucD?MT5rVkV?|w%S=szIPUbpQ#O40duLHS>8 zQO%Mf>(~Xf`~D^ZGA`w;2Y)@5*F@h#iFGW7``^6|t<`yzyv0J>$*O#-Wo^gCwMXX% z)wR{!{?!Lj`;F#TSq>M0DKfS|-Qjdw&f0pI1j{ zO%=BtwzRpFRZ=4K`a)!B^BsR>4(WuJ4#gF#fM|rj@@E(Th0=U++z}SYKgv1*SIcHn zoTa~EHF-abC^mI}gccf!N&E^PM`~_~uz~T&urW{>OOYF)JVLFg(io6yYlc{l<|HqK>u!KAdj?yg*v)9nxUa*@{(4PZ+xDqB zlI{dT6^o)GQ$s7s>TqupPYZ0h`u5y;vjl}&0cFr8%qei&RrK!qrH9{9l#=f#bYP}+ z;j}tr-)tA3(KziWeLui2P&h4>(@w{zwWvtQEP^f%zMUl7e!+0*JF11PO*;-#<&z`M zJ`cG3+Zp0j;T>3M>u6ncOaV|Q$i>vVEpAfyH1yE?pTK_ePnYt-?QuFmWbFu_AA(tl zeMGu8>=wL<{K$Fl#UuA2UmGDOnKqQ zRB$Q8%UBWi6tptqf|M!>kYkyMX~UhZ7Y^$x*Rn-VKDJidnXzb|kl3R;+&uVngJD|f zb299F6shuD$!lOGQGeVQ2j;^5&&D9vvhk?G_KX1J&5|PyG{(Ty(j5_`RO8J| z513Pg)e55PXPm0(GJbQTaUb#x`>|4tHzFLp?vJ4Dv?jzC(6zu}AS8mX346D&H!4hu zCoxrIZ2}SKJq@i>S`O0GpyLZ?li$(Kfq#Zb9K4S*(7^9N@S?p>UuB7dnILQkbuFD= zZYy-tQYTIkQ|Xq>6CiV{UCO$}*NX7Lv=A$Wx7Y%YZ`bcHY7dh|6t5#}DNY*>YbIzC zOqU+eqGhbLBx#5~7Na=RL${tVUN&G&T+x*0BI`JzI&U-BIN7Kq}7Wi8&;&%9+yo4X*)tcJxw*(nxSj=$N=HAeyn7D{Q`L?wQ~@5>O&Nw zyZzT%+0QWqDqPu3qq}aI0Eda@PLLR%{cdd2W`|f-oGGNGHm&Se4)~hX# z?s=SX_eXsn_!r^xe`fjBJPxctJai{i{`sP|Bj7vAM3~4&W+VhCvlTZE82O;qsX)S3 zhcFpGo7s=~Q+ z(&jr#ONkw4tTL-FhXRIil^hIkrL(X}wteF=W-Q(bGGuI2b{hsHZiVAz_93eC54)&5 zEup+~6-m;dMP4RWXn!towjb-EImNo6N+agQafAC?azT}bR}4;6_2PxYpXy{HMFs9*<#-7?S)$qy#%gl2VgqoDgy$go+OY&z~W0WQ1Bu_=m zAcS^vF&qoYU0;>=Ml6bx?QDubYKzC}Cu5UEG$Mpcxw)s3lg0*5@67%|(E3f>+?jCQ z?g@aAKetru>{99}0_uCG>02q8W{sL;>8E(vjNW7z#V)_KGrr7%AGm%K6d<@DBgQif ztdPfKd{iAQsORA!Z0wx+Rju3PR5EyC$@L64$|l~#V;;l4tLbYdeK)}=l9}7=b#rlp zr_aFcApbC2F4ZJ9juIhq0(}z@B|HMV$jlwv3eWe|uisVyd=uVLY%UlYi|TAdbYgwl zG1QWG?y{R}5YOyCUf$CXO=64l9mdpI2(8S-ZCCw1_%A{S@J~%ZMXi~>Wx4S|HRt-@ zOgDo79pEPXD7}w*RF!k39DD6SyqTPg{I59kg;B#E`$7Bl#cGg)n8YzUe${~A<~B64jW6brAay!E7ui4~gfh4ELi zB*e5$eKda=JHFO9slZuEyj|OtJBo$*NEnlnxl z=@5|R1UrU7YSN6x4-l_{v1kgg=F7ULEM`gZwS;VGCt3hPifc#|gCT3G;h3l-SZV(1 zUZ=C0S!0#bT>1OGlMI=I^MS6J@a&*jRsG5NsimA7_2vhO;xUeIY&TcU4-lm9AlNCG`@cdDtMTY z$(lc>R;E}14}xFh#5R)1Ow!ufG-BDzGest|(kyf4HJu$nE1qvGM3g$;u8(-?MINCT zeyR?B4&Ux1_v8KYg6!C_$EzjVpLB&hpQd?4KTnywgaW;1hNGqV{MODy!s_y+EWBeG zLd@I67baB3ft?7C1?LIHI{4F1gTAAx1DN;-Y;3*RV^}nK+VCtpk$-W7>#$7=Ry2> zXs19M*=oadc34Pp5XSJ!s8>@FSD2)*4`6_@&w*RJ74~vXYKtKzCA?#~>Z5+f70yts z%V628Ju5!5hUpBIrApX(aB967_(yMZ&dQoW-X40Fcv*NuSlc&co@{ahQ5^`Kk=ag( zWvN=jr~8#}1|vr;VM?BtcZZCu`)(}rfZ9Ez=2@|Q>DR}VwknwY>f#!!%45`AmvWbD6JtPmzX#* z3!=M7@nL|jjD5?BWMtOV;ED(<^M7WxtOXopk%DYov+us{!fxvk(t#S5SJx%iioj|Q z!}HbO&OjsQy(glq2eOMrf6NcQ;G&{1icNGigrK)=X2ZwFkXrL^W%566Y2#ojUQaSi zRRdni#Ld3zt6x18xKAhluFf{d?N4kA>^p1%d?f5Vc> z=YX4vYv6sM@pj0D1#()v=5neFXinWo(Hi_1To4#*vsx`Dql7ICl_7;oMZcP0dYibd zN>rUhUkiK@q!Y~2V@^gKRhU{lHmCuYW)iqQM^%|%MhojLwROB0rV7})1kHDVqkyoN z-)kO`I8D6V3STRNYsJT4j69{Czbfb#ILOV!yTO*I&(nP_%DJy`&n&rgI>e%Yb*gtZ z{T1pR=u|yrE}d2vM48=E?xA%YzgqPa9`QZAR<`?sUGQX$O(z^0TmD~J{j0*_np`+oZ#|kQZK;)dLAcA}k@I&2W zZ-wfM!RJb>;u1WR8^GlXL=Z7JQ9amIm=eDD zYMtr;{xor_NLt5OHL0|QYe4eB%08NotkVe4^@f}Vr=a{{GR?M^rc!WF&R!3YPW97v zXm}W}dVzqpu`U4l(hqm$A~ayTEAw0(34QQ#vC4xYxNzRCf9n9v>A4~j)_msncr2s< zi1qHbW)C$_gkS|^eZPM0Q20{t$vUv_9z}#{NqjmOk(J^-{-jsotutV~{@&LWv&Aay zj1YR;=>A@-RQn@K`3eW=_R5AZnOW@YIi<1U9LQ?Srip2odtaK^I*R=i;?0X7I?38d zwGpH_1X_n!xkN}4!%3^gYG+T9o=^;pi?r1?OGqjFa$-w|p9~YHu#}U~aF*K(aK1v$ zNapSD$0LG*7l;(+OuTie;vjrVX$#G?ERmW-X{oYk*G0Z6&83SW{b4X7`_$RWUamcL zt(ej&bNIn1P_+S~a~81^wf_0@MT_ph>85l;YQhxlf_}9G%gRxO_uB(I-3RdYl4+c0 zJ)|aZ?Of?Q;j77mw(+HCElTGz@(2(`)tMyd_m-YsB%kP({<`cQ&^~X$k}>0W*n<#R zx-DNW-T<)MG7Ff`^5~tJ4agA|!BY2X328aZJX!~7j2gV^o8j5K4MI?=rk%MRg^H(| z1butzp{du>Y4lbtNwFtOx%xF!H7V_EOI2(0-2kQ)J>BK22}eEwdc;k9wUE$@^`Is* zt#ns7C*DrTtly6?fd-FZ#^TZO+8(l#v>~SQA z?d;XGQ1CDMs{8I>WE|coS7B8Tn_H*0&ZKeGpW$r_0J38j-d6Ls@{xl995{BW_Pi;R zsn{<|{n^03vTM%lrYaWU!ML4MuVsOi_8qqKwP2qeF}WTmp<)2n7Q4Yj9tNf!Kr~JW za}K_n3t#LNcySVGxcuQ@JB6sQnQS~9Eqm&%3wqngyZKa6sYk1djD) zx%eFU)O>J0FtX7fH9j{Za-P2d@~WxQ86D*yUA=40K82B(VIM&>q;0M!=aS=o8LP(X zZt0u5QEyj!1hl z*hlwNwt3~W??)kfMQ$&(?9!k$N5i}!f2#He@AD*tRz#>ZC+U}Ur|Qb8rAyRf{uq&) z!bV~+YP9s2unZ_^PLZeT&c6h+g>fATMa8_D2pPf%)t=4fJ$P~uT07(3H^bo4Z2isC z*nSs4maL%gTJ()|`mgh8w$MPy2b*lE&(9yamVKcehQlD$gV4Zz2~mVO1jm<9j8z#r z6Fe-)Pv+?X$=`+r%FW<-f4C;;OgT88WKrm!2c-O1SJBWjw``z%!Y*c-JIcO(D*DE6 zs{hi%2>eq`MCl?)jh*;nb)(s$@fXx@8#NZ1gpbD$sLyw9WOOwkg~~22OL-v>wHS~_ z9PW@f2>Ic+@@z)TK)IvNFd^te;8lS5=owE;t3egBu z@C?Q(n!Av!79q#MKXO0#s0p@7;DF{s=U6G$C)XT8vf9I~Y1brGH%#w}r2a{zvZJTA z`)vE|&EHw=-}3e@vJ8TRE-dnDaBS?f%eoVjjxy`l|3hVRR%&;azy80=OqP3AHDY%I zrzMf`bmL&-Z3Khz9_tjgKEQDV+xSn#G`G6JT(Fuc>8Edp7o6b`{{V`_$0kHo5Jj(n zQe?(;1s2aloW#=S3J?8znQ@~A-HM(0ft!r|{TEj$e#mKCZ0Z(^gTB4b^r2*?J&uIZ z1RhYqqrjYXOTSs|47R6O5YWe!x&!$A63#91vWuYj4&LFyV}!8H_&nGw}VcpvE{=nHJw>NUfp9&p$o;J@^U=WU?W8ngA@PZ!2j z*3ef_(tV8vl)IYr7+aCV@N-+OaG*$6d7Q5RerFtoEuJcUp(C%S~_cgKnpF4oI-=KZFbbwtL+^i=_IU3~gpEmezUAjdufcfVj{Zbiq#dwqqu0p5SUFZ9kz!gsYv?X;pCP}#! z7Pn*?v&tsOD?$|)B)^A?*VBH2AIY%jVBeLHSpv+Ll{a~zbB*C8_=CEKW2vii5t^_R`fT8IGs;NHj@!uNJE> zp>6Uj1b+VTQOX^eiB}M>72NdgA^yZ+)_SCdqm7XVZvTomTxk|s!*gt)8xS4_KQRkQ zQg$+VHDPZI=f`gRdeBi|NRJOGqaB3JBvkE5!a8u)i*z&LInA?dOu4oP?;@Ys5bOo2 z`4uRC&s@P&Z0RNyj7$s=mJMt;0j_*hL_1M$+_NgoJ9>_rL0+gybtdXR@7wM zwB#(Y(u{;WP9=Mz<1?-k*H?zv3v5y^fPZwj0+C4aG{Ohu28vo`zQL>u!MW#yz0%8{ zQx**K;T4?*^cLkB#0|pR1h_9AV3T_3+gklXWHVFZWUp96p*iPCI|+9}5tVf*2n9wj zeL|*G=j?>PLhCvMipLG_xX&+m>5XPAu{nUBkoi6^ORU6Vyx`%p(8qX$g--r#U43(or8~jUpJQxjmHOD9ft`xp zF5Pci5F69?xRmh|OBGHz&o^n(a-hs)4KrGB}0JZuY|J zUxF7h3E9P?aHJ13nkRi|y>nlFw^hd~d_3ZV{8v_9;D0ZeVwAe(CQ6#Z19UkTZ~##>6aKXAUSxY)arc@`?Exi>aUPLV}8AxXMSY!SogS540_CA=un`2I9n(DNPXHgYf1w z?W`XnlnEm-y!AB@BO!u_J}6V)XVQJ;Qs(55jFUb`Mh#}3kGhoJT{|(7_y|Q%fyh(k za`ZZdRUMevtu&zMOknjDK2V^-GFHlIjYcdEcqIW=gKXy``xT}I6uGoD>Av8bX5pQ1 zV(Rrb`2B@0GFFoQ=(etpT=2OeHzRcxRA*EKObHQ|&v-Z7S{7uyr$6GMawjZ4d|x1H zp=&_>9}~?7I%R6pdeBM-#!2RR@N$-BLH@bPwu4~UJj=-noSR=q6AGvrEDDguPCK@Ol;n?SZWfL8<^q`&?!0>}q z=bMnNb!E+8g6*A!!QWsJ8c93mYt zIg^V%I7mk+!lC<$)jjoR7oBL=dn*w+VRg7TA!lD70Ryn*&H}^<+}`5YM9}Z4cZCAT z8A?5Bk_A3O#?lFX0hA5&lKQ!wnquI1j~liv2MgUX-c{RQx=yuohIkn#ify%<5tO>h zFwnZfExoLkPX+W-f0Y+dcSLH5A7`EO&X{~%vxTrKt`er&Q%#y z3i%cMR`k%vmeKWDLLx?K0S`9itc9^sY`m9RtZob6kc;#njC&9;U}Q6A)jOX;0pCEe ztFES@Z!RQ2x{yTQGOpa8C9|?CjOR-)2O|w2h+zwebml^Cu-e#1#WpWz#a9?%lva@t z_y3oFG^+iVT>6_~^Um*-%;+CTN2sMNf!Ea0UI5o-(^cYMrqtQP*7ZS$h{%!{SukFu@4 z>-)q_?h9D0AlkBo%#apmW}?)r`jzH6=!=Az#)^nk4S0;D)`Ah4xK_ZG!v z*C0}zC6`qa_0I!phgov2uxS_pF5gkQ7-?S=?`{!rNIj6-vQ#q5ST&w8P@SV!QPZo` zoABMxub}Uut#}4xc^yE9!C_BmKlj#qLfBTs`ass3u*slX!hTPH4UIV_{Zre(zj)ib zX*y*9M`m`1rW1uYc8O@1*rMDA^|3W7Lon{P&^d$_RYUGS-SHWR>ZGCfsIMN)*!6tL z6T6snzI-HDS}&TL=ax=*)xrTMVF$?3@kM!6HGE@n?YRylHa9ZR5;#@n zEuX>xEz13nHS?A=Re#K5)V!bOC!r?PA}slDs=-ZUZpQSm+{ad0RsI+OVjsAVfnk|# zb2jl5v{RuEXG$>lWi+eCn&=~2|Flx$>l>BKn#;-Aod?%c-c5K080c9LeinyQo_=sYfeNPAh6-3rTKFlSg z3{bd+U=araEdpFTQVaJNj|4{1AIr1Q29rG1n*Kc4Ra$C7f{Ub{&%+x+h3BF8zRX=1 zujPk;n|A6%5ovz9-yc3((6os69Lvn9ku&0ab>aIlWi%YL2uB}D_|_|Fdjw6M?56Hg z&;u&VL2@GP0u<~CViT=@A8e%KKu&Y^A$#J+Ag`P}i669EHDLZ6JhXTv$gVnJmM;ou z%v@0zU)dSUKwpBiY6@suVXl;)TxUi0kR`K^=LoHLlAX}wx|C}nFWgA0dH7D1kR>IynTxOOc^>>sZU!Dty>G5wTk+p z=nue;a(R0sq}49PC``WvG;imIY-l0u0{s(e9>Y0{%^8TN$p^~0k8oC4M8ivwDwB!< zKUsAgEu=885$g=s$;pa!(E{Zr2*a#o#EI2t2i2JMtD%ZhUG*e00qf3u7tI6WBwXR` zCxK#Duq{<8#sKYyz!fE~(wSR7PgIe0d0WR)AboKg&+G-vkf|Gm_Ft()sb>mX#ls^6 znEWpSXMto*KBA!~PSPOA0d4#SxoI0>@6l%>ctw)4Enzvs85ZvNxUjzG> z0%2`D-GO=++h!K}{DdubT?q9;PrYXW<$!TO9xVzfSbno*s@;j0Nf4{IabqM7YEx{q z73($wd*oIw@Eh$E?37PC)?a_Ncmj=0E9j>iD~J%aCC0-UEQ}3j=#LZiegg5fVvPu~ zevqy&We+MUAfAU)iZeg9vJ#M!;1sG-;z|XM3w>+?6xWcrv`S$Dec~6wGlCr461zUy zwl$T|*4vV^=LIx#bC$G%-d&39sK84tndTy4p(l05qVhQ@c$X){SemuGvJSOG{HJoQ z`T!bKBpL7djw<(PA$|+2&qe=hfBAne(OLto4|oU==W@Mk$rw>nv6&N`>3-|^gqH== z=8<&i2crP@^4_x0%IP0`j!*P?jh9c@-gUgZu&~v8=y{v$qDg88inc?>5!nq2$S>8> zkfu@tx+cx4pJ%GrM*h11Z->3UX+1^~&cz^xW&q?06be$z3t~CsjF&6{Mn6(%QNeNt ztFkF8Yh;uA=cer)?VMn>av-`j6O$Y#-B9y(7VNCV^c%;s|Z=Ru`+k^`1;m?|6D}-YxjtjfH1w?R4EGK4ERp7l3{K zZ@t4Izy9Ys0aAzgMWKU3@BgDB-ibxylX13blg>dm&Qu2>NiZ3CM2p z@jF39E6DIIeg7%pL(NU`2n25PK*MRnBH;?WFnJF{}$qwrfOpAW4``zc27ZL9SLW9^xUtm0)tUX>@ z@Q$L~+z?UGx238tue|>!Uhnh)hm!wd_7kLC_5amKq7~ehvjz;Qj;;5+Mnz02!|%M| zV4uv@(k`oK1qp$bb%n|2@?FmY?^FAI_v<|9?%Vsz0V|dNjUD>`(B=5wM%z_+1GM#s z4}h#>BohH2TRNp7x`NCP9N8JRGk82-HPA5hQN)es&qY7O#!`;VF5GsDtLg2q@JBbD zHpkpi`CLm@cA!DkMz(HQrRyuY=7dOitCFEc(V=O;VPztUtsOJ%V;4=6hHw$=_-}J8 zrj<8N4^EyuTR!-~)d4Fg^s4LsgsXf#r=IYuYNP-mR90GJk!p3V0tX5_ZTLC}I8tjw zLu}9PdKDzG*;G7-6WZG<#xmnSrc5D|+tPE0eU(`wwymNgc@ax_oK|V4&M=J72hS=8 zLn7?AqxRueBLij=6uz091jT;HQZgcp!&}OCF6|vWm)1Lfh0HW$rW(`nHs=I{{^Q}4CmmbpWmm8X;B2)wV2uQp` zH)xVChn$n9c+1bxwnG@f$T4}7Qnz;#LpO-0%M{p^vCF)@fW5uWmpq)hE3GT{ zipHaY4x{dOE*iTO6=a7&REbq)TZW9k9QfION8wVI_+&3F?Ni9R?T^fwc@*Hx4-Yb7II)c7MZmZ#2Nb7W{iPsQ^VP*b{T@16@mI>tq6`j zP%RWu1*C*g{a!99nW%*n6W7Rxb`C>y zJ1tHD;M|&-_9*X$^9f!#$Vsx8E`1++u-~go72U|Lv~40L@f>R4U_@^tAA>xSg-oPf zGH}2SA@!0m{w&{=#egQpP_1d-==R?561K*qP1O7O2g~Pc&%Xb4e$aB)i+8&Y zU=JXv|KH%A__uj1{v-cNr32_a*uMqo7dK@X0-5hk{_aE^?g*B=PmrE=t-YM!UJX8I zXiPq#@w0*1?Vs-ac&bh6bAb3Sz0v>u)WgDO$i9X>qbv-l9tEe98Y*flF;%{M`<&iC zs)?6k+!H3H)BVqCs&OA0LqkG;TKnY2z!&FQTNH9^C+gDppp=zJ(0@CrH0cRPZvA4h zkZw8!bGfC7{oRoH`$BUESdlOft1Yel1OAXq0h((>Z$z%F*j|3z97_w>De-80SZ^ywqt6ABwd;56J%@OfAz!w< zlrC=lq@sp!s*-HFVeb{XF)@W}6e%QZfe~@gc3^5Q##v_XH6Hm3Qr%>8T1ZTS5ldhW zmVHMZ@0lg7ghe}-r~*;`uL9Y&><}3V19;+TN)5=pPCDF$M75cz2U2Q+D3n>)dLx=5 zHMbz&^77AMM(|U1J3aQDWG}~uNtPTXZms=;iXZHayuSE?p8@YChNji;86GEPd^F`7 z&3H>CgifV~;z>~|yTs+`apv^(We=A4(rL1!IzAk|La`aLxC>nlD@8a*aZQZ`?5U z-T0_3QXS(hpH1Xo8&Er5{uAR4GH7;%gS$iYQq3O(vpp zI8R`8S)ssfZ;00*lJVByiBDYgII+szaGjfLu#w5}o3()pqz&gOP47|PO#WvVfXaWC z9{~ZN8NPcFZ((_;6;`7)lxAI)^q$$8`TUFX>Yh(=FP&6ji=k}~bi>U|q?9JZ@wAdmSF*se?&#VU0n_Hte!_O( zU64pGzw~2^`9=ABEYk@xio*_HUs&*6mD0i(4s(?GN%PHJ-2s8caAXTbS09 zWncc>*~qi)#XHN!KjEE=ki2oQ;GGB|D1#>WnhG-LeUx<^j=mT|vy~%iv(VM7jgJ1O zs1u;}0*;oD;^@2K@seO;4UmjPQhzFXNc>iDajEzszem3G8(|MJ-icSEf9tW6%_IMS zXNnWOcum{HJV+kA2mT`ckY6SQvySx7u@jU=J=RXBG1^HfCyl;oke~uz$BHaZ^;)qL zh^z2uDMw%7-lN>C2=AfNPRhxvCm5^di0c5RFC1ME!#aj^8DW!NzyUIHEH$+L=jwUo zdaye6B9xw0M!d7j9~>=KIYf-bu2a~@$lP3;$W?In4k8f3q`bp@fOjvD zx25k2EoMFj*PId>ajD1q$9@^F+RY0Qj$+%6Mr4`Z4i>99O33Ju9kI<(G2)_IO<}zvyvY9(XEiQTukkj1RLI@dIU~)_B&+%Vc|E?HL$+r1m zVDiPZckNcON%73xZp{AcXO|<>?uNdfa8+Ht4z&_FI^*xdIAYwF<=#UE#EOx#GS(q0 zz!h9r;Dw+4FP#qm8JV0P_V@8+w9`#N?{;f9U_PUd6wYc#>mKz^p5EBAeEv+JC`|kF zTm8F7$}azPMaip}3|(V0aWaZ}IVWedFH&tLdHt3lgsHKJEJj&9uGnWaG1w_A!4+>B zcuT=(8_unK`bm_6w${O%O6hxc_r^D`vzLy5nSbn7S<}NDvu@v`rRSy-?regJ_86JE z&i;X8UfdiP_wHHeV0H1)K!({Xa%gDiS?~KH!J$~KpVp=jF6f^2x#sQR9C(YL|BN(d zdTaIDZlSfD0K2r9aBexgY5FbM)@Na#sqbH42ERpbgO6|w%WbX zCbMinZs&UcvSs>3&W#)C^7gPT4hM3!U$zV<<>swtpiG6)Q+eCcUSCcAdh8+B^;A|! z%(h>@M>p5Wj1NavW$4a~Y}wm>_uZfRz7_BucXS2KeEQmr@wVAW>(8_<9zM0h=F()7 z=b!eVp}9ae_;{AfOS8MZXO69jwpXOSkLr13b?nDOVvh77xN*#S-SHZ4vBj`Typ7Nl zX1c%3oyH~zogL@t}*gX10=XyfV)4B$$ z(ZZ`)t1LDgu^P!JDDQT>AY`6RlBgT^^zA=XIo-U^?>fFEGJf(z%L;s8o!;EX^lDss zxBF;PvStjIt~!^o&~ASR9USy$c+?BSzArS~*z>8KD80#7^gQfr>;s1t2l90Ow30s= zo~-DI`N~*Wb(1 zSZi8d&-e{ZRn7q&)7MhNTAAA1)&0YImzu}Y^hQ579L!7KfIIPvX35k&#~;sav3c)x z=hF-1;F8r(kN(o4ld?W1SLf2T#O?c!CRMl==xiO;Fumg#mhZm)%mheQ;v@3$x2tyd zM7jDl#&N7&2wK?uOWQ=Bm_PORZ^E%-zJDIl{rJ8@Xok+o!NcQ!Nd@=Sh?lGGq}gwO zG+{)hEe^5Cj41{5R~ClFdm*n$`##4c(Z7BTv)A6{4M@Lcuo%(b;IAHRrEN_&K}TtTlMf$%I_#&pG`r6i1P7z?;We| zXaYCrLP;$)KkVQx6LQi;)mK7;>6d*IH+TNLt3x2Tv6qX^c@yzYGcN6lA9MSjL&NOO z1D;78mkch%euzk3d#hkyQ}f-*j<9cjf7XZc2&IP?W%>LLsxaf&laB9nn>GAGB2;c^XMzV zVr=<^{JonN6EcNc_n#J#l4`#n@2e;d_v*9XQO@(SFMLYV{Y2a@L2PJcen7y0`dj-Y zdZGB!cVE73L;FXLDE>rj0;VK_d!1!#F=7_i6H}fGWR&#VGe27yAIKBAKWlg&w9__4 zvRAy_;1wnL4A#tFX0qkU(yET1lE`N_bnobNBh^^?ZFE-_dxj)IJBEY4?`AE$-8WqB zUh;EfW|ErolRyq!a{X;cqK1)Gvd`N;>P3Uc`$KQI9GG7++gjQ5{V#VSN>&veB-LL0 zYO~JDu)|g-c*mB)%3IrNPj$8I53M^m!t5>A$>ff#o>EBn<{bQVwdRvk_!?n=$=%C1 z#pL$(UGJYTfnWEZPdZ+(a*xiwi0w(kWxpwZ*bw-{>hQIafdks3UO$`v@}N3(?6E%$|w#H*e5 z{ONl{zeC*_*G|e4&%PfW&tB@UOc~GG`Ps1KNuuHNz2ok~D}4iff{P-4c{cFFwIls8 z@wanN?p$g4%%yE|?=EY5s&!Ge{Puw_hhH>NOAIf}wM@Fx$0Gl)_OAV(>HUw7I3oAl z4vB-xHH^sRpyrYxcVW5ATDgpALl|wuY7t@*Emo+F+#+#s+=@^x8Omt7q1@?qta3i; zoFk6&7kt0(AGXJ5k3FBC*Wx*K0x#c0=h)|w;`8*R% zSk)T7T`{;rBI?VxGhW&1jfvN)E$&k|YOqAE8pVk>zD(R_s_qIrLHBB)0ZM;!>TO}l z+)#0BqvxCOppVze&ncHKKRuDfL9yzy__|>H^e*}hR>WQ6}D_=32hoe)D4ITEgchcS??nc$%RN`JPmxNwNDb)LmWe#h6 zV4XSyz2wx6tuyR^*g6@rYJH>{o^4IpNh{4x*WQ_zDCL-49N*!ZZ2GJ)-zGT_a_m4& zx^5fH^W{)SS)=)#rH7=C(^V#LJ7$DO=tmfPC3*kM+m z^_XtZ(p77b`+(4X`6>f;_7?4kM{>Vz7`{h#XHS!QD4m1j;eI^%+6Q?O)lI4h$tS}o zovRivh)=jZ4jCRb!r%%!B8;;IwsN*>k-&x9l=HoU%L67;)6exkHZu;W_GJ3xtA5H; zK~Tq)9Gf3Nty_X~Pl?T3Csq|wSi??i`Uu>f1LrKL$|DwB+@N6^!F#^SCUruefIl}m za6rMl=sem~S0UdpJaeQX^yc8DFClN<&3(j&R;R}g8}fn;;$=$sA6=Nz-5V#^fl!<5 zht^Y#m!8pHDlR^GjDyy3LCV-tQZ;xKVygX1s^07VINt`mZgd30KB`Ha1`)Lrlqt)2 zfQVL`Mck2n?;f*)=M855DjdSv^IU$V8!r(g9B~#~J0Na6q0@Jt70@ttk;em-FF4MX z$M7%FWpkot2p1ujoJ}QBo#=zum|0@Z1ooNG*&&jc%%{)X7NMCOfz4EH6gO4mg@og_L91qZbL<=j~Xsvbh!ZpdtNJ;~b^j8rH3k zMrRSp?Dsg&a!bN5!!Z5n13&YKygWUF_Hvi$yAXAxl_Jnc>r-#@VX14551}7-OoESk zr`#x;MCs;flv!228CYC4ym~fc{%u}!(yKks6lX`dYV~sW1`3u*jH)vE@XWmXk>yb* zP6$st2i+r*@|_hXS=f%vXZS@7PN@huX+!|CTo|;K;~(2UcxdSADMS7`7l$%+GxsZC zIrtwMl#J#OrWQYOZp#Nj;w*`xD?>DM(=L#ScCP?gS61%suW*2Y%?A&Cc$YuZ7Ctz0 zTc%@0|BEA;F+(p+mr(d^OsC@VO$81#WcIW6_EdIRrMToIS~D=mq6hN} zm}3aMcjqsY(rF$xr0Rs>F$%H=D&iM>u0N7|O_`Ed=bINauap(A?AStTnn>%(skkO( zSPENa-H0lemD~AYf*5yac|FND0Gq>ozt&P2jbYXIg1`Ve*x)6LJiRNlEC83^pnaaM;QzO=4f2KDMd3|5s&-VOJ zo)QaEi+@siEBA{DoY#z|Bt!I*JKWI!q~u+ZerRa$v^2lG`ccJMSyuupsf6@S^2GqT z=7dxv4`uW?&i$dp(iN2#TXBv&v@DKN#x6WtXuuLu9itPC&oRCy_vDI$ep{Z0R6e@f z^f{tZ=oE46Hht>)aOUoV-CeT!1`>>4HC5caGVB6Ai!3Hhl=P04x6UWOd(mp#VEsp@ zZ6Ne_Kq{5l>=dSL;NRj1)HjY31QPkNz7c2)^8W?CHiHw=9c@~*#1dKDE07M{l`4em zL1tWM?hZaTG1fs$9XGQ;sV`*ZS=DGD@_bs>D++rQtJ&ekz9DGR>`r%0d2x;k5*s>L z6r7UV6&`tkbLz6_twBD(#WKn5{%eaiSm&KMt9+Q&;dlXKYmZLmju}djO53G5DqL?- z@b!J@6`}!tA8Z8twY!?I{h-qb0TFLN#Q8osY#wILXYvbE$XN)~tj*{pU?kcdlN%cZ zZ4r14DROq@#o6BF?39xesAj8^n7I;4ve%HssNcEgOYNV+u`9aC($Imq^IKHG%_{m$ z6utYFnEaB`qw@}~>5zF;^RdYB$-5ppQ+lTtGPzD-KNU}oOd1KCxiDkKWuhv}W2B;) z#H_83g_cpZ*?nywS` z0do3pHGt2<0JXY`+5&Yb415DnbP#Y90^$!32w5$|W9eNX3G!mZk9Ny~39&rL9rO$P zc{9TL!r36M>^}2eP{8nAO>spo{oOG0`JOEfP7Ofo`da~mRe^hnK)yx*1!Yw|SwCQa zbz~SaB+SPpDin$JT1}vX&3b~^hc3B10Kq7b@NMhRjleB^Fh~?ubMNZwM&N$Vr;m34 zz;VEQ36O4n7$1RHfZy#q_7oZusji7 zPd8x!Z^rU2JN_6Ls99<6jV$|sC3h1Roy}Nm-Rchj_PL-Dz&Lh&ELYVwW%;I%eb38( zyA-eGrC;|yJ&QLcccZ$xM&f(51qeaE&21c5pWen`Pz{o6+L`u*@nbkWTp(0}ia8{r$5u{8p4(YLtQEou%n+`yDs QEq@`<05I!(i&wAy1le=pdH?_b diff --git a/examples/searchcommands_app/bash-prologue b/examples/searchcommands_app/bash-prologue deleted file mode 100644 index 12a06b4d8..000000000 --- a/examples/searchcommands_app/bash-prologue +++ /dev/null @@ -1,43 +0,0 @@ -########### -# Variables -########### - -declare -r scriptRoot="$(cd "$(dirname "$1")" && pwd)" -declare -r scriptName="$(basename "$1")" -declare -r scriptLongOptions="$2" -declare -r scriptOptions="$3" - -shift 3 - -########### -# Functions -########### - -function usage { - - man "${scriptName}" - exit 0 -} - -function error { - echo "${scriptName} error: $2" 1>&2 - exit $1 -} - -########### -# Constants -########### - -# useful for printing text to console... - -declare -r b="$(tput bold)" ; # bold -declare -r n="$(tput sgr0)" ; # normal -declare -r u="$(tput smul)" ; # underline -declare -r u_="$(tput rmul)" ; # underline off (neither $n nor $b defeat $u) - -########### -# Arguments -########### - -declare args=$(getopt --name "$scriptName" --options "$scriptOptions" --longoptions "$scriptLongOptions" -- $* || exit 1) -set -o errexit -o nounset diff --git a/examples/searchcommands_app/setup.py b/examples/searchcommands_app/setup.py deleted file mode 100755 index 24b124e0f..000000000 --- a/examples/searchcommands_app/setup.py +++ /dev/null @@ -1,489 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 -# -# Copyright © Splunk, Inc. All rights reserved. - -from __future__ import absolute_import, division, print_function -import os - - -if os.name == 'nt': - - def patch_os(): - import ctypes - - kernel32 = ctypes.windll.kernel32 - format_error = ctypes.FormatError - - create_hard_link = kernel32.CreateHardLinkW - create_symbolic_link = kernel32.CreateSymbolicLinkW - delete_file = kernel32.DeleteFileW - get_file_attributes = kernel32.GetFileAttributesW - remove_directory = kernel32.RemoveDirectoryW - - def islink(path): - attributes = get_file_attributes(path) - return attributes != -1 and (attributes & 0x400) != 0 # 0x400 == FILE_ATTRIBUTE_REPARSE_POINT - - os.path.islink = islink - - def link(source, link_name): - if create_hard_link(link_name, source, None) == 0: - raise OSError(format_error()) - - os.link = link - - def remove(path): - attributes = get_file_attributes(path) - if attributes == -1: - success = False - elif (attributes & 0x400) == 0: # file or directory, not symbolic link - success = delete_file(path) != 0 - elif (attributes & 0x010) == 0: # symbolic link to file - success = delete_file(path) != 0 - else: # symbolic link to directory - success = remove_directory(path) != 0 - if success: - return - raise OSError(format_error()) - - os.remove = remove - - def symlink(source, link_name): - if create_symbolic_link(link_name, source, 1 if os.path.isdir(source) else 0) == 0: - raise OSError(format_error()) - - os.symlink = symlink - - patch_os() - del locals()['patch_os'] # since this function has done its job - -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))) - -try: - from collections import OrderedDict -except: - from splunklib.ordereddict import OrderedDict - -from splunklib import six -from splunklib.six.moves import getcwd -from glob import glob -from itertools import chain -from setuptools import setup, Command -from subprocess import CalledProcessError, check_call, STDOUT - -import pip -import shutil -import sys - -project_dir = os.path.dirname(os.path.abspath(__file__)) - -# region Helper functions - - -def install_packages(app_root, distribution): - requires = distribution.metadata.requires - - if not requires: - return - - target = os.path.join(app_root, 'lib', 'packages') - - if not os.path.isdir(target): - os.mkdir(target) - - pip.main(['install', '--ignore-installed', '--target', target] + requires) - return - - -def splunk(*args): - check_call(chain(('splunk', ), args), stderr=STDOUT, stdout=sys.stdout) - return - - -def splunk_restart(uri, auth): - splunk('restart', "-uri", uri, "-auth", auth) - -# endregion - -# region Command definitions - - -class AnalyzeCommand(Command): - """ - setup.py command to run code coverage of the test suite. - - """ - description = 'Create an HTML coverage report from running the full test suite.' - - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - try: - from coverage import coverage - except ImportError: - print('Could not import the coverage package. Please install it and try again.') - exit(1) - return - c = coverage(source=['splunklib']) - c.start() - # TODO: instantiate and call TestCommand - # run_test_suite() - c.stop() - c.html_report(directory='coverage_report') - - -class BuildCommand(Command): - """ - setup.py command to create the application package file. - - """ - description = 'Package the app for distribution.' - - user_options = [ - ('build-number=', None, - 'Build number (default: private)'), - ('debug-client=', None, - 'Copies the file at the specified location to package/bin/_pydebug.egg and bundles it and _pydebug.conf ' - 'with the app'), - ('force', 'f', - 'Forcibly build everything'), - ('scp-version=', None, - 'Specifies the protocol version for search commands (default: 2)')] - - def __init__(self, dist): - - Command.__init__(self, dist) - - package = self.distribution.metadata - - self.package_name = '-'.join((package.name, package.version)) - self.build_base = os.path.join(project_dir, 'build') - self.build_dir = os.path.join(self.build_base, package.name) - self.build_lib = self.build_dir - - self.build_number = 'private' - self.debug_client = None - self.force = None - self.scp_version = 1 - - return - - def initialize_options(self): - return - - def finalize_options(self): - - self.scp_version = int(self.scp_version) - - if not (self.scp_version == 1 or self.scp_version == 2): - raise SystemError('Expected an SCP version number of 1 or 2, not {}'.format(self.scp_version)) - - self.package_name = self.package_name + '-' + six.text_type(self.build_number) - return - - def run(self): - - if self.force and os.path.isdir(self.build_dir): - shutil.rmtree(self.build_dir) - - self.run_command('build_py') - self._copy_package_data() - self._copy_data_files() - - if self.debug_client is not None: - try: - shutil.copy(self.debug_client, os.path.join(self.build_dir, 'bin', '_pydebug.egg')) - debug_conf = os.path.join(project_dir, 'package', 'bin', '_pydebug.conf') - if os.path.exists(debug_conf): - shutil.copy(debug_conf, os.path.join(self.build_dir, 'bin', '_pydebug.conf')) - except IOError as error: - print('Could not copy {}: {}'.format(error.filename, error.strerror)) - - install_packages(self.build_dir, self.distribution) - - # Link to the selected commands.conf as determined by self.scp_version (TODO: make this an install step) - - commands_conf = os.path.join(self.build_dir, 'default', 'commands.conf') - source = os.path.join(self.build_dir, 'default', 'commands-scpv{}.conf'.format(self.scp_version)) - - if os.path.isfile(commands_conf) or os.path.islink(commands_conf): - os.remove(commands_conf) - elif os.path.exists(commands_conf): - message = 'Cannot create a link at "{}" because a file by that name already exists.'.format(commands_conf) - raise SystemError(message) - - shutil.copy(source, commands_conf) - self._make_archive() - return - - def _copy_data_files(self): - for directory, path_list in self.distribution.data_files: - target = os.path.join(self.build_dir, directory) - if not os.path.isdir(target): - os.makedirs(target) - for path in path_list: - for source in glob(path): - if os.path.isfile(source): - shutil.copy(source, target) - pass - pass - pass - return - - def _copy_package_data(self): - for directory, path_list in six.iteritems(self.distribution.package_data): - target = os.path.join(self.build_dir, directory) - if not os.path.isdir(target): - os.makedirs(target) - for path in path_list: - for source in glob(path): - if os.path.isfile(source): - shutil.copy(source, target) - pass - pass - pass - return - - def _make_archive(self): - import tarfile - - build_dir = os.path.basename(self.build_dir) - archive_name = self.package_name + '.tar' - current_dir = getcwd() - os.chdir(self.build_base) - - try: - # We must convert the archive_name and base_dir from unicode to utf-8 due to a bug in the version of tarfile - # that ships with Python 2.7.2, the version of Python used by the app team's build system as of this date: - # 12 Sep 2014. - tar = tarfile.open(str(archive_name), 'w|gz') - try: - tar.add(str(build_dir)) - finally: - tar.close() - gzipped_archive_name = archive_name + '.gz' - if os.path.exists(gzipped_archive_name): - os.remove(gzipped_archive_name) - os.rename(archive_name, gzipped_archive_name) - finally: - os.chdir(current_dir) - - return - - -class LinkCommand(Command): - """ - setup.py command to create a symbolic link to the app package at $SPLUNK_HOME/etc/apps. - - """ - description = 'Create a symbolic link to the app package at $SPLUNK_HOME/etc/apps.' - - user_options = [ - ('debug-client=', None, 'Copies the specified PyCharm debug client egg to package/_pydebug.egg'), - ('scp-version=', None, 'Specifies the protocol version for search commands (default: 2)'), - ('splunk-home=', None, 'Overrides the value of SPLUNK_HOME.')] - - def __init__(self, dist): - Command.__init__(self, dist) - - self.debug_client = None - self.scp_version = 2 - self.splunk_home = os.environ['SPLUNK_HOME'] - self.app_name = self.distribution.metadata.name - self.app_source = os.path.join(project_dir, 'package') - - return - - def initialize_options(self): - pass - - def finalize_options(self): - - self.scp_version = int(self.scp_version) - - if not (self.scp_version == 1 or self.scp_version == 2): - raise SystemError('Expected an SCP version number of 1 or 2, not {}'.format(self.scp_version)) - - return - - def run(self): - target = os.path.join(self.splunk_home, 'etc', 'apps', self.app_name) - - if os.path.islink(target): - os.remove(target) - elif os.path.exists(target): - message = 'Cannot create a link at "{}" because a file by that name already exists.'.format(target) - raise SystemError(message) - - packages = os.path.join(self.app_source, 'lib') - - if not os.path.isdir(packages): - os.mkdir(packages) - - splunklib = os.path.join(packages, 'splunklib') - source = os.path.normpath(os.path.join(project_dir, '..', '..', 'splunklib')) - - if os.path.islink(splunklib): - os.remove(splunklib) - - os.symlink(source, splunklib) - - self._link_debug_client() - install_packages(self.app_source, self.distribution) - - commands_conf = os.path.join(self.app_source, 'default', 'commands.conf') - source = os.path.join(self.app_source, 'default', 'commands-scpv{}.conf'.format(self.scp_version)) - - if os.path.islink(commands_conf): - os.remove(commands_conf) - elif os.path.exists(commands_conf): - message = 'Cannot create a link at "{}" because a file by that name already exists.'.format(commands_conf) - raise SystemError(message) - - os.symlink(source, commands_conf) - os.symlink(self.app_source, target) - - return - - def _link_debug_client(self): - - if not self.debug_client: - return - - pydebug_egg = os.path.join(self.app_source, 'bin', '_pydebug.egg') - - if os.path.exists(pydebug_egg): - os.remove(pydebug_egg) - - os.symlink(self.debug_client, pydebug_egg) - - -class TestCommand(Command): - """ - setup.py command to run the whole test suite. - - """ - description = 'Run full test suite.' - - user_options = [ - ('commands=', None, 'Comma-separated list of commands under test or *, if all commands are under test'), - ('build-number=', None, 'Build number for the test harness'), - ('auth=', None, 'Splunk login credentials'), - ('uri=', None, 'Splunk server URI'), - ('env=', None, 'Test running environment'), - ('pattern=', None, 'Pattern to match test files'), - ('skip-setup-teardown', None, 'Skips test setup/teardown on the Splunk server')] - - def __init__(self, dist): - Command.__init__(self, dist) - - self.test_harness_name = self.distribution.metadata.name + '-test-harness' - self.uri = 'https://localhost:8089' - self.auth = 'admin:changeme' - self.env = 'test' - self.pattern = 'test_*.py' - self.skip_setup_teardown = False - - return - - def initialize_options(self): - pass # option values must be initialized before this method is called (so why is this method provided?) - - def finalize_options(self): - pass - - def run(self): - import unittest - - if not self.skip_setup_teardown: - try: - splunk( - 'search', '| setup environment="{0}"'.format(self.env), '-app', self.test_harness_name, - '-uri', self.uri, '-auth', self.auth) - splunk_restart(self.uri, self.auth) - except CalledProcessError as e: - sys.exit(e.returncode) - - current_directory = os.path.abspath(getcwd()) - os.chdir(os.path.join(project_dir, 'tests')) - print('') - - try: - suite = unittest.defaultTestLoader.discover('.', pattern=self.pattern) - unittest.TextTestRunner(verbosity=2).run(suite) # 1 = show dots, >1 = show all - finally: - os.chdir(current_directory) - - if not self.skip_setup_teardown: - try: - splunk('search', '| teardown', '-app', self.test_harness_name, '-uri', self.uri, '-auth', self.auth) - except CalledProcessError as e: - sys.exit(e.returncode) - - return - -# endregion - -current_directory = getcwd() -os.chdir(project_dir) - -try: - setup( - description='Custom Search Command examples', - name=os.path.basename(project_dir), - version='1.6.16', - author='Splunk, Inc.', - author_email='devinfo@splunk.com', - url='http://github.com/splunk/splunk-sdk-python', - license='http://www.apache.org/licenses/LICENSE-2.0', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Other Environment', - 'Intended Audience :: Information Technology', - 'License :: Other/Proprietary License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: System :: Logging', - 'Topic :: System :: Monitoring'], - packages=[ - 'lib.splunklib', 'lib.splunklib.searchcommands' - ], - package_dir={ - 'lib': os.path.join('package', 'lib'), - 'lib.splunklib': os.path.join('..', '..', 'splunklib'), - 'lib.splunklib.searchcommands': os.path.join('..', '..', 'splunklib', 'searchcommands') - }, - package_data={ - 'bin': [ - os.path.join('package', 'bin', 'app.py'), - os.path.join('package', 'bin', 'countmatches.py'), - os.path.join('package', 'bin', 'filter.py'), - os.path.join('package', 'bin', 'generatehello.py'), - os.path.join('package', 'bin', 'generatetext.py'), - os.path.join('package', 'bin', 'simulate.py'), - os.path.join('package', 'bin', 'sum.py') - ] - }, - data_files=[ - ('README', [os.path.join('package', 'README', '*.conf.spec')]), - ('default', [os.path.join('package', 'default', '*.conf')]), - ('lookups', [os.path.join('package', 'lookups', '*.csv.gz')]), - ('metadata', [os.path.join('package', 'metadata', 'default.meta')]) - ], - requires=[], - - cmdclass=OrderedDict(( - ('analyze', AnalyzeCommand), - ('build', BuildCommand), - ('link', LinkCommand), - ('test', TestCommand)))) -finally: - os.chdir(current_directory) diff --git a/setup.py b/setup.py index 903a1407e..93540373b 100755 --- a/setup.py +++ b/setup.py @@ -130,84 +130,6 @@ def run(self): run_test_suite_with_junit_output() -class DistCommand(Command): - """setup.py command to create .spl files for modular input and search - command examples""" - description = "Build modular input and search command example tarballs." - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - @staticmethod - def get_python_files(files): - """Utility function to get .py files from a list""" - python_files = [] - for file_name in files: - if file_name.endswith(".py"): - python_files.append(file_name) - - return python_files - - def run(self): - # Create random_numbers.spl and github_forks.spl - - app_names = ['random_numbers', 'github_forks'] - splunklib_arcname = "splunklib" - modinput_dir = os.path.join(splunklib_arcname, "modularinput") - - if not os.path.exists("build"): - os.makedirs("build") - - for app in app_names: - with closing(tarfile.open(os.path.join("build", app + ".spl"), "w")) as spl: - spl.add( - os.path.join("examples", app, app + ".py"), - arcname=os.path.join(app, "bin", app + ".py") - ) - - spl.add( - os.path.join("examples", app, "default", "app.conf"), - arcname=os.path.join(app, "default", "app.conf") - ) - spl.add( - os.path.join("examples", app, "README", "inputs.conf.spec"), - arcname=os.path.join(app, "README", "inputs.conf.spec") - ) - - splunklib_files = self.get_python_files(os.listdir(splunklib_arcname)) - for file_name in splunklib_files: - spl.add( - os.path.join(splunklib_arcname, file_name), - arcname=os.path.join(app, "bin", splunklib_arcname, file_name) - ) - - modinput_files = self.get_python_files(os.listdir(modinput_dir)) - for file_name in modinput_files: - spl.add( - os.path.join(modinput_dir, file_name), - arcname=os.path.join(app, "bin", modinput_dir, file_name) - ) - - spl.close() - - # Create searchcommands_app--private.tar.gz - # but only if we are on 2.7 or later - if sys.version_info >= (2,7): - setup_py = os.path.join('examples', 'searchcommands_app', 'setup.py') - - check_call(('python', setup_py, 'build', '--force'), stderr=STDOUT, stdout=sys.stdout) - tarball = 'searchcommands_app-{0}-private.tar.gz'.format(self.distribution.metadata.version) - source = os.path.join('examples', 'searchcommands_app', 'build', tarball) - target = os.path.join('build', tarball) - - shutil.copyfile(source, target) - - return - setup( author="Splunk, Inc.", @@ -215,8 +137,7 @@ def run(self): cmdclass={'coverage': CoverageCommand, 'test': TestCommand, - 'testjunit': JunitXmlTestCommand, - 'dist': DistCommand}, + 'testjunit': JunitXmlTestCommand}, description="The Splunk Software Development Kit for Python.", From cb993686d5c0b65ccc2d9e22ab92ad3986e116cd Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Fri, 22 Oct 2021 15:58:06 -0700 Subject: [PATCH 04/39] Remove build step from ci --- .github/workflows/test.yml | 8 +------- tests/test_examples.py | 4 ---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 123eb5045..eaf324152 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,13 +45,7 @@ jobs: echo password=changed! >> .splunkrc echo scheme=https >> .splunkrc echo version=${{ matrix.splunk }} >> .splunkrc - - name: Create build dir for ExamplesTestCase::test_build_dir_exists test case - run: | - cd ~ - cd /home/runner/work/splunk-sdk-python/splunk-sdk-python/ - python setup.py build - python setup.py dist - name: Install tox run: pip install tox - name: Test Execution - run: tox -e py + run: make test diff --git a/tests/test_examples.py b/tests/test_examples.py index 3b63fc6da..b187dbbbd 100755 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -36,7 +36,6 @@ DIR_PATH = os.path.dirname(os.path.realpath(__file__)) EXAMPLES_PATH = os.path.join(DIR_PATH, '..', 'examples') -BUILD_PATH = os.path.join(DIR_PATH, '..', 'build') def check_multiline(testcase, first, second, message=None): """Assert that two multi-line strings are equal.""" @@ -96,9 +95,6 @@ def test_async(self): except: pass - def test_build_dir_exists(self): - self.assertTrue(os.path.exists(BUILD_PATH), 'Run setup.py build, then setup.py dist') - def test_binding1(self): result = run("binding1.py") self.assertEqual(result, 0) From b583f8cb89226d5ad66beb8b8036777796281974 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Fri, 22 Oct 2021 16:13:20 -0700 Subject: [PATCH 05/39] Revert make command in CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaf324152..71ed1e667 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,4 +48,4 @@ jobs: - name: Install tox run: pip install tox - name: Test Execution - run: make test + run: tox -e py From c142bfd6f2bb9ce48b0b1dd47473d91c21c2b247 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Tue, 26 Oct 2021 11:58:13 -0700 Subject: [PATCH 06/39] Move modinputs to bin, cleanup setup.py imports --- examples/random_numbers/{ => bin}/random_numbers.py | 0 setup.py | 4 ---- 2 files changed, 4 deletions(-) rename examples/random_numbers/{ => bin}/random_numbers.py (100%) diff --git a/examples/random_numbers/random_numbers.py b/examples/random_numbers/bin/random_numbers.py similarity index 100% rename from examples/random_numbers/random_numbers.py rename to examples/random_numbers/bin/random_numbers.py diff --git a/setup.py b/setup.py index 93540373b..284c50983 100755 --- a/setup.py +++ b/setup.py @@ -15,13 +15,9 @@ # under the License. from setuptools import setup, Command -from contextlib import closing -from subprocess import check_call, STDOUT import os import sys -import shutil -import tarfile import splunklib From 5c8b2565f684bfe212de57b1a55ee0e56221d752 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Tue, 26 Oct 2021 12:01:14 -0700 Subject: [PATCH 07/39] Move mod inputs to bin, add splunklib dependency --- docker-compose.yml | 2 ++ examples/github_forks/{ => bin}/github_forks.py | 0 2 files changed, 2 insertions(+) rename examples/github_forks/{ => bin}/github_forks.py (100%) diff --git a/docker-compose.yml b/docker-compose.yml index a93a14c0a..6885cfd5f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,9 @@ services: - SPLUNK_APPS_URL=https://github.com/splunk/sdk-app-collection/releases/download/v1.1.0/sdkappcollection.tgz volumes: - ./examples/github_forks:/opt/splunk/etc/apps/github_forks + - ./splunklib:/opt/splunk/etc/apps/github_forks/lib/splunklib - ./examples/random_numbers:/opt/splunk/etc/apps/random_numbers + - ./splunklib:/opt/splunk/etc/apps/random_numbers/lib/splunklib - ./examples/searchcommands_app/package:/opt/splunk/etc/apps/searchcommands_app - ./splunklib:/opt/splunk/etc/apps/searchcommands_app/lib/splunklib - ./examples/twitted/twitted:/opt/splunk/etc/apps/twitted diff --git a/examples/github_forks/github_forks.py b/examples/github_forks/bin/github_forks.py similarity index 100% rename from examples/github_forks/github_forks.py rename to examples/github_forks/bin/github_forks.py From 8da1679bfcb18238ad8b67e34151d2377b259f86 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Tue, 26 Oct 2021 12:26:23 -0700 Subject: [PATCH 08/39] Random numbers mod input example working --- examples/random_numbers/README.md | 8 ++++++++ examples/random_numbers/bin/random_numbers.py | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 examples/random_numbers/README.md diff --git a/examples/random_numbers/README.md b/examples/random_numbers/README.md new file mode 100644 index 000000000..90172dff7 --- /dev/null +++ b/examples/random_numbers/README.md @@ -0,0 +1,8 @@ +splunk-sdk-python random_numbers example +======================================== + +This app provides an example of a modular input that generates a random number between the min and max values provided by the user during setup of the input. + +To run this example locally run `SPLUNK_VERSION=latest docker compose up -d` from the root of this repository which will mount this example alongside the latest version of splunklib within `/opt/splunk/etc/apps/random_numbers` and `/opt/splunk/etc/apps/random_numbers/lib/splunklib` within the `splunk` container. + +Once the docker container is up and healthy log into the Splunk UI and setup a new `Random Numbers` input by visiting this page: http://localhost:8000/en-US/manager/random_numbers/datainputstats and selecting the "Add new..." button next to the Local Inputs > Random Inputs. If no Random Numbers input appears then the script is likely not running properly, see https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/ModInputsDevTools for more details on debugging the modular input using the command line and relevant logs. \ No newline at end of file diff --git a/examples/random_numbers/bin/random_numbers.py b/examples/random_numbers/bin/random_numbers.py index f0727f0dd..b9673db99 100755 --- a/examples/random_numbers/bin/random_numbers.py +++ b/examples/random_numbers/bin/random_numbers.py @@ -17,6 +17,10 @@ from __future__ import absolute_import import random, sys import os +# NOTE: splunklib must exist within random_numbers/lib/splunklib for this +# example to run! To run this locally use `SPLUNK_VERSION=latest docker compose up -d` +# from the root of this repo which mounts this example and the latest splunklib +# code together at /opt/splunk/etc/apps/random_numbers sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) from splunklib.modularinput import * From d9e7044dbf817d79acd35f54d90d35eba2c7272d Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Tue, 26 Oct 2021 16:55:03 -0700 Subject: [PATCH 09/39] Fix github_forks example. --- examples/github_forks/README.md | 12 ++++++++ examples/github_forks/bin/github_forks.py | 35 +++++++++++++++++------ examples/random_numbers/README.md | 6 +++- 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 examples/github_forks/README.md diff --git a/examples/github_forks/README.md b/examples/github_forks/README.md new file mode 100644 index 000000000..6ba51ba6c --- /dev/null +++ b/examples/github_forks/README.md @@ -0,0 +1,12 @@ +splunk-sdk-python github_forks example +======================================== + +This app provides an example of a modular input that generates a random number between the min and max values provided by the user during setup of the input. + +To run this example locally run `SPLUNK_VERSION=latest docker compose up -d` from the root of this repository which will mount this example alongside the latest version of splunklib within `/opt/splunk/etc/apps/github_forks` and `/opt/splunk/etc/apps/github_forks/lib/splunklib` within the `splunk` container. + +Once the docker container is up and healthy log into the Splunk UI and setup a new `Github Repository Forks` input by visiting this page: http://localhost:8000/en-US/manager/github_forks/datainputstats and selecting the "Add new..." button next to the Local Inputs > Random Inputs. Enter values for a Github Repository owner and repo_name, for example owner = `splunk` repo_name = `splunk-sdk-python`. + +NOTE: If no Github Repository Forks input appears then the script is likely not running properly, see https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/ModInputsDevTools for more details on debugging the modular input using the command line and relevant logs. + +Once the input is created you should be able to see an event when running the following search: `source="github_forks://*"` the event should contain fields for `owner` and `repository` matching the values you input during setup and then a `fork_count` field corresponding to the number of forks the repo has according to the Github API. \ No newline at end of file diff --git a/examples/github_forks/bin/github_forks.py b/examples/github_forks/bin/github_forks.py index 2349bd686..5ffa4e409 100755 --- a/examples/github_forks/bin/github_forks.py +++ b/examples/github_forks/bin/github_forks.py @@ -15,10 +15,18 @@ # under the License. from __future__ import absolute_import -import sys, urllib2, json +import os +import sys +import json +# NOTE: splunklib must exist within github_forks/lib/splunklib for this +# example to run! To run this locally use `SPLUNK_VERSION=latest docker compose up -d` +# from the root of this repo which mounts this example and the latest splunklib +# code together at /opt/splunk/etc/apps/github_forks +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) from splunklib.modularinput import * from splunklib import six +from six.moves import http_client class MyScript(Script): """All modular inputs should inherit from the abstract base class Script @@ -87,11 +95,9 @@ def validate_input(self, validation_definition): # Get the values of the parameters, and construct a URL for the Github API owner = validation_definition.parameters["owner"] repo_name = validation_definition.parameters["repo_name"] - repo_url = "https://api.github.com/repos/%s/%s" % (owner, repo_name) - # Read the response from the Github API, then parse the JSON data into an object - response = urllib2.urlopen(repo_url).read() - jsondata = json.loads(response) + # Call Github to retrieve repo information + jsondata = _get_github_repos(owner, repo_name) # If there is only 1 field in the jsondata object,some kind or error occurred # with the Github API. @@ -125,9 +131,7 @@ def stream_events(self, inputs, ew): repo_name = input_item["repo_name"] # Get the fork count from the Github API - repo_url = "https://api.github.com/repos/%s/%s" % (owner, repo_name) - response = urllib2.urlopen(repo_url).read() - jsondata = json.loads(response) + jsondata = _get_github_repos(owner, repo_name) fork_count = jsondata["forks_count"] # Create an Event object, and set its fields @@ -139,5 +143,20 @@ def stream_events(self, inputs, ew): # Tell the EventWriter to write this event ew.write_event(event) + +def _get_github_repos(owner, repo_name): + # Read the response from the Github API, then parse the JSON data into an object + repo_path = "/repos/%s/%s" % (owner, repo_name) + connection = http_client.HTTPSConnection('api.github.com') + headers = { + 'Content-type': 'application/json', + 'User-Agent': 'splunk-sdk-python', + } + connection.request('GET', repo_path, headers=headers) + response = connection.getresponse() + body = response.read().decode() + return json.loads(body) + + if __name__ == "__main__": sys.exit(MyScript().run(sys.argv)) diff --git a/examples/random_numbers/README.md b/examples/random_numbers/README.md index 90172dff7..7ff4069f2 100644 --- a/examples/random_numbers/README.md +++ b/examples/random_numbers/README.md @@ -5,4 +5,8 @@ This app provides an example of a modular input that generates a random number b To run this example locally run `SPLUNK_VERSION=latest docker compose up -d` from the root of this repository which will mount this example alongside the latest version of splunklib within `/opt/splunk/etc/apps/random_numbers` and `/opt/splunk/etc/apps/random_numbers/lib/splunklib` within the `splunk` container. -Once the docker container is up and healthy log into the Splunk UI and setup a new `Random Numbers` input by visiting this page: http://localhost:8000/en-US/manager/random_numbers/datainputstats and selecting the "Add new..." button next to the Local Inputs > Random Inputs. If no Random Numbers input appears then the script is likely not running properly, see https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/ModInputsDevTools for more details on debugging the modular input using the command line and relevant logs. \ No newline at end of file +Once the docker container is up and healthy log into the Splunk UI and setup a new `Random Numbers` input by visiting this page: http://localhost:8000/en-US/manager/random_numbers/datainputstats and selecting the "Add new..." button next to the Local Inputs > Random Inputs. Enter values for the `min` and `max` values which the random number should be generated between. + +NOTE: If no Random Numbers input appears then the script is likely not running properly, see https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/ModInputsDevTools for more details on debugging the modular input using the command line and relevant logs. + +Once the input is created you should be able to see an event when running the following search: `source="random_numbers://*"` the event should contain a `number` field with a float between the min and max specified when the input was created. \ No newline at end of file From f93129fa44d540ad1efcab62bdced525caddb8c9 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Tue, 26 Oct 2021 16:58:07 -0700 Subject: [PATCH 10/39] Fix description --- examples/github_forks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/github_forks/README.md b/examples/github_forks/README.md index 6ba51ba6c..1a05c862f 100644 --- a/examples/github_forks/README.md +++ b/examples/github_forks/README.md @@ -1,7 +1,7 @@ splunk-sdk-python github_forks example ======================================== -This app provides an example of a modular input that generates a random number between the min and max values provided by the user during setup of the input. +This app provides an example of a modular input that generates the number of repository forks according to the Github API based on the owner and repo_name provided by the user during setup of the input. To run this example locally run `SPLUNK_VERSION=latest docker compose up -d` from the root of this repository which will mount this example alongside the latest version of splunklib within `/opt/splunk/etc/apps/github_forks` and `/opt/splunk/etc/apps/github_forks/lib/splunklib` within the `splunk` container. From e37c69146687aa051ffee8a4132d0bea7f793981 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Tue, 12 Oct 2021 20:31:50 +0530 Subject: [PATCH 11/39] Changes added to preserve the custom field --- splunklib/searchcommands/internals.py | 3 +++ splunklib/searchcommands/search_command.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index 85f9e0fe1..411c70253 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -508,6 +508,7 @@ def __init__(self, ofile, maxresultrows=None): self._chunk_count = 0 self._pending_record_count = 0 self._committed_record_count = 0 + self.custom_fields = set() @property def is_flushed(self): @@ -593,6 +594,8 @@ def _write_record(self, record): if fieldnames is None: self._fieldnames = fieldnames = list(record.keys()) + self._fieldnames.extend(self.custom_fields) + fieldnames.extend(self.custom_fields) value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) diff --git a/splunklib/searchcommands/search_command.py b/splunklib/searchcommands/search_command.py index 270569ad8..96b3a4c3b 100644 --- a/splunklib/searchcommands/search_command.py +++ b/splunklib/searchcommands/search_command.py @@ -173,6 +173,11 @@ def logging_level(self, value): raise ValueError('Unrecognized logging level: {}'.format(value)) self._logger.setLevel(level) + def add_field(self, current_record, field_name, field_value): + self._record_writer.custom_fields.add(field_name) + current_record[field_name] = field_value + return current_record + record = Option(doc=''' **Syntax: record= From ae817d927c092f9fde207b4fce9746f392b7549f Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Tue, 19 Oct 2021 17:45:03 +0530 Subject: [PATCH 12/39] Update README.md Description added for how to use add_field method to preserve conditionally added new fields and values. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 6b92a179a..21f22723f 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,31 @@ The test suite uses Python's standard library, the built-in `unittest` library, |/tests | Source for unit tests | |/utils | Source for utilities shared by the examples and unit tests | +### Customization +* When working with custom search commands such as Custom Streaming Commands or Custom Generating Commands, We may need to add new fields to the records based on certain conditions. +* Structural changes like this may not be preserved. +* Make sure to use ``add_field(record, fieldname, value)`` method from SearchCommand to add a new field and value to the record. + +Do +```python +class CustomStreamingCommand(StreamingCommand): + def stream(self, records): + for index, record in enumerate(records): + if index % 1 == 0: + self.add_field(record, "odd_record", "true") + yield record +``` + +Don't +```python +class CustomStreamingCommand(StreamingCommand): + def stream(self, records): + for index, record in enumerate(records): + if index % 1 == 0: + record["odd_record"] = "true" + yield record +``` + ### Changelog The [CHANGELOG](CHANGELOG.md) contains a description of changes for each version of the SDK. For the latest version, see the [CHANGELOG.md](https://github.com/splunk/splunk-sdk-python/blob/master/CHANGELOG.md) on GitHub. From 0875ba95f9bc5a37458f962c92570498a2c9069c Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Wed, 20 Oct 2021 19:32:41 +0530 Subject: [PATCH 13/39] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21f22723f..bdda18550 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ class CustomStreamingCommand(StreamingCommand): def stream(self, records): for index, record in enumerate(records): if index % 1 == 0: - self.add_field(record, "odd_record", "true") + record = self.add_field(record, "odd_record", "true") yield record ``` From 35a8ff1e97fb874b3635d14aa568d5811078c786 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Thu, 21 Oct 2021 14:19:50 +0530 Subject: [PATCH 14/39] Update internals.py --- splunklib/searchcommands/internals.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index 411c70253..e116d091c 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -573,6 +573,7 @@ def write_record(self, record): def write_records(self, records): self._ensure_validity() + records = list(records) write_record = self._write_record for record in records: write_record(record) @@ -593,9 +594,7 @@ def _write_record(self, record): fieldnames = self._fieldnames if fieldnames is None: - self._fieldnames = fieldnames = list(record.keys()) - self._fieldnames.extend(self.custom_fields) - fieldnames.extend(self.custom_fields) + self._fieldnames = fieldnames = {*list(record.keys())} | self.custom_fields value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From 114f2e8cecba76e2c32646433ccd45f18a8cdcdd Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Thu, 21 Oct 2021 14:20:17 +0530 Subject: [PATCH 15/39] Update search_command.py --- splunklib/searchcommands/search_command.py | 1 - 1 file changed, 1 deletion(-) diff --git a/splunklib/searchcommands/search_command.py b/splunklib/searchcommands/search_command.py index 96b3a4c3b..5c1eb9889 100644 --- a/splunklib/searchcommands/search_command.py +++ b/splunklib/searchcommands/search_command.py @@ -176,7 +176,6 @@ def logging_level(self, value): def add_field(self, current_record, field_name, field_value): self._record_writer.custom_fields.add(field_name) current_record[field_name] = field_value - return current_record record = Option(doc=''' **Syntax: record= From a622420f0a1161e583776319329a7f3494b46eb2 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Thu, 21 Oct 2021 16:48:42 +0530 Subject: [PATCH 16/39] Update internals.py --- splunklib/searchcommands/internals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index e116d091c..88d017db8 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -594,7 +594,7 @@ def _write_record(self, record): fieldnames = self._fieldnames if fieldnames is None: - self._fieldnames = fieldnames = {*list(record.keys())} | self.custom_fields + self._fieldnames = fieldnames = set(list(record.keys())) | self.custom_fields value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From c4995a6d26b284af2c68a014aaf06ccf43f75b39 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Thu, 21 Oct 2021 18:03:27 +0530 Subject: [PATCH 17/39] Update internals.py --- splunklib/searchcommands/internals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index 88d017db8..ae8b0393b 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -594,7 +594,8 @@ def _write_record(self, record): fieldnames = self._fieldnames if fieldnames is None: - self._fieldnames = fieldnames = set(list(record.keys())) | self.custom_fields + self._fieldnames = fieldnames = list(record.keys()) + self._fieldnames.extend(self.custom_fields) value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From c6ec68986f20e02ef636ceeb310f2f6720d8078e Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Fri, 22 Oct 2021 13:46:41 +0530 Subject: [PATCH 18/39] Merged fieldnames --- splunklib/searchcommands/internals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index ae8b0393b..41169bb44 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -594,8 +594,8 @@ def _write_record(self, record): fieldnames = self._fieldnames if fieldnames is None: - self._fieldnames = fieldnames = list(record.keys()) - self._fieldnames.extend(self.custom_fields) + self.custom_fields |= set(record.keys()) + self._fieldnames = fieldnames = [*self.custom_fields] value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From b9571363b8235924a4a0e77fc74d3561060a3a1f Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Fri, 22 Oct 2021 13:49:52 +0530 Subject: [PATCH 19/39] Fixed: test failed due to fieldname merged --- tests/searchcommands/test_internals_v2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/searchcommands/test_internals_v2.py b/tests/searchcommands/test_internals_v2.py index bdef65c4a..34e6b61c4 100755 --- a/tests/searchcommands/test_internals_v2.py +++ b/tests/searchcommands/test_internals_v2.py @@ -233,6 +233,8 @@ def test_record_writer_with_random_data(self, save_recording=False): self.assertGreater(writer._buffer.tell(), 0) self.assertEqual(writer._total_record_count, 0) self.assertEqual(writer.committed_record_count, 0) + fieldnames.sort() + writer._fieldnames.sort() self.assertListEqual(writer._fieldnames, fieldnames) self.assertListEqual(writer._inspector['messages'], messages) From d20f1946a73ff6d5ba6f7cb40b854ccc34406707 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Fri, 22 Oct 2021 13:55:09 +0530 Subject: [PATCH 20/39] Update internals.py --- splunklib/searchcommands/internals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index 41169bb44..a5d76c9c1 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -595,7 +595,7 @@ def _write_record(self, record): if fieldnames is None: self.custom_fields |= set(record.keys()) - self._fieldnames = fieldnames = [*self.custom_fields] + self._fieldnames = fieldnames = list(self.custom_fields) value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From d6952070b4fbc708aa40cc375bf059cc3c6ac7bd Mon Sep 17 00:00:00 2001 From: akaila-splunk Date: Mon, 25 Oct 2021 11:31:21 +0530 Subject: [PATCH 21/39] add gen_record() method for create a new record --- splunklib/searchcommands/generating_command.py | 8 ++++++++ splunklib/searchcommands/search_command.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/splunklib/searchcommands/generating_command.py b/splunklib/searchcommands/generating_command.py index e766effb8..6a2f2c6dd 100644 --- a/splunklib/searchcommands/generating_command.py +++ b/splunklib/searchcommands/generating_command.py @@ -213,7 +213,15 @@ def _execute(self, ifile, process): def _execute_chunk_v2(self, process, chunk): count = 0 + records = [] for row in process: + records.append(row) + count+=1 + if count == self._record_writer._maxresultrows: + break + + count = 0 + for row in records: self._record_writer.write_record(row) count += 1 if count == self._record_writer._maxresultrows: diff --git a/splunklib/searchcommands/search_command.py b/splunklib/searchcommands/search_command.py index 5c1eb9889..3c6329b18 100644 --- a/splunklib/searchcommands/search_command.py +++ b/splunklib/searchcommands/search_command.py @@ -177,6 +177,10 @@ def add_field(self, current_record, field_name, field_value): self._record_writer.custom_fields.add(field_name) current_record[field_name] = field_value + def gen_record(self, **record): + self._record_writer.custom_fields |= record.keys() + return {**record} + record = Option(doc=''' **Syntax: record= From 0003f65c521f421e04d2b5c8f3b39afe8f877680 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Mon, 25 Oct 2021 17:06:09 +0530 Subject: [PATCH 22/39] Update internals.py --- splunklib/searchcommands/internals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splunklib/searchcommands/internals.py b/splunklib/searchcommands/internals.py index a5d76c9c1..fa32f0b1c 100644 --- a/splunklib/searchcommands/internals.py +++ b/splunklib/searchcommands/internals.py @@ -594,8 +594,8 @@ def _write_record(self, record): fieldnames = self._fieldnames if fieldnames is None: - self.custom_fields |= set(record.keys()) - self._fieldnames = fieldnames = list(self.custom_fields) + self._fieldnames = fieldnames = list(record.keys()) + self._fieldnames.extend([i for i in self.custom_fields if i not in self._fieldnames]) value_list = imap(lambda fn: (str(fn), str('__mv_') + str(fn)), fieldnames) self._writerow(list(chain.from_iterable(value_list))) From c8f793d89cbdf70cffa6542d334a49e041d40d96 Mon Sep 17 00:00:00 2001 From: akaila-splunk Date: Tue, 26 Oct 2021 13:58:12 +0530 Subject: [PATCH 23/39] Update search_command.py --- splunklib/searchcommands/search_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splunklib/searchcommands/search_command.py b/splunklib/searchcommands/search_command.py index 3c6329b18..1d1061436 100644 --- a/splunklib/searchcommands/search_command.py +++ b/splunklib/searchcommands/search_command.py @@ -179,7 +179,7 @@ def add_field(self, current_record, field_name, field_value): def gen_record(self, **record): self._record_writer.custom_fields |= record.keys() - return {**record} + return record record = Option(doc=''' **Syntax: record= From aa3bab7dad6c07f17b1b5222512f44fff5174262 Mon Sep 17 00:00:00 2001 From: akaila-splunk Date: Wed, 27 Oct 2021 19:26:23 +0530 Subject: [PATCH 24/39] added test case for generating CSC and updated README.md --- README.md | 21 +++++++++++++++ .../searchcommands/generating_command.py | 13 +++++---- .../searchcommands/test_generator_command.py | 27 ++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bdda18550..124e7f98a 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,27 @@ class CustomStreamingCommand(StreamingCommand): record["odd_record"] = "true" yield record ``` +### Customization for Generating Custom Search Command +* Generating Custom Search Command is used to generate events using SDK code. +* Make sure to use ``gen_record()`` method from SearchCommand to add a new record and pass event data as a key=value pair separated by , (mentioned in below example). + +Do +```python +@Configuration() + class GeneratorTest(GeneratingCommand): + def generate(self): + yield self.gen_record(_time=time.time(), one=1) + yield self.gen_record(_time=time.time(), two=2) +``` + +Don't +```python +@Configuration() + class GeneratorTest(GeneratingCommand): + def generate(self): + yield {'_time': time.time(), 'one': 1} + yield {'_time': time.time(), 'two': 2} +``` ### Changelog diff --git a/splunklib/searchcommands/generating_command.py b/splunklib/searchcommands/generating_command.py index 6a2f2c6dd..6a75d2c27 100644 --- a/splunklib/searchcommands/generating_command.py +++ b/splunklib/searchcommands/generating_command.py @@ -216,18 +216,17 @@ def _execute_chunk_v2(self, process, chunk): records = [] for row in process: records.append(row) - count+=1 + count += 1 if count == self._record_writer._maxresultrows: break - count = 0 for row in records: self._record_writer.write_record(row) - count += 1 - if count == self._record_writer._maxresultrows: - self._finished = False - return - self._finished = True + + if count == self._record_writer._maxresultrows: + self._finished = False + else: + self._finished = True def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout, allow_empty_input=True): """ Process data. diff --git a/tests/searchcommands/test_generator_command.py b/tests/searchcommands/test_generator_command.py index 3b2281e8c..0af79f960 100644 --- a/tests/searchcommands/test_generator_command.py +++ b/tests/searchcommands/test_generator_command.py @@ -40,7 +40,6 @@ def generate(self): assert expected.issubset(seen) assert finished_seen - def test_allow_empty_input_for_generating_command(): """ Passing allow_empty_input for generating command will cause an error @@ -59,3 +58,29 @@ def generate(self): except ValueError as error: assert str(error) == "allow_empty_input cannot be False for Generating Commands" +def test_all_fieldnames_present_for_generated_records(): + @Configuration() + class GeneratorTest(GeneratingCommand): + def generate(self): + yield self.gen_record(_time=time.time(), one=1) + yield self.gen_record(_time=time.time(), two=2) + yield self.gen_record(_time=time.time(), three=3) + yield self.gen_record(_time=time.time(), four=4) + yield self.gen_record(_time=time.time(), five=5) + + generator = GeneratorTest() + in_stream = io.BytesIO() + in_stream.write(chunky.build_getinfo_chunk()) + in_stream.write(chunky.build_chunk({'action': 'execute'})) + in_stream.seek(0) + out_stream = io.BytesIO() + generator._process_protocol_v2([], in_stream, out_stream) + out_stream.seek(0) + + ds = chunky.ChunkedDataStream(out_stream) + fieldnames_expected = {'_time', 'one', 'two', 'three', 'four', 'five'} + fieldnames_actual = set() + for chunk in ds: + for row in chunk.data: + fieldnames_actual |= row.keys() + assert fieldnames_expected.issubset(fieldnames_actual) From cc17181d593b48716d6063290bcaada4afac75a5 Mon Sep 17 00:00:00 2001 From: akaila-splunk Date: Thu, 28 Oct 2021 14:52:38 +0530 Subject: [PATCH 25/39] updated search_command.py file --- splunklib/searchcommands/search_command.py | 2 +- tests/searchcommands/test_generator_command.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/splunklib/searchcommands/search_command.py b/splunklib/searchcommands/search_command.py index 1d1061436..5a626cc5c 100644 --- a/splunklib/searchcommands/search_command.py +++ b/splunklib/searchcommands/search_command.py @@ -178,7 +178,7 @@ def add_field(self, current_record, field_name, field_value): current_record[field_name] = field_value def gen_record(self, **record): - self._record_writer.custom_fields |= record.keys() + self._record_writer.custom_fields |= set(record.keys()) return record record = Option(doc=''' diff --git a/tests/searchcommands/test_generator_command.py b/tests/searchcommands/test_generator_command.py index 0af79f960..63ae3ac83 100644 --- a/tests/searchcommands/test_generator_command.py +++ b/tests/searchcommands/test_generator_command.py @@ -82,5 +82,5 @@ def generate(self): fieldnames_actual = set() for chunk in ds: for row in chunk.data: - fieldnames_actual |= row.keys() + fieldnames_actual |= set(row.keys()) assert fieldnames_expected.issubset(fieldnames_actual) From 0b006d6f7dbc4d72b8ae74b2fe725c18a539ac42 Mon Sep 17 00:00:00 2001 From: Tim Pavlik Date: Thu, 28 Oct 2021 15:36:57 -0700 Subject: [PATCH 26/39] Add search mode example --- examples/search_modes.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 examples/search_modes.py diff --git a/examples/search_modes.py b/examples/search_modes.py new file mode 100644 index 000000000..dbbb8442a --- /dev/null +++ b/examples/search_modes.py @@ -0,0 +1,41 @@ +import sys +import os +# import from utils/__init__.py +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from utils import * +import time +from splunklib.client import connect +from splunklib import results +from splunklib import six + +def cmdline(argv, flags, **kwargs): + """A cmdopts wrapper that takes a list of flags and builds the + corresponding cmdopts rules to match those flags.""" + rules = dict([(flag, {'flags': ["--%s" % flag]}) for flag in flags]) + return parse(argv, rules, ".splunkrc", **kwargs) + +def modes(argv): + opts = cmdline(argv, []) + kwargs_splunk = dslice(opts.kwargs, FLAGS_SPLUNK) + service = connect(**kwargs_splunk) + + # By default the job will run in 'smart' mode which will omit events for transforming commands + job = service.jobs.create('search index=_internal | head 10 | top host') + while not job.is_ready(): + time.sleep(0.5) + pass + reader = results.ResultsReader(job.events()) + # Events found: 0 + print('Events found with adhoc_search_level="smart": %s' % len([e for e in reader])) + + # Now set the adhoc_search_level to 'verbose' to see the events + job = service.jobs.create('search index=_internal | head 10 | top host', adhoc_search_level='verbose') + while not job.is_ready(): + time.sleep(0.5) + pass + reader = results.ResultsReader(job.events()) + # Events found: 10 + print('Events found with adhoc_search_level="verbose": %s' % len([e for e in reader])) + +if __name__ == "__main__": + modes(sys.argv[1:]) \ No newline at end of file From 465a56b33cdf4a642396f62529235e0295926aa6 Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 1 Nov 2021 10:58:54 +0530 Subject: [PATCH 27/39] Update client.py default value for owner set to "nobody" --- splunklib/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/splunklib/client.py b/splunklib/client.py index a9ae396a4..8c695ea63 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -756,6 +756,8 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query): # self.path to the Endpoint is relative in the SDK, so passing # owner, app, sharing, etc. along will produce the correct # namespace in the final request. + if owner is None: + owner = "nobody" if path_segment.startswith('/'): path = path_segment else: From d2ec7036a75ef0ec5e931bbcf5402eca93344ba4 Mon Sep 17 00:00:00 2001 From: akaila-splunk Date: Mon, 1 Nov 2021 11:37:53 +0530 Subject: [PATCH 28/39] Add Support for authorization tokens read from .splunkrc --- README.md | 10 ++++++++++ scripts/templates/splunkrc.template | 4 ++++ utils/__init__.py | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 6b92a179a..8e2c35c0f 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,18 @@ Save the file as **.splunkrc** in the current user's home directory. Examples are located in the **/splunk-sdk-python/examples** directory. To run the examples at the command line, use the Python interpreter and include any arguments that are required by the example. In the commands below, replace "examplename" with the name of the specific example in the directory that you want to run: +Using username and Password + python examplename.py --username="admin" --password="changeme" +Using Bearer token + + python examplename.py --bearerToken= + +Using Session key + + python examplename.py --sessionKey="" + If you saved your login credentials in the **.splunkrc** file, you can omit those arguments: python examplename.py diff --git a/scripts/templates/splunkrc.template b/scripts/templates/splunkrc.template index 7f00b04ba..b98f93af6 100644 --- a/scripts/templates/splunkrc.template +++ b/scripts/templates/splunkrc.template @@ -10,3 +10,7 @@ password=$password scheme=$scheme # Your version of Splunk (default: 6.2) version=$version +# Bearer token for authentication +#bearerToken= +# Session key for authentication +#sessionKey= diff --git a/utils/__init__.py b/utils/__init__.py index b74099ba9..f38027efe 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -69,6 +69,16 @@ def config(option, opt, value, parser): 'flags': ["--version"], 'default': None, 'help': 'Ignore. Used by JavaScript SDK.' + }, + 'splunkToken': { + 'flags': ["--bearerToken"], + 'default': None, + 'help': 'Bearer token for authentication' + }, + 'token': { + 'flags': ["--sessionKey"], + 'default': None, + 'help': 'Session key for authentication' } } From 8dea5eb3971fd168ea318e843c9ec7ee7b45b6dc Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 1 Nov 2021 14:41:32 +0530 Subject: [PATCH 29/39] Update client.py --- splunklib/client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/splunklib/client.py b/splunklib/client.py index 8c695ea63..87e9ed7c8 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -403,6 +403,7 @@ class Service(_BaseService): def __init__(self, **kwargs): super(Service, self).__init__(**kwargs) self._splunk_version = None + self._kvstore_owner = None @property def apps(self): @@ -675,12 +676,27 @@ def splunk_version(self): self._splunk_version = tuple([int(p) for p in self.info['version'].split('.')]) return self._splunk_version + @property + def kvstore_owner(self): + if self._kvstore_owner is None: + self._kvstore_owner = "nobody" + #self.namespace['owner'] = "nobody" + return self._kvstore_owner + + @kvstore_owner.setter + def kvstore_owner(self, value): + self._kvstore_owner = value + #self.namespace['owner'] = value + @property def kvstore(self): """Returns the collection of KV Store collections. :return: A :class:`KVStoreCollections` collection of :class:`KVStoreCollection` entities. """ + self.namespace['owner'] = self.kvstore_owner + # if self.namespace['owner'] is None: + # self.namespace['owner'] = "nobody" return KVStoreCollections(self) @property @@ -756,8 +772,6 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query): # self.path to the Endpoint is relative in the SDK, so passing # owner, app, sharing, etc. along will produce the correct # namespace in the final request. - if owner is None: - owner = "nobody" if path_segment.startswith('/'): path = path_segment else: From 834f570894c54618fe09fb435f00f3fad755f07f Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Mon, 1 Nov 2021 15:21:39 +0530 Subject: [PATCH 30/39] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 124e7f98a..16e4f448d 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ The test suite uses Python's standard library, the built-in `unittest` library, * When working with custom search commands such as Custom Streaming Commands or Custom Generating Commands, We may need to add new fields to the records based on certain conditions. * Structural changes like this may not be preserved. * Make sure to use ``add_field(record, fieldname, value)`` method from SearchCommand to add a new field and value to the record. +* ___Note:__ Usage of ``add_field`` method is completely optional, if you are not facing any issues with field retention._ Do ```python From ff239603baecac3eefcd364c3c327a9c70c3337f Mon Sep 17 00:00:00 2001 From: Artem Rys Date: Tue, 2 Nov 2021 12:34:30 +0100 Subject: [PATCH 31/39] chore: remove unused imports --- splunklib/binding.py | 2 -- splunklib/modularinput/event_writer.py | 1 - 2 files changed, 3 deletions(-) diff --git a/splunklib/binding.py b/splunklib/binding.py index 8d47d244f..b36539feb 100644 --- a/splunklib/binding.py +++ b/splunklib/binding.py @@ -30,7 +30,6 @@ import logging import socket import ssl -import sys from base64 import b64encode from contextlib import contextmanager from datetime import datetime @@ -39,7 +38,6 @@ from xml.etree.ElementTree import XML from splunklib import six -from splunklib.six import StringIO from splunklib.six.moves import urllib from .data import record diff --git a/splunklib/modularinput/event_writer.py b/splunklib/modularinput/event_writer.py index 3e4321016..b868a18ff 100644 --- a/splunklib/modularinput/event_writer.py +++ b/splunklib/modularinput/event_writer.py @@ -15,7 +15,6 @@ from __future__ import absolute_import import sys -from io import TextIOWrapper, TextIOBase from splunklib.six import ensure_str from .event import ET From f847b4106ebaf92ef04207ee99cff6eceba1513c Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Wed, 3 Nov 2021 12:20:46 +0530 Subject: [PATCH 32/39] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16e4f448d..2a8ea22ec 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ class CustomStreamingCommand(StreamingCommand): def stream(self, records): for index, record in enumerate(records): if index % 1 == 0: - record = self.add_field(record, "odd_record", "true") + self.add_field(record, "odd_record", "true") yield record ``` From 1ffab118f22f8c74611c8d42f082816227fb350d Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Wed, 3 Nov 2021 15:23:24 +0530 Subject: [PATCH 33/39] Update test_streaming_command.py --- .../searchcommands/test_streaming_command.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/searchcommands/test_streaming_command.py b/tests/searchcommands/test_streaming_command.py index dcc00b53e..ffe6a7376 100644 --- a/tests/searchcommands/test_streaming_command.py +++ b/tests/searchcommands/test_streaming_command.py @@ -27,3 +27,71 @@ def stream(self, records): output = chunky.ChunkedDataStream(ofile) getinfo_response = output.read_chunk() assert getinfo_response.meta["type"] == "streaming" + + +def test_field_preservation_negative(): + @Configuration() + class TestStreamingCommand(StreamingCommand): + + def stream(self, records): + for index, record in enumerate(records): + if index % 2 != 0: + record["odd_field"] = True + else: + record["even_field"] = True + yield record + + cmd = TestStreamingCommand() + ifile = io.BytesIO() + ifile.write(chunky.build_getinfo_chunk()) + data = list() + for i in range(0, 10): + data.append({"in_index": str(i)}) + ifile.write(chunky.build_data_chunk(data, finished=True)) + ifile.seek(0) + ofile = io.BytesIO() + cmd._process_protocol_v2([], ifile, ofile) + ofile.seek(0) + output_iter = chunky.ChunkedDataStream(ofile).__iter__() + output_iter.next() + output_records = [i for i in output_iter.next().data] + + # Assert that count of records having "odd_field" is 0 + assert len(list(filter(lambda r: "odd_field" in r, output_records))) == 0 + + # Assert that count of records having "even_field" is 10 + assert len(list(filter(lambda r: "even_field" in r, output_records))) == 10 + + +def test_field_preservation_positive(): + @Configuration() + class TestStreamingCommand(StreamingCommand): + + def stream(self, records): + for index, record in enumerate(records): + if index % 2 != 0: + self.add_field(record, "odd_field", True) + else: + self.add_field(record, "even_field", True) + yield record + + cmd = TestStreamingCommand() + ifile = io.BytesIO() + ifile.write(chunky.build_getinfo_chunk()) + data = list() + for i in range(0, 10): + data.append({"in_index": str(i)}) + ifile.write(chunky.build_data_chunk(data, finished=True)) + ifile.seek(0) + ofile = io.BytesIO() + cmd._process_protocol_v2([], ifile, ofile) + ofile.seek(0) + output_iter = chunky.ChunkedDataStream(ofile).__iter__() + output_iter.next() + output_records = [i for i in output_iter.next().data] + + # Assert that count of records having "odd_field" is 10 + assert len(list(filter(lambda r: "odd_field" in r, output_records))) == 10 + + # Assert that count of records having "even_field" is 10 + assert len(list(filter(lambda r: "even_field" in r, output_records))) == 10 From d6fc1a3ca6bfee513e1d7cfa8d937c162d2a0352 Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 8 Nov 2021 14:34:58 +0530 Subject: [PATCH 34/39] adding kvstore_owner as new property commenting the need to set kvstore owner to default value of "nobody" as new property is created for the kvstore owner with default to "nobody" --- splunklib/client.py | 11 +++++++---- tests/test_kvstore_batch.py | 2 +- tests/test_kvstore_conf.py | 6 +++--- tests/test_kvstore_data.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/splunklib/client.py b/splunklib/client.py index 87e9ed7c8..5c48f0510 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -678,25 +678,28 @@ def splunk_version(self): @property def kvstore_owner(self): + """Returns the KVStore owner for this instance of Splunk. + + By default is the kvstore owner is not set, it will return "nobody" + :return: A string with the KVStore owner. + """ if self._kvstore_owner is None: self._kvstore_owner = "nobody" - #self.namespace['owner'] = "nobody" return self._kvstore_owner @kvstore_owner.setter def kvstore_owner(self, value): self._kvstore_owner = value - #self.namespace['owner'] = value @property def kvstore(self): """Returns the collection of KV Store collections. + sets the owner for the namespace, before retrieving the KVStore Collection + :return: A :class:`KVStoreCollections` collection of :class:`KVStoreCollection` entities. """ self.namespace['owner'] = self.kvstore_owner - # if self.namespace['owner'] is None: - # self.namespace['owner'] = "nobody" return KVStoreCollections(self) @property diff --git a/tests/test_kvstore_batch.py b/tests/test_kvstore_batch.py index 14806a699..d32b665e6 100755 --- a/tests/test_kvstore_batch.py +++ b/tests/test_kvstore_batch.py @@ -26,7 +26,7 @@ class KVStoreBatchTestCase(testlib.SDKTestCase): def setUp(self): super(KVStoreBatchTestCase, self).setUp() - self.service.namespace['owner'] = 'nobody' + #self.service.namespace['owner'] = 'nobody' self.service.namespace['app'] = 'search' confs = self.service.kvstore if ('test' in confs): diff --git a/tests/test_kvstore_conf.py b/tests/test_kvstore_conf.py index a587712e4..a24537288 100755 --- a/tests/test_kvstore_conf.py +++ b/tests/test_kvstore_conf.py @@ -25,16 +25,16 @@ class KVStoreConfTestCase(testlib.SDKTestCase): def setUp(self): super(KVStoreConfTestCase, self).setUp() - self.service.namespace['owner'] = 'nobody' + #self.service.namespace['owner'] = 'nobody' self.service.namespace['app'] = 'search' self.confs = self.service.kvstore if ('test' in self.confs): self.confs['test'].delete() def test_owner_restriction(self): - self.service.namespace['owner'] = 'admin' + self.service.kvstore_owner = 'admin' self.assertRaises(client.HTTPError, lambda: self.confs.list()) - self.service.namespace['owner'] = 'nobody' + self.service.kvstore_owner = 'nobody' def test_create_delete_collection(self): self.confs.create('test') diff --git a/tests/test_kvstore_data.py b/tests/test_kvstore_data.py index 1551f1c69..6ddeae688 100755 --- a/tests/test_kvstore_data.py +++ b/tests/test_kvstore_data.py @@ -27,7 +27,7 @@ class KVStoreDataTestCase(testlib.SDKTestCase): def setUp(self): super(KVStoreDataTestCase, self).setUp() - self.service.namespace['owner'] = 'nobody' + #self.service.namespace['owner'] = 'nobody' self.service.namespace['app'] = 'search' self.confs = self.service.kvstore if ('test' in self.confs): From f958a9c7b2f069967563befb28577dd32555b423 Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 8 Nov 2021 17:13:26 +0530 Subject: [PATCH 35/39] Update test_kvstore_conf.py --- tests/test_kvstore_conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_kvstore_conf.py b/tests/test_kvstore_conf.py index a24537288..f16a8da87 100755 --- a/tests/test_kvstore_conf.py +++ b/tests/test_kvstore_conf.py @@ -33,6 +33,7 @@ def setUp(self): def test_owner_restriction(self): self.service.kvstore_owner = 'admin' + self.confs = self.service.kvstore self.assertRaises(client.HTTPError, lambda: self.confs.list()) self.service.kvstore_owner = 'nobody' From ee65a75db3044f5a1a214af8570f3f05807fa21f Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 8 Nov 2021 18:08:21 +0530 Subject: [PATCH 36/39] reload the kvstore once owner is changed --- splunklib/client.py | 1 + tests/test_kvstore_conf.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/splunklib/client.py b/splunklib/client.py index 5c48f0510..ce1906a96 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -690,6 +690,7 @@ def kvstore_owner(self): @kvstore_owner.setter def kvstore_owner(self, value): self._kvstore_owner = value + self.kvstore @property def kvstore(self): diff --git a/tests/test_kvstore_conf.py b/tests/test_kvstore_conf.py index f16a8da87..a24537288 100755 --- a/tests/test_kvstore_conf.py +++ b/tests/test_kvstore_conf.py @@ -33,7 +33,6 @@ def setUp(self): def test_owner_restriction(self): self.service.kvstore_owner = 'admin' - self.confs = self.service.kvstore self.assertRaises(client.HTTPError, lambda: self.confs.list()) self.service.kvstore_owner = 'nobody' From bc20898e75bf7c962be757cfa813063b4f4e0d30 Mon Sep 17 00:00:00 2001 From: Abhi Shah Date: Mon, 8 Nov 2021 18:24:57 +0530 Subject: [PATCH 37/39] Update client.py --- splunklib/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/splunklib/client.py b/splunklib/client.py index ce1906a96..21d27a6e0 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -689,6 +689,9 @@ def kvstore_owner(self): @kvstore_owner.setter def kvstore_owner(self, value): + """ + kvstore is refreshed, when the owner value is changed + """ self._kvstore_owner = value self.kvstore From 099b7fa86776e8c40743e74a4e79930e25000dc5 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Thu, 11 Nov 2021 14:00:07 +0530 Subject: [PATCH 38/39] release-1.6.18 changes --- CHANGELOG.md | 12 ++++++++++++ README.md | 22 +++++++++++++++++++++- splunklib/__init__.py | 2 +- splunklib/binding.py | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c49f5c74..9bd37e422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Splunk Enterprise SDK for Python Changelog +## Version 1.6.18 + +### Bug fixes +* [#405](https://github.com/splunk/splunk-sdk-python/pull/405) Fix searchcommands_app example +* [#406](https://github.com/splunk/splunk-sdk-python/pull/406) Fix mod inputs examples +* [#407](https://github.com/splunk/splunk-sdk-python/pull/407) Modified Streaming and Generating Custom Search Command + +### Minor changes +* [#408](https://github.com/splunk/splunk-sdk-python/pull/408) Add search mode example +* [#409](https://github.com/splunk/splunk-sdk-python/pull/409) Add Support for authorization tokens read from .splunkrc +* [#413](https://github.com/splunk/splunk-sdk-python/pull/413) Default kvstore owner to nobody + ## Version 1.6.17 ### Bug fixes diff --git a/README.md b/README.md index 4661dfe5f..1436ad240 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # The Splunk Enterprise Software Development Kit for Python -#### Version 1.6.17 +#### Version 1.6.18 The Splunk Enterprise Software Development Kit (SDK) for Python contains library code and examples designed to enable developers to build applications using the Splunk platform. @@ -71,6 +71,26 @@ To run the examples and unit tests, you must put the root of the SDK on your PYT The SDK command-line examples require a common set of arguments that specify the host, port, and login credentials for Splunk Enterprise. For a full list of command-line arguments, include `--help` as an argument to any of the examples. +### Following are the different ways to connect to Splunk Enterprise +#### Using username/password +```python +import splunklib.client as client + service = client.connect(host=, username=, password=, autoLogin=True) +``` + +#### Using bearer token +```python +import splunklib.client as client +service = client.connect(host=, splunkToken=, autologin=True) +``` + +#### Using session key +```python +import splunklib.client as client +service = client.connect(host=, token=, autologin=True) +``` + +### #### Create a .splunkrc convenience file To connect to Splunk Enterprise, many of the SDK examples and unit tests take command-line arguments that specify values for the host, port, and login credentials for Splunk Enterprise. For convenience during development, you can store these arguments as key-value pairs in a text file named **.splunkrc**. Then, the SDK examples and unit tests use the values from the **.splunkrc** file when you don't specify them. diff --git a/splunklib/__init__.py b/splunklib/__init__.py index 36f8a7e0b..41c261fdc 100644 --- a/splunklib/__init__.py +++ b/splunklib/__init__.py @@ -16,5 +16,5 @@ from __future__ import absolute_import from splunklib.six.moves import map -__version_info__ = (1, 6, 17) +__version_info__ = (1, 6, 18) __version__ = ".".join(map(str, __version_info__)) diff --git a/splunklib/binding.py b/splunklib/binding.py index dbbee8530..94cc55818 100644 --- a/splunklib/binding.py +++ b/splunklib/binding.py @@ -1389,7 +1389,7 @@ def request(url, message, **kwargs): head = { "Content-Length": str(len(body)), "Host": host, - "User-Agent": "splunk-sdk-python/1.6.17", + "User-Agent": "splunk-sdk-python/1.6.18", "Accept": "*/*", "Connection": "Close", } # defaults From ae709beefee5c949aa230bce88206c41a0c68a32 Mon Sep 17 00:00:00 2001 From: vmalaviya Date: Fri, 12 Nov 2021 11:32:42 +0530 Subject: [PATCH 39/39] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd37e422..7edf338d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,13 @@ ### Bug fixes * [#405](https://github.com/splunk/splunk-sdk-python/pull/405) Fix searchcommands_app example * [#406](https://github.com/splunk/splunk-sdk-python/pull/406) Fix mod inputs examples -* [#407](https://github.com/splunk/splunk-sdk-python/pull/407) Modified Streaming and Generating Custom Search Command +* [#407](https://github.com/splunk/splunk-sdk-python/pull/407) Fixed issue with Streaming and Generating Custom Search Commands dropping fields that aren't present in the first row of results. More details on how to opt-in to this fix can be found here: +https://github.com/splunk/splunk-sdk-python/blob/develop/README.md#customization [ [issue#401](https://github.com/splunk/splunk-sdk-python/issues/401) ] ### Minor changes * [#408](https://github.com/splunk/splunk-sdk-python/pull/408) Add search mode example -* [#409](https://github.com/splunk/splunk-sdk-python/pull/409) Add Support for authorization tokens read from .splunkrc -* [#413](https://github.com/splunk/splunk-sdk-python/pull/413) Default kvstore owner to nobody +* [#409](https://github.com/splunk/splunk-sdk-python/pull/409) Add Support for authorization tokens read from .splunkrc [ [issue#388](https://github.com/splunk/splunk-sdk-python/issues/388) ] +* [#413](https://github.com/splunk/splunk-sdk-python/pull/413) Default kvstore owner to nobody [ [issue#231](https://github.com/splunk/splunk-sdk-python/issues/231) ] ## Version 1.6.17