From 04b847f39cccdd5937faf309fe8a81699d9b8919 Mon Sep 17 00:00:00 2001 From: livehybrid <5527349+livehybrid@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:30:28 +0000 Subject: [PATCH] feat: allow wildcards in .uccignore file (#1012) Currently it is not possible to put wildcards in the .uccignore file, this can be problematic if you do not know the exact filename that needs to be ignored, for example a developer may be developing a TA on a Mac which could create darwin related binaries in the lib/ folder. Other "ignore" files, such as .gitignore and .dockerignore allow for wildcards and therefore developers may expect that same with uccignore. .uccignore: ``` ... lib/**/*darwin*.so ``` Before: ``` ... WARNING: While ignoring the files mentioned in .uccignore '/output//lib/**/*darwin*.so was not found INFO: Removed ['', '/output//lib/**/*darwin*.so'] files ... ``` Note, in the above it does not expand the wildcards, the WARNING says that there was no match, however the INFO contradicts and suggests that it has been removed (which it has not). After: ``` ... INFO: Removed ['/output//lib/_cffi_backend.cpython-39-darwin.so', '/output//lib/charset_normalizer/md__mypyc.cpython-39-darwin.so', '/output//lib/charset_normalizer/md.cpython-39-darwin.so'] files ... ``` Note: The list of removed files is now representative of what was removed, rather than the wildcard, so the developer can see exactly what has been ignored/removed. This also fixes #1011 --------- Co-authored-by: sgoral --- docs/uccignore.md | 26 +- splunk_add_on_ucc_framework/commands/build.py | 39 +- tests/smoke/test_ucc_build.py | 88 + .../.uccignore | 4 + .../LICENSES/Apache-2.0.txt | 1 + .../globalConfig.json | 1416 +++++++++++++++++ .../package/README.txt | 1 + .../package/app.manifest | 54 + .../bin/splunk_ta_uccexample_custom_rh.py | 20 + .../splunk_ta_uccexample_rh_three_custom.py | 49 + .../package/default/eventtypes.conf | 4 + .../package/default/tags.conf | 4 + .../package/lib/requirements.txt | 1 + .../package/static/appIcon.png | Bin 0 -> 3348 bytes .../package/static/appIconAlt.png | Bin 0 -> 3348 bytes .../package/static/appIconAlt_2x.png | Bin 0 -> 6738 bytes .../package/static/appIcon_2x.png | Bin 0 -> 6738 bytes 17 files changed, 1688 insertions(+), 19 deletions(-) create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/.uccignore create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/LICENSES/Apache-2.0.txt create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/globalConfig.json create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/README.txt create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/app.manifest create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_custom_rh.py create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_rh_three_custom.py create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/eventtypes.conf create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/tags.conf create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/lib/requirements.txt create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon.png create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIconAlt.png create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIconAlt_2x.png create mode 100644 tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon_2x.png diff --git a/docs/uccignore.md b/docs/uccignore.md index 9e4efd566..07b51d781 100644 --- a/docs/uccignore.md +++ b/docs/uccignore.md @@ -5,4 +5,28 @@ add-on recursively overrides the output folder. It is expected to be placed in the same folder as `globalConfig` file to have effect. -You will see a warning message in case ignored file is not found in the output folder. +Uccignore supports wildcard expressions, thanks to which we can find all files matching a specific pattern. + +e.g. for given file structure + +``` +... +└── lib + └── 3rdparty + │ ├── linux + │ ├   └── pycache.pyc + │ ├── linux_with_deps + │ ├   └── pycache.pyc + │ └── windows + │ └── pycache.pyc + └── requests + │ └── pycache.pyc + └── urllib + └── pycache.pyc +``` + +we can remove all `.pyc` files by adding `lib/**/pycache.pyc` to the .uccignore file. +If we want to remove all `.pyc` files just from the `3rdparty` directory, we need to change pattern to `lib/3rdparty/**/pycache.pyc`. +If we want to remove only for one specific platform, we need to provide the exact path e.g. **`lib/3rdparty/windows/pycache.pyc`**. + +In case no file is found for the specified pattern, you will see an appropriate warning message. diff --git a/splunk_add_on_ucc_framework/commands/build.py b/splunk_add_on_ucc_framework/commands/build.py index 4ca985543..6f6a3db4d 100644 --- a/splunk_add_on_ucc_framework/commands/build.py +++ b/splunk_add_on_ucc_framework/commands/build.py @@ -14,6 +14,7 @@ # limitations under the License. # import configparser +import glob import json import logging import os @@ -195,30 +196,32 @@ def _get_ignore_list( os.path.join(output_directory, addon_name, utils.get_os_path(path)) ).strip() for path in ignore_list + if path.strip() ] return ignore_list -def _remove_listed_files(ignore_list: List[str]) -> None: +def _remove_listed_files(ignore_list: List[str]) -> List[str]: """ Return path of files/folders to removed in output folder. Args: - ignore_list (list): List of files/folder to removed in output directory. - + ignore_list (list): List of files/folder patterns to be removed in output directory. """ - for path in ignore_list: - if os.path.exists(path): - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path, ignore_errors=True) - else: - logger.warning( - "While ignoring the files mentioned in .uccignore {} was not found".format( - path - ) - ) + removed_list = [] + for pattern in ignore_list: + paths = glob.glob(pattern, recursive=True) + if not paths: + logger.warning(f"No files found for the specified pattern: {pattern}") + continue + for path in paths: + if os.path.exists(path): + if os.path.isfile(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + removed_list.append(path) + return removed_list def generate_data_ui( @@ -629,9 +632,9 @@ def generate( os.path.abspath(os.path.join(source, os.pardir, ".uccignore")), output_directory, ) - _remove_listed_files(ignore_list) - if ignore_list: - logger.info(f"Removed {ignore_list} files") + removed_list = _remove_listed_files(ignore_list) + if removed_list: + logger.info("Removed:\n{}".format("\n".join(removed_list))) utils.recursive_overwrite(source, os.path.join(output_directory, ta_name)) logger.info("Copied package directory") diff --git a/tests/smoke/test_ucc_build.py b/tests/smoke/test_ucc_build.py index 396017b75..15dc73276 100644 --- a/tests/smoke/test_ucc_build.py +++ b/tests/smoke/test_ucc_build.py @@ -429,3 +429,91 @@ def summarize_types(raw_expected_logs): # summary messages must be the same but might come in different order assert log_line.message in expected_logs.keys() assert log_line.levelname == expected_logs[log_line.message] + + +@pytest.mark.skipif(sys.version_info >= (3, 8), reason=PYTEST_SKIP_REASON) +def test_ucc_generate_with_everything_uccignore(caplog): + with tempfile.TemporaryDirectory() as temp_dir: + package_folder = path.join( + path.dirname(path.realpath(__file__)), + "..", + "testdata", + "test_addons", + "package_global_config_everything_uccignore", + "package", + ) + build.generate(source=package_folder, output_directory=temp_dir) + + expected_warning_msg = ( + f"No files found for the specified pattern: " + f"{temp_dir}/Splunk_TA_UCCExample/bin/wrong_pattern" + ) + + edm1 = "Removed:" + edm2 = f"\n{temp_dir}/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_rh_example_input_one.py" + edm3 = f"\n{temp_dir}/Splunk_TA_UCCExample/bin/example_input_one.py" + edm4 = f"\n{temp_dir}/Splunk_TA_UCCExample/bin/splunk_ta_uccexample_rh_example_input_two.py" + + assert expected_warning_msg in caplog.text + assert (edm1 + edm2 + edm3 + edm4) in caplog.text or ( + edm1 + edm3 + edm2 + edm4 + ) in caplog.text + + expected_folder = path.join( + path.dirname(__file__), + "..", + "testdata", + "expected_addons", + "expected_output_global_config_everything", + "Splunk_TA_UCCExample", + ) + actual_folder = path.join(temp_dir, "Splunk_TA_UCCExample") + _compare_app_conf(expected_folder, actual_folder) + files_to_be_equal = [ + ("README.txt",), + ("default", "alert_actions.conf"), + ("default", "eventtypes.conf"), + ("default", "inputs.conf"), + ("default", "restmap.conf"), + ("default", "tags.conf"), + ("default", "splunk_ta_uccexample_settings.conf"), + ("default", "web.conf"), + ("default", "server.conf"), + ("default", "data", "ui", "alerts", "test_alert.html"), + ("default", "data", "ui", "nav", "default.xml"), + ("default", "data", "ui", "views", "configuration.xml"), + ("default", "data", "ui", "views", "inputs.xml"), + ("default", "data", "ui", "views", "dashboard.xml"), + ("default", "data", "ui", "views", "splunk_ta_uccexample_redirect.xml"), + ("bin", "splunk_ta_uccexample", "modalert_test_alert_helper.py"), + ("bin", "example_input_two.py"), + ("bin", "example_input_three.py"), + ("bin", "example_input_four.py"), + ("bin", "import_declare_test.py"), + ("bin", "splunk_ta_uccexample_rh_account.py"), + ("bin", "splunk_ta_uccexample_rh_three_custom.py"), + ("bin", "splunk_ta_uccexample_rh_example_input_four.py"), + ("bin", "splunk_ta_uccexample_custom_rh.py"), + ("bin", "splunk_ta_uccexample_rh_oauth.py"), + ("bin", "splunk_ta_uccexample_rh_settings.py"), + ("bin", "test_alert.py"), + ("README", "alert_actions.conf.spec"), + ("README", "inputs.conf.spec"), + ("README", "splunk_ta_uccexample_account.conf.spec"), + ("README", "splunk_ta_uccexample_settings.conf.spec"), + ("metadata", "default.meta"), + ] + helpers.compare_file_content( + files_to_be_equal, + expected_folder, + actual_folder, + ) + files_to_exist = [ + ("static", "appIcon.png"), + ("static", "appIcon_2x.png"), + ("static", "appIconAlt.png"), + ("static", "appIconAlt_2x.png"), + ] + for f in files_to_exist: + expected_file_path = path.join(expected_folder, *f) + assert path.exists(expected_file_path) diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/.uccignore b/tests/testdata/test_addons/package_global_config_everything_uccignore/.uccignore new file mode 100644 index 000000000..9ab92ea97 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/.uccignore @@ -0,0 +1,4 @@ +**/**one.py + +bin/splunk_ta_uccexample_rh_example_input_two.py +bin/wrong_pattern diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/LICENSES/Apache-2.0.txt b/tests/testdata/test_addons/package_global_config_everything_uccignore/LICENSES/Apache-2.0.txt new file mode 100644 index 000000000..33195b0f8 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/LICENSES/Apache-2.0.txt @@ -0,0 +1 @@ +dummy apache license \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/globalConfig.json b/tests/testdata/test_addons/package_global_config_everything_uccignore/globalConfig.json new file mode 100644 index 000000000..646918be7 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/globalConfig.json @@ -0,0 +1,1416 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "account", + "warning": { + "create": { + "message": "Some warning for account text create", + "alwaysDisplay": true + }, + "edit": { + "message": "Some warning for account text edit" + }, + "clone": { + "message": "Some warning for account text clone" + } + }, + "table": { + "actions": [ + "edit", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Auth Type", + "field": "auth_type" + } + ] + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "string", + "errorMsg": "Length of ID should be between 1 and 50", + "minLength": 1, + "maxLength": 50 + }, + { + "type": "regex", + "errorMsg": "Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "field": "name", + "help": "Enter a unique name for this account.", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Environment", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "login.example.com", + "label": "Value1" + }, + { + "value": "test.example.com", + "label": "Value2" + }, + { + "value": "other", + "label": "Other" + } + ], + "display": true + }, + "help": "", + "field": "custom_endpoint", + "defaultValue": "login.example.com", + "required": true + }, + { + "type": "text", + "label": "Endpoint URL", + "help": "Enter the endpoint URL.", + "field": "endpoint", + "options": { + "display": false, + "requiredWhenVisible": true + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "account_checkbox", + "help": "This is an example checkbox for the account entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "account_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the account entity", + "required": true, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "account_multiple_select", + "help": "This is an example multipleSelect for account entity", + "required": true, + "options": { + "items": [ + { + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "oauth", + "field": "oauth", + "label": "Not used", + "options": { + "auth_type": [ + "basic", + "oauth" + ], + "basic": [ + { + "oauth_field": "username", + "label": "Username", + "help": "Enter the username for this account.", + "field": "username" + }, + { + "oauth_field": "password", + "label": "Password", + "encrypted": true, + "help": "Enter the password for this account.", + "field": "password" + }, + { + "oauth_field": "security_token", + "label": "Security Token", + "encrypted": true, + "help": "Enter the security token.", + "field": "token" + }, + { + "oauth_field": "some_text", + "label": "Disabled on edit for oauth", + "help": "Enter text for field disabled on edit", + "field": "basic_oauth_text", + "required": false, + "options": { + "disableonEdit": true + } + } + ], + "oauth": [ + { + "oauth_field": "client_id", + "label": "Client Id", + "field": "client_id", + "help": "Enter the Client Id for this account." + }, + { + "oauth_field": "client_secret", + "label": "Client Secret", + "field": "client_secret", + "encrypted": true, + "help": "Enter the Client Secret key for this account." + }, + { + "oauth_field": "redirect_url", + "label": "Redirect url", + "field": "redirect_url", + "help": "Copy and paste this URL into your app." + }, + { + "oauth_field": "endpoint_token", + "label": "Token endpoint", + "field": "endpoint_token", + "help": "Put here endpoint used for token acqusition ie. login.salesforce.com" + }, + { + "oauth_field": "endpoint_authorize", + "label": "Authorize endpoint", + "field": "endpoint_authorize", + "help": "Put here endpoint used for authorization ie. login.salesforce.com" + }, + { + "oauth_field": "oauth_some_text", + "label": "Disabled on edit for oauth", + "help": "Enter text for field disabled on edit", + "field": "oauth_oauth_text", + "required": false, + "options": { + "disableonEdit": true, + "enable": false + } + } + ], + "auth_code_endpoint": "/services/oauth2/authorize", + "access_token_endpoint": "/services/oauth2/token", + "oauth_timeout": 30, + "oauth_state_enabled": false + } + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "field": "config1_help_link", + "type": "helpLink", + "options": { + "text": "Add-on configuration documentation", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "field": "config2_help_link", + "type": "helpLink", + "options": { + "text": "SSL configuration documentation", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Account" + }, + { + "name": "proxy", + "warning": { + "config": { + "message": "Some warning for account text config" + } + }, + "entity": [ + { + "type": "checkbox", + "label": "Enable", + "field": "proxy_enabled" + }, + { + "type": "singleSelect", + "label": "Proxy Type", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "http", + "label": "http" + }, + { + "value": "socks4", + "label": "socks4" + }, + { + "value": "socks5", + "label": "socks5" + } + ] + }, + "defaultValue": "http", + "field": "proxy_type" + }, + { + "type": "text", + "label": "Host", + "validators": [ + { + "type": "string", + "errorMsg": "Max host length is 4096", + "minLength": 0, + "maxLength": 4096 + }, + { + "type": "regex", + "errorMsg": "Proxy Host should not have special characters", + "pattern": "^[a-zA-Z]\\w*$" + } + ], + "field": "proxy_url" + }, + { + "type": "text", + "label": "Port", + "validators": [ + { + "type": "number", + "range": [ + 1, + 65535 + ] + } + ], + "field": "proxy_port" + }, + { + "type": "text", + "label": "Username", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of username is 50", + "minLength": 0, + "maxLength": 50 + } + ], + "field": "proxy_username" + }, + { + "type": "text", + "label": "Password", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of password is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "encrypted": true, + "field": "proxy_password" + }, + { + "type": "checkbox", + "label": "Reverse DNS resolution", + "field": "proxy_rdns" + } + ], + "options": { + "saveValidator": "function(formData) { if(!formData.proxy_enabled || formData.proxy_enabled === '0') {return true; } if(!formData.proxy_url) { return 'Proxy Host can not be empty'; } if(!formData.proxy_port) { return 'Proxy Port can not be empty'; } return true; }" + }, + "title": "Proxy" + }, + { + "name": "logging", + "warning": { + "config": { + "message": "Some warning for account text config" + } + }, + "entity": [ + { + "type": "singleSelect", + "label": "Log level", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "DEBUG", + "label": "DEBUG" + }, + { + "value": "INFO", + "label": "INFO" + }, + { + "value": "WARNING", + "label": "WARNING" + }, + { + "value": "ERROR", + "label": "ERROR" + }, + { + "value": "CRITICAL", + "label": "CRITICAL" + } + ] + }, + "defaultValue": "INFO", + "field": "loglevel" + } + ], + "title": "Logging" + }, + { + "name": "custom_abc", + "title": "Customized tab", + "entity": [ + { + "field": "testString", + "label": "Test String", + "type": "text", + "validators": [ + { + "type": "string", + "maxLength": 10, + "minLength": 5 + } + ] + }, + { + "field": "testNumber", + "label": "Test Number", + "type": "text", + "validators": [ + { + "type": "number", + "range": [ + 1, + 10 + ] + } + ] + }, + { + "field": "testRegex", + "label": "Test Regex", + "type": "text", + "validators": [ + { + "type": "regex", + "pattern": "^\\w+$", + "errorMsg": "Characters of Name should match regex ^\\w+$ ." + } + ] + }, + { + "field": "testEmail", + "label": "Test Email", + "type": "text", + "validators": [ + { + "type": "email" + } + ] + }, + { + "field": "testIpv4", + "label": "Test Ipv4", + "type": "text", + "validators": [ + { + "type": "ipv4" + } + ] + }, + { + "field": "testDate", + "label": "Test Date", + "type": "text", + "validators": [ + { + "type": "date" + } + ] + }, + { + "field": "testUrl", + "label": "Test Url", + "type": "text", + "validators": [ + { + "type": "url" + } + ] + } + ] + } + ], + "title": "Configuration", + "description": "Set up your add-on", + "subDescription": { + "text": "Configuration page - Ingesting data from to Splunk Cloud? Have you tried the new Splunk Data Manager yet?\nData Manager makes AWS data ingestion simpler, more automated and centrally managed for you, while co-existing with AWS and/or Kinesis TAs.\nRead our [[blogPost]] to learn more about Data Manager and it's availability on your Splunk Cloud instance.", + "links": [ + { + "slug": "blogPost", + "link": "https://splk.it/31oy2b2", + "linkText": "blog post" + } + ] + } + }, + "inputs": { + "services": [ + { + "name": "example_input_one", + "warning": { + "create": { + "message": "Some warning for account text create" + }, + "edit": { + "message": "Some warning for account text edit" + }, + "clone": { + "message": "Some warning for account text clone" + }, + "delete": { + "message": "Some warning for account text delete" + } + }, + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_one_checkbox", + "help": "This is an example checkbox for the input one entity", + "defaultValue": true + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_one_radio", + "defaultValue": "yes", + "help": "This is an example radio button for the input one entity", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "field": "singleSelectTest", + "label": "Single Select Group Test", + "type": "singleSelect", + "options": { + "createSearchChoice": true, + "autoCompleteFields": [ + { + "label": "Group1", + "children": [ + { + "value": "one", + "label": "One" + }, + { + "value": "two", + "label": "Two" + } + ] + }, + { + "label": "Group2", + "children": [ + { + "value": "three", + "label": "Three" + }, + { + "value": "four", + "label": "Four" + } + ] + } + ] + } + }, + { + "field": "multipleSelectTest", + "label": "Multiple Select Test", + "type": "multipleSelect", + "defaultValue": "a|b", + "options": { + "delimiter": "|", + "items": [ + { + "value": "a", + "label": "A" + }, + { + "value": "b", + "label": "B" + } + ] + } + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "text", + "label": "Object", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object", + "help": "The name of the object to query for.", + "required": true + }, + { + "type": "text", + "label": "Object Fields", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "field": "object_fields", + "help": "Object fields from which to collect data. Delimit multiple fields using a comma.", + "required": true + }, + { + "type": "text", + "label": "Order By", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "LastModifiedDate", + "field": "order_by", + "help": "The datetime field by which to query results in ascending order for indexing.", + "required": true + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The datetime after which to query and index records, in this format: \"YYYY-MM-DDThh:mm:ss.000z\".\nDefaults to 90 days earlier from now.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "type": "text", + "label": "Limit", + "validators": [ + { + "type": "string", + "errorMsg": "Max length of text input is 8192", + "minLength": 0, + "maxLength": 8192 + } + ], + "defaultValue": "1000", + "field": "limit", + "help": "The maximum number of results returned by the query.", + "required": false + }, + { + "type": "textarea", + "label": "Example Textarea Field", + "field": "example_textarea_field", + "help": "Help message", + "options": { + "rowsMin": 3, + "rowsMax": 15 + }, + "required": true + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + } + ], + "title": "Example Input One" + }, + { + "name": "example_input_two", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds .", + "required": true + }, + { + "type": "singleSelect", + "label": "Index", + "validators": [ + { + "type": "string", + "errorMsg": "Length of index name should be between 1 and 80.", + "minLength": 1, + "maxLength": 80 + } + ], + "defaultValue": "default", + "options": { + "endpointUrl": "data/indexes", + "denyList": "^_.*$", + "createSearchChoice": true + }, + "field": "index", + "required": true + }, + { + "type": "singleSelect", + "label": "Example Account", + "options": { + "referenceName": "account" + }, + "help": "", + "field": "account", + "required": true + }, + { + "type": "multipleSelect", + "label": "Example Multiple Select", + "field": "input_two_multiple_select", + "help": "This is an example multipleSelect for input two entity", + "required": true, + "options": { + "items": [ + { + "value": "one", + "label": "Option One" + }, + { + "value": "two", + "label": "Option Two" + } + ] + } + }, + { + "type": "checkbox", + "label": "Example Checkbox", + "field": "input_two_checkbox", + "help": "This is an example checkbox for the input two entity" + }, + { + "type": "radio", + "label": "Example Radio", + "field": "input_two_radio", + "help": "This is an example radio button for the input two entity", + "required": true, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": true + } + }, + { + "type": "radio", + "label": "Use existing data input?", + "field": "use_existing_checkpoint", + "defaultValue": "yes", + "help": "Data input already exists. Select `No` if you want to reset the data collection.", + "required": false, + "options": { + "items": [ + { + "value": "yes", + "label": "Yes" + }, + { + "value": "no", + "label": "No" + } + ], + "display": false + } + }, + { + "type": "text", + "label": "Query Start Date", + "validators": [ + { + "type": "regex", + "errorMsg": "Invalid date and time format", + "pattern": "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}z)?$" + } + ], + "field": "start_date", + "help": "The date and time, in \"YYYY-MM-DDThh:mm:ss.000z\" format, after which to query and index records. \nThe default is 90 days before today.", + "tooltip": "Changing this parameter may result in gaps or duplication in data collection.", + "required": false + }, + { + "field": "example_help_link", + "label": "", + "type": "helpLink", + "options": { + "text": "Help Link", + "link": "https://docs.splunk.com/Documentation" + } + }, + { + "field": "apis", + "label": "APIs/Interval (in seconds)", + "type": "checkboxGroup", + "options": { + "groups": [ + { + "label": "EC2", + "options": { + "isExpandable": true + }, + "fields": [ + "ec2_volumes", + "ec2_instances", + "ec2_reserved_instances", + "ebs_snapshots", + "rds_instances", + "rds_reserved_instances", + "ec2_key_pairs", + "ec2_security_groups", + "ec2_images", + "ec2_addresses" + ] + }, + { + "label": "ELB", + "options": { + "isExpandable": true + }, + "fields": [ + "classic_load_balancers", + "application_load_balancers" + ] + }, + { + "label": "VPC", + "options": { + "isExpandable": true + }, + "fields": [ + "vpcs", + "vpc_network_acls", + "vpc_subnets" + ] + } + ], + "rows": [ + { + "field": "ec2_volumes", + "checkbox": { + "defaultValue": true + }, + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_instances", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_reserved_instances", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ebs_snapshots", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "rds_instances", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "rds_reserved_instances", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_key_pairs", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_security_groups", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_images", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "ec2_addresses", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "classic_load_balancers", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "application_load_balancers", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "vpcs", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "vpc_network_acls", + "input": { + "defaultValue": 3600, + "required": true + } + }, + { + "field": "vpc_subnets", + "input": { + "defaultValue": 3600, + "required": true + } + } + ] + } + } + ], + "title": "Example Input Two" + }, + { + "name": "example_input_three", + "restHandlerName": "splunk_ta_uccexample_rh_three_custom", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + } + ], + "title": "Example Input Three" + }, + { + "name": "example_input_four", + "restHandlerModule": "splunk_ta_uccexample_custom_rh", + "restHandlerClass": "CustomRestHandler", + "entity": [ + { + "type": "text", + "label": "Name", + "validators": [ + { + "type": "regex", + "errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.", + "pattern": "^[a-zA-Z]\\w*$" + }, + { + "type": "string", + "errorMsg": "Length of input name should be between 1 and 100", + "minLength": 1, + "maxLength": 100 + } + ], + "field": "name", + "help": "A unique name for the data input.", + "required": true + }, + { + "type": "text", + "label": "Interval", + "validators": [ + { + "type": "regex", + "errorMsg": "Interval must be an integer.", + "pattern": "^\\-[1-9]\\d*$|^\\d*$" + } + ], + "field": "interval", + "help": "Time interval of the data input, in seconds.", + "required": true + } + ], + "title": "Example Input Four" + } + ], + "title": "Inputs", + "description": "Manage your data inputs", + "subDescription": { + "text": "Input page - Ingesting data from to Splunk Cloud? Have you tried the new Splunk Data Manager yet?\nData Manager makes AWS data ingestion simpler, more automated and centrally managed for you, while co-existing with AWS and/or Kinesis TAs.\nRead our [[blogPost]] to learn more about Data Manager and it's availability on your Splunk Cloud instance.", + "links": [ + { + "slug": "blogPost", + "link": "https://splk.it/31oy2b2", + "linkText": "blog post" + } + ] + }, + "table": { + "actions": [ + "edit", + "enable", + "delete", + "clone" + ], + "header": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Account", + "field": "account" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled" + } + ], + "moreInfo": [ + { + "label": "Name", + "field": "name" + }, + { + "label": "Interval", + "field": "interval" + }, + { + "label": "Index", + "field": "index" + }, + { + "label": "Status", + "field": "disabled", + "mapping": { + "true": "Disabled", + "false": "Enabled" + } + }, + { + "label": "Example Account", + "field": "account" + }, + { + "label": "Object", + "field": "object" + }, + { + "label": "Object Fields", + "field": "object_fields" + }, + { + "label": "Order By", + "field": "order_by" + }, + { + "label": "Query Start Date", + "field": "start_date" + }, + { + "label": "Limit", + "field": "limit" + } + ] + } + }, + "dashboard": { + "panels": [ + { + "name": "addon_version" + }, + { + "name": "events_ingested_by_sourcetype" + }, + { + "name": "errors_in_the_addon" + } + ] + } + }, + "alerts": [ + { + "name": "test_alert", + "label": "Test Alert", + "description": "Description for test Alert Action", + "activeResponse": { + "task": [ + "Create", + "Update" + ], + "supportsAdhoc": true, + "subject": [ + "endpoint" + ], + "category": [ + "Information Conveyance", + "Information Portrayal" + ], + "technology": [ + { + "version": [ + "1.0.0" + ], + "product": "Test Incident Update", + "vendor": "Splunk" + } + ], + "drilldownUri": "search?q=search%20index%3D\"_internal\"&earliest=0&latest=", + "sourcetype": "test:incident" + }, + "entity": [ + { + "type": "text", + "label": "Name", + "field": "name", + "defaultValue": "xyz", + "required": true, + "help": "Please enter your name" + }, + { + "type": "checkbox", + "label": "All Incidents", + "field": "all_incidents", + "defaultValue": 0, + "required": false, + "help": "Tick if you want to update all incidents/problems" + }, + { + "type": "singleSelect", + "label": "Table List", + "field": "table_list", + "options": { + "items": [ + { + "value": "Incident", + "label": "incident" + }, + { + "value": "Problem", + "label": "problem" + } + ] + }, + "help": "Please select the table", + "required": false, + "defaultValue": "problem" + }, + { + "type": "radio", + "label": "Action:", + "field": "action", + "options": { + "items": [ + { + "value": "Update", + "label": "update" + }, + { + "value": "Delete", + "label": "delete" + } + ] + }, + "help": "Select the action you want to perform", + "required": true, + "defaultValue": "two" + }, + { + "type": "singleSelectSplunkSearch", + "label": "Select Account", + "field": "account", + "search": "| rest /servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account | dedup title", + "options": { + "items": [ + { + "label": "earliest", + "value": "-4@h" + }, + { + "label": "latest", + "value": "now" + } + ] + }, + "valueField": "title", + "labelField": "title", + "help": "Select the account from the dropdown", + "required": true + } + ] + } + ], + "meta": { + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "5.36.2Ref7d7543", + "displayName": "Splunk UCC test Add-on", + "schemaVersion": "0.0.3", + "_uccVersion": "5.36.2" + } +} diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/README.txt b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/README.txt new file mode 100644 index 000000000..530a9e548 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/README.txt @@ -0,0 +1 @@ +Just a readme \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/app.manifest b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/app.manifest new file mode 100644 index 000000000..a99395790 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/app.manifest @@ -0,0 +1,54 @@ +{ + "schemaVersion": "2.0.0", + "info": { + "title": "Splunk Add-on for UCC Example", + "id": { + "group": null, + "name": "Splunk_TA_UCCExample", + "version": "7.0.1" + }, + "author": [ + { + "name": "Splunk Inc.", + "email": null, + "company": null + } + ], + "releaseDate": null, + "description": "Splunk Add-on for UCC Example", + "classification": { + "intendedAudience": null, + "categories": [], + "developmentStatus": null + }, + "commonInformationModels": null, + "license": { + "name": null, + "text": "LICENSES/Apache-2.0.txt", + "uri": null + }, + "privacyPolicy": { + "name": null, + "text": null, + "uri": null + }, + "releaseNotes": { + "name": null, + "text": "./README.txt", + "uri": null + } + }, + "dependencies": null, + "tasks": null, + "inputGroups": null, + "incompatibleApps": null, + "platformRequirements": null, + "supportedDeployments": [ + "_standalone", + "_distributed" + ], + "targetWorkloads": [ + "_search_heads", + "_indexers" + ] +} diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_custom_rh.py b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_custom_rh.py new file mode 100644 index 000000000..77a709101 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_custom_rh.py @@ -0,0 +1,20 @@ +import import_declare_test + +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler + + +class CustomRestHandler(AdminExternalHandler): + def __init__(self, *args, **kwargs): + AdminExternalHandler.__init__(self, *args, **kwargs) + + def handleList(self, confInfo): + AdminExternalHandler.handleList(self, confInfo) + + def handleEdit(self, confInfo): + AdminExternalHandler.handleEdit(self, confInfo) + + def handleCreate(self, confInfo): + AdminExternalHandler.handleCreate(self, confInfo) + + def handleRemove(self, confInfo): + AdminExternalHandler.handleRemove(self, confInfo) diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_rh_three_custom.py b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_rh_three_custom.py new file mode 100644 index 000000000..396a11231 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/bin/splunk_ta_uccexample_rh_three_custom.py @@ -0,0 +1,49 @@ +import import_declare_test + +from splunktaucclib.rest_handler.endpoint import ( + field, + validator, + RestModel, + DataInputModel, +) +from splunktaucclib.rest_handler import admin_external, util +from splunktaucclib.rest_handler.admin_external import AdminExternalHandler +import logging + +util.remove_http_proxy_env_vars() + + +fields = [ + field.RestField( + 'interval', + required=True, + encrypted=False, + default=None, + validator=validator.Pattern( + regex=r"""^\-[1-9]\d*$|^\d*$""", + ) + ), + + field.RestField( + 'disabled', + required=False, + validator=None + ) + +] +model = RestModel(fields, name=None) + + + +endpoint = DataInputModel( + 'example_input_three', + model, +) + + +if __name__ == '__main__': + logging.getLogger().addHandler(logging.NullHandler()) + admin_external.handle( + endpoint, + handler=AdminExternalHandler, + ) diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/eventtypes.conf b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/eventtypes.conf new file mode 100644 index 000000000..6e6b4fcdd --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/eventtypes.conf @@ -0,0 +1,4 @@ + +# Just something +[UCC_NOT_GENERATED] +search = index=_internal sourcetype=splunkd \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/tags.conf b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/tags.conf new file mode 100644 index 000000000..4c63aaf25 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/default/tags.conf @@ -0,0 +1,4 @@ + +[eventtype=UCC_NOT_GENERATED] +notalert = enabled + diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/lib/requirements.txt b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/lib/requirements.txt new file mode 100644 index 000000000..4e63a1a57 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/lib/requirements.txt @@ -0,0 +1 @@ +splunktaucclib diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon.png b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..88f67e7257157937dd747b21af2c7af4d3432386 GIT binary patch literal 3348 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 DQ5yUX literal 0 HcmV?d00001 diff --git a/tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon_2x.png b/tests/testdata/test_addons/package_global_config_everything_uccignore/package/static/appIcon_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c638b3f159fc4047a35e86d577c49cb0234f6933 GIT binary patch literal 6738 zcma)Bby!sC+a6LH1W6HwMrs(kTV&{^Lt=mdhMHj*O36V$1OaJ8LMf4uP6?$`9h8t1 z7(i5F5NY_(-Tm!;*X|eRx=y|4zVGLLo;Ut@Pn@ygO==1@3IG5=4bjmuxwz~6JjqBe zuIAtBZeH97{7i0Y0IJ5=KVQ6%BXz9(002scpCfEWWevqD=L=qorQyv3j{ z2q&0WkT>!I4FD(wDO@~y!_ZJ*khhnQpF)r__b-IP#q-Z*5I67_1nsHJZDn8#)I^|Q zKxr{4F>!7c3Lp@ugmQ6JFwxTfgMRU*%2dWz~Dcj zeEj~%>mox?5EKali;080y+QxR^h2A#{xbQmOh2<=Bn)H%^F#QfoM9Jmo_`ZU!(IOe zou6NRS@<{FMZN!b-~Xw}MaoJSXafTUEf@+O0CUksA-sPT1_?#^!G4-l=Kj;=-&sG2 zztU8I`}m=uKF%MjUpe{Yx%`VV!N3w3QxS#2;_R!Uq}LR>};3|0rrXliQ7Yu=ER zm(|pklKNBmZ`8k(l|cVU_tzl%z3>05#Xo~e%4vY*4 z)MX{XQgRpR{Gt07^>0Cc{|5PIrT&nW|G#AaM*WAZ5en`G_knt8{T2FWNBsXv^}Dlw zk@#DfpJPqo0&>v}(4P}W1*`=6N88`<7n%y1C>RutK$#&BUMlK8*8`xjI|7aHb4MV7 znwmffaj+Cn&;aTT_xag){a5+^8|e={CD6}V`nOs8`;zdh^p}1iFxxT1SluW}i z002ffL`&T)hyZtsG9kCuUuC~#e{+03cK@^8i*I&6BEexqGQIvzRL>L0b#<|X8V?Gd zLKqsJK7VTQ{0bku+@jcEyySY?-6C^$v)>A?lTd)cpi5VsoujD37Mt2VU+f15ZrwQ! zbvrxU3g4Rd&oF;q-&S`L=sNaTUUDtRZ*1gb-)SMUvq|}CQgP23!_Hyoqe9-`xDSw6 z3A^d>$Xfovt?gES+9amwv6TS}6(hTu`=KL_G#@L~I`o7~;w8s;`Bk!eiSl%;buh$h z@7qr46N&f8iBFek4++xJ?Y(0BlC&Lb%PL&3){wsOqk6k09ZV0G7E<>R0FGFuU5u1& zfzvlG`b|vR2C!04WR!}79CJ(>PE6UVG_nlxxtD6IA9*Q!(IevBQKSKoVbJABUf`!^ zH9$fwVeUDD2|Jzb47(T%26ugga;n5k^gJ!5oX&N;^qyR+IqBj75r9m$){qLbQrt_7 zAt2$WlW4Iqrr|N{=0{9pDg1^_vzYOW6QbcE zn!QT(?4j$f_LM`+vI3qegm<-uYl7&mHUOEV1iPJBQb$rH^X21oB2C0OKIACAh`$tc z1=vuIp-I?iHDG?b=JJ8XD9?k50nlSX%(q&wAs;dIHsU^dBvm3-h*;|ZhUM9kSs2z? z-37Uik|+%A2x9fWA(5;Gclz`eW9UOh!NXQFet1$c{_>zcf{w~2nTVpUfG8(YpOp-f z7}s!KDRuQlo)WDY?^Y*ug{idh+@SHYss{0EwwlWWdFnYc#{Y*G1GOHI3Y^aS5i_b$V(42)B<7 zvk#UDEg6zh7&okF5HuRY%#tSDl);(h1@A!7CG(_+s4I2}YJx!GKGD|`t0ZDoDfV-I zYEvv)FMdN->;d=tfR-cdqH@|!OG#1D%Nm<9h`Ax|GJ@M2)|dBW=Cwdi8(5s*mdD7v zb`nV)9E9>T%#ESavZqp-nk47V+pN#ZB^af~N{kG(3~%-ln}e9VqDferhO#ruj%!&y zQftZYp^9h4$J0e#jFv9feb2^QhppCqUjFui9#~`?-oUN1-^_B;`KvFW&g3gqvsq_F zkNk+sNjCmB+86eM!fGMqI#g4u^7Xit|VLD->TE;}TNB11nh1N~qDtJpfPmJ%7yrtiQ2p;kVK_n~8@+1)P$u4KEqW%`~*?Bf~N za|Xvo-;V?l$H^$m`O?JacFk@;d$}$ZvC9!B3{qp$v>m++k+staRbF}@bR+1ayAl$5 zK$<(3LzY=cg|B4{Qaz8-?9;lf<72yHiyaSWzs0ec$+;D(q0K-X#IXp75-rCTOJK#?shjj*=qLKT&ZvNG%@(;vOD65x zyzKV~o258BFqd{=*)*uskB%X;el6pt2ouWMO(Ri=)d(3@}P zG*62-CuBuz+d2eoEzTd+ON*wvL(J&Qif%QdE$kJyYnAplRk;fcb;;tHwlC2UYNR|( zc`L+1#?y}>cxlF81-mV<=RBix^74IJ<8ahW2e&k(2ksK+r2S>^l0#ABcbmJ>zI}8! zKGhnqnD#p@BfleezzEz*xAS{r<->`-fcmU@r)eSU`cg|$CxN0?bX?ImID$2u7q3@C z%llA86dV$rqdQKbm13NbF_5J`%$nB>wB$D&iu=T4V%s>2(G(PIH&ph@yNt|uK zaa@0p=45Zp%69EFq4^*-g9eVU7o!|1$F;ScSv?Pl=gZ)T@XzbQ>^Yi}4fQ^3pQ5iC zzzld?2bGOm>C9y{`|?VHb8p%%o1|M4d|@}ryBz*mcm0ic*OYs;c<4ddl_)D2O3#Sn zggy|&?lX05MR7bk>Szg)<)pje$yVIvdo3-5_H>(CI_yEC+5pI z*<8?;$61hPcGeKzG%Q+~-)&ay$jY$ITJ(l$E8V}6cJP~QsP3y4sVRXZ)#E^XY|^|y zlFI2MRuy@m>pbCBVmoLLmy5Lv7g!;Mxi2-}n*0v8I^E(aF&}13=xd~rT?)a^Bm>bIK#Gaui*BaTasrwW*Mnz}-n)MuV+++0;Rn6v##d8c{~R zIs}-xRb#z5^Mk~?vDtL6u;#M${Ot#Qbo9++-(Zx*WmPw3UO0~I8+G#7;iR!9{<*V+ z@(9j5=00t{v1XPZFo+tn7@=ZP+{SaZ>6e=p4Wu?OAYB?`ZaH1lX6C}3r5UV!Xs4IM zZfH-fME1S@YQf?oWI>LlSE|6(+jK;FPud0|Y4-;_x0Hp?Dr)Ks$9qG8D?NAcM(2B$wbk|7 z9L>AXGTUYyOUaC8?k4mC4pZm3rH7{loy2{$Ixd(!EDzL!$YHE9v}e~NJ=4o8f}Aw? z>7A{ldS0)vjC=q&??55E3?Ik3>^SWCotF!3O83s!s!CTDZz2YjXE;*$Mu%M>;}Y<) zX6r{x^+Tmi^My=>BY8TKY zNrJgI^>F>x+x3Ez{h2odv`OuelB3+y=u;+mmBYw~NIVZZbWO>`>xxkNhzf1?lBvdbzGzM^icu3UV<*@NJ3CT zM}nl_VCPemgm?(#%LhsiKv-O7Vg1#mb z)bhk-j$j0G!_mV!E@l{4nVK|Q#@S9JYdA1i!+^E*iwfDLG!j-bYR3~De%w?|7yXbs z1yxPMtI>)Cm-Y0I8gzy1QAO5sv@+91D!r(e^?wdZ>ea?QI8{y8(+s7v68dr*k<$CX z8*4nn+f@HG*);TKjWeILTGhrZ@J)wl(ziCtD8*6;p!wL2V`I|V(p)6%!Ccc2&vAb8 zrAHR<6GtM;JAnD!&38?()B6uvhioTi#RRy*CuiotWg}7*vdwNqZs600(Bq2&@qF6-4D*cjYT$UkZOGI--U+ z)l31JLHv<7_QQO6l>N&P`;N2s0WIQOs4HvF)>O{x343x~cN+o+ctA_Q;7tPS`HAaS zEqqcC=AA+?WJ}CP?QcfWug{*59(rGG`XUxMQ*sTj-VY@{A;s}IlQF$FwH7|F*+RJV z;y<8vABkdXiu5?n9=sS*&J45Nr~x)mj&A-q>abkt4eS&iz3d$PkyA{=MM7MMgCqNO zfp00s<2GJEbX|BdiZ4*kDJnLb)M+EvPl=W_>WW!Q*FUiTL_@u#X%2IT>{lv#eZ#nKewBIH)xoHy^ zAF@sRpMZl`1yoJKLEU3huYT;vs^3IPNr_r7e;!L&hTTGMwdT*9KP)yZO#;&~YQor8a z0L;4^tnFD;8eA#z>{0FtPLc+NGr7n8eE4j!MW4 ze|G-kPaT=CQ*7J!M480FPTv)h?VB}p|{n%;x1EP zN725T(AbxokY{EVjovBP6qJ0>h8qh0U8>@xS)j>TvS~P}QeH~2Cz}?5LI*QIz7s_q z?fW8|J=L$~G);gSrrazkiseysHWO0h3ax2!VTo-$au?l{o)iC^{5|Pf$`h?ldk0}n z^)jIrrofg{aodL__^aO)URyFUBhwwf7LZJ8e|*c4URT3o!dw>5|JgTT zmtTY}LCUm9P^q*dq_DB{iJ(!ZCIcsAp@)@SPu#NJ*Yav&e(G+Gvq>bt#G!-Wl2)H+ z9lV-$m>HZTrz6VfIYPqPALqr^i8gTKjU?ya5wX>aI18}-F*Q$VLf;Ex03JdMUpX3E zW=JmIxhB+?d#@?bp8Oy$MqT>B;^V%tu10aO#91ck3}{M-!RtZp*3Zvt9sqXf^(EIJ zIg_M$edOYZ?avAF%hj~b8iHi@&19gwuU}}19UuDoGq+Nw#$AJ&E0krLCMt|d5SvD^O0eh2q8D&o$D z2ojPc@Q(HK;#c-%L-!bCYUNhfwsWZ5#H^k{N^)E8a4JT)G4p8B2paNcenq3M*h+M< z7CWjw4+?WY*@$I~5@w|YQkjg%1xO8nC9drwS69;e>|0#!tO{C`WTrR-^q~o!QuMrj zYZqZAt1Rn?C0j{7bBe;n=T*s!=Skw~mP2lZ%^t% zb&4$#{hzq3CT`I}WEc^_t>nqE*ZURQhvqZb!!HlId>64v>`u%moNC#Sy8q)hd*O#I z@AC6Ve1Tc59h;85T+g$gd~Ie{l;GO8iD>d-DGB>{-NxQFe<<6i=H$?<$<=LzUaqCA zc6SOdl$TiIZc9x%fuS;95myaYe+5-bl=lG5n3Dtx&)3Qro-PJXxCoDwqE<~s!d3P* z8m7Nz3%#ph>r!(bEL$grqYHG8SV$REZmG|k<1_3?nXb?muPa*U`UQFC_1hPjJO&9g z=`aUUZ5{L;P*rA$V6S9V)BpvY#5JNQlZ)SCu2h_W1%GX*%+A8=m7-gQWer~8jo^8W zuNIf^4$)g1O6%_Drq%4~6v^OgPsvzU@L5^H{)xWS;0ySuSU$DA7@Z^BS=Z&R}#v$!OKCp3jM;=2oO?Jn|^wq1&6S0k0b z8tzq)cd5Bq$Yu-7A{S&8@!qEEQy(WT;CL(v6P4V+`$fGaiW@3EA#G$gvV3ELHzL-L zS<<&(TGA<6I}iVOMVbBfaAUi8lTvgrmjiST&y0Oxk#CT%6m0x~_m=H5St=6NbFbtH z*YTx#e0!!uR^Ggm{WLr;Wg1Mmdp&`Kc-TR$!^8h#$AhfM^SamxTGvIWW7Kcp1lnG{ zB5K4sz-5*9Iz%B%uj!;wB{k#%H&BdQ^tBHZ)E-I=OtI)RF|5ZCtyBgNo06i)F1En( z6Bn?eot1BRFHDr7s5K_;T@jTa0LGG%b}!eTI>qEFJUlPF@?vwtC-JNgBMyfxu1q^} zPORyn&=U!qh9(i{Edil=?o-Y3*g=UmxY-YnGnSq%h8C{Btna&ec{e8d34(*qU-%SD zxGjgUDC^+3Qg?t-h)0|ru21d}UsekamsekHdBk4$&LPT*{T_MyKr>6SB=FU3zyfa= z$OMoWD7=?Z+C755vO2zF+lp)2%U-GbEO|prosje(T>1)Smzpqvn%Cm~+xskJCxKBV zr~UKw!?#Wfpl=%hG>b)VE9}{df;xa4pgEOyf~^``ty;U?0{kyK?W(do%u(WAnEmkcM-SwNp;ncKBj&#V DQ5yUX literal 0 HcmV?d00001