From 2fc806db09d3d947fbaf4f6bbbef979dfc19f727 Mon Sep 17 00:00:00 2001 From: Shivam Sandbhor Date: Sat, 18 Mar 2023 23:58:43 +0530 Subject: [PATCH 1/2] Fix cloud check tests Signed-off-by: Shivam Sandbhor --- bin/splunklib/__init__.py | 17 +- bin/splunklib/binding.py | 126 +++++-- bin/splunklib/client.py | 348 +++++++++++++----- bin/splunklib/data.py | 4 +- bin/splunklib/modularinput/__init__.py | 0 bin/splunklib/modularinput/argument.py | 0 bin/splunklib/modularinput/event.py | 0 bin/splunklib/modularinput/event_writer.py | 26 +- .../modularinput/input_definition.py | 0 bin/splunklib/modularinput/scheme.py | 0 bin/splunklib/modularinput/script.py | 3 +- bin/splunklib/modularinput/utils.py | 9 +- .../modularinput/validation_definition.py | 0 bin/splunklib/ordereddict.py | 128 ------- bin/splunklib/results.py | 112 +++++- bin/splunklib/searchcommands/__init__.py | 8 +- bin/splunklib/searchcommands/decorators.py | 5 +- bin/splunklib/searchcommands/environment.py | 0 .../searchcommands/eventing_command.py | 7 + .../searchcommands/external_search_command.py | 0 .../searchcommands/generating_command.py | 64 +++- bin/splunklib/searchcommands/internals.py | 113 +++--- .../searchcommands/reporting_command.py | 4 +- .../searchcommands/search_command.py | 136 ++++--- .../searchcommands/streaming_command.py | 7 + bin/splunklib/searchcommands/validators.py | 48 ++- bin/splunklib/six.py | 13 + default/app.conf | 0 default/commands.conf | 3 +- default/searchbnf.conf | 0 metadata/default.meta | 0 static/appIcon.png | Bin 15257 -> 3048 bytes static/appIcon_2x.png | Bin 49971 -> 9192 bytes 33 files changed, 778 insertions(+), 403 deletions(-) mode change 100755 => 100644 bin/splunklib/__init__.py mode change 100755 => 100644 bin/splunklib/binding.py mode change 100755 => 100644 bin/splunklib/client.py mode change 100755 => 100644 bin/splunklib/data.py mode change 100755 => 100644 bin/splunklib/modularinput/__init__.py mode change 100755 => 100644 bin/splunklib/modularinput/argument.py mode change 100755 => 100644 bin/splunklib/modularinput/event.py mode change 100755 => 100644 bin/splunklib/modularinput/event_writer.py mode change 100755 => 100644 bin/splunklib/modularinput/input_definition.py mode change 100755 => 100644 bin/splunklib/modularinput/scheme.py mode change 100755 => 100644 bin/splunklib/modularinput/script.py mode change 100755 => 100644 bin/splunklib/modularinput/utils.py mode change 100755 => 100644 bin/splunklib/modularinput/validation_definition.py delete mode 100755 bin/splunklib/ordereddict.py mode change 100755 => 100644 bin/splunklib/results.py mode change 100755 => 100644 bin/splunklib/searchcommands/__init__.py mode change 100755 => 100644 bin/splunklib/searchcommands/decorators.py mode change 100755 => 100644 bin/splunklib/searchcommands/environment.py mode change 100755 => 100644 bin/splunklib/searchcommands/eventing_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/external_search_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/generating_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/internals.py mode change 100755 => 100644 bin/splunklib/searchcommands/reporting_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/search_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/streaming_command.py mode change 100755 => 100644 bin/splunklib/searchcommands/validators.py mode change 100755 => 100644 bin/splunklib/six.py mode change 100755 => 100644 default/app.conf mode change 100755 => 100644 default/commands.conf mode change 100755 => 100644 default/searchbnf.conf mode change 100755 => 100644 metadata/default.meta diff --git a/bin/splunklib/__init__.py b/bin/splunklib/__init__.py old mode 100755 new mode 100644 index 929a631..31787bd --- a/bin/splunklib/__init__.py +++ b/bin/splunklib/__init__.py @@ -16,5 +16,20 @@ from __future__ import absolute_import from splunklib.six.moves import map -__version_info__ = (1, 6, 12) +import logging + +DEFAULT_LOG_FORMAT = '%(asctime)s, Level=%(levelname)s, Pid=%(process)s, Logger=%(name)s, File=%(filename)s, ' \ + 'Line=%(lineno)s, %(message)s' +DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S %Z' + + +# To set the logging level of splunklib +# ex. To enable debug logs, call this method with parameter 'logging.DEBUG' +# default logging level is set to 'WARNING' +def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT): + logging.basicConfig(level=level, + format=log_format, + datefmt=date_format) + +__version_info__ = (1, 7, 3) __version__ = ".".join(map(str, __version_info__)) diff --git a/bin/splunklib/binding.py b/bin/splunklib/binding.py old mode 100755 new mode 100644 index b0ed20e..85cb8d1 --- a/bin/splunklib/binding.py +++ b/bin/splunklib/binding.py @@ -31,6 +31,7 @@ import socket import ssl import sys +import time from base64 import b64encode from contextlib import contextmanager from datetime import datetime @@ -38,8 +39,8 @@ from io import BytesIO from xml.etree.ElementTree import XML +from splunklib import __version__ from splunklib import six -from splunklib.six import StringIO from splunklib.six.moves import urllib from .data import record @@ -49,6 +50,7 @@ except ImportError as e: from xml.parsers.expat import ExpatError as ParseError +logger = logging.getLogger(__name__) __all__ = [ "AuthenticationError", @@ -70,7 +72,7 @@ def new_f(*args, **kwargs): start_time = datetime.now() val = f(*args, **kwargs) end_time = datetime.now() - logging.debug("Operation took %s", end_time-start_time) + logger.debug("Operation took %s", end_time-start_time) return val return new_f @@ -296,8 +298,7 @@ def wrapper(self, *args, **kwargs): with _handle_auth_error("Autologin failed."): self.login() with _handle_auth_error( - "Autologin succeeded, but there was an auth error on " - "next request. Something is very wrong."): + "Authentication Failed! If session token is used, it seems to have been expired."): return request_fun(self, *args, **kwargs) elif he.status == 401 and not self.autologin: raise AuthenticationError( @@ -346,7 +347,8 @@ def _authority(scheme=DEFAULT_SCHEME, host=DEFAULT_HOST, port=DEFAULT_PORT): "http://splunk.utopia.net:471" """ - if ':' in host: + # check if host is an IPv6 address and not enclosed in [ ] + if ':' in host and not (host.startswith('[') and host.endswith(']')): # IPv6 addresses must be enclosed in [ ] in order to be well # formed. host = '[' + host + ']' @@ -454,6 +456,12 @@ class Context(object): :type splunkToken: ``string`` :param headers: List of extra HTTP headers to send (optional). :type headers: ``list`` of 2-tuples. + :param retires: Number of retries for each HTTP connection (optional, the default is 0). + NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER AND BLOCK THE + CURRENT THREAD WHILE RETRYING. + :type retries: ``int`` + :param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s). + :type retryDelay: ``int`` (in seconds) :param handler: The HTTP request handler (optional). :returns: A ``Context`` instance. @@ -471,7 +479,8 @@ class Context(object): """ def __init__(self, handler=None, **kwargs): self.http = HttpLib(handler, kwargs.get("verify", False), key_file=kwargs.get("key_file"), - cert_file=kwargs.get("cert_file")) # Default to False for backward compat + cert_file=kwargs.get("cert_file"), context=kwargs.get("context"), # Default to False for backward compat + retries=kwargs.get("retries", 0), retryDelay=kwargs.get("retryDelay", 10)) self.token = kwargs.get("token", _NoAuthenticationToken) if self.token is None: # In case someone explicitly passes token=None self.token = _NoAuthenticationToken @@ -500,13 +509,13 @@ def get_cookies(self): return self.http._cookies def has_cookies(self): - """Returns true if the ``HttpLib`` member of this instance has at least - one cookie stored. + """Returns true if the ``HttpLib`` member of this instance has auth token stored. - :return: ``True`` if there is at least one cookie, else ``False`` + :return: ``True`` if there is auth token present, else ``False`` :rtype: ``bool`` """ - return len(self.get_cookies()) > 0 + auth_token_key = "splunkd_" + return any(auth_token_key in key for key in self.get_cookies().keys()) # Shared per-context request headers @property @@ -519,23 +528,27 @@ def _auth_headers(self): :returns: A list of 2-tuples containing key and value """ + header = [] if self.has_cookies(): return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))] elif self.basic and (self.username and self.password): token = 'Basic %s' % b64encode(("%s:%s" % (self.username, self.password)).encode('utf-8')).decode('ascii') - return [("Authorization", token)] elif self.bearerToken: token = 'Bearer %s' % self.bearerToken - return [("Authorization", token)] elif self.token is _NoAuthenticationToken: - return [] + token = [] else: # Ensure the token is properly formatted if self.token.startswith('Splunk '): token = self.token else: token = 'Splunk %s' % self.token - return [("Authorization", token)] + if token: + header.append(("Authorization", token)) + if self.get_cookies(): + header.append(("Cookie", _make_cookie_header(list(self.get_cookies().items())))) + + return header def connect(self): """Returns an open connection (socket) to the Splunk instance. @@ -618,7 +631,7 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query): """ path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) - logging.debug("DELETE request to %s (body: %s)", path, repr(query)) + logger.debug("DELETE request to %s (body: %s)", path, repr(query)) response = self.http.delete(path, self._auth_headers, **query) return response @@ -681,7 +694,7 @@ def get(self, path_segment, owner=None, app=None, headers=None, sharing=None, ** path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) - logging.debug("GET request to %s (body: %s)", path, repr(query)) + logger.debug("GET request to %s (body: %s)", path, repr(query)) all_headers = headers + self.additional_headers + self._auth_headers response = self.http.get(path, all_headers, **query) return response @@ -724,7 +737,12 @@ def post(self, path_segment, owner=None, app=None, sharing=None, headers=None, * :type headers: ``list`` of 2-tuples. :param query: All other keyword arguments, which are used as query parameters. - :type query: ``string`` + :param body: Parameters to be used in the post body. If specified, + any parameters in the query will be applied to the URL instead of + the body. If a dict is supplied, the key-value pairs will be form + encoded. If a string is supplied, the body will be passed through + in the request unchanged. + :type body: ``dict`` or ``str`` :return: The response from the server. :rtype: ``dict`` with keys ``body``, ``headers``, ``reason``, and ``status`` @@ -754,14 +772,20 @@ def post(self, path_segment, owner=None, app=None, sharing=None, headers=None, * headers = [] path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) - logging.debug("POST request to %s (body: %s)", path, repr(query)) + + # To avoid writing sensitive data in debug logs + endpoint_having_sensitive_data = ["/storage/passwords"] + if any(endpoint in path for endpoint in endpoint_having_sensitive_data): + logger.debug("POST request to %s ", path) + else: + logger.debug("POST request to %s (body: %s)", path, repr(query)) all_headers = headers + self.additional_headers + self._auth_headers response = self.http.post(path, all_headers, **query) return response @_authentication @_log_duration - def request(self, path_segment, method="GET", headers=None, body="", + def request(self, path_segment, method="GET", headers=None, body={}, owner=None, app=None, sharing=None): """Issues an arbitrary HTTP request to the REST path segment. @@ -790,9 +814,6 @@ def request(self, path_segment, method="GET", headers=None, body="", :type app: ``string`` :param sharing: The sharing mode of the namespace (optional). :type sharing: ``string`` - :param query: All other keyword arguments, which are used as query - parameters. - :type query: ``string`` :return: The response from the server. :rtype: ``dict`` with keys ``body``, ``headers``, ``reason``, and ``status`` @@ -821,13 +842,28 @@ def request(self, path_segment, method="GET", headers=None, body="", path = self.authority \ + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + all_headers = headers + self.additional_headers + self._auth_headers - logging.debug("%s request to %s (headers: %s, body: %s)", + logger.debug("%s request to %s (headers: %s, body: %s)", method, path, str(all_headers), repr(body)) - response = self.http.request(path, - {'method': method, - 'headers': all_headers, - 'body': body}) + + if body: + body = _encode(**body) + + if method == "GET": + path = path + UrlEncoded('?' + body, skip_encode=True) + message = {'method': method, + 'headers': all_headers} + else: + message = {'method': method, + 'headers': all_headers, + 'body': body} + else: + message = {'method': method, + 'headers': all_headers} + + response = self.http.request(path, message) + return response def login(self): @@ -1065,7 +1101,7 @@ def __init__(self, message, cause): # # Encode the given kwargs as a query string. This wrapper will also _encode -# a list value as a sequence of assignemnts to the corresponding arg name, +# a list value as a sequence of assignments to the corresponding arg name, # for example an argument such as 'foo=[1,2,3]' will be encoded as # 'foo=1&foo=2&foo=3'. def _encode(**kwargs): @@ -1132,12 +1168,14 @@ class HttpLib(object): If using the default handler, SSL verification can be disabled by passing verify=False. """ - def __init__(self, custom_handler=None, verify=False, key_file=None, cert_file=None): + def __init__(self, custom_handler=None, verify=False, key_file=None, cert_file=None, context=None, retries=0, retryDelay=10): if custom_handler is None: - self.handler = handler(verify=verify, key_file=key_file, cert_file=cert_file) + self.handler = handler(verify=verify, key_file=key_file, cert_file=cert_file, context=context) else: self.handler = custom_handler self._cookies = {} + self.retries = retries + self.retryDelay = retryDelay def delete(self, url, headers=None, **kwargs): """Sends a DELETE request to a URL. @@ -1223,6 +1261,8 @@ def post(self, url, headers=None, **kwargs): headers.append(("Content-Type", "application/x-www-form-urlencoded")) body = kwargs.pop('body') + if isinstance(body, dict): + body = _encode(**body).encode('utf-8') if len(kwargs) > 0: url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True) else: @@ -1249,7 +1289,16 @@ def request(self, url, message, **kwargs): its structure). :rtype: ``dict`` """ - response = self.handler(url, message, **kwargs) + while True: + try: + response = self.handler(url, message, **kwargs) + break + except Exception: + if self.retries <= 0: + raise + else: + time.sleep(self.retryDelay) + self.retries -= 1 response = record(response) if 400 <= response.status: raise HTTPError(response) @@ -1285,7 +1334,10 @@ def __init__(self, response, connection=None): self._buffer = b'' def __str__(self): - return self.read() + if six.PY2: + return self.read() + else: + return str(self.read(), 'UTF-8') @property def empty(self): @@ -1344,7 +1396,7 @@ def readinto(self, byte_array): return bytes_read -def handler(key_file=None, cert_file=None, timeout=None, verify=False): +def handler(key_file=None, cert_file=None, timeout=None, verify=False, context=None): """This class returns an instance of the default HTTP request handler using the values you provide. @@ -1356,6 +1408,8 @@ def handler(key_file=None, cert_file=None, timeout=None, verify=False): :type timeout: ``integer`` or "None" :param `verify`: Set to False to disable SSL verification on https connections. :type verify: ``Boolean`` + :param `context`: The SSLContext that can is used with the HTTPSConnection when verify=True is enabled and context is specified + :type context: ``SSLContext` """ def connect(scheme, host, port): @@ -1369,6 +1423,10 @@ def connect(scheme, host, port): if not verify: kwargs['context'] = ssl._create_unverified_context() + elif context: + # verify is True in elif branch and context is not None + kwargs['context'] = context + return six.moves.http_client.HTTPSConnection(host, port, **kwargs) raise ValueError("unsupported scheme: %s" % scheme) @@ -1378,7 +1436,7 @@ def request(url, message, **kwargs): head = { "Content-Length": str(len(body)), "Host": host, - "User-Agent": "splunk-sdk-python/1.6.12", + "User-Agent": "splunk-sdk-python/%s" % __version__, "Accept": "*/*", "Connection": "Close", } # defaults diff --git a/bin/splunklib/client.py b/bin/splunklib/client.py old mode 100755 new mode 100644 index de2f53a..33156bb --- a/bin/splunklib/client.py +++ b/bin/splunklib/client.py @@ -62,6 +62,7 @@ import datetime import json import logging +import re import socket from datetime import datetime, timedelta from time import sleep @@ -75,6 +76,8 @@ namespace) from .data import record +logger = logging.getLogger(__name__) + __all__ = [ "connect", "NotSupportedError", @@ -97,6 +100,7 @@ PATH_INDEXES = "data/indexes/" PATH_INPUTS = "data/inputs/" PATH_JOBS = "search/jobs/" +PATH_JOBS_V2 = "search/v2/jobs/" PATH_LOGGER = "/services/server/logger/" PATH_MESSAGES = "messages/" PATH_MODULAR_INPUTS = "data/modular-inputs" @@ -224,7 +228,10 @@ def _load_atom_entries(response): # Load the sid from the body of the given response -def _load_sid(response): +def _load_sid(response, output_mode): + if output_mode == "json": + json_obj = json.loads(response.body.read()) + return json_obj.get('sid') return _load_atom(response).response.sid @@ -295,7 +302,7 @@ def connect(**kwargs): :type port: ``integer`` :param scheme: The scheme for accessing the service (the default is "https"). :type scheme: "https" or "http" - :param verify: Enable (True) or disable (False) SSL verrification for + :param verify: Enable (True) or disable (False) SSL verification for https connections. (optional, the default is True) :type verify: ``Boolean`` :param `owner`: The owner context of the namespace (optional). @@ -318,6 +325,13 @@ def connect(**kwargs): :type username: ``string`` :param `password`: The password for the Splunk account. :type password: ``string`` + :param retires: Number of retries for each HTTP connection (optional, the default is 0). + NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER. + :type retries: ``int`` + :param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s). + :type retryDelay: ``int`` (in seconds) + :param `context`: The SSLContext that can be used when setting verify=True (optional) + :type context: ``SSLContext`` :return: An initialized :class:`Service` connection. **Example**:: @@ -365,7 +379,7 @@ class Service(_BaseService): :type port: ``integer`` :param scheme: The scheme for accessing the service (the default is "https"). :type scheme: "https" or "http" - :param verify: Enable (True) or disable (False) SSL verrification for + :param verify: Enable (True) or disable (False) SSL verification for https connections. (optional, the default is True) :type verify: ``Boolean`` :param `owner`: The owner context of the namespace (optional; use "-" for wildcard). @@ -384,6 +398,11 @@ class Service(_BaseService): :param `password`: The password, which is used to authenticate the Splunk instance. :type password: ``string`` + :param retires: Number of retries for each HTTP connection (optional, the default is 0). + NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER. + :type retries: ``int`` + :param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s). + :type retryDelay: ``int`` (in seconds) :return: A :class:`Service` instance. **Example**:: @@ -401,6 +420,8 @@ class Service(_BaseService): def __init__(self, **kwargs): super(Service, self).__init__(**kwargs) self._splunk_version = None + self._kvstore_owner = None + self._instance_type = None @property def apps(self): @@ -552,6 +573,8 @@ def parse(self, query, **kwargs): :type kwargs: ``dict`` :return: A semantic map of the parsed search query. """ + if not self.disable_v2_api: + return self.post("search/v2/parser", q=query, **kwargs) return self.get("search/parser", q=query, **kwargs) def restart(self, timeout=None): @@ -673,12 +696,50 @@ def splunk_version(self): self._splunk_version = tuple([int(p) for p in self.info['version'].split('.')]) return self._splunk_version + @property + def splunk_instance(self): + if self._instance_type is None : + splunk_info = self.info; + if hasattr(splunk_info, 'instance_type') : + self._instance_type = splunk_info['instance_type'] + else: + self._instance_type = '' + return self._instance_type + + @property + def disable_v2_api(self): + if self.splunk_instance.lower() == 'cloud': + return self.splunk_version < (9,0,2209) + return self.splunk_version < (9,0,2) + + @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" + return self._kvstore_owner + + @kvstore_owner.setter + def kvstore_owner(self, value): + """ + kvstore is refreshed, when the owner value is changed + """ + self._kvstore_owner = value + self.kvstore + @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 return KVStoreCollections(self) @property @@ -699,7 +760,26 @@ class Endpoint(object): """ def __init__(self, service, path): self.service = service - self.path = path if path.endswith('/') else path + '/' + self.path = path + + def get_api_version(self, path): + """Return the API version of the service used in the provided path. + + Args: + path (str): A fully-qualified endpoint path (for example, "/services/search/jobs"). + + Returns: + int: Version of the API (for example, 1) + """ + # Default to v1 if undefined in the path + # For example, "/services/search/jobs" is using API v1 + api_version = 1 + + versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path) + if versionSearch: + api_version = int(versionSearch.group(1)) + + return api_version def get(self, path_segment="", owner=None, app=None, sharing=None, **query): """Performs a GET operation on the path segment relative to this endpoint. @@ -757,10 +837,28 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query): if path_segment.startswith('/'): path = path_segment else: + if not self.path.endswith('/') and path_segment != "": + self.path = self.path + '/' path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing) # ^-- This was "%s%s" % (self.path, path_segment). # That doesn't work, because self.path may be UrlEncoded. + + # Get the API version from the path + api_version = self.get_api_version(path) + + # Search API v2+ fallback to v1: + # - In v2+, /results_preview, /events and /results do not support search params. + # - Fallback from v2+ to v1 if Splunk Version is < 9. + # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)): + # path = path.replace(PATH_JOBS_V2, PATH_JOBS) + + if api_version == 1: + if isinstance(path, UrlEncoded): + path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True) + else: + path = path.replace(PATH_JOBS_V2, PATH_JOBS) + return self.service.get(path, owner=owner, app=app, sharing=sharing, **query) @@ -813,11 +911,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query): apps.get('nonexistant/path') # raises HTTPError s.logout() apps.get() # raises AuthenticationError - """ + """ if path_segment.startswith('/'): path = path_segment else: + if not self.path.endswith('/') and path_segment != "": + self.path = self.path + '/' path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing) + + # Get the API version from the path + api_version = self.get_api_version(path) + + # Search API v2+ fallback to v1: + # - In v2+, /results_preview, /events and /results do not support search params. + # - Fallback from v2+ to v1 if Splunk Version is < 9. + # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)): + # path = path.replace(PATH_JOBS_V2, PATH_JOBS) + + if api_version == 1: + if isinstance(path, UrlEncoded): + path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True) + else: + path = path.replace(PATH_JOBS_V2, PATH_JOBS) + return self.service.post(path, owner=owner, app=app, sharing=sharing, **query) @@ -828,35 +944,24 @@ class Entity(Endpoint): ``Entity`` provides the majority of functionality required by entities. Subclasses only implement the special cases for individual entities. - For example for deployment serverclasses, the subclass makes whitelists and - blacklists into Python lists. + For example for saved searches, the subclass makes fields like ``action.email``, + ``alert_type``, and ``search`` available. An ``Entity`` is addressed like a dictionary, with a few extensions, - so the following all work:: - - ent['email.action'] - ent['disabled'] - ent['whitelist'] - - Many endpoints have values that share a prefix, such as - ``email.to``, ``email.action``, and ``email.subject``. You can extract - the whole fields, or use the key ``email`` to get a dictionary of - all the subelements. That is, ``ent['email']`` returns a - dictionary with the keys ``to``, ``action``, ``subject``, and so on. If - there are multiple levels of dots, each level is made into a - subdictionary, so ``email.body.salutation`` can be accessed at - ``ent['email']['body']['salutation']`` or - ``ent['email.body.salutation']``. + so the following all work, for example in saved searches:: + + ent['action.email'] + ent['alert_type'] + ent['search'] You can also access the fields as though they were the fields of a Python object, as in:: - ent.email.action - ent.disabled - ent.whitelist + ent.alert_type + ent.search However, because some of the field names are not valid Python identifiers, - the dictionary-like syntax is preferrable. + the dictionary-like syntax is preferable. The state of an :class:`Entity` object is cached, so accessing a field does not contact the server. If you think the values on the @@ -953,7 +1058,10 @@ def __getitem__(self, key): def _load_atom_entry(self, response): elem = _load_atom(response, XNAME_ENTRY) if isinstance(elem, list): - raise AmbiguousReferenceException("Fetch from server returned multiple entries for name %s." % self.name) + apps = [ele.entry.content.get('eai:appName') for ele in elem] + + raise AmbiguousReferenceException( + "Fetch from server returned multiple entries for name '%s' in apps %s." % (elem[0].entry.title, apps)) else: return elem.entry @@ -1059,8 +1167,6 @@ def content(self): def disable(self): """Disables the entity at this endpoint.""" self.post("disable") - if self.service.restart_required: - self.service.restart(120) return self def enable(self): @@ -1110,6 +1216,36 @@ def reload(self): self.post("_reload") return self + def acl_update(self, **kwargs): + """To update Access Control List (ACL) properties for an endpoint. + + :param kwargs: Additional entity-specific arguments (required). + + - "owner" (``string``): The Splunk username, such as "admin". A value of "nobody" means no specific user (required). + + - "sharing" (``string``): A mode that indicates how the resource is shared. The sharing mode can be "user", "app", "global", or "system" (required). + + :type kwargs: ``dict`` + + **Example**:: + + import splunklib.client as client + service = client.connect(...) + saved_search = service.saved_searches["name"] + saved_search.acl_update(sharing="app", owner="nobody", app="search", **{"perms.read": "admin, nobody"}) + """ + if "body" not in kwargs: + kwargs = {"body": kwargs} + + if "sharing" not in kwargs["body"]: + raise ValueError("Required argument 'sharing' is missing.") + if "owner" not in kwargs["body"]: + raise ValueError("Required argument 'owner' is missing.") + + self.post("acl", **kwargs) + self.refresh() + return self + @property def state(self): """Returns the entity's state record. @@ -1444,7 +1580,7 @@ def iter(self, offset=0, count=None, pagesize=None, **kwargs): if pagesize is None or N < pagesize: break offset += N - logging.debug("pagesize=%d, fetched=%d, offset=%d, N=%d, kwargs=%s", pagesize, fetched, offset, N, kwargs) + logger.debug("pagesize=%d, fetched=%d, offset=%d, N=%d, kwargs=%s", pagesize, fetched, offset, N, kwargs) # kwargs: count, offset, search, sort_dir, sort_key, sort_mode def list(self, count=None, **kwargs): @@ -1812,8 +1948,6 @@ class StoragePasswords(Collection): instance. Retrieve this collection using :meth:`Service.storage_passwords`. """ def __init__(self, service): - if service.namespace.owner == '-' or service.namespace.app == '-': - raise ValueError("StoragePasswords cannot have wildcards in namespace.") super(StoragePasswords, self).__init__(service, PATH_STORAGE_PASSWORDS, item=StoragePassword) def create(self, password, username, realm=None): @@ -1831,6 +1965,9 @@ def create(self, password, username, realm=None): :return: The :class:`StoragePassword` object created. """ + if self.service.namespace.owner == '-' or self.service.namespace.app == '-': + raise ValueError("While creating StoragePasswords, namespace cannot have wildcards.") + if not isinstance(username, six.string_types): raise ValueError("Invalid name: %s" % repr(username)) @@ -1862,6 +1999,9 @@ def delete(self, username, realm=None): :return: The `StoragePassword` collection. :rtype: ``self`` """ + if self.service.namespace.owner == '-' or self.service.namespace.app == '-': + raise ValueError("app context must be specified when removing a password.") + if realm is None: # This case makes the username optional, so # the full name can be passed in as realm. @@ -1872,7 +2012,7 @@ def delete(self, username, realm=None): name = UrlEncoded(realm, encode_slash=True) + ":" + UrlEncoded(username, encode_slash=True) # Append the : expected at the end of the name - if name[-1] is not ":": + if name[-1] != ":": name = name + ":" return Collection.delete(self, name) @@ -2086,10 +2226,6 @@ def submit(self, event, host=None, source=None, sourcetype=None): if source is not None: args['source'] = source if sourcetype is not None: args['sourcetype'] = sourcetype - # The reason we use service.request directly rather than POST - # is that we are not sending a POST request encoded using - # x-www-form-urlencoded (as we do not have a key=value body), - # because we aren't really sending a "form". self.service.post(PATH_RECEIVERS_SIMPLE, body=event, **args) return self @@ -2517,9 +2653,9 @@ def list(self, *kinds, **kwargs): kinds = self.kinds if len(kinds) == 1: kind = kinds[0] - logging.debug("Inputs.list taking short circuit branch for single kind.") + logger.debug("Inputs.list taking short circuit branch for single kind.") path = self.kindpath(kind) - logging.debug("Path for inputs: %s", path) + logger.debug("Path for inputs: %s", path) try: path = UrlEncoded(path, skip_encode=True) response = self.get(path, **kwargs) @@ -2630,7 +2766,14 @@ def oneshot(self, path, **kwargs): class Job(Entity): """This class represents a search job.""" def __init__(self, service, sid, **kwargs): - path = PATH_JOBS + sid + # Default to v2 in Splunk Version 9+ + path = "{path}{sid}" + # Formatting path based on the Splunk Version + if service.disable_v2_api: + path = path.format(path=PATH_JOBS, sid=sid) + else: + path = path.format(path=PATH_JOBS_V2, sid=sid) + Entity.__init__(self, service, path, skip_refresh=True, **kwargs) self.sid = sid @@ -2684,7 +2827,11 @@ def events(self, **kwargs): :return: The ``InputStream`` IO handle to this job's events. """ kwargs['segmentation'] = kwargs.get('segmentation', 'none') - return self.get("events", **kwargs).body + + # Search API v1(GET) and v2(POST) + if self.service.disable_v2_api: + return self.get("events", **kwargs).body + return self.post("events", **kwargs).body def finalize(self): """Stops the job and provides intermediate results for retrieval. @@ -2737,9 +2884,8 @@ def pause(self): return self def results(self, **query_params): - """Returns a streaming handle to this job's search results. To get a - nice, Pythonic iterator, pass the handle to :class:`splunklib.results.ResultsReader`, - as in:: + """Returns a streaming handle to this job's search results. To get a nice, Pythonic iterator, pass the handle + to :class:`splunklib.results.JSONResultsReader` along with the query param "output_mode='json'", as in:: import splunklib.client as client import splunklib.results as results @@ -2748,7 +2894,7 @@ def results(self, **query_params): job = service.jobs.create("search * | head 5") while not job.is_done(): sleep(.2) - rr = results.ResultsReader(job.results()) + rr = results.JSONResultsReader(job.results(output_mode='json')) for result in rr: if isinstance(result, results.Message): # Diagnostic messages may be returned in the results @@ -2773,24 +2919,26 @@ def results(self, **query_params): :return: The ``InputStream`` IO handle to this job's results. """ query_params['segmentation'] = query_params.get('segmentation', 'none') - return self.get("results", **query_params).body + + # Search API v1(GET) and v2(POST) + if self.service.disable_v2_api: + return self.get("results", **query_params).body + return self.post("results", **query_params).body def preview(self, **query_params): """Returns a streaming handle to this job's preview search results. - Unlike :class:`splunklib.results.ResultsReader`, which requires a job to - be finished to - return any results, the ``preview`` method returns any results that have - been generated so far, whether the job is running or not. The - returned search results are the raw data from the server. Pass - the handle returned to :class:`splunklib.results.ResultsReader` to get a - nice, Pythonic iterator over objects, as in:: + Unlike :class:`splunklib.results.JSONResultsReader`along with the query param "output_mode='json'", + which requires a job to be finished to return any results, the ``preview`` method returns any results that + have been generated so far, whether the job is running or not. The returned search results are the raw data + from the server. Pass the handle returned to :class:`splunklib.results.JSONResultsReader` to get a nice, + Pythonic iterator over objects, as in:: import splunklib.client as client import splunklib.results as results service = client.connect(...) job = service.jobs.create("search * | head 5") - rr = results.ResultsReader(job.preview()) + rr = results.JSONResultsReader(job.preview(output_mode='json')) for result in rr: if isinstance(result, results.Message): # Diagnostic messages may be returned in the results @@ -2816,7 +2964,11 @@ def preview(self, **query_params): :return: The ``InputStream`` IO handle to this job's preview results. """ query_params['segmentation'] = query_params.get('segmentation', 'none') - return self.get("results_preview", **query_params).body + + # Search API v1(GET) and v2(POST) + if self.service.disable_v2_api: + return self.get("results_preview", **query_params).body + return self.post("results_preview", **query_params).body def searchlog(self, **kwargs): """Returns a streaming handle to this job's search log. @@ -2905,7 +3057,12 @@ class Jobs(Collection): """This class represents a collection of search jobs. Retrieve this collection using :meth:`Service.jobs`.""" def __init__(self, service): - Collection.__init__(self, service, PATH_JOBS, item=Job) + # Splunk 9 introduces the v2 endpoint + if not service.disable_v2_api: + path = PATH_JOBS_V2 + else: + path = PATH_JOBS + Collection.__init__(self, service, path, item=Job) # The count value to say list all the contents of this # Collection is 0, not -1 as it is on most. self.null_count = 0 @@ -2941,19 +3098,19 @@ def create(self, query, **kwargs): if kwargs.get("exec_mode", None) == "oneshot": raise TypeError("Cannot specify exec_mode=oneshot; use the oneshot method instead.") response = self.post(search=query, **kwargs) - sid = _load_sid(response) + sid = _load_sid(response, kwargs.get("output_mode", None)) return Job(self.service, sid) def export(self, query, **params): - """Runs a search and immediately starts streaming preview events. - This method returns a streaming handle to this job's events as an XML - document from the server. To parse this stream into usable Python objects, - pass the handle to :class:`splunklib.results.ResultsReader`:: + """Runs a search and immediately starts streaming preview events. This method returns a streaming handle to + this job's events as an XML document from the server. To parse this stream into usable Python objects, + pass the handle to :class:`splunklib.results.JSONResultsReader` along with the query param + "output_mode='json'":: import splunklib.client as client import splunklib.results as results service = client.connect(...) - rr = results.ResultsReader(service.jobs.export("search * | head 5")) + rr = results.JSONResultsReader(service.jobs.export("search * | head 5",output_mode='json')) for result in rr: if isinstance(result, results.Message): # Diagnostic messages may be returned in the results @@ -3002,14 +3159,14 @@ def itemmeta(self): def oneshot(self, query, **params): """Run a oneshot search and returns a streaming handle to the results. - The ``InputStream`` object streams XML fragments from the server. To - parse this stream into usable Python objects, - pass the handle to :class:`splunklib.results.ResultsReader`:: + The ``InputStream`` object streams fragments from the server. To parse this stream into usable Python + objects, pass the handle to :class:`splunklib.results.JSONResultsReader` along with the query param + "output_mode='json'" :: import splunklib.client as client import splunklib.results as results service = client.connect(...) - rr = results.ResultsReader(service.jobs.oneshot("search * | head 5")) + rr = results.JSONResultsReader(service.jobs.oneshot("search * | head 5",output_mode='json')) for result in rr: if isinstance(result, results.Message): # Diagnostic messages may be returned in the results @@ -3157,7 +3314,7 @@ def dispatch(self, **kwargs): :return: The :class:`Job`. """ response = self.post("dispatch", **kwargs) - sid = _load_sid(response) + sid = _load_sid(response, kwargs.get("output_mode", None)) return Job(self.service, sid) @property @@ -3181,12 +3338,15 @@ def fired_alerts(self): item=AlertGroup) return c - def history(self): + def history(self, **kwargs): """Returns a list of search jobs corresponding to this saved search. + :param `kwargs`: Additional arguments (optional). + :type kwargs: ``dict`` + :return: A list of :class:`Job` objects. """ - response = self.get("history") + response = self.get("history", **kwargs) entries = _load_atom_entries(response) if entries is None: return [] jobs = [] @@ -3549,13 +3709,20 @@ class KVStoreCollections(Collection): def __init__(self, service): Collection.__init__(self, service, 'storage/collections/config', item=KVStoreCollection) - def create(self, name, indexes = {}, fields = {}, **kwargs): + def __getitem__(self, item): + res = Collection.__getitem__(self, item) + for k, v in res.content.items(): + if "accelerated_fields" in k: + res.content[k] = json.loads(v) + return res + + def create(self, name, accelerated_fields={}, fields={}, **kwargs): """Creates a KV Store Collection. :param name: name of collection to create :type name: ``string`` - :param indexes: dictionary of index definitions - :type indexes: ``dict`` + :param accelerated_fields: dictionary of accelerated_fields definitions + :type accelerated_fields: ``dict`` :param fields: dictionary of field definitions :type fields: ``dict`` :param kwargs: a dictionary of additional parameters specifying indexes and field definitions @@ -3563,10 +3730,10 @@ def create(self, name, indexes = {}, fields = {}, **kwargs): :return: Result of POST request """ - for k, v in six.iteritems(indexes): + for k, v in six.iteritems(accelerated_fields): if isinstance(v, dict): v = json.dumps(v) - kwargs['index.' + k] = v + kwargs['accelerated_fields.' + k] = v for k, v in six.iteritems(fields): kwargs['field.' + k] = v return self.post(name=name, **kwargs) @@ -3580,18 +3747,20 @@ def data(self): """ return KVStoreCollectionData(self) - def update_index(self, name, value): - """Changes the definition of a KV Store index. + def update_accelerated_field(self, name, value): + """Changes the definition of a KV Store accelerated_field. - :param name: name of index to change + :param name: name of accelerated_fields to change :type name: ``string`` - :param value: new index definition - :type value: ``dict`` or ``string`` + :param value: new accelerated_fields definition + :type value: ``dict`` :return: Result of POST request """ kwargs = {} - kwargs['index.' + name] = value if isinstance(value, basestring) else json.dumps(value) + if isinstance(value, dict): + value = json.dumps(value) + kwargs['accelerated_fields.' + name] = value return self.post(**kwargs) def update_field(self, name, value): @@ -3619,7 +3788,7 @@ def __init__(self, collection): self.service = collection.service self.collection = collection self.owner, self.app, self.sharing = collection._proper_namespace() - self.path = 'storage/collections/data/' + UrlEncoded(self.collection.name) + '/' + self.path = 'storage/collections/data/' + UrlEncoded(self.collection.name, encode_slash=True) + '/' def _get(self, url, **kwargs): return self.service.get(self.path + url, owner=self.owner, app=self.app, sharing=self.sharing, **kwargs) @@ -3640,6 +3809,11 @@ def query(self, **query): :return: Array of documents retrieved by query. :rtype: ``array`` """ + + for key, value in query.items(): + if isinstance(query[key], dict): + query[key] = json.dumps(value) + return json.loads(self._get('', **query).body.read().decode('utf-8')) def query_by_id(self, id): @@ -3652,7 +3826,7 @@ def query_by_id(self, id): :return: Document with id :rtype: ``dict`` """ - return json.loads(self._get(UrlEncoded(str(id))).body.read().decode('utf-8')) + return json.loads(self._get(UrlEncoded(str(id), encode_slash=True)).body.read().decode('utf-8')) def insert(self, data): """ @@ -3664,6 +3838,8 @@ def insert(self, data): :return: _id of inserted object :rtype: ``dict`` """ + if isinstance(data, dict): + data = json.dumps(data) return json.loads(self._post('', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) def delete(self, query=None): @@ -3686,7 +3862,7 @@ def delete_by_id(self, id): :return: Result of DELETE request """ - return self._delete(UrlEncoded(str(id))) + return self._delete(UrlEncoded(str(id), encode_slash=True)) def update(self, id, data): """ @@ -3700,7 +3876,9 @@ def update(self, id, data): :return: id of replaced document :rtype: ``dict`` """ - return json.loads(self._post(UrlEncoded(str(id)), headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) + if isinstance(data, dict): + data = json.dumps(data) + return json.loads(self._post(UrlEncoded(str(id), encode_slash=True), headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) def batch_find(self, *dbqueries): """ @@ -3734,4 +3912,4 @@ def batch_save(self, *documents): data = json.dumps(documents) - return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) + return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) \ No newline at end of file diff --git a/bin/splunklib/data.py b/bin/splunklib/data.py old mode 100755 new mode 100644 index dedbb33..f9ffb86 --- a/bin/splunklib/data.py +++ b/bin/splunklib/data.py @@ -161,8 +161,8 @@ def load_value(element, nametable=None): text = element.text if text is None: return None - text = text.strip() - if len(text) == 0: + + if len(text.strip()) == 0: return None return text diff --git a/bin/splunklib/modularinput/__init__.py b/bin/splunklib/modularinput/__init__.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/modularinput/argument.py b/bin/splunklib/modularinput/argument.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/modularinput/event.py b/bin/splunklib/modularinput/event.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/modularinput/event_writer.py b/bin/splunklib/modularinput/event_writer.py old mode 100755 new mode 100644 index 4d0b21f..5f8c5aa --- a/bin/splunklib/modularinput/event_writer.py +++ b/bin/splunklib/modularinput/event_writer.py @@ -15,8 +15,7 @@ from __future__ import absolute_import import sys -from io import TextIOWrapper, TextIOBase -from splunklib.six import ensure_text +from splunklib.six import ensure_str from .event import ET try: @@ -43,15 +42,8 @@ def __init__(self, output = sys.stdout, error = sys.stderr): :param output: Where to write the output; defaults to sys.stdout. :param error: Where to write any errors; defaults to sys.stderr. """ - if isinstance(output, TextIOBase): - self._out = output - else: - self._out = TextIOWrapper(output) - - if isinstance(error, TextIOBase): - self._err = error - else: - self._err = TextIOWrapper(error) + self._out = output + self._err = error # has the opening tag been written yet? self.header_written = False @@ -63,7 +55,7 @@ def write_event(self, event): """ if not self.header_written: - self._out.write(ensure_text("")) + self._out.write("") self.header_written = True event.write_to(self._out) @@ -71,10 +63,12 @@ def write_event(self, event): def log(self, severity, message): """Logs messages about the state of this modular input to Splunk. These messages will show up in Splunk's internal logs. + :param severity: ``string``, severity of message, see severities defined as class constants. :param message: ``string``, message to log. """ - self._err.write(ensure_text("%s %s\n" % (severity, message))) + + self._err.write("%s %s\n" % (severity, message)) self._err.flush() def write_xml_document(self, document): @@ -83,11 +77,11 @@ def write_xml_document(self, document): :param document: An ``ElementTree`` object. """ - data = ET.tostring(document) - self._out.write(ensure_text(data)) + self._out.write(ensure_str(ET.tostring(document))) self._out.flush() def close(self): """Write the closing tag to make this XML well formed.""" - self._out.write(ensure_text("")) + if self.header_written: + self._out.write("") self._out.flush() diff --git a/bin/splunklib/modularinput/input_definition.py b/bin/splunklib/modularinput/input_definition.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/modularinput/scheme.py b/bin/splunklib/modularinput/scheme.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/modularinput/script.py b/bin/splunklib/modularinput/script.py old mode 100755 new mode 100644 index a254dfa..8595dc4 --- a/bin/splunklib/modularinput/script.py +++ b/bin/splunklib/modularinput/script.py @@ -105,8 +105,7 @@ def run_script(self, args, event_writer, input_stream): return 1 except Exception as e: - err_string = EventWriter.ERROR + str(e) - event_writer._err.write(err_string) + event_writer.log(EventWriter.ERROR, str(e)) return 1 @property diff --git a/bin/splunklib/modularinput/utils.py b/bin/splunklib/modularinput/utils.py old mode 100755 new mode 100644 index 853694a..3d42b63 --- a/bin/splunklib/modularinput/utils.py +++ b/bin/splunklib/modularinput/utils.py @@ -64,11 +64,14 @@ def parse_parameters(param_node): def parse_xml_data(parent_node, child_node_tag): data = {} for child in parent_node: + child_name = child.get("name") if child.tag == child_node_tag: if child_node_tag == "stanza": - data[child.get("name")] = {} + data[child_name] = { + "__app": child.get("app", None) + } for param in child: - data[child.get("name")][param.get("name")] = parse_parameters(param) + data[child_name][param.get("name")] = parse_parameters(param) elif "item" == parent_node.tag: - data[child.get("name")] = parse_parameters(child) + data[child_name] = parse_parameters(child) return data diff --git a/bin/splunklib/modularinput/validation_definition.py b/bin/splunklib/modularinput/validation_definition.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/ordereddict.py b/bin/splunklib/ordereddict.py deleted file mode 100755 index 9495566..0000000 --- a/bin/splunklib/ordereddict.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -from UserDict import DictMixin - - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other diff --git a/bin/splunklib/results.py b/bin/splunklib/results.py old mode 100755 new mode 100644 index 20501c5..8543ab0 --- a/bin/splunklib/results.py +++ b/bin/splunklib/results.py @@ -23,7 +23,7 @@ accessing search results while avoiding buffering the result set, which can be very large. -To use the reader, instantiate :class:`ResultsReader` on a search result stream +To use the reader, instantiate :class:`JSONResultsReader` on a search result stream as follows::: reader = ResultsReader(result_stream) @@ -34,18 +34,19 @@ from __future__ import absolute_import -from io import BytesIO +from io import BufferedReader, BytesIO from splunklib import six + +from splunklib.six import deprecated + try: import xml.etree.cElementTree as et except: import xml.etree.ElementTree as et -try: - from collections import OrderedDict # must be python 2.7 -except ImportError: - from .ordereddict import OrderedDict +from collections import OrderedDict +from json import loads as json_loads try: from splunklib.six.moves import cStringIO as StringIO @@ -54,9 +55,11 @@ __all__ = [ "ResultsReader", - "Message" + "Message", + "JSONResultsReader" ] + class Message(object): """This class represents informational messages that Splunk interleaves in the results stream. @@ -67,6 +70,7 @@ class Message(object): m = Message("DEBUG", "There's something in that variable...") """ + def __init__(self, type_, message): self.type = type_ self.message = message @@ -80,6 +84,7 @@ def __eq__(self, other): def __hash__(self): return hash((self.type, self.message)) + class _ConcatenatedStream(object): """Lazily concatenate zero or more streams into a stream. @@ -92,6 +97,7 @@ class _ConcatenatedStream(object): s = _ConcatenatedStream(StringIO("abc"), StringIO("def")) assert s.read() == "abcdef" """ + def __init__(self, *streams): self.streams = list(streams) @@ -110,6 +116,7 @@ def read(self, n=None): del self.streams[0] return response + class _XMLDTDFilter(object): """Lazily remove all XML DTDs from a stream. @@ -123,6 +130,7 @@ class _XMLDTDFilter(object): s = _XMLDTDFilter("") assert s.read() == "" """ + def __init__(self, stream): self.stream = stream @@ -153,6 +161,8 @@ def read(self, n=None): n -= 1 return response + +@deprecated("Use the JSONResultsReader function instead in conjuction with the 'output_mode' query param set to 'json'") class ResultsReader(object): """This class returns dictionaries and Splunk messages from an XML results stream. @@ -180,6 +190,7 @@ class ResultsReader(object): print "Message: %s" % result print "is_preview = %s " % reader.is_preview """ + # Be sure to update the docstrings of client.Jobs.oneshot, # client.Job.results_preview and client.Job.results to match any # changes made to ResultsReader. @@ -260,16 +271,16 @@ def _parse_results(self, stream): # So we'll define it here def __itertext(self): - tag = self.tag - if not isinstance(tag, six.string_types) and tag is not None: - return - if self.text: - yield self.text - for e in self: - for s in __itertext(e): - yield s - if e.tail: - yield e.tail + tag = self.tag + if not isinstance(tag, six.string_types) and tag is not None: + return + if self.text: + yield self.text + for e in self: + for s in __itertext(e): + yield s + if e.tail: + yield e.tail text = "".join(__itertext(elem)) values.append(text) @@ -291,5 +302,72 @@ def __itertext(self): raise +class JSONResultsReader(object): + """This class returns dictionaries and Splunk messages from a JSON results + stream. + ``JSONResultsReader`` is iterable, and returns a ``dict`` for results, or a + :class:`Message` object for Splunk messages. This class has one field, + ``is_preview``, which is ``True`` when the results are a preview from a + running search, or ``False`` when the results are from a completed search. + + This function has no network activity other than what is implicit in the + stream it operates on. + + :param `stream`: The stream to read from (any object that supports``.read()``). + + **Example**:: + + import results + response = ... # the body of an HTTP response + reader = results.JSONResultsReader(response) + for result in reader: + if isinstance(result, dict): + print "Result: %s" % result + elif isinstance(result, results.Message): + print "Message: %s" % result + print "is_preview = %s " % reader.is_preview + """ + + # Be sure to update the docstrings of client.Jobs.oneshot, + # client.Job.results_preview and client.Job.results to match any + # changes made to JSONResultsReader. + # + # This wouldn't be a class, just the _parse_results function below, + # except that you cannot get the current generator inside the + # function creating that generator. Thus it's all wrapped up for + # the sake of one field. + def __init__(self, stream): + # The search/jobs/exports endpoint, when run with + # earliest_time=rt and latest_time=rt, output_mode=json, streams a sequence of + # JSON documents, each containing a result, as opposed to one + # results element containing lots of results. + stream = BufferedReader(stream) + self.is_preview = None + self._gen = self._parse_results(stream) + + def __iter__(self): + return self + + def next(self): + return next(self._gen) + __next__ = next + def _parse_results(self, stream): + """Parse results and messages out of *stream*.""" + for line in stream.readlines(): + strip_line = line.strip() + if strip_line.__len__() == 0: continue + parsed_line = json_loads(strip_line) + if "preview" in parsed_line: + self.is_preview = parsed_line["preview"] + if "messages" in parsed_line and parsed_line["messages"].__len__() > 0: + for message in parsed_line["messages"]: + msg_type = message.get("type", "Unknown Message Type") + text = message.get("text") + yield Message(msg_type, text) + if "result" in parsed_line: + yield parsed_line["result"] + if "results" in parsed_line: + for result in parsed_line["results"]: + yield result diff --git a/bin/splunklib/searchcommands/__init__.py b/bin/splunklib/searchcommands/__init__.py old mode 100755 new mode 100644 index c56c510..8a92903 --- a/bin/splunklib/searchcommands/__init__.py +++ b/bin/splunklib/searchcommands/__init__.py @@ -134,9 +134,13 @@ .. topic:: References - 1. `Search command style guide `__ + 1. `Custom Search Command manual: `__ - 2. `Commands.conf.spec `_ + 2. `Create Custom Search Commands with commands.conf.spec `_ + + 3. `Configure seach assistant with searchbnf.conf `_ + + 4. `Control search distribution with distsearch.conf `_ """ diff --git a/bin/splunklib/searchcommands/decorators.py b/bin/splunklib/searchcommands/decorators.py old mode 100755 new mode 100644 index 36590a7..d8b3f48 --- a/bin/splunklib/searchcommands/decorators.py +++ b/bin/splunklib/searchcommands/decorators.py @@ -17,10 +17,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals from splunklib import six -try: - from collections import OrderedDict # must be python 2.7 -except ImportError: - from ..ordereddict import OrderedDict +from collections import OrderedDict # must be python 2.7 from inspect import getmembers, isclass, isfunction from splunklib.six.moves import map as imap diff --git a/bin/splunklib/searchcommands/environment.py b/bin/splunklib/searchcommands/environment.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/searchcommands/eventing_command.py b/bin/splunklib/searchcommands/eventing_command.py old mode 100755 new mode 100644 index 1481cee..27dc13a --- a/bin/splunklib/searchcommands/eventing_command.py +++ b/bin/splunklib/searchcommands/eventing_command.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from splunklib import six from splunklib.six.moves import map as imap from .decorators import ConfigurationSetting @@ -135,8 +136,14 @@ def fix_up(cls, command): raise AttributeError('No EventingCommand.transform override') SearchCommand.ConfigurationSettings.fix_up(command) + # TODO: Stop looking like a dictionary because we don't obey the semantics + # N.B.: Does not use Python 2 dict copy semantics def iteritems(self): iteritems = SearchCommand.ConfigurationSettings.iteritems(self) return imap(lambda name_value: (name_value[0], 'events' if name_value[0] == 'type' else name_value[1]), iteritems) + # N.B.: Does not use Python 3 dict view semantics + if not six.PY2: + items = iteritems + # endregion diff --git a/bin/splunklib/searchcommands/external_search_command.py b/bin/splunklib/searchcommands/external_search_command.py old mode 100755 new mode 100644 diff --git a/bin/splunklib/searchcommands/generating_command.py b/bin/splunklib/searchcommands/generating_command.py old mode 100755 new mode 100644 index 46858cb..6a75d2c --- a/bin/splunklib/searchcommands/generating_command.py +++ b/bin/splunklib/searchcommands/generating_command.py @@ -15,10 +15,12 @@ # under the License. from __future__ import absolute_import, division, print_function, unicode_literals +import sys from .decorators import ConfigurationSetting from .search_command import SearchCommand +from splunklib import six from splunklib.six.moves import map as imap, filter as ifilter # P1 [O] TODO: Discuss generates_timeorder in the class-level documentation for GeneratingCommand @@ -203,19 +205,57 @@ def _execute(self, ifile, process): """ if self._protocol_version == 2: - result = self._read_chunk(ifile) + self._execute_v2(ifile, self.generate()) + else: + assert self._protocol_version == 1 + self._record_writer.write_records(self.generate()) + self.finish() - if not result: - return + 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 - metadata, body = result - action = getattr(metadata, 'action', None) + for row in records: + self._record_writer.write_record(row) - if action != 'execute': - raise RuntimeError('Expected execute action, not {}'.format(action)) + if count == self._record_writer._maxresultrows: + self._finished = False + else: + self._finished = True - self._record_writer.write_records(self.generate()) - self.finish() + def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout, allow_empty_input=True): + """ Process data. + + :param argv: Command line arguments. + :type argv: list or tuple + + :param ifile: Input data file. + :type ifile: file + + :param ofile: Output data file. + :type ofile: file + + :param allow_empty_input: For generating commands, it must be true. Doing otherwise will cause an error. + :type allow_empty_input: bool + + :return: :const:`None` + :rtype: NoneType + + """ + + # Generating commands are expected to run on an empty set of inputs as the first command being run in a search, + # also this class implements its own separate _execute_chunk_v2 method which does not respect allow_empty_input + # so ensure that allow_empty_input is always True + + if not allow_empty_input: + raise ValueError("allow_empty_input cannot be False for Generating Commands") + else: + return super(GeneratingCommand, self).process(argv=argv, ifile=ifile, ofile=ofile, allow_empty_input=True) # endregion @@ -324,6 +364,8 @@ def fix_up(cls, command): if command.generate == GeneratingCommand.generate: raise AttributeError('No GeneratingCommand.generate override') + # TODO: Stop looking like a dictionary because we don't obey the semantics + # N.B.: Does not use Python 2 dict copy semantics def iteritems(self): iteritems = SearchCommand.ConfigurationSettings.iteritems(self) version = self.command.protocol_version @@ -334,6 +376,10 @@ def iteritems(self): lambda name_value: (name_value[0], 'stateful') if name_value[0] == 'type' else (name_value[0], name_value[1]), iteritems) return iteritems + # N.B.: Does not use Python 3 dict view semantics + if not six.PY2: + items = iteritems + pass # endregion diff --git a/bin/splunklib/searchcommands/internals.py b/bin/splunklib/searchcommands/internals.py old mode 100755 new mode 100644 index 9a2a4e9..1ea2833 --- a/bin/splunklib/searchcommands/internals.py +++ b/bin/splunklib/searchcommands/internals.py @@ -19,10 +19,7 @@ from io import TextIOWrapper from collections import deque, namedtuple from splunklib import six -try: - from collections import OrderedDict # must be python 2.7 -except ImportError: - from ..ordereddict import OrderedDict +from collections import OrderedDict from splunklib.six.moves import StringIO from itertools import chain from splunklib.six.moves import map as imap @@ -35,6 +32,7 @@ import os import re import sys +import warnings from . import environment @@ -74,7 +72,7 @@ def set_binary_mode(fh): class CommandLineParser(object): - """ Parses the arguments to a search command. + r""" Parses the arguments to a search command. A search command line is described by the following syntax. @@ -232,7 +230,7 @@ def replace(match): _escaped_character_re = re.compile(r'(\\.|""|[\\"])') - _fieldnames_re = re.compile(r"""("(?:\\.|""|[^"])+"|(?:\\.|[^\s"])+)""") + _fieldnames_re = re.compile(r"""("(?:\\.|""|[^"\\])+"|(?:\\.|[^\s"])+)""") _options_re = re.compile(r""" # Captures a set of name/value pairs when used with re.finditer @@ -505,8 +503,9 @@ def __init__(self, ofile, maxresultrows=None): self._inspector = OrderedDict() self._chunk_count = 0 - self._record_count = 0 - self._total_record_count = 0 + self._pending_record_count = 0 + self._committed_record_count = 0 + self.custom_fields = set() @property def is_flushed(self): @@ -524,6 +523,30 @@ def ofile(self): def ofile(self, value): self._ofile = set_binary_mode(value) + @property + def pending_record_count(self): + return self._pending_record_count + + @property + def _record_count(self): + warnings.warn( + "_record_count will be deprecated soon. Use pending_record_count instead.", + PendingDeprecationWarning + ) + return self.pending_record_count + + @property + def committed_record_count(self): + return self._committed_record_count + + @property + def _total_record_count(self): + warnings.warn( + "_total_record_count will be deprecated soon. Use committed_record_count instead.", + PendingDeprecationWarning + ) + return self.committed_record_count + def write(self, data): bytes_type = bytes if sys.version_info >= (3, 0) else str if not isinstance(data, bytes_type): @@ -547,6 +570,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) @@ -555,8 +579,7 @@ def _clear(self): self._buffer.seek(0) self._buffer.truncate() self._inspector.clear() - self._record_count = 0 - self._flushed = False + self._pending_record_count = 0 def _ensure_validity(self): if self._finished is True: @@ -569,6 +592,7 @@ def _write_record(self, record): if fieldnames is None: 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))) @@ -651,9 +675,9 @@ def _write_record(self, record): values += (repr(value), None) self._writerow(values) - self._record_count += 1 + self._pending_record_count += 1 - if self._record_count >= self._maxresultrows: + if self.pending_record_count >= self._maxresultrows: self.flush(partial=True) try: @@ -690,7 +714,7 @@ def flush(self, finished=None, partial=None): RecordWriter.flush(self, finished, partial) # validates arguments and the state of this instance - if self._record_count > 0 or (self._chunk_count == 0 and 'messages' in self._inspector): + if self.pending_record_count > 0 or (self._chunk_count == 0 and 'messages' in self._inspector): messages = self._inspector.get('messages') @@ -728,9 +752,9 @@ def flush(self, finished=None, partial=None): print(level, text, file=stderr) self.write(self._buffer.getvalue()) - self._clear() self._chunk_count += 1 - self._total_record_count += self._record_count + self._committed_record_count += self.pending_record_count + self._clear() self._finished = finished is True @@ -748,37 +772,36 @@ class RecordWriterV2(RecordWriter): def flush(self, finished=None, partial=None): RecordWriter.flush(self, finished, partial) # validates arguments and the state of this instance - inspector = self._inspector - - if self._flushed is False: - - self._total_record_count += self._record_count - self._chunk_count += 1 - - # TODO: DVPL-6448: splunklib.searchcommands | Add support for partial: true when it is implemented in - # ChunkedExternProcessor (See SPL-103525) - # - # We will need to replace the following block of code with this block: - # - # metadata = [ - # ('inspector', self._inspector if len(self._inspector) else None), - # ('finished', finished), - # ('partial', partial)] - - if len(inspector) == 0: - inspector = None - - if partial is True: - finished = False - metadata = [item for item in (('inspector', inspector), ('finished', finished))] - self._write_chunk(metadata, self._buffer.getvalue()) - self._clear() + if partial or not finished: + # Don't flush partial chunks, since the SCP v2 protocol does not + # provide a way to send partial chunks yet. + return - elif finished is True: - self._write_chunk((('finished', True),), '') + if not self.is_flushed: + self.write_chunk(finished=True) - self._finished = finished is True + def write_chunk(self, finished=None): + inspector = self._inspector + self._committed_record_count += self.pending_record_count + self._chunk_count += 1 + + # TODO: DVPL-6448: splunklib.searchcommands | Add support for partial: true when it is implemented in + # ChunkedExternProcessor (See SPL-103525) + # + # We will need to replace the following block of code with this block: + # + # metadata = [item for item in (('inspector', inspector), ('finished', finished), ('partial', partial))] + # + # if partial is True: + # finished = False + + if len(inspector) == 0: + inspector = None + + metadata = [item for item in (('inspector', inspector), ('finished', finished))] + self._write_chunk(metadata, self._buffer.getvalue()) + self._clear() def write_metadata(self, configuration): self._ensure_validity() @@ -793,7 +816,7 @@ def write_metric(self, name, value): self._inspector['metric.' + name] = value def _clear(self): - RecordWriter._clear(self) + super(RecordWriterV2, self)._clear() self._fieldnames = None def _write_chunk(self, metadata, body): @@ -818,4 +841,4 @@ def _write_chunk(self, metadata, body): self.write(metadata) self.write(body) self._ofile.flush() - self._flushed = False + self._flushed = True diff --git a/bin/splunklib/searchcommands/reporting_command.py b/bin/splunklib/searchcommands/reporting_command.py old mode 100755 new mode 100644 index 3d6b357..9470861 --- a/bin/splunklib/searchcommands/reporting_command.py +++ b/bin/splunklib/searchcommands/reporting_command.py @@ -253,7 +253,7 @@ def fix_up(cls, command): cls._requires_preop = False return - f = vars(command)[b'map'] # Function backing the map method + f = vars(command)['map'] # Function backing the map method # EXPLANATION OF PREVIOUS STATEMENT: There is no way to add custom attributes to methods. See [Why does # setattr fail on a method](http://stackoverflow.com/questions/7891277/why-does-setattr-fail-on-a-bound-method) for a discussion of this issue. @@ -266,7 +266,7 @@ def fix_up(cls, command): # Create new StreamingCommand.ConfigurationSettings class - module = command.__module__ + b'.' + command.__name__ + b'.map' + module = command.__module__ + '.' + command.__name__ + '.map' name = b'ConfigurationSettings' bases = (StreamingCommand.ConfigurationSettings,) diff --git a/bin/splunklib/searchcommands/search_command.py b/bin/splunklib/searchcommands/search_command.py old mode 100755 new mode 100644 index b2835ee..dd11391 --- a/bin/splunklib/searchcommands/search_command.py +++ b/bin/splunklib/searchcommands/search_command.py @@ -22,10 +22,7 @@ import io -try: - from collections import OrderedDict # must be python 2.7 -except ImportError: - from ..ordereddict import OrderedDict +from collections import OrderedDict from copy import deepcopy from splunklib.six.moves import StringIO from itertools import chain, islice @@ -124,6 +121,7 @@ def __init__(self): self._default_logging_level = self._logger.level self._record_writer = None self._records = None + self._allow_empty_input = True def __str__(self): text = ' '.join(chain((type(self).name, str(self.options)), [] if self.fieldnames is None else self.fieldnames)) @@ -172,6 +170,14 @@ 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 + + def gen_record(self, **record): + self._record_writer.custom_fields |= set(record.keys()) + return record + record = Option(doc=''' **Syntax: record= @@ -398,7 +404,7 @@ def flush(self): :return: :const:`None` """ - self._record_writer.flush(partial=True) + self._record_writer.flush(finished=False) def prepare(self): """ Prepare for execution. @@ -413,7 +419,7 @@ def prepare(self): """ pass - def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout): + def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout, allow_empty_input=True): """ Process data. :param argv: Command line arguments. @@ -425,10 +431,16 @@ def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout): :param ofile: Output data file. :type ofile: file + :param allow_empty_input: Allow empty input records for the command, if False an Error will be returned if empty chunk body is encountered when read + :type allow_empty_input: bool + :return: :const:`None` :rtype: NoneType """ + + self._allow_empty_input = allow_empty_input + if len(argv) > 1: self._process_protocol_v1(argv, ifile, ofile) else: @@ -634,6 +646,19 @@ def _process_protocol_v1(self, argv, ifile, ofile): debug('%s.process finished under protocol_version=1', class_name) + def _protocol_v2_option_parser(self, arg): + """ Determines if an argument is an Option/Value pair, or just a Positional Argument. + Method so different search commands can handle parsing of arguments differently. + + :param arg: A single argument provided to the command from SPL + :type arg: str + + :return: [OptionName, OptionValue] OR [PositionalArgument] + :rtype: List[str] + + """ + return arg.split('=', 1) + def _process_protocol_v2(self, argv, ifile, ofile): """ Processes records on the `input stream optionally writing records to the output stream. @@ -656,7 +681,7 @@ def _process_protocol_v2(self, argv, ifile, ofile): # noinspection PyBroadException try: debug('Reading metadata') - metadata, body = self._read_chunk(ifile) + metadata, body = self._read_chunk(self._as_binary_stream(ifile)) action = getattr(metadata, 'action', None) @@ -704,7 +729,7 @@ def _process_protocol_v2(self, argv, ifile, ofile): if args and type(args) == list: for arg in args: - result = arg.split('=', 1) + result = self._protocol_v2_option_parser(arg) if len(result) == 1: self.fieldnames.append(str(result[0])) else: @@ -776,7 +801,6 @@ def _process_protocol_v2(self, argv, ifile, ofile): # noinspection PyBroadException try: debug('Executing under protocol_version=2') - self._records = self._records_protocol_v2 self._metadata.action = 'execute' self._execute(ifile, None) except SystemExit: @@ -833,6 +857,8 @@ def _decode_list(mv): _encoded_value = re.compile(r'\$(?P(?:\$\$|[^$])*)\$(?:;|$)') # matches a single value in an encoded list + # Note: Subclasses must override this method so that it can be called + # called as self._execute(ifile, None) def _execute(self, ifile, process): """ Default processing loop @@ -846,21 +872,38 @@ def _execute(self, ifile, process): :rtype: NoneType """ - self._record_writer.write_records(process(self._records(ifile))) - self.finish() + if self.protocol_version == 1: + self._record_writer.write_records(process(self._records(ifile))) + self.finish() + else: + assert self._protocol_version == 2 + self._execute_v2(ifile, process) @staticmethod - def _read_chunk(ifile): + def _as_binary_stream(ifile): + naught = ifile.read(0) + if isinstance(naught, bytes): + return ifile + + try: + return ifile.buffer + except AttributeError as error: + raise RuntimeError('Failed to get underlying buffer: {}'.format(error)) + + @staticmethod + def _read_chunk(istream): # noinspection PyBroadException + assert isinstance(istream.read(0), six.binary_type), 'Stream must be binary' + try: - header = ifile.readline() + header = istream.readline() except Exception as error: raise RuntimeError('Failed to read transport header: {}'.format(error)) if not header: return None - match = SearchCommand._header.match(header) + match = SearchCommand._header.match(six.ensure_str(header)) if match is None: raise RuntimeError('Failed to parse transport header: {}'.format(header)) @@ -870,14 +913,14 @@ def _read_chunk(ifile): body_length = int(body_length) try: - metadata = ifile.read(metadata_length) + metadata = istream.read(metadata_length) except Exception as error: raise RuntimeError('Failed to read metadata of length {}: {}'.format(metadata_length, error)) decoder = MetadataDecoder() try: - metadata = decoder.decode(metadata) + metadata = decoder.decode(six.ensure_str(metadata)) except Exception as error: raise RuntimeError('Failed to parse metadata of length {}: {}'.format(metadata_length, error)) @@ -887,16 +930,18 @@ def _read_chunk(ifile): body = "" try: if body_length > 0: - body = ifile.read(body_length) + body = istream.read(body_length) except Exception as error: raise RuntimeError('Failed to read body of length {}: {}'.format(body_length, error)) - return metadata, body + return metadata, six.ensure_str(body) _header = re.compile(r'chunked\s+1.0\s*,\s*(\d+)\s*,\s*(\d+)\s*\n') def _records_protocol_v1(self, ifile): + return self._read_csv_records(ifile) + def _read_csv_records(self, ifile): reader = csv.reader(ifile, dialect=CsvDialect) try: @@ -921,51 +966,37 @@ def _records_protocol_v1(self, ifile): record[fieldname] = value yield record - def _records_protocol_v2(self, ifile): + def _execute_v2(self, ifile, process): + istream = self._as_binary_stream(ifile) while True: - result = self._read_chunk(ifile) + result = self._read_chunk(istream) if not result: return metadata, body = result action = getattr(metadata, 'action', None) - if action != 'execute': raise RuntimeError('Expected execute action, not {}'.format(action)) - finished = getattr(metadata, 'finished', False) + self._finished = getattr(metadata, 'finished', False) self._record_writer.is_flushed = False - if len(body) > 0: - reader = csv.reader(StringIO(body), dialect=CsvDialect) + self._execute_chunk_v2(process, result) - try: - fieldnames = next(reader) - except StopIteration: - return + self._record_writer.write_chunk(finished=self._finished) - mv_fieldnames = dict([(name, name[len('__mv_'):]) for name in fieldnames if name.startswith('__mv_')]) + def _execute_chunk_v2(self, process, chunk): + metadata, body = chunk - if len(mv_fieldnames) == 0: - for values in reader: - yield OrderedDict(izip(fieldnames, values)) - else: - for values in reader: - record = OrderedDict() - for fieldname, value in izip(fieldnames, values): - if fieldname.startswith('__mv_'): - if len(value) > 0: - record[mv_fieldnames[fieldname]] = self._decode_list(value) - elif fieldname not in record: - record[fieldname] = value - yield record - - if finished: - return + if len(body) <= 0 and not self._allow_empty_input: + raise ValueError( + "No records found to process. Set allow_empty_input=True in dispatch function to move forward " + "with empty records.") - self.flush() + records = self._read_csv_records(StringIO(body)) + self._record_writer.write_records(process(records)) def _report_unexpected_error(self): @@ -1036,6 +1067,8 @@ def fix_up(cls, command_class): """ return + # TODO: Stop looking like a dictionary because we don't obey the semantics + # N.B.: Does not use Python 2 dict copy semantics def iteritems(self): definitions = type(self).configuration_setting_definitions version = self.command.protocol_version @@ -1044,7 +1077,9 @@ def iteritems(self): lambda setting: (setting.name, setting.__get__(self)), ifilter( lambda setting: setting.is_supported_by_protocol(version), definitions))) - items = iteritems + # N.B.: Does not use Python 3 dict view semantics + if not six.PY2: + items = iteritems pass # endregion @@ -1054,8 +1089,7 @@ def iteritems(self): SearchMetric = namedtuple('SearchMetric', ('elapsed_seconds', 'invocation_count', 'input_count', 'output_count')) - -def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None): +def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None, allow_empty_input=True): """ Instantiates and executes a search command class This function implements a `conditional script stanza `_ based on the value of @@ -1078,6 +1112,8 @@ def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys :type output_file: :code:`file` :param module_name: Name of the module calling :code:`dispatch` or :const:`None`. :type module_name: :code:`basestring` + :param allow_empty_input: Allow empty input records for the command, if False an Error will be returned if empty chunk body is encountered when read + :type allow_empty_input: bool :returns: :const:`None` **Example** @@ -1115,4 +1151,4 @@ def stream(records): assert issubclass(command_class, SearchCommand) if module_name is None or module_name == '__main__': - command_class().process(argv, input_file, output_file) + command_class().process(argv, input_file, output_file, allow_empty_input) diff --git a/bin/splunklib/searchcommands/streaming_command.py b/bin/splunklib/searchcommands/streaming_command.py old mode 100755 new mode 100644 index 9d900c3..fa075ed --- a/bin/splunklib/searchcommands/streaming_command.py +++ b/bin/splunklib/searchcommands/streaming_command.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from splunklib import six from splunklib.six.moves import map as imap, filter as ifilter from .decorators import ConfigurationSetting @@ -172,6 +173,8 @@ def fix_up(cls, command): raise AttributeError('No StreamingCommand.stream override') return + # TODO: Stop looking like a dictionary because we don't obey the semantics + # N.B.: Does not use Python 2 dict copy semantics def iteritems(self): iteritems = SearchCommand.ConfigurationSettings.iteritems(self) version = self.command.protocol_version @@ -185,4 +188,8 @@ def iteritems(self): lambda name_value1: (name_value1[0], 'stateful') if name_value1[0] == 'type' else (name_value1[0], name_value1[1]), iteritems) return iteritems + # N.B.: Does not use Python 3 dict view semantics + if not six.PY2: + items = iteritems + # endregion diff --git a/bin/splunklib/searchcommands/validators.py b/bin/splunklib/searchcommands/validators.py old mode 100755 new mode 100644 index f3e2e52..22f0e16 --- a/bin/splunklib/searchcommands/validators.py +++ b/bin/splunklib/searchcommands/validators.py @@ -95,7 +95,9 @@ def __call__(self, value): try: return Code.object(compile(value, 'string', self._mode), six.text_type(value)) except (SyntaxError, TypeError) as error: - raise ValueError(error.message) + message = str(error) + + six.raise_from(ValueError(message), error) def format(self, value): return None if value is None else value.source @@ -199,6 +201,48 @@ def format(self, value): return None if value is None else six.text_type(int(value)) +class Float(Validator): + """ Validates float option values. + + """ + def __init__(self, minimum=None, maximum=None): + if minimum is not None and maximum is not None: + def check_range(value): + if not (minimum <= value <= maximum): + raise ValueError('Expected float in the range [{0},{1}], not {2}'.format(minimum, maximum, value)) + return + elif minimum is not None: + def check_range(value): + if value < minimum: + raise ValueError('Expected float in the range [{0},+∞], not {1}'.format(minimum, value)) + return + elif maximum is not None: + def check_range(value): + if value > maximum: + raise ValueError('Expected float in the range [-∞,{0}], not {1}'.format(maximum, value)) + return + else: + def check_range(value): + return + + self.check_range = check_range + return + + def __call__(self, value): + if value is None: + return None + try: + value = float(value) + except ValueError: + raise ValueError('Expected float value, not {}'.format(json_encode_string(value))) + + self.check_range(value) + return value + + def format(self, value): + return None if value is None else six.text_type(float(value)) + + class Duration(Validator): """ Validates duration option values. @@ -386,4 +430,4 @@ def format(self, value): return self.__call__(value) -__all__ = ['Boolean', 'Code', 'Duration', 'File', 'Integer', 'List', 'Map', 'RegularExpression', 'Set'] +__all__ = ['Boolean', 'Code', 'Duration', 'File', 'Integer', 'Float', 'List', 'Map', 'RegularExpression', 'Set'] diff --git a/bin/splunklib/six.py b/bin/splunklib/six.py old mode 100755 new mode 100644 index 5fe9f8e..d13e50c --- a/bin/splunklib/six.py +++ b/bin/splunklib/six.py @@ -978,3 +978,16 @@ def python_2_unicode_compatible(klass): del i, importer # Finally, add the importer to the meta path import hook. sys.meta_path.append(_importer) + +import warnings + +def deprecated(message): + def deprecated_decorator(func): + def deprecated_func(*args, **kwargs): + warnings.warn("{} is a deprecated function. {}".format(func.__name__, message), + category=DeprecationWarning, + stacklevel=2) + warnings.simplefilter('default', DeprecationWarning) + return func(*args, **kwargs) + return deprecated_func + return deprecated_decorator \ No newline at end of file diff --git a/default/app.conf b/default/app.conf old mode 100755 new mode 100644 diff --git a/default/commands.conf b/default/commands.conf old mode 100755 new mode 100644 index d3b8fb9..c2e2fdc --- a/default/commands.conf +++ b/default/commands.conf @@ -6,4 +6,5 @@ requires_srinfo = true stderr_dest = message supports_rawargs = true supports_getinfo = true -supports_multivalues = true \ No newline at end of file +supports_multivalues = true +python.version = python3 \ No newline at end of file diff --git a/default/searchbnf.conf b/default/searchbnf.conf old mode 100755 new mode 100644 diff --git a/metadata/default.meta b/metadata/default.meta old mode 100755 new mode 100644 diff --git a/static/appIcon.png b/static/appIcon.png index 71a273edee358bdf31228ebc3602ea4bb2bb5782..6b1b681ef252dc857eba93675277664044687254 100644 GIT binary patch literal 3048 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 literal 49971 zcmV)BK*PU@P)`fbnewELV~7ujC^B=`jCz zSKwO<*gxJS_;>V4{|2nUp8)%>6|esW!1q-y{l5Xg{yV*MpS#=tKCVLpuwVXkzx*?Q z{}mMLmoR#LUIhEJJdwXkkIj^S%5V5R0hX2mtgMva*8|v=ja=SCM`sS1to|JRmhzMQ zFMYq(vuHggL!Oa-effBs=lJ9+_4&#%Comd^WHg={hQRWYkJXhjYIPs3>!7VIk7Ch- z>j8$D9!UZNt=F;fhbL~wljrwE*SzKB-y{TD?&8z%^Dk>*)BDEnCjS=zhyXYMY{+$A zixGV~vD{cB>W{p^2|_>~1ZzhA7Dqtc2g()T?1z)MeDOMFCKu3FY{T}QgLw6go!GoJ z3zPShAIn#p>iuBw!})%#U#5l{UqHY%5ALal`Q#Nhc#eMK!;+YvWdnS`L>%+1VmSu> zD@iOlk0=0^7Giw#(KtT+ui0E2)r)vrYG7WU~y7Z#IEVrRbF)=ky)uTg zA6&z&n-i$lV;~7oD0tYua|2#HwgWGp+>U(Ff@Sk{b(jRK;cCD>VKL=Len#p+cpO|b zzeiZ`hr+<``95FC!Zt&guNlmI4Ztc(OH0TL4a9MTI0<2zHq69Ey$;;Jw}f-&?%?$4 z%UDe74(cguv;|XuV8E z3KaT%1F?jg227KdR>3t+0n86J|v#^XnG_}y=>;_{_Yj6GgO96E@T2uU2kvLg%)x8d;79XNhs zKelb_f^BO{o|x(yJr%HN9GiwX-;8zrnkMaYQ2Un!tO^;mSxYY}|P_5W7&2|BW@7E=cZ0|I1;^Y>*cx*FX*tZ$3 z&EzDJShf*mJbAeq+53}OK7URvGTnPK>zB7!V_{RkDhtavTXIfq-kEeoiQfP$5(}db zXYuP_U&OU5k1;t>MHKN^xo8<01<2(Rtl!v$AO7fd9C>jQvN=PnuR4krPp~G!dG@0G zgBhf6;Ele;fK?gtT6QbF$c)p7FLg^25G4950mdFxaOLs{&V6(Zw{A@!2r@8?f+SOM z93V-26p9g!o!Eq9$G76pq3y_cULzd%oU>m8xYV7t+Shck3G{S4ssQ;E6QRJ(zY(x0 z)1kewIz#0J3|iRO#Dn|Oc>ABuo4M^6MHc< zoJTgNWAP^s;16x`z5!4^?=8PYfaPJ*y7JMQ1hgK0t0m)Y=MCo_=FL-Xn0)2cOXE2G z{soNOnZWF94Plf)9A^b6v^+o^nVgS9N7mz|<6Cj`=x$_lUYeXM;5I#c^8jRhq{on& zPW7|UYzFWisYhy-vs(eOz9ULT)fxCC7@XKGE#qHZun49qtgU zib~=Lrd3C=Xkpjx4S4Cq9vnKn2_2mlBwEr-y0P-tSlK@^+Wr6^_bmi0U8bUqK3_aY z!J%S6Iz6#a2^}BfSfEk{KKkG$e*Mp9F+Ej6-FFbhF2c}3l0>j=#;88LY=C_S266P* zR-8Do7dZkp#o)8b76K-IwkB;GXcX2|16D0ImGwUju&p6M)2uc$b3%M5>Mlvhco_KX zvnl-j&rV}xWEzVLArR-}Z*gR4M#G2aM(FOz;pp*Q_}-g`&_9rYt#c_=NO($&`mfO1 ze#-#cl8`hmaI2|nS~Ep&sTh`^Gv+;%aj}jB86<{>FyhURU|WD~ z$H?Sj?A+CZBS*L4wb%9`msNljO6yscK=9Q{2epO;D#KSHfwrz`1o^o*y=76-$|gXp zpQDNv3be8qV~Az_+rRytP;*O5Aq?bTB#zK`>WjRq56_En_{FXG(Vv~frp+BF6jRoZ z^i|3NGy?Ipp5qT-{r(tl@ht@G7x^sZeidM8VJt-rB~ZNUty!FT{~|73xP#SICM6CG z(?!TaLJ~<8$+lu-vN5)8@5YNqcjDBkeGLoyOu#n6fs{v0Z%?!0m8Ln(I99vlHGpM& znwmC(l?8(q+U!ZImjJhJOymFgf8W8whqG8+rRB}QWKketsKZ#k4#Nmwnm+ctFpM|f zIx5A4fqtI50(8@BwFJw}Q}|qO;@C@j@aCKQkJwJBHDmpjtB}88 zz&>wLJqygw0&NqpLaqxBt1K*yEiKLLbQQn))digU@CKF^1B3zjN<}!aDj+%m{uKML zoe=9bbmG{tz4-Bu51~+WRXETR5=0R{128RGXSFI+ZpY70V^v+WY5`|z@l#zwiU(!w zLd60IJ3#BoH?&}ysx8x!^UJ@9Am-cGagbiP6u9Zd z<3-liRTmC;H&`L!O%DTLY^;Qz{eK_e!ns?hS8c?xC$ET-7^bZNo3J*E5UvxWuRo6$ zkM6-={^d~=+dO5BG+-HWL=me^v|P%9Dhq7NA|Tzwgovy}wHBjNsiW>kh$F?1nuY<_ zbCAzvkjq(c9Kez!hB7$iI$hSq&E54mSr;xvwZg29kW+~kqJ&AlF90kB`v z&42lY^OR?%U~RypC2v=?b`}bVFuCc8fl>*$e|HhT{KXkuzH}F%?2#O=6g05B$G5r98)ctSaWd;0@#b6KE(g= zfBqfj7pjP=$;CpYqgtxxGPe~7rR=}zSB9Ao|HU(_T zCOpT=eyw|(zW8|__Sv8P6#)AOIodDrCeONn>PEAwC0YX7z{J4(T!fo9Ch_Zk{s=d3 zJVqSRZ4$7`oikKqeLjhNm}U&eiQ#z(4j??pNR|KzbpixmLRTrC!*t5% z>ay|X_mAU;KR$w79zAhL8iJy48n9mi0#A9$zc^r3^7KRrF?EZL zntKbuuH9GD4l0W<_PB~m7at0xb@%R!aFwEqRYF9?fQe-$1w@fd2+Iy}=t&VPIp_eLf#J~4|Vwh%@@6fps!@lO!N zJ{-qDcUKWxwhrUi@jckRdmY+4JP8p9&?q9~LFPgMmS$GSy*NSW8xo6O{PZ^d`mcY5 zsSqb*+*^a;bkEgJCYYv4r1sGCAemz31rWE;1o^n}RfHXfB z;QaYJ`0(si5!jGAGYnSIgf!E_8OErpyh`vgc8=mI28Y|QXYVGQI&~b|w|5FMYqg}2 ztXZ}N*RznxK*ZSfYJ!_LW^w7_Eu21m0i{wbF>EZBm6Vr~C5m;Gus4EXRFKOXICks> zeDAHJ*tn&DLSA_p1=y65OZBg*0Z8NKKUNI;)j<19z<$%Fy=f&|0ox+WYt-DI0Iaq` z636NXE#b{;(|G^A8@PUL6w^~>Wnm*$gHpiqb4ad-F!(THal@OpY{aQkFJa%oUUYWT zf(BS#u48Gjg!%bpIn1{9b_@=7!nRF3eq6!n_s`?px$BbH#StGYxLG1!OykKYBCY1b zacy|6Er5000K@A#@#8-`g%|b?3kxwjTfx%uDuN(DHj_bnX8{9)Srm&7C@lNr{Fngt z^5xqoRS0}8LTU&Uz}TNA9wbx?gT(_TcLxsaz-w(DDkB%mc=uV0zM?|yq-60X_l3Xo(K zVCmkaU_eV4B9<~HDInf#3&TT0IC^X^j=Z!Hg<_1`w?}aQ-WaB*nDGY^OWKZ&fq@~k zwROVxBYgJRecTM#q=cH>7&hB&OiBk88z7{Z#LYFdcX~K_bU%7~yRo=1 zkJ;H7l*?r~MamzGZ8_}NxfwflZ9!je9%oM9!^I0XaO?I%_yOaSjMh*nW`O4)rCk9m z-y=eyU}5jxt$5|Neb~Fd5A6i3gf>F*X~sog*9NfkeGp5PYIDDR1lQLB?ANoxUl*_h zh=MHj1lKN4;=SKp$E_QWF*99}SPzK`2}priOlnL5iuwWq)_`T&=1*hM?j_X(1c(@>X z2$+tFafP`E1+Y>$2`FBt5IjacZ(+|K0(L+49q2`SM+#V?vq_n)HG=8yb?p8PS;(*Z zwqFadU+=?}8{&;t;QJ;Ap-F4i!{uonJB_uIB zXQ_jgyQ|t?EVj}3!p4i!OB}QnIxsZcgN>WI5X39Ebm?IHPgbH#GlJ)-iW9pZ!XRu2&sm@#<>qBf z?AWypyLP^S3+J!n(de{jAy~2v#RDleLuf4BhlWUr&-wnDjEQ~wcHs3l4rA}WF0^&% zWU!GNv}AWJAy%`D_3fmz}sq;01}11<%Q$tv!qO4hLae z#^l5l>NUP5FBV!Bl|izfpBEu!(k8FBh5wa`7ZnR8Ej`n}RPv}KhWtBX)dHguKCN|S z(nQNFEi#IhqQIP?1E1I1*Mih{|NdT>QZoSqnBZK*48UEtt> z-T2OTU&OAx?P$}?pds6897%42{2 ztFical@uMF3QN)_%f(M$uR_cgB}n?F>P89A`82Jjd^iE1lCFl3WX&;=U*-FlxOsC1 zXHMV5rOWp*HomL`+eQX7j1;`Yp^rEw#iUBKsszStv-vHVkyS#6#9RG#3a%9D|w)O>s%&nfS8v{R77^A|VmTGe3g`+4cnLWTn3l2;;2>k#i z>S%BCaO~(leD}@6*t9vLhHNygiM|Ud$HI4NCSd&7R3f8Ks@XIt$E<**5Wp`z4}yz~ zS2MPNNcZE(C8Yo1f>#HiY<1fA*3#P4dVo){lj)0}4cM0U`8Dq)7m_>BgxwLSNl?d}*0`EZQ3v5FPE(Jx8YfUz$sga3fVBwLKraC4B~1{~^E8?) z6<`_Pr=Pj@YOie@dJ?-|Y+=)TD zjw4sN?$;#LVY`orZ6d(2JY+o1B&Z`_OmOPdQT*UX$0UAb8F;M$OYOBD?Ih!j)*1i> zPvzw#{%M45s`{7SFWOh$cmBs9%3{o(YSgK6vkiob3|C#wI!8%oKs1+kdW6pp4W3;% zkmQ}fP;Wr|MRM~a*y56S$V@!wuC!^_H&HBE8d7K_8(kSsstLZ<_1sdD)W`jV$8XrI zMtgxQ^JXJowF=ySxPX87Sx^{jETbmNK$TB}#!pXgq|)uST3Nw`e5$h5TouhVIiI)-$qGV^ZSJl(VZq$y$OVUkJ$W@almedeZU3MR%Y zs8AEj=UcqfD&S~!*w|;M)uDV3&qSqIo^?tR4E@@0G zv0-4sj2$?ZjU?68>gy))wdJF4or8Uc*5TNRo!Ga3Yr}A+u6~Wxltj^l!6Y;NOr=ZL zKc?|E4UO)OXzldXHiKF)8dXMq9&-lOwH2l%9X`dq7n$xMu;DH6i|| z3VJG4P}Y${#&`lsB%)TRN;qV_F}Y%cTtopiRm&_X={Ee91`UZQ8VE|ks+XUxm9K$| z+!v_?>e1^-H9$;>!hH9PbUsZM=?F-1*-AFoxQ~si&3Dm5CiF}K={DuX zHeAW5S65=(yEBVx*GF;l+9TmHgOH;vdEvCwP`9}#W2oS)aM6sU8#6pw!KrbT(st=Z zmP3$HmnrrX`;ZEE)W5ZxPd}z5eq9UN7U$SV3YE*`@uvnvatUEQBDju)?yffM+PMh_ zj%>h=J>BT+wxs}}8*kTGGUW{zO6Xy5nh|LX_s|}iAIC^R8LP%T8-0?>@5ZnthtbS5 zthfqyjqS z{E@7LzWmLooVZY-S;9O<9)LrmRhvap8b@4PL1Ni3vN>3}cEIgIVzsfX(IB>-#s_>c zV4tz=k*`MtOMt8as~9^4Sc4_bcoC@2A)X(_^6WUO)m7N81+Q%r3cdSbw(pdg0OERu zqtBjpF>S+BuO9-p9A2M$VF+@$H<+>xO(X>?vG4hdSV63 zD=f$6Jn!1VPQDtJ!bc3`No; zL`5v>r&xnE(qlj^Y9ZuwVi<7<*M^J@-L-oN2M%t+=B@o07%GC%QlpdMb9N~Oo;rVV z`}A%pU^N}B71(K$sP4p7nhV=sLNtE|!PFI0R%Q@dHZtw)DD-ZC*|{0;1`u*& zlwxex&i&HI_LTwK?7mmAtDda_3w5ccAg%z_$4C}OP?@@kg}E_AQ5BBo!0p_H&JC}@ z?s-x5A8{oL(74p~vXzz-9Y>~rYuuox9JWrtd0}W}Wza-}a&fS<5aHq7d0f138@Fzb zVrij@aw$YDNRU_z2U3ZhUO)O4q+x6mP|dq45vCkUi_>nQbd;M;pZ`Sct1a=`3{AzY z&Co+Fpfx#ss{a-6w6LOF@ND5RLPR0OJElkrrTr(S0g2$c33__+*s^s12M+DPp+j5I z*2xA)U5ZXWj-R;Z5-aZ}OEF@Gtj|djFuHx+BGiCw>T|^hXgiR=tdGMVJB#Y*Z&6>J zKx8@aid`u7ZiX{(2{<(~1PJR{LIbZv8Rrf!6^Pv~^XPV-lxr9juvZQdVwSdg4urs{x&syhKe% zAs5`zavhntduI{nKe~@g7w%$WYyn6-1R+OA_=>!?l;DLfR)o*wiY0q(ewPwwN($?k zQN@4?m|{ugp{#NGds9bM{GM9zCMcT|wbd}=v|6U(di6XNY{@mC)>X39G+kHU8E3xk0&03)pDW-Nb|7MsMedJckq}6zH7!f563<+~%IF7( zR&OJUOQJKC+J0dlx_15r?BTb7goW%hBiCGBYB{vmRJVEUYD;fsV={Gmp;P;$%d7-0 zW7m&9c!;-ubxB%EN~-}3lW{9=ZykvpAU4^XuUt66s(`A%DnG~idkZC{m<5I*>bq2% z8IPxNVWT!eDo4RCWlj|k^=SJvK5qJG4UngrBsr? z3x$v8>lBZ}5wpw*sEydXX%H`;+=5qL+lHQQo;d+ajYKmdX(X2VeB^M_RD%DOlDY~G zn<T7CD`>*m_|)zmGWi=g}vz9{ui*< z{Q$7KG>4%rNa}8wu}gh{mpZ*-Y5bRFuyjQ%8(NZ+wO-ehqi1LkMFxKJ&Q1LEZ$6T? zYnfSWuqwnXS(TCbJvAiC(l+Bt1>|O8m39NFwims0^zn)TZmrB2s+)?LpJdiE`4h(q|KfRK@_dEc5z9&yL`5a&}rd~(pA((ev071 zPmxXL)b)prIyw+{9)#Eb5;8-FVRUQ(j5Z_&nWPpDzvZx=_0Q+X3w0DLD>A)^0T3?$ zwK4c(*HC)=5t8x3uxc=6PW9N2-tnn^P~g?ywj@BpHe!ho;5T! zO*qn;&^9UyMZkIjSaIj6??_jlid68<+c)tye|;9q%YYwIQKfEXIySR$!5e^TR?pDNYyV0@bB&?wWfYprzopSu? zFy}9U*XQYBHGL%4R=EIS2K+H#TQcQ(Y0s02-o1yP z{p>uZSuF4a&AIYA%%NrA5_Xm2id{S!O<*q692TGbsYy=wyej=mp9}1iDC5ha&hn+ngtBS z1aP4#FDKlt9yVe~HId-}ivkR-(o0PMDnrt;e$`>~CWwXo1^m%}KsKJzCF>Zmkww+) zN9=7tarhvt^)CbMI{{~1Gj~~2+kZB4p99#`1*Ab6Z&?Y+@e8};H2>>Zb8hHkAEXx)70u9AeE{ z39V**r4gsf0#xrC#e5H)NTq?Urlv$rr`1a8J4?!uw8u!C+YI4V7edR;Yj|K?61T)k z(;BJ`>B5*QIOYy)DOX*z`CJZLH*dhvW1Dd3&>(ucsq_LI#<kjT&v zOX6$hD#Vh5kXtY5`=-=*v4m!+I8ZJZwF7|-TDUXO|Qf7w!kp@1+b!rNKvbeT=RDn`I6(UsWeRf&XZs`KvyQ?R2u3r z0(D|)5x4H#!^p@u#>YySosCdhjZm#r5r%B+GvSC?j3wbsYxgNg#YLy%-X+7#wj$I4BJq|f1_c&B{UEkNtth58R9vp zB7xd~glY~8ro+B7`X$ySiDV15<)W=nKwrOy-hL0eb`N0Zjtv+b=tiNSwT9^`;k+?Y zVth9d_K;)O^JL8&B4iK{M5!WEmkN|vTWujFF_~Whmam}l@NLw`-$o{xmR29>vM@{p zaR(Bs6IOl`+~L>ZY<>;M?a)-7Ua|7+$q`Cxq5I`~y6BJN1;jJ&Vt(Xrfzl0Feg#e( z!)pjx)P=0(H^bZX3Y<-^!OU%iX)r9v%ekewiyTH87YI360+M+kTmZs~6d`1Kidh7b zHW+3Hkm&@>3~IF)lT$0WGctzrpWecq+mErj9KnnWvM&{j8gW#DV+HVRAC6IjYt~^= z^lY&qkTD%;AU&Os|7KS!JtX5mZS_Sf&fN%^ktfW`O@yI|Ff!C5Oc2Jr83hE$BSKN; zZ?Rm;jos)zK~kSXJJ=my`moFpjv2yp5_p~gTla8rrhwX;OJ(G(DUIMz`o|Js7P&C| zAVEN-nv#=ct7jjv)^)P{E-M@q+ z;cfqoUkS}h8-M0sc(S^s4OE`V@CT`sx$Ad>JvadlBSTJK(tdN<^ z!^9%;nF@-T5;D#*3SJ4hjEQ{CMKNb1=b6a5fNLv#uPyB+wDLlF2|x(YAhJ*oEYy4p zmAZj)6)05`td>K0lxqn}wGIU6M_>%7Fhn5aE3HLRhJ^}T+ebcIM?MoFpN-L$Pf*MO zSvP{`#K?L;#-$M3kcaTD5h=S+MtJ&>iCW#1W2r|xmKdv*5Y@oMO3lDhIYu?iq8jwT zk9!d^1Gi#CMjcKD=8w+FfHW@qJpr2v=*5*KRV4K>_~UP5`QG2bt=)ku?I)Hz zq=!p3_q9>WZGtuQBAoRv!|B=%!y;fi8eOQeR%t4DpzI(h&m*ahpuTbq_1az3>ocg= zrvS4C!|sRaY(`t>9^}^@N4eaFOE;GA>-TQr{=+5AEipRH!(?Y_$f8yq8EYEF+!EIJ zM;Pj>p_pAj!JUO?N60z^MvROd!83ihW&j(3SUSZ_sAP#IB}(Y(Gw`Df>XCm79sa(ZKBEDaOAkLs3XW+*f)FK!4&_pFLPz`L9gDh739G0qCOwM*- zY^I2k--D1F=~07Vg~(=ntQ*SW)ss8%%F)f}?+OrC?qOl{6Zvk1ld9ZqW?{PpWZO2w z&hLX&H~CiSvkNFT%l+YKziT9hF+lNBpD% zM*r(*-~J;QT`!7ZUHL+4Vs0A-L;L!t37&!|7HfS|P>hMkq)0A=^}kbtE$nQCql$<;ClWgE7R(3X*t9 z6(X!5n9eS=_Z&iQ_!T@Fi*WYx49=XtkI9)TDt;EWm4nU1Dv05sinh!YdfJzht@T1_L1~TIw@Rf{D_i_#Bv00m}KF{4*bwY z%?E0(-b81+g^r?ywnBugOK?`;m^Ii&RjeeUKS6!^5{$*`aIq{DWn$8Ml2%lCNIh;x()&8vw*C-C z&k2}Z2(2YIilx2^U>kv)l$ffBqg8}U_YjW1hv3mW$kgt`WaUf0eOft^kic1wsN;YF z?7H0?XVM&?D}yY!xmz7ZSdzree|Qbc^Y;)`#$;t$ETbA#u`mHP!p5RwMqJqsFtS^DV5N<0%EpVa=sOZTkuhf9MmEkJ|}*seQ%P)Fx-gY zSm}cnG=gR6kO7PtHAqd>ek8?`ly%b{QL)rYW5DAf_nJ_$PtH4W;TxT(C7qaEF5t#U zH!j^MVzImtzT=^0#qcea&|0Wr*R}zid~p~j4|SotIERIM@4;XC1a5F2p1q2YEm=~< z;zoR1;AM8BW9TT*+YjU!*J{y<#0EAsg@92GE?0XewR<0xhb{YQs2;%V%5RbiwO!WZ} zadk0SA#I~G?ASup975Q>58k?0;BBD9FhWo(BdAx9gf&j~-xbAw-RFrp?jm2{KWHOc)`H6?A7G zV7Pk@2X~s-v)({HnUY5;K2d`-RO`94mZSpRl)RU0Ri{3Z@T6PA>#ag%43R2J3Zugu zTZHPGInZ70ibqxivZ8~ua9Rd#C{8sUftmJ-H5p<>{I-gfg~R6DSlt+9*j!>6DSgty z&Mda!#@!)Yxz&fc(l%5r4>dbNO%h6_VO}@TjzhaU@WVIOVbf3rrP0%<&z^x*y9w7` zMr4u%cmqz6x`LKYhOY-$`Xv=u6R`E!+lVI40uyK8RS4Ls zsxwPjPzB?xZ&%QRD8C)Kp<{42Y(-pN#PY%aBSluU4a(CkwKR=JRKN`hijghB`z_4Y0RW3=8C6^d1qTRcX zq3(ISxHp3Xn}A$Aj$$${=b}1QnZ2fzQ7Rr${q-srRz(IyJEffix)LQGN6pTXdO9tv zVIj0VSv;%>(ULGWrw6hV#>Wgj6s0BO>m|uhtu0D_pa_4dhUt69_IXio>0M^{B#t4X zN8n^cRmk9Mq#Pg?GZS22kwWEJEzS4^XbxQ3~kUJh+7}boceZ z?dw8PUBTkLD~OjK!>difP0DKbnuLJWRm;SHZ?+@oI0<*?yKn|yht)=rKd+@uwxr$Z zNee5nsk$f66u+e0j?M&=cp1UM2!io52*=(-mV%dvW4L9b3r1cCDQ4cJOl*yipC{C~v7ZGN9kmLuD^@{LJ8*#Oc z`pPWKU;!Rh;4qfcoyROPP*kFJwTXlxvh=tiSx&r;(pJjsTFU~(aHvFw7E4Obp@qoE zqipnIIqt&PViDKw_u}eE7Zxg;5SXOA2tb0HVNZbeqJ?c6ZTzR7?85F{KxOnag6WUo zl<&ZfmxKl2Vu_eYC!XjJ949a1M8haT5XbPj35v-|ga@yXL;E0;>JUrg4-uE<;nr7? zVNA}%jBE)DGopAiP&e8Ub(}zU@HM!DuLA8u0JyEH4h35Kzn>BpJRh)P6nLSy(ZmYE zg%Q-o&cGjg7g>LlAwQBxl~P4qtfzbgvbR77+}t3tZAH{prcf=-3SA>rzsQ7b+5!%O zMI0~{ZY*^ITFEa>f2!G0 zBfLacHA50&PX~2$b7=eW-&S4dOm<@aIU@nJi4?1w; zQ8$+Tjex~Yi*!F;H5<39-NyFK4*ufLwqoaw7?sg82&O)PTONTEttbjgs%S)cGrm`0 zjatkR8xIL!hAfVQVo#R@H|50z7}XlQD1;{=5JxHTxFbaLLZ*qD*#@Ki81h4}!s=&< zb{$|80hwi{^YUj#u+Ouwx>MI+6~n9{Nf^U^hT8aP)F1yE8GjU>8Og;VM6Yv4#`tg; z977HvGz_JD7r_m!myp~#Dv&N^ZhE0-uL@vQvZ#90GLo^|P6y1M7vQdc5$N6vI4pD+ zK)C`eJVHEu8MV1-gE{Qop2ZJO z4P*0$8Y-h7BAB`WtNZ{?vZ6&hTsT_WAsSxA1_}2n`<|K7I6~mEjoWZt)j8|e;U)&S zPown6lDcI7y^$a^Ecm8@y4eP^;}{CVufgg&0Tc%TvrTq)l&a_R3ti6%2^vCPqR0nB zf@t*t{E7Eb9eoEGpC=efAkBl~yi!%P;O%FY>54#um7_3Z3|v$4H&boZqtYJLs>~#a zt1*p2crJn#Rq03s^1t!jdZ>t=!Xy?xdk@L-ZDfK;xL8$5qOd7y!K9rKq94#< zi~LRZuMx0`WgwJGevVg>_~QsC-owh>e}G%N2PY0>MujQj9*!$l+@WeA;oL1P zebd@jm6=f%XNx7~tky)wWDXN1zB=%gi?&Q8wu72Ego?EV`Tj%5Z2Tb}kGeQ>v4pc1 z?qgsjLN*1E$=L)ldC@{8>Ba5E zbvQfPjho{+%q?*bgkHpPyI5y=-#9{Km*BQXICNqwzWee9yf9oxrhW_4cix9pdx$LN z;U=pvnIuWy9v8CWq2t6+Cij@)AARptk%{N77Ncb_^s~jfavAt`JKWw^ zP}uSojE=*A*P#}<-qG^Oc?C~K2hT6fY7Z%MO<97A=Masb#{7++!&tooJF3AW$H;Xk z!VIN87cNtPpvsEe|4OD*a}tPz0;EM4$@z^qjw~ZPKe%_0>{iIRuBL^$(T7qpfK1nJ zv~Bt!>d7`-y=UU>_ite2!35@(>TukGNDyP+L=-T<=U7daU6-+~=MIkTSi;`H2k6bk z$g>g_mj#HVa2S3Fb(Bu_keE-oV$NHo^_8q#rfM^$2B`OB0u2_*1ZxC0OyQ?50TCQBA#NLdrPbmZ^jST81vVsG*+IY#mZM5ylL>hfGY_ zTII?8_TeBf97J9pyunwI-}*fu{{moTk#ILhJ$LI1a;|5G1gWe|ss44%I9@_Bdl|L+ zr{T}vLRg)IZIwI)B(4!9)+RTVRaqBOlbh;ug~D(^@q4}XAjA>&!*w3e~1Z3Mg5 z&EwFP1#Ik$QAie$ix(6<#N%lw=hXqEJ1a_FB%?$Xvp}9+s4Q6USsHb=!s&hyg`t;V zZ8!v&orujgHI|2fm4t_rGO>Gq zFOD1=!1loq-uy*G6Ca|sbPqRIGkgMZW}8&iWIBwd5}&UmcuvyQ9ObU)5HR46 zSCEt+!iWULH{;pTXB(SnK% zQ|&Th*iowA*2I9zk8qqZIWLZ%Y|gHR?%6gWkwP1qcV_yG+pPeB z9U$OHdubMA#f%A`iftvF6>#Xj1cnk6#rs9fmAdiqqdvTIs~hvtCPbWoBgKwbMvdA@ zgw5;waqwU-PQJ1c>xb$vR&OAAdDGQ%;n7o$@GxlIc zDp^iUm1L{aRRtBBT@-0kF!KaXEJuhs%gFXHtimQ_`d)xFbPVX+1K(aJS4mwn)niA3 z>)CZK>6!by>QluH>CiQPC-^w`pM2>oP@VuLZ=y1F9xJ6=NH7JDL&`!AZk&--P1%`D zh+$AJOHj>zp+HOKYFnh0R85+&2uud#77|Xwrl7{xq8e!jW;XAj=5B@4a~NLVaabLD zFED2tqLq zf->Ued#F78C2CV2ARkU3!+RY7l|X90sIA6hg4LAv`~x~ zl;~;z03ZNKL_t(r7ACq8`JH$;mBqQyZk%~AfTd&uVo9@AJC0{1XwPME>XjGqo$sti zKk3rSRg}ig!kfQF*yk1wbjeZ@BW@K` zlUW2PKR~j03z)xy>e3yQD~}Mz^Ad}>u?si$1h8s%X_bgkT`#U)PqGuhnzSB_d)1tN z-?R~$HX@7GgPg4D!;-X-LywaLtLA12337w4!s|JN$psr9T%5xDr>|prY!OjS4NH@X zknCu<3eOlv(VJC8ge`Rp=O3WQ8G(hWbk@q+Vv;3Oqq&F}JFBv(Y-1!Gq8cX5Xgwy# z?G6~-Cs5e-6Br#w0jIq=0YxWp66Pc<3NQiTfP^Xd_kNDm(RY!lkHV9LRh@rqhw!z9 zmBlIXrz?eN67Qzo+?cT>8Hy-sLyRGmtG&2!KgOl;BCbviVkH?A3#)Vxlqv$ntc%x9 z9>I6t*nsux5`^VTSbF$A?1fKJsNR%BOA;Pgyn5z>BvZ|u97Xs}Lm*q;s{)GJCRFtz zFbR}y+(_~1vimI;t;NJP5=tv}2VoaB!0y=tqwg4u!fwRgW`OPZjfN;Svh&#%_8EZH z9!Tp|$vcw&s{#HDlIaVmJbE8VNI}!&TULi z)lgopvr;UXGi&T*8Mw)sgIRg(+G69t=4EW}A4OmG4xFR}i?KB?FB6H75=mNTUNLnu zL~TSD$t;*|&F~OWewQDB)Bh^m%|8Grzk_UoK#^k56t^ac4S%N994R z#@&e61E=|A8Qf>Y*uHfb$6xHhYcCI=GyfRNkIx{S`4E}PHDpcdc0F0J)MQ969cN3( zHBg+mwCqSup#(v~UeJh1l}-prHL_E0T`E*I85f2)wvT|K7Zed$?J%?J;r6|Z?7&GF z?fYSPEE;gZ=uql;Xnk&JHZ7~OuCL?Qcy0vs>5s&%S;43Pl`fMvR{$2WVk478HSIyy zs%|{B0qVS!IGVHv$$ndEk<5-A#7+)%f+wPbWmZaxClI$1Sph2UPUKmp?R^a^;Rc-k z@Bx1QPd8C2aZEX_GOYm1m3oM-9m6%2VZ{sBG-P1!#s$2vaT0?CmLXT*T7XoPDL`k; zEOeImYQ19&0c*>#1qrnaIYf3FvK^dc@(Rp#Zy?F-*4?N&dQIsu0W9B-9mBNXD@ewF zgZkKeh~}@ts4l=usGzDMvLXdo@d`F9TG*J@o`zxthN+t>6YevcM>QJ2ioX$$=KFE( z`YImIJD899P>$OrtA|MRXscAae0NtK`*&yY=Rewm%|k1w%zTXc^jTPBE#flFC<8NM zjH%Q^o=a<~ek(QYCGi(fe|!a%$q#|03vk0RNl4U6q#8%H zO|Xz4)>T%fN?RmV5HXADnwKd~9CJ=!N`;4shl9wb)>93oaHXxB7mZ+L`*?_Yb}x$S zzYC-DBqo=8@b3F}@r$?bAP9?!hh<4I;l(k)@X)kOct#a=xPspH5L*Z4@WSRLZ0wmp z-Yp|*`EXhNibL2j6AK`liq3= zhuYOf#dk9FjBZqXtq)9pf?)C-DpQvbtxh6~G8~i;T0TOnCQJd(!;T%v$YmU+Q5N|g z8~H*M;U{^l_yd?&>c^e24%~bYVzy+WVzi^4usw!duG~Aqme3|ixTAQ49UC3|)qg&Y zy<07Wi=U!A`99+1PvC^BQZ=J@q-WzbS49+hHHwSq{e&WsFhR;f!VN2VCa2aK5nro! z7ngl~`Ik_VNrF~%2%#loPb0GnN%0We9x^N^VRp0Kn}q|WTkN8IzKMM%VAEl6Oit{i zgrq)?%EODOO@0h>=@LADTyCt2?O0C@M1^Hh*dgv;&+yixeFV0t)|7R8ZZ{MrBD|)S zHW9J0s!xk9n1@fo+hCQ3fQ?Yk?m^pzw_tR>jK>RI_|5zG@vC3oMHF|aHg3L;fK~Cm zre%ZjW;>{$VAasyK8@|`SFmGv6$70S@=ghEvI0A*z?Etn6D98Q&19pFFbM@HiJ3z! z&Y_AE#%eudayM*Q;Lg_N~P1FgNv2Y9ZsmoXxy?|h86nVP@ z&)`_f3L>*CS_L-PWD-{Zt7egr8f1*0L(OPIDJo#5+>4R%BCbCuVrIFBN=TRM%HraA zv;T}WPD5A+buQb6ZTz?Y@&@+p%pqR9gq4ZkB3L{JFJ2bei7ShWJ!o2bY1u+UQtT4j zrbs6fN)K6Zk-aLIqo|NTyaD-VE>KPVsd^<_EHHaTd9+D7=Z@JBDBHhvz){3W>lgp$ZPWD1$;GWt}42N{LT z7)G~9NWq}lGb%wo6DyaZl2C;J^qRh{;zc3MrL4!_Guw>B`qbRr$gevEtLIhBRfh4- zd-w6Pf4q$#Dymj+TJMP5s)a~~=$h~-iisn*VFd+y5(8a}*wDXqS)1kAW?RF|h40%#I---woGoS4oHRoVw#f?;k*? zTnDPlH5iKzQJcGihu2S|I)4{k*#N~%1-4s<>DFL7yw(;na9~H=MKM7T#E7B<{zx7-Mul7zxwl&*uKF*Fn0l! zS2JcdyO-BWi>?WvLHA#b8CRL)75}uHv?z-tCut~2lyoeR) zExl>X+Lc(kOa*-3ab){nLEYPpGv_AocfY)a*+tb&YH<<@lX6-%izP+s6JmH|{rxJ8 zXa-qt5pDSr+HwSoeq;?4G8R0WmyF#GAuQQulW`lfSVtNU%f5@y?#9+#M=-o)FIHF_ zcZ+E6=!IiDux(py@u_!KVKuK-53s!IV_{|nU0xk-xP*%zzJuvUBk0IEXwRAOvL@V& z0o&n%G2zNCJREWd_(6mqNKgqZ%$H-#u7sFgcClLTK{Z;3$n2DuQtlV6F?U0>9r#g* zAS%OhVhpeA#<9J9`0?wz(N|bTW&C4=D;Ho@ufq!G;KVHasF+kGj}+nX?pV^H%NRan zD?6z>si2rJ9lHKl_PBKzf>5|aZG@iHKgn3S5>OijR&N0dBZy}2BAI8?+7e=RWjYCbTEUPU zU0yO=*&aPr%L{i&2_N@7;75~)X7=jCHk7zINhOG444@12D-eX+2@(8P?nv?;Te;SG<2G>fRj!VpoiDhZ5bSH8M7GY!91!mB6tW9Oy;y#MaoxN-TKYz^gF4lKvg+t;gM zatwv&VVIn<9Y*$;jcw_il^E4*LoLXm=I2F1sM^(igkeo>_s6=l%-&}9KluC(OD+~tv5 zFwt@bTx4W+m`Jnpn8P^eXy~O%YWNE023rw4JC+TeeRl(#j-7D(_Q9YABR?#qZ5kN? zZ_7q^U!WyuD!UOc1N8+Y%a0JQ+=Rby5oYxftkN9p@}h_&m>AZr1k7QBS{9L?M^rCL zfGs`ybk8vdg{fXBsU?Z^is9IGxE{Be3}Lxd#4Z6_L2Ok;$n8Y2&|Ve^^oRw43~CsL z?`(kEwI7ASW2h!W7#X*5@!B{>A5CCxZW)zofLc9~*{~ELtISnVG9oMyS)3YOq4(KT zDl(aP!cC3!?t)USV`r}tbI`4IcXwjnzCGB#e-B*K#c$s^gULsnaAF`|C?Ml`lIW3x zhl|#OyD+LPOyeYrdYD7GmPNVF{xy;^%oPk7$4ajt6eZ8Z z1U%bNUBGS)E<+u&2G`Fnt&0xtpjgT!R-cz>5NSR4;J@j3keRFpI??i%P{qz3QS~bH#=EK?q;AUZ;gn z)hZHRF2ypWn;2VV5@hoRa{oVT@A+NVeVz+GyPbXxdItf5BnURKH<9Y1C91f^cH--M zGxsJdE13`X514;qX04fz^JT_~@3j+Kwk5~56m^MJ>=o=p@9p$+cAfRSzr6uU5oJpm zc)^m$g9BjicfY^4JS82#Oxn<6Rp@3JF>=0$wdFj)OTc$ifR{qWZ$-&%fS2e)wrv}l zI<_H}?Zec34fh|8;@?7Yxcml#SR4(Uqj1)ANGI`^v zP3>Fa78tOR?}TXt$Yv90ZEZqRa}M3>IKPYh#a;tKro2(p+)&X_|& zFCcE#5VPt+h~;|O4yb=%!m%yb^%!g?hME&Ysb-)^d9|90nn#JG5T!@Cc;-dvndp&g zieq^?9w3$Uk&3xUn|UOSyd)@CG1;p5u@sQ#hL!0@u4_B2rdA;LoQS17?&I+RyL_YwzW6k@@7M~%G;sCWeVjdg3pcNiqgbHM7Nv{S_D=+wBhZl4hDr=i>2`5G>_azs zYN=T>SKMk9&eOob=D~G%_L+U?>25$a7eg{_Vt&EJ$o&Vnd+QPwr|u)>ub@3uLtCbX zOuUBX1|ONE3*88W!d`PV6e~JPMFT4!nYe@IE*ln`1$+~npe1J|)Iv#8cZJ7NsbZ_9I#VRHzl)U)33we zJXRj6(3(l8DjrGHXv`T{SgK)ix`cc8CvgA%D5j=nP%IP> z(`+;)JTxagWD_=W=_-=(iX;%*kD=z8sMO=AR8lAu60(@7dz{oLXG66JVJSi#FH(p| zHssGo%o0b6s&NBHmR^92RYh}b1U(J&IItsuEdvhX#u$uXOlHTv*@9GKKa$x7=ui9~ zK#UF8*9_R;?*9${=RP{U1(ZbZiOrD!(7`4Fqj45bAv#f7F3tZF^K6)qf50y*{cyO6jH~jOxV_-IqeoYvy*soR{}dXIY=8Lh+|X%iI9VtYDYYgf_3C) z(Bf>o9_gnZiQUI$SnuZl3GePd!Es2}rREZeh@}5!QK_dfy_CR(hdsDB(uJw&Hu&6D z2;C%7r;c{(p)|Zq)uRoGs>3*FCW()oF_#l&P(U0NG5EDiO3>SI+!X3=Bix`#x=79e zU24&ahA-6#EinTb*wBW3`!{3n-mO?#a`52?XL0BD7#0`U0duxT?w3Lz@zWy(no267 z?#+#x4U5g5?5h=cgux?bjwuU9+{K<<-S{s*c^+Mz3E3nmxlf={2}C*Z&I6^F0)ar*Rq{Ps7WV1AAhZ(oM%O5|CCO?5)KI$V5M zFhqQgBP$L|l*zQ@Fan6E!xh+w$21HM_T$Krz1Zw8wFU>*RBFKkat_n^B;N zSq9g&%&;zE!7_5@O>{L(semPu8KX#HN}R+Uo^r7!SXKkxlKe|X)c-?4Ga!; z;>8z^Ae+hI){XnPa_J`S-JL<5p0i#;ko2@?Czyv4l2t8^I=N?3&HAut{2XRjHfs*4 zG?s=~I)SFP4EF8r#Sgx{A1y6heTH)PkynEceDL8Ue)XH*%Sdbgp}lzhmHp^zPaqZ& z8iY0D2GY-+KR1gv-uMs`<7L!sGA{xdZLv$2O`X8?_?VF)OF9vcT*dbyaZ^|XWY20I zEXx(Vz`;YiuyglzsasE-Ifr-OJAqP}lMnumYzi4@gik6WrYViDiPp9(vJE7SS}0fQ zSXe4zd9ehyrXdz2VG`%7v0Sia8Kj)ll_>}5H*6HuELhG@DGkXoX0U2((zPWtC2pg) zWeNv(#IU2+LB<Fy`RDNnOXBdup&Gt7Y%Uz zW)$izxH8s@Pp)S$TG%AO28DMGZE+-Exz7%TD#H3qWPt>Y&nHxQ(#AM+*Q>C!3fA|v zVB7XhIP~NW#Nryp#;Z7SibBPUm|t2_H0{V-jW?AuUv6rwI=XstIDY(j?A$(pdd0?> zQx|de^mQyP*>D|BDb)Oz2Oo(iRV@Zbk4dKHaEVKXaD1>T7erkuJ+5PLOCJvJ-+*J! zZ$(3>t`mt~xZ%9?gAYgW1_Rcq;=rN3_|EHxv93KOOA^J8Q{WJu?pH1?;E#VigL`)u zkS~-_w_WI#1&x9np$rmB6=79{;(qFpXTbPkKTqjS*R7zjF^O&4*W=me4xzKV4I^U< zIC<&}&VF_Up375=Vb8OJDVntyhx#_+2@Q=+89e{OQEcDQheS%l{Reqmx;l(o!w*oH zuL5-oaXl&9M%$}Nz$%=A!V;4{#D__OmK(8XL?$>j z+!>_&MR;7E6K5N5gk{BHzVK_{n`Br#{D0t%{R-vM0z78{nokK!BDV-q$e`{u^rax z{To_@^1IB&=FS2xU%rn|PFzBb`f;2(5QiX~CCZ@_)m1|xt7G$)^*C~9H--kgQCP|2 z*61f`8PJDoz7ZVJVC|FN%dpFHs zL)#o$(-kEAJWRiWx|Ty7&9HL4NK)qW!2borzd_T5xi{cV{VVbd6L6~2vYs^ps_iKO z3NL8D_{tz|j>fZW%i~cgk*_TKxODLr-uU&0Sk7yx)syfYLK&$n706=8A>zq$k!_4) z-`+tSIk**_?TsjxUEI8R9}n(NVt#%Z<#G+BQVo?F4ca)%3{Qg4yNe2pN;y!er4R?q z06q5d5$xYjmuzmcLduFL#xTm`GVlH1!+ZG6Z{C#c(&0n9as2pGXm4#$ivW3@lHuWO zdUT|S3m0zV#HUvN`1_oM?qPV0E z)B|8{rGj_gJ%j7#Cop-h0=Ghu& z!JNU9Ll5!vRzf@PBS9V@X-q+Yiq?jz(Sc;!R-~I+p$-0TAt3&1P6<#s34iev@)NgV z7aqcJCZIdZvNoi6Mi8X1P#eO;NNVWDHi5+%8}+i@#e~2+1>4Gp*w<9EOPPGlg{ymTD2+-{ z5YFlOV0xZ3u_--3OJW{JhaTX_mI-vE9#BD8nS1ClRE!>!&0b{t4kOjs12q3Igd2X{ zfOSTI@_pDdci}EyN07e)tu`q(^Mo>cehdX~BNprZxNryf?7D-Jw+;nwAarb2!$HNQ zRO?Mqh2ejRt`%7a3f)Da;Vs+R@S`6+gMp2Wh-WOJU@LiYL#l|G@d7?Qc^79tyN3rO z1)*CL%19|3SqD(SEEK6q#WXZ#6WBPg4uhKp(B0LF&h`c*Qx>XqALq{B!>Lo}Fgdv( zb5X+x{i_7O8m?r;agd)ivwSV>OGid7QPpg?8{nH0#-hhcn9lMYRzuh z+>hhOpO$3_H;G)9OwKOg&cjjMzWWe!(*^i$Tqxj5D?+Mf#i|ITxAwIF03ZNKL_t(p z9L%PB_N~XE!<+HMu3lssD0&jIUkQx#0?f@+aOKQ6e*depC@mTikUYWIJS(Lok-Zbh zAtx8|_a2*CZtR$#93+Aoa;As=<}wZpj$zlp7@A@a6!nu2rICS?90VG-BS!XdOFIxh z5CYJ@ZoqnFz%BrVF$4=|;ZD8}t@;3PN-%;NJWoe4=t05n#np$vXTv6D%j>XgZUm5O8$IIUh$#+>cvu5Yt^WWHs#E)q}tM@$+cwhzW>SdGr$c!g2yAFZ;MP zyo^7-a~7Acj>98pIP?-=L~ydo(B*)aM-jh&DrTXzC5^sy?Krr92L?BHid5~%Q{#C1 z?e{S=y9|d?^IBYGS=OR1>vS4=(D#6ybREsj7XJFLzlVMM)+0t1bqFRB5yB8}%n88R zv-5cG{r6=rziZbJo_cC0nwvT9Qzb?OHRWZgR+(j8{Ez?fuQ-4H223L>-Fy_VN`yb+ z;4<*4NW^UH*w&A~{L!n(0y-@`MMwB4pk*HrPXiVK z@sG#EK7P|B5Frn!PXkjQqjcvDShf2w{4z{2bnsE~o3P@x;K8hp%MY4xZ>}3N)h+OY zjF1S0Nx(ogYY6zs*(K6&2{H3|Kvs~j9Bkd(fxY{9JnclTQE5wYL7+r=sM04*E#_wH zc=LA`aq{#qN)<=+|HWU5j8D0SL{|Zp9z)!Siwq}KrPC=N-+JLN_8r_KYv2>7ZsMJP z`v`@S+IZ1ugGevY+2SaMO^pb{Rgp;e=<3YkC;$01?ApCfhU+57AC9&q2Zqptix-#h z@yDOaym!ZrO*nXP3mTg!Y_aC?B{~zF-?BE(13&%g+c)$(qC!gMc7#m1~4?IsmLJZk}8bUW3xn06XZ{NbrD^r+SnnyKM%MqJ;-ap=7jw}P! zcoBKX(I>ZFF_e)B=FycZl>HpaK@O8ECT>i0;Oa;N#tVb+#gdUi z!=yfQepo|baQ?~ElvZm|$sQ#n<1UUK*^d1OHevUkbx39uno4b_MJWI#K}a4@Sh4Z; zJ2&y^sT-J?TS46kX~rS1U7CECh#?Dclb{&~zPAj+DC4zPj^fahN2QZJdHNzg{OB~w zRenWMknj<4Pk~EFli{pb)vxQInQglKKjo&1v*EuHe9qEY^3dAQvA)(p*#$N96vQ4M5WYBsabct?3|O5%al*d0n(w*hvPE-OY+E(MuwsKgi#xXqIi8D9jc(AkqPS65hGHFuA z$zD(eU(IgS;8ugmhP)F-38|Ea*S>ue2lfx4uRjA*Tsi~PgksIXwW-#nE>O-p`0TS0 zoIEp(;X9+!>5~>h&o0(ZZ17}qM#_e4b=Y*Qz%+~ai|;>&0|$>{IbXx6)0bsNS>y&t z=EsE6MML#SOI2mqrsf#7Z0W)4uOG$GP+RC07^+c*X;$HUfveXRar*Qb8OU$lGJu^s zHzAkf`#&16s+Q%~RBOP`e|`ohPh3EuKmc}3X&>;Y4eQ+~U|p>Y+s~tOU8BsJH}?0V zRxabMU;iFs_gA2MEeO1n2uh1Nk7$+9*_+3`vN#I-8hUz~uzhDgo_>BSHg9i{ag=Dt zt}f9PSBn9aU()d4UIBmn{TbXGeu!F~4->mHo_u168lE0}u$0=3?^Xb>jI6bQo`xxG z?Ow#Lp*Y&IC775&Oka@)LY_lC12?@Jv94zj?|cr(wF2>uZ1rr!7=RFdjw}2724pfxMaYb-eWKNM z08}f$zx?t7PMy4f#ic4dmzzO8JZlo_k*}GjS5fuyXzobknP(4R_Y*_NrA++xUw(tT z*Qa3T<1qY`%y8M%M*6NoSND~d7SUtdw)NxS;jMULUmrUAcz;+5gv3xun6xpfiVLD` zr!YV3;x|YDQ+l8^kG#<`pFuwvU7lCqJ8TgtyM<9x$1c&q&+3vwi8E9(D;pNv} z#L&=yz#uiIQ{i5RT^qBFLux({C={X*Jn+r|OFS*sR zpmT$k(vfJ0#XGHB>2w-)!OVliP9R&=&Odr0~Hzzr%wY_fTD^ zz#tEdwZD`U{0vnZS(|DBVo3|zc5K1E1KZKJz8RTjQOr})sIm!Tt-=gh34uu|6ssEE z|L`nsU%Q3U;tIT)Crb|1AhPBsm59L_2)Zs|_{b!Dv}7t6=qzEdyM$b_1e1hSVKva+ zRnh9HS9+EseaLK%uIkTr*I%Ej41P#ekXx&t@CtB4!4NB@#$9HX_&Ffu8jPXvn67 zA!?=L&=a)R2?Lkeg8o8+PR)te_;MO@-iNP>Z(2XQ? zD+Qyu6`Af{Y}|7Y@pvCT{pc1>pBlmV#4;RSbP%BIK{pw+q2ssgT-k`ZXlybuG}M9D zzw;!vZf$`njx6LUsa30@bC3e0V`5?+4Ajh37k508Kn|sJy}qO4}kd8Dx(uoD)+0XROXRMYdCPA56?cc5xaIZV)V`#ERR2g zvp5aET7pyOy4HtndrHKP^fs=^%s8w>8odLXuzsMAaVp!gSSkxG(JU&0g5+U#^(qL; z#EuxS%@oF_W>J`%fU`1#dSMyG5)Ji8*J5O3^4wyhtj zmTm;Di>3KFNv_JImz}yu2*Rtm@aqIoYsmGG`(t8Z;yy|%)3B?H2 z;)g$a4m)>s2+%}S38fKI#_Y`K%xsHGdCbqxA|5x;)RaXwlZGL1Jb70EvQukSiV7FV z=YgO7)4MVya2yk!%h8%r+>UrnYHAh;FRET$Kr-%O$M!}XJGKE&KGlZB@iVAQ-$tgl zy@hIN8M;?OJfO$~g*qsUPNH5G&Fx)i8QcUd9z${DhA<&en}Q{TMy16?(xAhmiiR0x z1`Zwl+J}%`e+Z>gGv0jTA}*X?K%o%G`wH2P2!5J_2%?`MNJ9MfA!;+ zv3Fk&vKjTz2{KI<2h4zk&KRgv>!?&ph{X(~l9mcqY0Omlz{!c28v8({9AIXqj(`5= zcW~jt4FuxxrN&xH&p`ag!}%FWo)tLt6huK|)ft7#GP1&eC3{1NrOdLteqIf+z(#_loo~bU$^w)L$Rf~l@Qe(+ zcpJ1-8{*CDVKxpyYuXIN*Qv~^r@vBN>+yKl5E&#cfgW`U7J)jI3m*cD!>CVRN0}Bk zR$Z)%2=pX9n}I_PyKU&ANXV>Wkme!2Ppbao6X_Zah_&oRqUR{I#;x#bE10@}150xw z@JiE2_=|}9Wq3%*#U>)Xd&3Z7T{K$uQ60UC>e4W@@*Ttof0S9JVlWUK!|8#or{Uy= zknG-vc-J8;E@bh`U!23`iz}$sVj@f&FcWi=h$?*1vI;XC!6;J8){)B@cq}h?>DpPF8UJ`UpPx_#z%Wn3M$u zM{MG36XF_#GA-&!9_)Gvj$IUUxVBague`P$-~YiAu=CeoPk#n?`V=z$l1N5zO2Qeq zU|6X;YiMw+UTB$3$n_pXtgR3JN*>GOlUN*o0Bv~!31|7ls_n$bU~%_%xp-!hK-j63V0JP@Ee^(kLtS6sHb@ z+Z3LDL{Ehdd@|Az>mJ@P-eg;5oUYl1c_q_r%?FQTeO0)N|vTz^u#ryDA?jYtZ z16C`{+#oV-+tAQI2xM~zYBkg+E~7So30mQTP?d!-8j|;j#PW1gYHBCD6WRW!WzIJ> zmBcT9aRJw^6k$6F$q*8-2BFCnnHQ7wO4%JYK57bJi-Gd)U7N9Y|0WzdJb+vy)w$JG z6>fegek{8psnsHnFV>zaU=4y$2t#y%QaQk_;TfDca~U^oj9_7*2+w2p%~FQfYHh4V z&a*+Oi9 z*}NN>-e+J^9*DK>!UBrpx8Y7-gH;+8=7*o;_3lBcdkc)t4bVC^$yzR$J&Wpte}!4O zEz+yhKM4| zuh?;gUBlz_VL6vm7HgrZvWQ?%!Eg-752HAC5rye1NO}uM1+RkE2zz$1{O{rp)H1bw&F>s3VI3H4BUnTXxi`+s_{J-9o6ugU!BMBjWRsW zEV*4G#8A)(B?J<%tYP_ED^m`^K)7|WY~Qx653haaDfITHk;$4cMNEQ@+uEuWK%^BJ zRs!lF6`wDPVnEPib8}T(ym%X@PF=*z%!*KL3^qMnwkWNmP*Yb;E6BvP>t*;ZM@}}f zSsO2WYdikphX;@~CxN*Wa7KTRtUCo$Q2vVEPE%yh^ikJ=YGMnLozEcE`yz~n9@U9+ zOtLZ#fAmv$GgnY96%cFNhE(5PplvgdX+e-`facbK`LkFV{w2)vEkW&($EBz}Yk(dZ z_YoT_nqbWE6yPvWb>pb(J+Lx^XxZ>2jNbi#N%t5o33$q>x$zpF?eQu0D>hwm2&VtNmtJ617nm7-=_z*CdTal@H+-Ny-Ud%=_xqvTTv))0;{=cp{pqmYw-l^2fs$f9uvT{ za#b|olZzHwcUDuoknKB;ME6V3lMEErK|VkQ?g#I~pE!#`K96|sVI(&{4`kOv{Fy}S zs*GUq92Rf<5_<6lOt%Qlu8M>+6>nGy3DZQldTPeXQ8JkmwzyR`LQic+s%bY;UHhQ5 zZ3WCW1V$zlTu}k5u7oPvANTPUSOR8C)r82?D4+cAqPU|tE3`nmh+y$1^p$(?YYT|8 z`JsUty9z=OMbVl1_6enRCF4~Zqj@khy)b%@!x(rSuqas>8f|e?GItBppS^{YyM$ct z6VNt13p8vAi*oWzszBi;YGd!AKKd3?!K_lL7XPg(>_X z?mJg7HbTA_1D1{}WczXgp#oM`vNAN~?34&6C0s|{0h8?Bw1wW@PV{f+L`z!|$+RwR zGZeNEo|BXW;mwn^n*w?}j)(dA0;ZhjYIpw`Y5M_8^1VV-4l$~Z<)UWjsHOL#Vbk|u zb-n^vEsB&W1K|Y(_uqy;egZ2iMI_e0fb^DE5G2=y+Gs4b@(2nSv2^>_z{*t^)kT=~ zGLiw!p6QUtQ$$Hq@nj|1TOzEOwc#C&n7bitJ67~{2yR4k7MPY@_mr~bOZUDiJO?7B(5PTTBA^IWTcPr$C*BIZ)Yc~dEq&QFyM6w-l@9_2VIA}49c z1VBvznCa#Eta{c))fs<9wHf35R+tVUvp^;cuCfW@@*?uO3Ox1jNWfMr2tu>Acrw7? zj$S--Y&VV^=t4{FGn8-qBT}_H!iE*t5b?n#Z9sLnu@u~f!)V<6eQ0ej!GjX2khMp! zj9}y=)F(c}%5njzzNe7e{35(qyP|&y!LR_UdP~HZYcm(%ERG;$7myIibMoUH!7p<; zK?4=dYC@v14@eFG$@MVOJCWg(Bs7*HKYb?!PUbN69Z7m;@ILi(d3A%VX_9}aRb6D>&h?1k2K7--&w zAVyYz(smU+tK~bGyYdGK*i6?D(6b-NZjw=otPcqyb|&CWo++A(H1dbiL)^H{(>s^s1XFsVh7F>4{IVH4s z5IBfbTs>=uC4qID+Hm-p?Kt+rAiBNFDBt)g;+0`BPEmGJj+A4hE9vPbpyer~ z2EGlnJP%iRc~Q++Krns=wW&*3T3AA=_aK@EpM@81l{!>GDXTDR_YlmUL3QFhYO{Bd za`K3KMFgn8)~awU51Q2g+sna9u0ytaKhU&SHPjqiu^y(<2PKL^NzxEg7NUODI{B-` zHow9}Mh%@w5#}Whkx-%1zOrIqy`cnv)&6-Ls6B)|c^<`yo6w7MNZQM)3A2=EfE~~Y zwi#x!6Pf-)(7Fx+4Wx+oNU$o5tOu;z$NbHI6F;p?=Md1f2gqy)4ICJ-E_81S{tQPg z@4#EW2eZy6o-Tea>YP8Cx^%;O$M;~Z|4Y1g%Eu?4Uc$&I)lV1@20X6l#Og>nqev-0 zBMmMCWd6ymk~POdDz{un zzGukH5C1&vuUqq^V4;nWeL6A@ZtB|!d znDvLi+&R?8&!IYV8%cW+F}Da9H8>{ubX11Pq3$*ymRgTo-w~ko5Rj#ZSw=O>YIqUa z(WyCC*SplNU2LDYvwVp8;rEg9myzk*46S1i zknRr+$T%kBGGj_Q*Xr1NsLtJlQ7#}&ol|NjFteBz;!Ok4yT1dy``dWyg91MK=sad- z7E!4xVF{51Wm6^7c@;{sl?v4kmEV;@fRWOpjxP?xl~}WBu}mNafOQ@b{;FA$`Zw(G zSFy06X$KoKmIP}<)#XcLBr;>gwzBV}sg0-Uk4s7~BO!k!oRZ-~cO0NW(gCIiRKAd&7zw*L^& zaR6xCEv9=4L`>A0g5GJQkx0M_!9HBU{0HW+Qm=(KY?VAAgu7^#$_5~U^RaWk2b8YD z8UGL~leeK&ONe`Fgr#iV9YN>VdMC{6dgL}gtpYJlGqlc-Si#%o!e_wV{us$1k8Im! z=p8$OWUuIws-MMy=}#gkUP5*90~98%z$h*w<5s21WM@j~W3p`sM)&JL`!W3UAExmB z2WPOfRD^AZyM3j(CjqGfQ(*%XTu}uT>sS&Kxn|{Fo~kZXgkix%*&Zpq+2Koct4yg% z00oj2dtJF`Dp1u!zzog@MPXmnLM3cF_%L|3%!4UiY1b&U!L@94Rbi2I>r|tKExR}1 zZ~p7c*p{1yfA0&-%l;`U7>55@OPHHHXS<`IjbvbjiDeUq`4)T$U8?vD1 z@Sio7Vw%be?VG^TS=i(6B0u{Orb{FNHSiVNini^xABSUfB9A0;8GF`>%X3biHIS?0I= ze_}G&j;O||RxZG?xiFyOV@T#i6|t;=p(h6L4}bR^>}Z*X|KLry3l~wXjv~;Op&3m^~vKPQH(=>u|&qAuaNphS`jC#|~HzlgBFkofV6{6}Rmqs3il%ABvBm{}b_pM(=F!d3f$#+p%9*6GHWS1A1O^lL* zq~)cnw<4MAM`~*rutp!il0anTmBqVTp2ht5B_#9;8k+lIW;X&HQHcT}!Qcd0a5`}f z{`AMlPh3J!nn#LVJH^yU6^S<^*R>Ny$8p&41Ng_^j^G2*V#+m{c}l>_8Z^?0QsQ*# zSXWtB4jI{0g+>+fy`y?mttA<1WIBA#P*UnwH9%KEsOFR^hlY)oJPd0zL;kG-yQqhQ z;%dHcqgp9SiJ(-=R4GQQDLv+4{pMc$_kZ{<_Vg5?PrZZM{5e#L_YnjO!uHkC2G46o zLo?;sj{;5mfb@1q@g3HbVtQS82q!;^<$NBo#(w13H&{(73zKepYpY#YRu}8CY%gN`4oLE|1GRt!x5gWY~^f>jxc6bJ`}1iVDnY0mEd`tM1*dV ztYInDuh-Gj*M|T8_us|-4Ryo|pQ1W@7Uh-OfIAC|C4tr)Bj?G@g zY`!1A_}6iqIdciNElj!)ZyGTMLO)y?u6@;FaP8tLyp{^s)ua#(?4xj2)afW_)%!AY ztL3i(xX9>C>R_cmKpch`H~S83yNY_fA^{tX#w1}IoZ-~b)!Tx<|J&DaXtRfmeGa9m zQ>ZKt!|-Nc>1B9Y8*H~3nT8>x+7Cc)KLBL6E1?R4O<02){JzWZJTh;q(9A}}60Oj* zq_Q0qVj>kFM}hKHR7X!^Y3wGfdR}w~;&j~M0)b0dKL)2}!FAGbk{glU`Z~p?2#x!0h{CLMPg=0*F>) zF=ioGaH=`P;w?yaJpqjY+qzTgSk^L*fKfu^k6RQc7gJA)+@q?O8Ej>U|AIY^VD18H zqaVR7j>2~e0xH)G3#Fh9b+Z#4o1TT=^ejdu8}O^Q$8h1ob?N9s7qD>763tdsR#g-h zWsdPxA1de`3E1f6tNecDOZH^}yBh3Lny5!abY4Sub6#xfbFp@1&{A+soX=%~p^S$; z%|=Ie3;yd*zJ({Z>1Z;BQJVY|<;82TFfIDJ9vWf$jWCmaNHslyM9(2;M8=u48B)L> zh3F%Lf^{;pIBDr@gcR^c7Aam4sNY2}e-`EG^C-^WhvgJN3|5SblYk8&U&nwoVb@cz z6YG)L`Z{8}{|YceP_|sM@L~Nk1N4h8du+f)RmN&b5TO&O@S{0pcGpQ1{4UguUUY0e2DkAjhHv}$?R(?6di@T3 z8WM}liOQ}@=Txb;L;Xv z|2s+?vUU}Mrh~T5Cj8Z3Jd3Ax#?h7=LwWQ=l$I_drq95{vRqGBYlLsK!AcLJp=&>k zHrBCSfRRI>rPTQlE*eywqxfX%6rwCq9k7>y;tfk1R956kdJP(3cW== zBw6e^Vd+By#{yj3evPP5{q;i+NDszdbsgTXxa2F)KYtJ;c@|g z_+$#V?~JH!KAd1nrV|D5M*>zh24M{p2CLk=uzXfEu5xkxT!xhZj9#QF+{b0uF9vK> z-9|H9&$rXxS%2tATVR;6iT<=g0XZ$igdB87q>ix%om`u&ypaEfZZfJ`E(bo zCfXRSM=aF=Jwe!8I;52F_zy22zln@3ih#2Wr#OM)+-*4dF<8|xq)c8gI?hnPHi@da z3CM0m!@vtD8ryK<^c>zhvw)G2Db-wsnN_9^5}TYpM9o%MYp%9cO5;m{Ds0NWG{Z*M zVztqFG+@`VvwDv(L(4^0MTp9->Tg`HE~S7f2b6M)$?+If3I#p2!io zfNjICRp1pCU>EM8ym%F+KaT_#H$fdbF|i&2FrlEa1gwF2ErGJpjnu{$klXn^XpI{H zI>tyN8;(L(8_=&;8+>JkjVg|CnxfpC#jRcWh%#Tw0JD-;jx+b(hOu%QIx{Rs&Xj1B zxtLOrre{~6Wf8Dm^=ai!R8JCSvH?cCNo2zb`4dMH*|CT>kpwGI_nuo786ejy!!2{u zQWE_pa*3FQh-`9@gq_$5qxlJ>`k%qFy%FzzG=Wbp6fiY4zZS4^Caa@QIW($cN{yDH zb}Gu0e`dh0mJjOd*D~yv1Z-sK9&XrF@1=-?D!Z~AsL6Gc>!HMJWlqg&%I+Z7ki%1l zw&A5io!GS_j_T;AsLq^5+ zG|sY*67{qVbv*=j4WLGFeM%&VNby%rm7$3opL#ks(g3+`piV;%&fEKrBDeij7;T%O zQPL-ZsjiJEzFxG-t@+#l@k_owIvt~wEBjz&fXupumN&@H!=L>W)v*s@70yHVX=y{_ zWGb(ynt}nvfF%x<$2JG%O7~Q)F?}`xCjU-!Wz>o`tZ1bMrhYzymapZ^l2YWt83+e( zl8s$45w54U!D!iqc?Q*~6R_&H5%=i@OthS`Sto$rC2fX+chn)EED$wbSx1YOEA5m>r}5$F4p{VY z)fI5vC9dn-B9a)&se!B$1$s%%3FRsSxQ+qWHpRq)=5bp0KBNbqM||CO7#Xf-6+-QE zP4CwX*{=-P)#=0MYS`#Ta2vxXuL>+r!JYaPwds?Hl`cc`^U70-0%3wcSIrmENr6U` zKkYrK&ri}~0MD^O*>yT+iwm{fQuDPCIjBi?t;@eFd3{Fpn&~vxw){vYli0SY6R$kA9xpw&9`?+6)TT~AFJDK( zpP((X%ye1CDJqICI9yKlsO&;{dd`6Zn?dNqjOoGyvni)43OA-SQlh3!Cai9Y)G9#+ zo~`EC6y8$`FdAH2S3&DCU|SKi?L=bzQDipmhSAWf#NGt;7L^5mVy*Qh!Tcvn0?9q$ zqDQ`7*6iwd(uFO{)!qoIP(8qG!@rkv~tM zGP9mx$ykb>RbuqZazi#P3b%1Htlk4?+Hw$PGq;&a7D^Fel=Au4{dj!93Lqm?9#N&7YEYCYiUgw`unEc| zn7ND6=qK=&FC$UAr8XnN3}BEd9YSUZu0qAlS+uW|9ij=!Y!?-^d$N&3}pW@yFO)Q6^09AA~ z1zL?1RwOTmC5Kv-{<$^m7fXUaeH)Zy2S3z+Obi9-!0mFoRgsOIY>&boKms;wjv@~< zD$I1Y#PRBjoAK8_*av6%I;xYOB3QYMxHBs2U6x{MO-!t9H1lO$tH^_b5~ILL*vxTU zMh8nCVH{jiT#nF5!mZh4$?HP38!FjQf=URFu8&%k&`64S#Nk@4u(KOrb?rjSjw6bx zD+>z+c2U8~e}27>$IoHK{6X+aDzGBtRyd;Laih~+hD(B}TPTctf*^k#ar+)jRAhZ9 z%@#)^Jl>fpNDk!kAhh#ToiQoXT+6aXRStj=8id`e3_%!}!V@ENLGgunp~=DL@`9I^ z=ud|=teJz}{wz|x&%@2`#qeDpKmDhVF)~g83teGfmAA!O@P2MN7#b%(Heka{99G(@ zQTytERe;6MmkBCh)u+_3%%2{&TXj_HRjFk~U`^aOcx{wwwYcI6*Um#M4m9Qhynbv8 z{`MzF#Kc+xc8TDWk$Jyf2eB^Y7)bDeZ)0_ewg8-#1fXJ;v(+DiX~t%2sjhw z=uiMGmXs9&?q_Z6c&Jv$KjVAHRKTYCVRk%$ww=$wY*XuAGCd?vS8Lh-p8|HRmRsGf ztJ+U8A=k3Z0Ly^A0B`g%mhOEFqk11nZxkk-p(U);dREa|)G}9LP#K`=IFzR$yryf& zC}vRBvTF87P?2ogWSq0AL>bO%Lk<-e6Iy_p)&Q;J7?Qm&p=|BI)!{0B{x6?md^#M6 z%bax$gBmq-lGRs(N(IsvGpt;R)p?)Nul-X2ySBoO<`C))RO_hBHlhyQwmBW(NQ*}~ z15~+pOJz-96+z;@QdlGomGVG|L=~^SvnU!Fhj^ zaEvC@tR5Jx+tIcE7|f0#SsL$()CG;e%xWtm18;%W@mq2hw+Ps? za37w>!rhM%v&WJ2#}&(96_Td_yUd_!+873`0BMLkQY&1Qw<0bBpIT)i!O;ov-;~`H z`mhY-LnMxXGt`ZhT7v9QGy?6fAi3@pEO|q?bR&;n{q_{5=afirg!+%dCz^|@L4TA5 z9~ZFE<>k$d6apWgVIK|FwXY1vR|+04I9t&k@WSapy)N~uTGgt6l`ALEyqGxeFcSxc z4c%nW7IFMrTkyaB-3ze%5jeA-z@9w`t9nN?4209NWzS!D;71ad71z!4n=9)c7y zR;a4zdZ?wB6bIrbA?kyS1EIQ5bVSv16ymE@Ia;K*LK3#th^o;Evw17J4;+WtwF59{ z$w`UNP(%OgFS^#oTB{R*)yj$?tDLqXG!1W!LNW5zGhk|-wusn8Eqp1MlT$(UonPJH>V~NEHfCJqq;n?$= z@ppgwA~Mztg2hv)PksnX6k7PYZ~@1A8L=wl`>PsOB?@(N2(v;mZP=I+gcCM-tX;WH zpk-B)@+V+~kITGYfhBk} zVAZdsR*I1ppAEyG1S%tNADqVg{Zoi@3*yfU20*<^b+c+s8)`gDFosyuF!-nopl+o4 zrzm1eF>i($mQS*R;jyt&^ke=!;ixCXj&USb*IHn7e+P+mui@cR7e2c%jSoJ!ilqex zYgBDM`VfXnE2~d?lwB22I|{VVU%)8CK0aV2G$q!e?-e~N>I$k8N8wvVd%)#7wk}fw zP79*bjRC6?zxtl){S>QL7Q|W^&pfpW|LuSI7TOz1FqcoGGWI@VBqRoPwOkV53EUHd3Mb=kknFOdV`Bs$%Jml&wyt)m~;m^jNg513{s#-IQ z+y->-I|ie7uhP;Y@=ovzYxm%B!TDu>{$~a3+V2xVl6} z0Xim<>lJOxRlurxH3V0zK3-u^ONQkoP#IR`AufJHUnx4qkb}jFm;-dtrsc6BI^AJC z$p5xb*IQxreh=|=ujA%aBhH=~!|9WEkY5hj1z}E67vqZ;Eea|*^d50miLO8t)sG9< zi0HCbE<{;uEgaSYR!~>IBfKqQ1{ka=yGCQ;GFB~Xm-xDuN=Mxl#f( zOkSMPx)mD%1*fH{i9apYCKvs%2bJ*5f=6OB{U5vfXtC}344P!~jf zFV?vld*woc0irYyY@-A5zV9Qp?loMv7srVccX9R7C`$Q|UGVP$ z_KO!RI<~(o!~VGet4Mxd_CQ2u6cth!bgo;I6|7^cMZsFIs;`gc!vddGjuGKvB@APq zo?rv`!CyR!?HjXbs9!?i&YOsru8Z=bAhB2gkcBI>akYq0kCvK|Kl(w5DbBe$Vq~c% zJ%-e|be`mi$FZU+4P~w4_;9GvMIn%oO~LzSQwLVd!pQWXW5-i4`kqu`C}wLo&Qc)y zulw;8i-Wb}kiT3sN|NIw3fTH>VDU1Xu@hLHxP}-Ch@q&+a*rM}l3}BxN$EbRV^L)i z*_F$DPn}bu%M~^ShEJ0+dUYv!46`e}q9T)Ej=JO>L=+s>m?$?x}$Fg;gBw^JF_1 zMU4DAON9`2DGfGf+aggTC!9s>GizwxVNvoApgGL2+!(NI;8CwiFGcBRz;>Z+=rD}+ zPXjIFleH=z-3V~=H6!*^#*m z6P5%KGD-eU9XW!RC76P!p|I%p$WD+aon0%NE;Zf~@*j^Y-CI@779t_}V3el!SW1JnjeMDirV`Y?PGF`rWkBJSvm z7Qo@GzV5-VIkMQ4skuEu{a1%T=S!kZXD4>{Lmz{)6#((?>h zbtEyfngSq_VWKnw60o7*94*f^YG2WD+9MiiJ*@uk!*6>L@19-8`yXAz)N~$IJEU_z zGVuRmeHjJk+Whs4P1m1a68!rKLG&P~%pDnKC{z~fSH;_sE4)fvRNs%sv0&IkSfPx( zII^dzi|(UuLo<#(vjZ2QN_rr@=T&$t)#Mfq!pKG>0Ka5%%XGM&IM{B&V8?bAykII3l)ZpppWv9^2j4c&u(&5+GS28RxGt< zs z?3;c;0PB_dQJr@$3M-$0x8Omvi%e~a^Ke@~*9UB;FD_rq-6e&CxCU={AKIsPAw2&s zCWcwz;&}R+rsqYv+#2lYyj$C?8||)^+HKLy0yqmxA>K&jZ)izn8*7u^#zp2rsQ*NE zm*jLk&IZUMZk7-m@J>X;Lh^k!w( z^>zSj?t?}3{Kg{45i;khU?q52igoUQSXdK6b`~~maG-h$Kc_0qkkipWw}j1Gm*HnW z-i-AFXAph!FYvE^Al=6<-z(hJNOvoB(=34l7wV`*dE~i+ ziBX9Ku+0{SrL@Gl=A;9dfF%u*3hbORUPfH#MQLC)$}66Nzw}YWrA0Oe_x4QejRBj@ z=1c-CJ1PCE?;$$*8lnsDqCRp4Ij=5`egeui{Y#hD(nU%E%Y`#70yZ-WV7YC32y>K_ zZz7Ziw>I(|1uxV;%?)znq{xnvCn$wkK-4TFhh9`?t%tw(8H|=T;(!19S9tX`j>Y6` z+iFM0fZBESN1ZF+`d~G8tibL3(dTA658Q}NgF(h^N*M;4nzyK2F5I^8UCGKyTeuyK zFnveoI?3;AibEDewj1c3l^6NV7k|1H8|DwfJNYtj`5iPTt{7k=bFG+lNx!XHS*oSe zZD7UGRzr|Wz$#InE1r)EC~!^%M;+0B4MQ}-2vMUYR-3#I5_JezmX+H<9-&`Goa;t@ z)(VtX{TR9BJJ2ps_)jR~lLFG|8Qkdu)~-rjcDmi8@GifD*3nns4Ze$3?Gk*Fi-nz~ z4tx@rNVP@(srA)^F(Or}Vorz6S@s~Lg$-I_VY$Ez6c{u(tDtLcYJmhRC+an;h=ZBv zo3{nQqMf*cRrp`89LDQ!>_twKI!R`K^76n9bLkc*0pK@J! zpQfK&s}M;6WTHt6cT-6&I!q+(63ZT4w>E2|F)OXGGR2y93)Nm9Yu7EnuU_1aA1oV# zf94g$gYRNu?5q-Cq$+8&4ePXU$ve3)X-UP(x=vGzRhUAw;!mMX3rlV+gNqogW`rgI z+lplZB`r9qkUE5fTnB1UaNb%M^Qv4u001BWNklv z<;I@VB}j)Kx`7@9EMF$ujit4utRT9r0X&YWA)O5sjI?H;8O*^PYUs~@9Ovo_@S9gY z$L>7`;Gqj%o1}}$0L}hA8K6@EHd(feb=G4riy}*vq-#1x)iUet(=QjGvsz)CBdk{+F4oVE}7D8>Qubn&Q-CEQoKHOw9{nuXu}`3vlQ`w;xN3@@f8>h1YWQ!lCWkt}er3(zIID} zBn1LD&5+WZt^`ZYfK&Q*Q)ecj^mOf`<^+=&E`bPdP@upLLboAtA#=`Nmr@|Q^^%%RBOm+bFW1T+a) z5_Yt}BZV6VQZ!cW27FFn6z**fKC6#~dr;W$0*aenMA)@L0DF550l!A9eWcg?IIT%> zZ0s5rN1ZNsL;BuvVK>GPIl#6w&NQ`iZEr~d%iOA_yOls8MK7_i3Pc60wNOizfRaee z=dwJjl@@`y4GPI|VmG&^1}A&waW$Tau(|{T>vkaS*@n;0l<~(sNAcmlFGPsL6P3Ja z0=6@8a2*qnJ(erQs+E)qVIqv76)ow870XJ{vZ|GqV4Y)e?K#MPVh32dM)VcgpG9q1 z&ID^={yn|;@Bgq9JGPXN8{a34t?`S8QHY08h=(kPZM%T1vj)ZTEex=dgRtvR&J729 zh`-4kY#TYsindyansf;$pr;oNFCe;ioaQZnWf@qGtYz9pegK6Hzd&izFA!B%C{1$O z8ath zD1A3|2{bLnHY>Gm61w^g%pK^#fBUbGkPzn#Bb>SV+93xnZ0$5s4^|Mu0)Ph_~ zHxjg($#KOZD|gG_oLbCW62f~ZM>0KEA;*Y2dAArwx42djl>lXz-^=*_0 ztko^;1+zN1fL|L7S>u-R@vKpE)MN)>=@Pi$MH6CP=!;=!8(>F+0r)f5U|`eJsC%oi z_tP47y?;igcE^&6Az;7FPiNQbr0ZTUJZoZfekR=k`A6TiP5L0g%KWP`vUKB0DTjIq zJBaG>IsohZSVR%J za&hA6wMG>m88RY~ruK*>faUZ%NrbJ=+zX5|I=_&ivuE#Rwemp-Px!LP6 zd-Ky6Yc9p^{a5gp{pWG?*m=p*a%kJP|G7SB6?SPVOmgxzO@?44P($Im60lnCR97LT zly?GFEp5t8ar|PX&r3aD{6`+Aj{cbyJpbH-c=o}5%;`CY=DD{qbYdS$xQa3dZwM$g zU8f~*ok^>1n+9CjOuL3JEdA=hW}of0wHji!+)7sg$K2D$HNZ*-GLOx!1a_kcLtiMO z^;|!4tDZ&m-k%`uUk|_7ooV&HwX<>qz`7`xU&(TcHW;oQkDygMh4!i4XdHVTU1NvL zC2(7O`XSVXFdC<22%N*MOXL8njc~j&>6$X&BE>CX!;8OV4U3!$)r!z=wnh4hlcj6L zMF?iIf9Pop*XH51_b=d`1A{nz;-WM>-cG~YwM&@=Q0JqrE~cA`GmV^)5Q%Cx!Kz%> zEPXHw*lbqfT3=gJg?2%3*OHiAIl>f}?(HeznJ2g6nTO|K(ZCQI=lA03v3F5{Qa*({ zOc_tv26Vn92`vg(*Y%Nd%v4aELp1W51>hJ*va%R-$p|QxXmcFUZnD`7ESVwFIT4z^ zq_qlA5BlM+*om&S&%mFv0e*#?Sl8-(M*+({T02Wq%go&25Sk<3AUwGTty6EJI{ul= z<>GR%-zytLNVt(HItcvjd@5j7D2`kKr>Ditfp7^dOQ3$NW5aVjVnhThw zh_hr4e3}`#SvaWPgJUSD8mq9Lq?p*mjP3#+-F`2gdt@$FFKeQ4;XPbA`Zmh#K~!*A zRbT|L1l6^Gl}=eBkIKhqiQL#%?TF-o7UGGz_^U=yZ37mV|wuAn}21g#Uh5uM$OYVAt|QWeoU z^34G2`VQqCr)nhn) zp^8`b9K%P4hj3wV#6}j8SThyeCPQWF{A*__<@z~Vx6P}@fDVNdu7C`QGr*Yd>Z)V1 zecmJxOgvk=FdSf|4cu#?+Lgoo_pQLQkIciS^*OXI?UTmPQgjiO_@cxPlL5=XRjva| zc{#vJ(~kg_z-3vRBR*U3nhoKwdQZ(5IhOE1~GBzE7VW!MtuGQRO??SfR!9_CUnSJ zSog+RuB^-ZN!N>fNq+f2+yVMrVqHxok#Pb0-+5z=f-V95*f7_JsCT2xNv$q>2uDu% zc=@$2@ad5;3=MJk88r|ltBl;trnwW)3G(#BOa{MMKcN9Ig{5sTqG}^^w^R_%1zM@^ zng##l3!(EfIx8z-r_B1|AUftrTPT$S{9xlEJp0&uY}+ye?V$s>dSnlZ%`>R9F9_#A zn?C7wS*p+gE55B*11&U57Kfb$G3x~6>@xpK(Gm{8HXw4>O1huT<&!q?wy4dx9g%)) z-S0tr-j7gT{uoM&e+a*qit0==-gqk4l;-97fK^Vpbh##B64MpKKx zwQ&?dpjm-i0j%cy)U-?mSO;+D5@<<~X%*@|c>-FxOilI)&Dx5}l7~=U^dLU{vVq_I z=>WbyQOEd%eh0FUt^=`a?$--$Dl3|`B-xiaUsShMTGwVvCEq10Fp1}ySfY;ixt`U| z1hHxl^;HBE>wPl%Jw&nOVeOiG@a$s?@z4XaWq`);(cLK2PM{K1=3uSJi5?`{s5Va^C(~7K9kA{c zOa`nZqb*1i05J_iaI)ErfVHw)9Qvqxeeegip=;UWC@uOCK0Y{%m;SI1C(crsLUX{K z7SMV4n5Xo)~$$^H*7d)(7F&jVLu^5j(JRg4j3ydD!gJAp!x?0rPY6>Xjvs-REkj73c z29eA`MwbAYZ^yxRB9ApV{LR??I)1^#Cs8rX`` zk_S*(av%IzOLf>BrB!e0y?3t34O(Q}SPsn+=#43hD?sxMYNrlh?D$*AjT}ZbJS8fG zBH?PQ>S!hk;r7p-0UCV-Tj03?aLqF^aT!(^!RT0Lx)jqoRH~=Ql*sSwG>UI_#(Oml9AC4ha3wsDg-Hf*G7whV+1c^%}Yiw zs5?TbDRw6~-&HKE`sk86vjQyr;X0*0XKp}Y;eDtszaKt_1^V59SN&@OmY>sJLTTa{s^J;aYC%TZ#c5^6;~o?jJ%*mu&m!zzi?=^Gi+}k)`*41c@4;x6ZjEXZq^&LP zQ>IYVyr%)kQSI!Lrrl3kS5B=RpU>~kT&ooZ1gzOytAm~%`)1NcQ`^}ztH42bBETl; z7aimgE}8sz6~LC>lO z;Ll%gfMq^*IsTP)0HN=BJ{RC>q_F?3kzo67Oj!JYMKDV2MOQfk9 zwMi*p9ifaISG8iU*UQR<&aX<8n#lxWkVPOwVqGbVBWV=@AblzJIP{{p^a;#Z{S!jx=lv-A2v9IR_OupZBZBSst-2l*s(pbVa4vzaHTBA*dMN!@fr@5Gfj|exw8?di z+9Dt=J77`>Pzx(UC&-D2oc4`qvl9X9I{3tj^N^f7QCWgu_BzbG_hAH!wg8;nC88R4 z(88*pX$*TAo;Cq6;v74QiKFjg&&g5o1eR;B z=F&(;KxV3{6QO(onMtDqGB`RRBv@7KIFz0z`mUrQaxR#$?WGkwhgvWTl{L?!u=-~> za=w6FyAR{-w+~}vnCjg{1ObV}dU84HU4AE0IcEPPPpb2>)G(SAPFiKmPh%E<6)elM zN^w9$(%g4R!JQmK67X5enzisZx2*b7Xx(+9c3U(CBGy?cjS!Mt!K$C((6JD2yzwday?+EF9BS?7kjoX|Q+A#dI8mZj++~l z9n_tMl~dV}g9h%ue<6PP%LlP+p@$ix`!V+QYv>yO1Rf}=(H5n0RdGq^cjBiu>94X? zn?bEKj?fqwea?J2YV^AK5R4s!h)uwXY(EJ!s%RL|kVu9{^}Gn!5q~z&vls)LcObuV zJCNh7Fsd2eQJ2648|LQ`uoK6C!GowDe;?!LKS9~QA`~|5vsd>}^QLAO#Lu+tTM7iG z?y5hL3RBv5HJ5J_I);r zH{LjePd++<(P3pa5v+3aRm_*ej0t1~?o@7p0+s-EH(oeeUe)`YJLBs6lKivD-N^={ zLlca}HC?2p&+Ei2Q&O%STNtVm<~@7*t6>(zB+skW5cwHv^)w-xe>H%-{9|zYv3N2 z@W?E!3#@421*rBrr8svko0O1k`%JC%tpj!HSLkdmsZ@#jI9(&oAFv^J1c@JFIUM*rufg1(Al*&{xxrDeX|X z1ujU{w3hTeJHSfDArRFbMGY?wj%UeZj6qg)KIW|3jvN6?VK~Zf-l9UmP10IMH?t9w zKn)l^44i%swNv{re)$WOgAo+GVVgqjlG=Z^MKr+Be2d71yQn3jMR^75}aCOe(_~YweKEN zuU+-dBKYrrVeZW>)1dT2GdTv8OU0TI$(p`$FN z6iN%uZc76*C;GKG9IeuW!ko1ztauKWy0+psfA|o4_8i2}l@MW*fq~=ocJu^fES^h> zW`Z5L%aJv_uJtR+7(uX}HKDu?G+M%>WslKyS9!!GB24q-pG99>zYZ91pH>c`Q zTv#q(r5TEE>9w$MaRuA%Uxr5?T8Rzw$1w8o?-5+wi+1xW!f+hzxGs@9<)4_?C!`q8 zDpSDr3AvzLHByTp>5t=;Xthlo1R?im_F0?*i(5E9jh8{48nQtTa@E=Bp1&Ts<&OaK zbc*{Ow6OXS)6zQkh*99&JE(vDGQtZ7&=|cSZM`{uf7A|=k$PCFai2}Ji^lPzp&?Wbfk+QZt$ z!rex*F&>HtIHXOgPPRLFA?D31Vaw*lc>K|I*t{ab$ibHpQDJi89MB#^;89qEnSc-h zfiH1Bse(ElUSlfFNa(g;slJYEhn`QlKqaTHz!j-YX^zyIG=p3IYtm=-OUPGx(KUA^ z{N+1?#@68ubhNPbJ3PsZ;k)(^|st zu`U96hFKk6zfvmXRnM(Rh%NMobQZN~#Tchm%eM5NnjSUudw{O_=v%M_#YGR|Vzdzd z{Limp&pV%?(XOBo6(s1=p(Pm?$N8)B`BEUK&&j4uGOx2dvx9o)CC~}a>|-5|xCdN- zmg2bM0-F4KDV2)`7RlfQNC6&6&{DB7KRRw<&g?ukZ&-+(J2qp>x;#cc{}USL_DT<) z-yTBV8$n+DSe3}6WG6KnXtDX8Jbxm|pUq9$ev&R#GOR7p2-nAK*oydnRHadC%$z#2 zGKEqX%CnciTk#aI>@lFa_6`EpRabRaQ#Xu&J+%v?pZo^6dB~i zkGeBu(3#z?)O6XDLwi<6tuTG6X+sE*GV`$F3H0{luzu|V?0kG1 zwr%Xj$d|h?arS-G$4&#`2mr7*dJi0GU6l zLv_V3=2jB~GPb_qmEl$T(Z66F3Vo|_sn(4@zx4?YA32TKFH7H0j>;(Xr3C!uUnfNb z77~1ruBXAGg2(|e>w4-*bN;B!9TF3iO~xPTVp^pE~P}K zqC^LaMI9=*Nbt*mn1hm)U0_aOI)}nV$;n&_vB}CyWUfMDR<|Q@Xr4`7G(ZQr0L7kp zC@y~r`PEMWGuBUSVa-T(BG7p#*Bd{dScuH_$g%RrU&ob?UqXKLD+D1UPEWTL&ViK_ zma2eIj)qq+En{0II8)0tNK`@?mGTw^R4}BKAoGWbXq~Xv&IO1m2T!oFH=bi6CFu{* zlZpe*GbjM18K}%$0I#wDBh4y4KXMTlFS90Fgy-jxmsG7UqbCW#wA!V>rv>Nn#BH$S zGjMbkRCY#JZg%-wTav^NP7(oGS0dwVq+Fon90<=>ERo|dX!I}lF^NLJT6RV}Wof=_ zNr8oT6Q@n4q*!nr0v< zHMJ!dhxoO60SmwO0007zNklqs#Gi zlDp7DB*3O{h$n$pY3Y+FuiXh$uLIbrkZSkcSV$u^9abA2LG;aQxbo3UDAkUlC|d(h zcmW;@#OrQ_tVl^@47rj(AyR5 zCMNQDdA`RC84KBpdokXmlh2?**;z8|oYF>a=tq(x?gUrHTS#It1*=`z*+l{B0Tuqt zyfkq^r#_r|e@V;jwVrL36Vt(uaY2p?OttgGT&9$2Besq8dZPhqSH6#8!9$l{1Hvmn zeGrHm=_GJ-lXT#2#*j*#mqHl?GdYGv?2x~yPt<_u6tTBneiO7!pjEfW_!|AY-K-06 zxg&70W3#E9!zlEjxMT;a>z=v3g}p7nPW7U=iFou3>X(in7mhj!43$=t#I^nAsyV>P z@0{Lbye9wVl%;3Rp`ncUFlxotFafl-uA07X3~Q$SC9-7{uC z0j<+hvu$3YZukErX|Boh?ER`;l-@4@C)hhlL$a&*1q3tZA>X$M$lpOe?5(Zz_s{;% zx&k+FH~%V2GOZ~xf0cLs{-A#|D{yB(d1sDt7XW)_KKL!2?Yr=nxAggL`^nS%V$*!u zZGVjK@P&5)u;1ag`~I!IqZRn>u89B8fc@^4@fLCM|Lq66#qaTLo_?Eaa2Ei3o1gUC zEYA0@owNdfyXW9809G=Sx7^Ef_c3?vXZ~CK7I%LgzuhOeOMv}$ANu=O-^CTU%U9|D Y1C$K~A4N@hhyVZp07*qoM6N<$f(MZ1wg3PC From 31dac3532687767df3c76499529305c17aaa3b83 Mon Sep 17 00:00:00 2001 From: Shivam Sandbhor Date: Tue, 21 Mar 2023 11:39:55 +0530 Subject: [PATCH 2/2] Add app inspect workflow (#12) * Add app inspect workflow Signed-off-by: Shivam Sandbhor --- .github/workflows/appinspect.yaml | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/appinspect.yaml diff --git a/.github/workflows/appinspect.yaml b/.github/workflows/appinspect.yaml new file mode 100644 index 0000000..e88a79d --- /dev/null +++ b/.github/workflows/appinspect.yaml @@ -0,0 +1,57 @@ +name: App inspect tests +on: + push: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9.16' + + - name: Install Splunk Packaging Toolkit + run: | + curl https://download.splunk.com/misc/packaging-toolkit/splunk-packaging-toolkit-1.0.1.tar.gz -o /tmp/spl.tar.gz + pip install /tmp/spl.tar.gz + + - name: Create Splunk App Package + run: | + rm -rf .git .github .gitignore + slim package . + cp crowdsec-splunk-app-*.tar.gz /tmp/crowdsec-splunk-app.tar.gz + + - name: Retrieve App Inspect Report + run: | + TOKEN=$(curl -u '${{ secrets.SPLUNKBASE_USERNAME }}:${{ secrets.SPLUNKBASE_PASSWORD }}' --url 'https://api.splunk.com/2.0/rest/login/splunk' | jq -r .data.token) + echo "::add-mask::$TOKEN" + REPORT_HREF=$(curl -X POST \ + -H "Authorization: bearer $TOKEN" \ + -H "Cache-Control: no-cache" \ + -F "app_package=@/tmp/crowdsec-splunk-app.tar.gz" \ + --url "https://appinspect.splunk.com/v1/app/validate"| jq -r .links[1].href) + REPORT_URL="https://appinspect.splunk.com$REPORT_HREF" + sleep 10 + curl -X GET \ + -H "Authorization: bearer $TOKEN" \ + --url $REPORT_URL > /tmp/report + + - name: Upload App Inspect Report + uses: actions/upload-artifact@v2 + with: + name: report + path: /tmp/report + + - name: Check App Inspect Report Results + run: | + if grep -q '"result": "failure"' /tmp/report; then + echo "::error::App inspect check failed" + exit 1 + else + exit 0 + fi