illumio-pylo 0.3.11__tar.gz → 0.3.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.github/workflows/make-binaries.yml +40 -2
- {illumio_pylo-0.3.11/illumio_pylo.egg-info → illumio_pylo-0.3.13}/PKG-INFO +2 -1
- illumio_pylo-0.3.13/docs/ENV_CREDENTIALS.md +205 -0
- illumio_pylo-0.3.13/examples/example_env_credentials.py +77 -0
- illumio_pylo-0.3.13/examples/explorer_query.py +101 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/examples/extend_cli.py +1 -0
- illumio_pylo-0.3.13/examples/filter_query_example.py +122 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/APIConnector.py +138 -106
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/CredentialsManager.py +168 -3
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/Explorer.py +619 -14
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/JsonPayloadTypes.py +64 -4
- illumio_pylo-0.3.13/illumio_pylo/FilterQuery.py +892 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Helpers/exports.py +1 -1
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/LabelCommon.py +13 -3
- illumio_pylo-0.3.13/illumio_pylo/LabelDimension.py +109 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/LabelStore.py +97 -38
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/WorkloadStore.py +58 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/__init__.py +9 -3
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/__init__.py +5 -2
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/__init__.py +1 -0
- illumio_pylo-0.3.13/illumio_pylo/cli/commands/credential_manager.py +767 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/label_delete_unused.py +0 -3
- illumio_pylo-0.3.13/illumio_pylo/cli/commands/traffic_export.py +358 -0
- illumio_pylo-0.3.13/illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +638 -0
- illumio_pylo-0.3.13/illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +217 -0
- illumio_pylo-0.3.13/illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +581 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/update_pce_objects_cache.py +1 -2
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/ven_duplicate_remover.py +79 -59
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/workload_export.py +29 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/cli.py +4 -1
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/health_monitoring.py +5 -1
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13/illumio_pylo.egg-info}/PKG-INFO +2 -1
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo.egg-info/SOURCES.txt +13 -2
- illumio_pylo-0.3.11/requirements.txt → illumio_pylo-0.3.13/illumio_pylo.egg-info/requires.txt +2 -1
- illumio_pylo-0.3.11/illumio_pylo.egg-info/requires.txt → illumio_pylo-0.3.13/requirements.txt +1 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/setup.py +1 -1
- illumio_pylo-0.3.13/tests/test_env_credentials.py +198 -0
- illumio_pylo-0.3.13/tests/test_filter_query.py +344 -0
- illumio_pylo-0.3.13/tests/test_nested_queries.py +34 -0
- illumio_pylo-0.3.11/examples/explorer_query.py +0 -84
- illumio_pylo-0.3.11/illumio_pylo/Query.py +0 -331
- illumio_pylo-0.3.11/illumio_pylo/cli/commands/credential_manager.py +0 -216
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.devcontainer/Dockerfile +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.devcontainer/devcontainer.json +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.gitattributes +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.github/workflows/doxygen-publish.yml +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.github/workflows/python-publish.yml +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/.gitignore +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/LICENSE +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/README.md +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/check_unique_hostnames.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/check_unique_services.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/delete_all_workloads.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/delete_unused_services.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/explorer_report_exporter.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/export_rules_to_firewall.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/generate-random-workloads.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/healthcheck_log.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/import-labels.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/import_workloads_placeholders.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/iplists_stats_duplicates_unused_finder.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/recalculate_explorer_logs.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/recalculate_explorer_logs_multithreaded.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/rules_exporter.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/rules_exporter_special.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/test.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/test_change_workload_desc.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/test_query.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/test_query2.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/test_securityprincipals.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/ven_idle_to_illumination.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/dev_playground/ven_reassign_pce.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/AuditLog.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/ClusterHealth.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/RuleSearchQuery.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/API/__init__.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/AgentStore.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Exception.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Helpers/__init__.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Helpers/functions.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/IPList.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/IPMap.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Label.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/LabelGroup.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/LabeledObject.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Organization.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/ReferenceTracker.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Rule.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Ruleset.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/RulesetStore.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/SecurityPrincipal.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Service.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/SoftwareVersion.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/VirtualService.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/VirtualServiceStore.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/Workload.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/WorkloadStoreSubClasses.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/NativeParsers.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/__main__.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/iplist_analyzer.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/iplist_import_from_file.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/ruleset_export.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/utils/LabelCreation.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/utils/__init__.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/utils/misc.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/ven_compatibility_report_export.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/ven_idle_to_visibility.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/ven_upgrader.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/workload_import.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/workload_reset_names_to_null.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/workload_update.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/cli/commands/workload_used_in_rule_finder.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/docs/Doxygen +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/tmp.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/__init__.py +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/credentials.example.json +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/resources/iplists-import-example.csv +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/resources/iplists-import-example.xlsx +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/resources/workload-exporter-filter-example.csv +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/resources/workloads-import-example.csv +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo/utilities/resources/workloads-import-example.xlsx +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo.egg-info/dependency_links.txt +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/illumio_pylo.egg-info/top_level.txt +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/pyproject.toml +0 -0
- {illumio_pylo-0.3.11 → illumio_pylo-0.3.13}/setup.cfg +0 -0
|
@@ -20,6 +20,7 @@ jobs:
|
|
|
20
20
|
- name: Change Pylo Version if this is a DEV build
|
|
21
21
|
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
|
22
22
|
run: |
|
|
23
|
+
pwd
|
|
23
24
|
echo "Update version to append '-dev-' and current date ISO format:"
|
|
24
25
|
# for double quoted strings version
|
|
25
26
|
sed -i -E "s/__version__ = \"([0-9]+\.[0-9]+\.[0-9]+)\"/__version__ = '\\1-dev-$(date +'%Y%m%d')'/" illumio_pylo/__init__.py
|
|
@@ -28,12 +29,49 @@ jobs:
|
|
|
28
29
|
grep __version__ illumio_pylo/__init__.py
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
- name: copy scripts and pkg to build folder
|
|
33
|
+
run: |
|
|
34
|
+
# build the package
|
|
35
|
+
pip install build
|
|
36
|
+
python -m build
|
|
37
|
+
echo "current working directory:"
|
|
38
|
+
pwd
|
|
39
|
+
# get package whl filename for later use, it is in dist/
|
|
40
|
+
package_filename=$(find dist/ -name "illumio_pylo-*.whl" | sed 's|dist/||')
|
|
41
|
+
echo "*** Package filename is: $package_filename"
|
|
42
|
+
mkdir cli-build
|
|
43
|
+
# move package to cli-build
|
|
44
|
+
mv "dist/$package_filename" cli-build/
|
|
45
|
+
|
|
46
|
+
# create requirements.txt that points to the local package
|
|
47
|
+
echo "illumio_pylo@file:./cli-build/$package_filename" > cli-build/requirements.txt
|
|
48
|
+
#echo "pyinstaller==6.16.0" >> cli-build/requirements.txt
|
|
49
|
+
echo "*** Created cli-build/requirements.txt with contents:"
|
|
50
|
+
cat cli-build/requirements.txt
|
|
51
|
+
|
|
52
|
+
# copy cli.py removing any sys.path.insert lines
|
|
53
|
+
grep -v 'sys.path.insert' illumio_pylo/utilities/cli.py > cli-build/cli.py
|
|
54
|
+
mv illumio_pylo/utilities/health_monitoring.py cli-build/
|
|
55
|
+
|
|
56
|
+
# delete illumio_pylo to avoid confusion
|
|
57
|
+
rm -rf illumio_pylo/
|
|
58
|
+
|
|
59
|
+
echo "*** Contents of cli-build/:"
|
|
60
|
+
find cli-build/
|
|
61
|
+
|
|
31
62
|
- name: Make executables
|
|
32
63
|
uses: cpainchaud/pyinstaller-action-windows@main
|
|
33
64
|
with:
|
|
34
65
|
path: ./
|
|
35
|
-
spec:
|
|
36
|
-
|
|
66
|
+
spec: ./cli-build/
|
|
67
|
+
requirements: ./cli-build/requirements.txt
|
|
68
|
+
collect_data: illumio_pylo
|
|
69
|
+
extra_python_paths: Z:\\github\\workspace\\;C:\\Windows\\System32\\downlevel
|
|
70
|
+
|
|
71
|
+
- name: show spec files
|
|
72
|
+
run: |
|
|
73
|
+
echo "Showing the spec files created by PyInstaller:"
|
|
74
|
+
find ../ -name "*.spec" -exec cat {} \;
|
|
37
75
|
|
|
38
76
|
- name: rename executables
|
|
39
77
|
run: |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: illumio_pylo
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.13
|
|
4
4
|
Summary: A set of tools and library for working with Illumio PCE
|
|
5
5
|
Home-page: https://github.com/cpainchaud/pylo
|
|
6
6
|
Author: Christophe Painchaud
|
|
@@ -193,6 +193,7 @@ Requires-Dist: paramiko~=3.4.0
|
|
|
193
193
|
Requires-Dist: prettytable~=3.10.0
|
|
194
194
|
Requires-Dist: requests~=2.32.0
|
|
195
195
|
Requires-Dist: xlsxwriter~=3.2.0
|
|
196
|
+
Requires-Dist: flask~=2.2.0
|
|
196
197
|
Dynamic: author
|
|
197
198
|
Dynamic: author-email
|
|
198
199
|
Dynamic: description
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Environment Variable Credentials
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `CredentialProfile.from_environment_variables()` feature allows you to provide Illumio PCE credentials via environment variables instead of storing them in credential files on disk. This is particularly useful for:
|
|
6
|
+
|
|
7
|
+
- CI/CD pipelines
|
|
8
|
+
- Docker containers
|
|
9
|
+
- Security-conscious environments where credentials shouldn't be written to disk
|
|
10
|
+
- Temporary credential usage
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
To use environment variable credentials, simply specify the profile name `'ENV'` (case-insensitive) when loading credentials:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import illumio_pylo as pylo
|
|
18
|
+
|
|
19
|
+
# Load credentials from environment variables
|
|
20
|
+
credentials = pylo.get_credentials_from_file('ENV')
|
|
21
|
+
|
|
22
|
+
# Or use with APIConnector
|
|
23
|
+
connector = pylo.APIConnector.create_from_credentials_in_file('ENV')
|
|
24
|
+
|
|
25
|
+
# Or use with Organization
|
|
26
|
+
org = pylo.Organization.get_from_api_using_credential_file('ENV')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Required Environment Variables
|
|
30
|
+
|
|
31
|
+
- **`PYLO_FQDN`**: Fully qualified domain name of the PCE (e.g., `pce.example.com`)
|
|
32
|
+
- **`PYLO_API_USER`**: API username
|
|
33
|
+
- **`PYLO_API_KEY`**: API key (can be encrypted with `$encrypted$:` prefix)
|
|
34
|
+
|
|
35
|
+
## Optional Environment Variables
|
|
36
|
+
|
|
37
|
+
- **`PYLO_PORT`**: Port number
|
|
38
|
+
- Default: `8443` for standard PCE
|
|
39
|
+
- Default: `443` for illum.io hosted domains (SaaS)
|
|
40
|
+
- Valid range: 1-65535
|
|
41
|
+
|
|
42
|
+
- **`PYLO_ORG_ID`**: Organization ID
|
|
43
|
+
- Default: `1` for standard PCE
|
|
44
|
+
- **Required** for illum.io hosted domains (no default)
|
|
45
|
+
- Must be a positive integer
|
|
46
|
+
|
|
47
|
+
- **`PYLO_VERIFY_SSL`**: Verify SSL certificate
|
|
48
|
+
- Default: `true`
|
|
49
|
+
- Accepts: `true`, `false`, `1`, `0`, `yes`, `no`, `y`, `n` (case-insensitive)
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
### Basic Example (Standard PCE)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Set environment variables
|
|
57
|
+
export PYLO_FQDN="pce.example.com"
|
|
58
|
+
export PYLO_API_USER="api_12345"
|
|
59
|
+
export PYLO_API_KEY="your_api_key_here"
|
|
60
|
+
|
|
61
|
+
# Run your Python script
|
|
62
|
+
python your_script.py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import illumio_pylo as pylo
|
|
67
|
+
|
|
68
|
+
# Load from environment
|
|
69
|
+
org = pylo.Organization.get_from_api_using_credential_file('ENV')
|
|
70
|
+
print(f"Connected to {org.connector.fqdn}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Illumio SaaS Example
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# For illum.io domains, ORG_ID is required
|
|
77
|
+
export PYLO_FQDN="mycompany.illum.io"
|
|
78
|
+
export PYLO_API_USER="api_12345"
|
|
79
|
+
export PYLO_API_KEY="your_api_key_here"
|
|
80
|
+
export PYLO_ORG_ID="5"
|
|
81
|
+
|
|
82
|
+
# Port defaults to 443 for illum.io domains
|
|
83
|
+
python your_script.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Custom Configuration
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Custom port and disable SSL verification
|
|
90
|
+
export PYLO_FQDN="pce.internal.local"
|
|
91
|
+
export PYLO_API_USER="api_12345"
|
|
92
|
+
export PYLO_API_KEY="your_api_key_here"
|
|
93
|
+
export PYLO_PORT="9443"
|
|
94
|
+
export PYLO_ORG_ID="2"
|
|
95
|
+
export PYLO_VERIFY_SSL="false"
|
|
96
|
+
|
|
97
|
+
python your_script.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Docker Container Example
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
docker run -e PYLO_FQDN="pce.example.com" \
|
|
104
|
+
-e PYLO_API_USER="api_12345" \
|
|
105
|
+
-e PYLO_API_KEY="your_api_key" \
|
|
106
|
+
your-container:latest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Encrypted API Key
|
|
110
|
+
|
|
111
|
+
If you have an encrypted API key (generated with the `cred-manager` tool), you can use it directly:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
export PYLO_FQDN="pce.example.com"
|
|
115
|
+
export PYLO_API_USER="api_12345"
|
|
116
|
+
export PYLO_API_KEY='$encrypted$:ssh-ChaCha20Poly1305:...'
|
|
117
|
+
|
|
118
|
+
python your_script.py
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The API key will be automatically decrypted using your SSH agent.
|
|
122
|
+
|
|
123
|
+
## Checking Availability
|
|
124
|
+
|
|
125
|
+
You can check if required environment variables are set before attempting to load:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from illumio_pylo.API.CredentialsManager import is_env_credentials_available
|
|
129
|
+
|
|
130
|
+
if is_env_credentials_available():
|
|
131
|
+
print("Environment credentials are available")
|
|
132
|
+
credentials = pylo.get_credentials_from_file('ENV')
|
|
133
|
+
else:
|
|
134
|
+
print("Missing required environment variables")
|
|
135
|
+
# Fall back to file-based credentials
|
|
136
|
+
credentials = pylo.get_credentials_from_file('default')
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Validation and Error Handling
|
|
140
|
+
|
|
141
|
+
The implementation includes comprehensive validation:
|
|
142
|
+
|
|
143
|
+
- **Missing required variables**: Clear error message listing which variables are missing
|
|
144
|
+
- **Invalid port**: Must be a valid integer between 1-65535
|
|
145
|
+
- **Invalid org_id**: Must be a positive integer
|
|
146
|
+
- **Invalid verify_ssl**: Must be a boolean-like value
|
|
147
|
+
- **illum.io domains**: Automatically defaults port to 443 and requires ORG_ID
|
|
148
|
+
|
|
149
|
+
Example error messages:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Missing required environment variables for ENV profile: PYLO_FQDN, PYLO_API_KEY.
|
|
153
|
+
Required: PYLO_FQDN, PYLO_API_USER, PYLO_API_KEY.
|
|
154
|
+
Optional: PYLO_PORT, PYLO_ORG_ID, PYLO_VERIFY_SSL
|
|
155
|
+
|
|
156
|
+
Invalid PYLO_PORT value 'abc': must be a valid port number (1-65535)
|
|
157
|
+
|
|
158
|
+
PYLO_ORG_ID is required for illum.io domains (no default available)
|
|
159
|
+
|
|
160
|
+
Invalid PYLO_VERIFY_SSL value 'maybe': must be true/false/1/0/yes/no/y/n (case-insensitive)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Security Considerations
|
|
164
|
+
|
|
165
|
+
### Advantages
|
|
166
|
+
- Credentials never written to disk
|
|
167
|
+
- Easier to rotate in CI/CD environments
|
|
168
|
+
- No risk of accidentally committing credentials to version control
|
|
169
|
+
- Better integration with secrets management tools (AWS Secrets Manager, HashiCorp Vault, etc.)
|
|
170
|
+
|
|
171
|
+
### Best Practices
|
|
172
|
+
1. **Never log environment variables** - Be careful not to print or log the actual API key
|
|
173
|
+
2. **Use encrypted keys when possible** - Leverage SSH agent encryption for additional security
|
|
174
|
+
3. **Rotate credentials regularly** - Environment-based approach makes rotation easier
|
|
175
|
+
4. **Use secrets management** - In production, use proper secrets management tools to inject environment variables
|
|
176
|
+
5. **Limit scope** - Set environment variables only for the specific process that needs them
|
|
177
|
+
|
|
178
|
+
### Example with AWS Secrets Manager
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import boto3
|
|
182
|
+
import json
|
|
183
|
+
import os
|
|
184
|
+
import illumio_pylo as pylo
|
|
185
|
+
|
|
186
|
+
# Fetch credentials from AWS Secrets Manager
|
|
187
|
+
client = boto3.client('secretsmanager')
|
|
188
|
+
secret = client.get_secret_value(SecretId='pylo/pce/credentials')
|
|
189
|
+
creds = json.loads(secret['SecretString'])
|
|
190
|
+
|
|
191
|
+
# Set environment variables
|
|
192
|
+
os.environ['PYLO_FQDN'] = creds['fqdn']
|
|
193
|
+
os.environ['PYLO_API_USER'] = creds['api_user']
|
|
194
|
+
os.environ['PYLO_API_KEY'] = creds['api_key']
|
|
195
|
+
|
|
196
|
+
# Use ENV profile
|
|
197
|
+
org = pylo.Organization.get_from_api_using_credential_file('ENV')
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Notes
|
|
201
|
+
|
|
202
|
+
- The profile name is always `'ENV'` (case-insensitive: `'env'`, `'Env'`, `'ENV'` all work)
|
|
203
|
+
- The `originating_file` attribute is set to `'environment'` for env-based profiles
|
|
204
|
+
- Environment credentials are **never** included in `get_all_credentials()` - they must be explicitly requested
|
|
205
|
+
- Environment credentials don't require any credential files to exist on disk
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Practical example of using environment variable credentials with Illumio Pylo
|
|
4
|
+
This demonstrates how to connect to a PCE using only environment variables
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import illumio_pylo as pylo
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
# Check if environment credentials are available
|
|
11
|
+
from illumio_pylo.API.CredentialsManager import is_env_credentials_available
|
|
12
|
+
|
|
13
|
+
if not is_env_credentials_available():
|
|
14
|
+
print("ERROR: Required environment variables are not set!")
|
|
15
|
+
print("Please set: PYLO_FQDN, PYLO_API_USER, PYLO_API_KEY")
|
|
16
|
+
print("\nExample:")
|
|
17
|
+
print(" export PYLO_FQDN='pce.example.com'")
|
|
18
|
+
print(" export PYLO_API_USER='api_12345'")
|
|
19
|
+
print(" export PYLO_API_KEY='your_api_key_here'")
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
print("Environment credentials detected!")
|
|
23
|
+
print("-" * 60)
|
|
24
|
+
|
|
25
|
+
# Load credentials from environment
|
|
26
|
+
try:
|
|
27
|
+
credentials = pylo.get_credentials_from_file('ENV')
|
|
28
|
+
print(f"Credentials loaded:")
|
|
29
|
+
print(f" FQDN: {credentials.fqdn}")
|
|
30
|
+
print(f" Port: {credentials.port}")
|
|
31
|
+
print(f" Org ID: {credentials.org_id}")
|
|
32
|
+
print(f" API User: {credentials.api_user}")
|
|
33
|
+
print(f" Verify SSL: {credentials.verify_ssl}")
|
|
34
|
+
print(f" Source: {credentials.originating_file}")
|
|
35
|
+
print("-" * 60)
|
|
36
|
+
except pylo.PyloEx as e:
|
|
37
|
+
print(f"ERROR loading credentials: {e}")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
# Example 1: Create an APIConnector
|
|
41
|
+
print("\nExample 1: Creating APIConnector from ENV profile")
|
|
42
|
+
try:
|
|
43
|
+
connector = pylo.APIConnector.create_from_credentials_in_file('ENV')
|
|
44
|
+
print(f"✓ APIConnector created for {connector.fqdn}:{connector.port}")
|
|
45
|
+
|
|
46
|
+
# Test connection by getting software version
|
|
47
|
+
version = connector.get_software_version()
|
|
48
|
+
print(f"✓ Connected successfully! PCE Version: {version}")
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"✗ Connection failed: {e}")
|
|
52
|
+
print(" (This is expected if the PCE is not accessible)")
|
|
53
|
+
|
|
54
|
+
# Example 2: Load Organization data
|
|
55
|
+
print("\nExample 2: Loading Organization from ENV profile")
|
|
56
|
+
try:
|
|
57
|
+
org = pylo.Organization.get_from_api_using_credential_file(
|
|
58
|
+
'ENV',
|
|
59
|
+
list_of_objects_to_load=[
|
|
60
|
+
pylo.ObjectTypes.LABEL,
|
|
61
|
+
pylo.ObjectTypes.WORKLOAD
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
print(f"✓ Organization loaded successfully!")
|
|
65
|
+
print(f" Labels: {len(org.LabelStore.itemsByHref)}")
|
|
66
|
+
print(f" Workloads: {len(org.WorkloadStore.itemsByHref)}")
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"✗ Failed to load organization: {e}")
|
|
70
|
+
print(" (This is expected if the PCE is not accessible)")
|
|
71
|
+
|
|
72
|
+
print("\n" + "=" * 60)
|
|
73
|
+
print("Example complete!")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
if __name__ == '__main__':
|
|
77
|
+
exit(main())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In this example we will show how to query traffic logs from the PCE using the Explorer V2 APIs.
|
|
3
|
+
We will make a query for all traffic logs matching the following conditions:
|
|
4
|
+
- Source (consumer) has a label 'E-PRODUCTION' or 'E-PREPROD' or is part of IPList 'I-Prod-Networks'
|
|
5
|
+
- Destination (provider) can be any workload or IP
|
|
6
|
+
- Service is TCP port 80 or 443 or ICMP protocol
|
|
7
|
+
- Policy decision is 'allowed'
|
|
8
|
+
- Traffic was detected within the last 5 days
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) # only required if you run this script from the examples folder without installing pylo
|
|
15
|
+
import illumio_pylo as pylo
|
|
16
|
+
|
|
17
|
+
# PCE connection parameters
|
|
18
|
+
pce_hostname = 'pce1.company.com'
|
|
19
|
+
pce_port = 9443
|
|
20
|
+
pce_api_user = 'api_xxxxxxxxx'
|
|
21
|
+
pce_api_key = 'xxxxxxxxxxxxxxxxxxxx'
|
|
22
|
+
pce_org_id = 1
|
|
23
|
+
pce_verify_ssl = True
|
|
24
|
+
|
|
25
|
+
print("Loading organization from PCE '{}'... ".format(pce_hostname), end='', flush=True)
|
|
26
|
+
organization = pylo.get_organization(pce_hostname, pce_port, pce_api_user, pce_api_key, pce_org_id, pce_verify_ssl)
|
|
27
|
+
print("OK!")
|
|
28
|
+
|
|
29
|
+
# Create a new V2 Explorer query with a max of 1500 results
|
|
30
|
+
explorer_query = organization.connector.new_explorer_query_v2(max_results=1500)
|
|
31
|
+
|
|
32
|
+
# Define source filter criteria
|
|
33
|
+
source_labels_names = ['E-PRODUCTION', 'E-PREPROD']
|
|
34
|
+
source_ip_list_name = 'I-Prod-Networks'
|
|
35
|
+
|
|
36
|
+
# Create a source filter that combines labels and IPList (treated with OR logic)
|
|
37
|
+
source_filter = explorer_query.filters.new_source_filter()
|
|
38
|
+
|
|
39
|
+
# Lookup and add Label objects to the source filter
|
|
40
|
+
for label_name in source_labels_names:
|
|
41
|
+
label_search_result = organization.LabelStore.find_label_by_name(label_name, raise_exception_if_not_found=False, case_sensitive=False)
|
|
42
|
+
if len(label_search_result) == 0:
|
|
43
|
+
raise pylo.PyloEx("Label '{}' not found in PCE!".format(label_name))
|
|
44
|
+
elif len(label_search_result) > 1:
|
|
45
|
+
raise pylo.PyloEx("Multiple labels found for name '{}', please use a more specific name!".format(label_name))
|
|
46
|
+
source_filter.add_label(label_search_result[0])
|
|
47
|
+
|
|
48
|
+
# Lookup and add IPList object to the source filter
|
|
49
|
+
source_iplist = organization.IPListStore.find_by_name(source_ip_list_name)
|
|
50
|
+
if source_iplist is None:
|
|
51
|
+
raise pylo.PyloEx("IPList '{}' not found in PCE!".format(source_ip_list_name))
|
|
52
|
+
source_filter.add_iplist(source_iplist)
|
|
53
|
+
|
|
54
|
+
# Filter by services (ICMP, HTTP, HTTPS)
|
|
55
|
+
explorer_query.filters.service_include_add_protocol(1) # ICMP
|
|
56
|
+
explorer_query.filters.service_include_add('tcp/80') # HTTP
|
|
57
|
+
explorer_query.filters.service_include_add('tcp/443') # HTTPS
|
|
58
|
+
|
|
59
|
+
# Filter by policy decision (only allowed traffic)
|
|
60
|
+
explorer_query.filters.filter_on_policy_decision_allowed()
|
|
61
|
+
|
|
62
|
+
# Filter by time range (last 5 days)
|
|
63
|
+
explorer_query.filters.set_time_from_x_days_ago(5)
|
|
64
|
+
|
|
65
|
+
# Execute the query and retrieve traffic logs
|
|
66
|
+
print("Querying PCE for traffic logs matching the filter... ", end='', flush=True)
|
|
67
|
+
traffic_logs = explorer_query.execute()
|
|
68
|
+
records = traffic_logs.get_all_records()
|
|
69
|
+
print("OK! Found {} traffic log(s)".format(len(records)))
|
|
70
|
+
|
|
71
|
+
# Print the results
|
|
72
|
+
for record in records:
|
|
73
|
+
# Format source information
|
|
74
|
+
if record.source_is_workload():
|
|
75
|
+
workload = record.get_source_workload(organization)
|
|
76
|
+
# Get labels as a formatted string
|
|
77
|
+
label_values = [record.source_workload_labels_by_type.get(lt) for lt in organization.LabelStore.label_types]
|
|
78
|
+
label_values = [lv for lv in label_values if lv is not None]
|
|
79
|
+
labels_str = '|'.join(label_values) if label_values else 'unlabeled'
|
|
80
|
+
consumer_text = "Workload '{}' ({})".format(workload.name if workload else record.source_workload_hostname, labels_str)
|
|
81
|
+
else:
|
|
82
|
+
consumer_text = "IP '{}'".format(record.source_ip)
|
|
83
|
+
|
|
84
|
+
# Format destination information
|
|
85
|
+
if record.destination_is_workload():
|
|
86
|
+
workload = record.get_destination_workload(organization)
|
|
87
|
+
# Get labels as a formatted string
|
|
88
|
+
label_values = [record.destination_workload_labels_by_type.get(lt) for lt in organization.LabelStore.label_types]
|
|
89
|
+
label_values = [lv for lv in label_values if lv is not None]
|
|
90
|
+
labels_str = '|'.join(label_values) if label_values else 'unlabeled'
|
|
91
|
+
provider_text = "Workload '{}' ({})".format(workload.name if workload else record.destination_workload_hostname, labels_str)
|
|
92
|
+
else:
|
|
93
|
+
provider_text = "IP '{}'".format(record.destination_ip)
|
|
94
|
+
|
|
95
|
+
# Format service information
|
|
96
|
+
service_text = record.service_to_str()
|
|
97
|
+
|
|
98
|
+
# Print the traffic log entry
|
|
99
|
+
print("Traffic log: {} -> {} via {} (policy decision: {})".format(
|
|
100
|
+
consumer_text, provider_text, service_text, record.policy_decision_string))
|
|
101
|
+
|
|
@@ -23,6 +23,7 @@ class MyBuiltInParser: # optional, if you want to use built-in parsers
|
|
|
23
23
|
label_type='env', # optional, it will ensure that selected labels are of a specified type
|
|
24
24
|
is_required=False, allow_multiple=True)
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
def fill_parser(parser: argparse.ArgumentParser):
|
|
27
28
|
""" This function will be called by the CLI to fill the parser with the arguments of your command """
|
|
28
29
|
parser.add_argument('--sort-by-name', '-s', action='store_true',
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Using the Filter Query feature to search workloads
|
|
3
|
+
|
|
4
|
+
This example demonstrates how to use the find_workloads_matching_query() method
|
|
5
|
+
to filter workloads using a SQL-like query syntax.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
|
12
|
+
|
|
13
|
+
import illumio_pylo as pylo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
# Load organization from PCE (using credentials file or cached data)
|
|
18
|
+
# Replace 'your-pce-hostname' with your actual PCE hostname
|
|
19
|
+
pce_hostname = 'your-pce-hostname'
|
|
20
|
+
|
|
21
|
+
print(f"Loading PCE configuration from {pce_hostname}...")
|
|
22
|
+
org = pylo.Organization(1)
|
|
23
|
+
|
|
24
|
+
# Option 1: Load from API using saved credentials
|
|
25
|
+
# org.load_from_api_using_credential_file(pce_hostname)
|
|
26
|
+
|
|
27
|
+
# Option 2: Load from cache (if you've previously cached the data)
|
|
28
|
+
try:
|
|
29
|
+
org.load_from_cache_or_saved_credentials(pce_hostname)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"Failed to load PCE data: {e}")
|
|
32
|
+
print("\nTo use this example, you need to either:")
|
|
33
|
+
print(" 1. Configure credentials in ~/.pylo/credentials.json")
|
|
34
|
+
print(" 2. Have cached PCE data available")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
print(f"Loaded {len(org.WorkloadStore.workloads)} workloads from PCE\n")
|
|
38
|
+
|
|
39
|
+
# Example queries
|
|
40
|
+
example_queries = [
|
|
41
|
+
# Find workloads by name
|
|
42
|
+
"name == 'web-server-01'",
|
|
43
|
+
|
|
44
|
+
# Find workloads by partial name match
|
|
45
|
+
"name contains 'web'",
|
|
46
|
+
|
|
47
|
+
# Find workloads by name using regex
|
|
48
|
+
"name matches 'web-.*-[0-9]+'",
|
|
49
|
+
|
|
50
|
+
# Find workloads by IP address
|
|
51
|
+
"ip_address == '192.168.1.100'",
|
|
52
|
+
|
|
53
|
+
# Find online workloads
|
|
54
|
+
"online == true",
|
|
55
|
+
|
|
56
|
+
# Find workloads by label
|
|
57
|
+
"env == 'Production'",
|
|
58
|
+
"label.app == 'WebApp' and label.env == 'Production'",
|
|
59
|
+
|
|
60
|
+
# Complex query with OR
|
|
61
|
+
"(name == 'server-01' or name == 'server-02') and online == true",
|
|
62
|
+
|
|
63
|
+
# Find workloads with old heartbeat
|
|
64
|
+
"last_heartbeat <= '2024-01-01'",
|
|
65
|
+
|
|
66
|
+
# Find workloads in a specific mode
|
|
67
|
+
"mode == 'enforced'",
|
|
68
|
+
|
|
69
|
+
# Find deleted workloads (need to pass include_deleted=True)
|
|
70
|
+
"deleted == true",
|
|
71
|
+
|
|
72
|
+
# Combine multiple conditions
|
|
73
|
+
"hostname contains 'prod' and env == 'Production' and not deleted == true",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
print("=" * 60)
|
|
77
|
+
print("Filter Query Examples")
|
|
78
|
+
print("=" * 60)
|
|
79
|
+
|
|
80
|
+
for query in example_queries:
|
|
81
|
+
print(f"\nQuery: {query}")
|
|
82
|
+
try:
|
|
83
|
+
results = org.WorkloadStore.find_workloads_matching_query(query)
|
|
84
|
+
print(f"Found {len(results)} workload(s)")
|
|
85
|
+
for wkl in results[:5]: # Show first 5 results
|
|
86
|
+
print(f" - {wkl.get_name()} ({wkl.href})")
|
|
87
|
+
if len(results) > 5:
|
|
88
|
+
print(f" ... and {len(results) - 5} more")
|
|
89
|
+
except pylo.PyloEx as e:
|
|
90
|
+
print(f"Error: {e}")
|
|
91
|
+
|
|
92
|
+
# Interactive query mode
|
|
93
|
+
print("\n" + "=" * 60)
|
|
94
|
+
print("Interactive Query Mode")
|
|
95
|
+
print("Enter a query to search workloads, or 'quit' to exit")
|
|
96
|
+
print("=" * 60)
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
query = input("\nQuery> ").strip()
|
|
101
|
+
if query.lower() in ('quit', 'exit', 'q'):
|
|
102
|
+
break
|
|
103
|
+
if not query:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
results = org.WorkloadStore.find_workloads_matching_query(query)
|
|
107
|
+
print(f"Found {len(results)} workload(s)")
|
|
108
|
+
for wkl in results[:10]:
|
|
109
|
+
labels = wkl.get_labels_str()
|
|
110
|
+
print(f" - {wkl.get_name()} | {labels} | online={wkl.online}")
|
|
111
|
+
if len(results) > 10:
|
|
112
|
+
print(f" ... and {len(results) - 10} more")
|
|
113
|
+
except pylo.PyloEx as e:
|
|
114
|
+
print(f"Query error: {e}")
|
|
115
|
+
except KeyboardInterrupt:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
print("\nGoodbye!")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == '__main__':
|
|
122
|
+
main()
|