illumio-pylo 0.3.11__py3-none-any.whl → 0.3.13__py3-none-any.whl
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/API/APIConnector.py +138 -106
- illumio_pylo/API/CredentialsManager.py +168 -3
- illumio_pylo/API/Explorer.py +619 -14
- illumio_pylo/API/JsonPayloadTypes.py +64 -4
- illumio_pylo/FilterQuery.py +892 -0
- illumio_pylo/Helpers/exports.py +1 -1
- illumio_pylo/LabelCommon.py +13 -3
- illumio_pylo/LabelDimension.py +109 -0
- illumio_pylo/LabelStore.py +97 -38
- illumio_pylo/WorkloadStore.py +58 -0
- illumio_pylo/__init__.py +9 -3
- illumio_pylo/cli/__init__.py +5 -2
- illumio_pylo/cli/commands/__init__.py +1 -0
- illumio_pylo/cli/commands/credential_manager.py +555 -4
- illumio_pylo/cli/commands/label_delete_unused.py +0 -3
- illumio_pylo/cli/commands/traffic_export.py +358 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +638 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +217 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +581 -0
- illumio_pylo/cli/commands/update_pce_objects_cache.py +1 -2
- illumio_pylo/cli/commands/ven_duplicate_remover.py +79 -59
- illumio_pylo/cli/commands/workload_export.py +29 -0
- illumio_pylo/utilities/cli.py +4 -1
- illumio_pylo/utilities/health_monitoring.py +5 -1
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/METADATA +2 -1
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/RECORD +29 -24
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/WHEEL +1 -1
- illumio_pylo/Query.py +0 -331
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/licenses/LICENSE +0 -0
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/top_level.txt +0 -0
|
@@ -46,6 +46,10 @@ class CredentialProfile:
|
|
|
46
46
|
|
|
47
47
|
self.raw_json: Optional[CredentialFileEntry] = None
|
|
48
48
|
|
|
49
|
+
def is_api_key_encrypted(self) -> bool:
|
|
50
|
+
"""Check if the API key is encrypted (starts with $encrypted$:)."""
|
|
51
|
+
return self.api_key.startswith("$encrypted$:")
|
|
52
|
+
|
|
49
53
|
@staticmethod
|
|
50
54
|
def from_credentials_file_entry(credential_file_entry: CredentialFileEntry, originating_file: Optional[str] = None):
|
|
51
55
|
return CredentialProfile(credential_file_entry['name'],
|
|
@@ -57,11 +61,104 @@ class CredentialProfile:
|
|
|
57
61
|
credential_file_entry['verify_ssl'],
|
|
58
62
|
originating_file)
|
|
59
63
|
|
|
64
|
+
@staticmethod
|
|
65
|
+
def from_environment_variables() -> 'CredentialProfile':
|
|
66
|
+
"""
|
|
67
|
+
Create a CredentialProfile from environment variables.
|
|
68
|
+
This profile is only accessible when specifically requesting profile name 'ENV'.
|
|
69
|
+
|
|
70
|
+
Required environment variables:
|
|
71
|
+
- PYLO_FQDN: Fully qualified domain name of the PCE
|
|
72
|
+
- PYLO_API_USER: API username
|
|
73
|
+
- PYLO_API_KEY: API key (can be encrypted with $encrypted$: prefix)
|
|
74
|
+
|
|
75
|
+
Optional environment variables:
|
|
76
|
+
- PYLO_PORT: Port number (default: 8443, or 443 for illum.io domains)
|
|
77
|
+
- PYLO_ORG_ID: Organization ID (default: 1, required for illum.io domains)
|
|
78
|
+
- PYLO_VERIFY_SSL: Verify SSL certificate (default: true, accepts: true/false/1/0)
|
|
79
|
+
|
|
80
|
+
:return: CredentialProfile with name='ENV'
|
|
81
|
+
:raises PyloEx: If required environment variables are missing or invalid
|
|
82
|
+
"""
|
|
83
|
+
# Check for required environment variables
|
|
84
|
+
fqdn = os.environ.get('PYLO_FQDN')
|
|
85
|
+
api_user = os.environ.get('PYLO_API_USER')
|
|
86
|
+
api_key = os.environ.get('PYLO_API_KEY')
|
|
87
|
+
|
|
88
|
+
missing_vars = []
|
|
89
|
+
if not fqdn:
|
|
90
|
+
missing_vars.append('PYLO_FQDN')
|
|
91
|
+
if not api_user:
|
|
92
|
+
missing_vars.append('PYLO_API_USER')
|
|
93
|
+
if not api_key:
|
|
94
|
+
missing_vars.append('PYLO_API_KEY')
|
|
95
|
+
|
|
96
|
+
if missing_vars:
|
|
97
|
+
raise PyloEx("Missing required environment variables for ENV profile: {}. "
|
|
98
|
+
"Required: PYLO_FQDN, PYLO_API_USER, PYLO_API_KEY. "
|
|
99
|
+
"Optional: PYLO_PORT, PYLO_ORG_ID, PYLO_VERIFY_SSL".format(', '.join(missing_vars)))
|
|
100
|
+
|
|
101
|
+
# Determine if this is an illum.io domain
|
|
102
|
+
is_illumio_domain = 'illum.io' in fqdn.lower()
|
|
103
|
+
|
|
104
|
+
# Parse PORT with validation
|
|
105
|
+
port_str = os.environ.get('PYLO_PORT')
|
|
106
|
+
if port_str:
|
|
107
|
+
try:
|
|
108
|
+
port = int(port_str)
|
|
109
|
+
if port <= 0 or port > 65535:
|
|
110
|
+
raise ValueError("Port must be between 1 and 65535")
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
raise PyloEx("Invalid PYLO_PORT value '{}': must be a valid port number (1-65535)".format(port_str))
|
|
113
|
+
else:
|
|
114
|
+
# Default port based on domain type
|
|
115
|
+
port = 443 if is_illumio_domain else 8443
|
|
116
|
+
|
|
117
|
+
# Parse ORG_ID with validation
|
|
118
|
+
org_id_str = os.environ.get('PYLO_ORG_ID')
|
|
119
|
+
if org_id_str:
|
|
120
|
+
try:
|
|
121
|
+
org_id = int(org_id_str)
|
|
122
|
+
if org_id <= 0:
|
|
123
|
+
raise ValueError("Organization ID must be positive")
|
|
124
|
+
except ValueError as e:
|
|
125
|
+
raise PyloEx("Invalid PYLO_ORG_ID value '{}': must be a positive integer".format(org_id_str))
|
|
126
|
+
else:
|
|
127
|
+
# For illum.io domains, org_id is mandatory
|
|
128
|
+
if is_illumio_domain:
|
|
129
|
+
raise PyloEx("PYLO_ORG_ID is required for illum.io domains (no default available)")
|
|
130
|
+
org_id = 1
|
|
131
|
+
|
|
132
|
+
# Parse VERIFY_SSL with validation
|
|
133
|
+
verify_ssl_str = os.environ.get('PYLO_VERIFY_SSL', 'true').lower()
|
|
134
|
+
if verify_ssl_str in ['true', '1', 'yes', 'y']:
|
|
135
|
+
verify_ssl = True
|
|
136
|
+
elif verify_ssl_str in ['false', '0', 'no', 'n']:
|
|
137
|
+
verify_ssl = False
|
|
138
|
+
else:
|
|
139
|
+
raise PyloEx("Invalid PYLO_VERIFY_SSL value '{}': must be true/false/1/0/yes/no/y/n (case-insensitive)".format(verify_ssl_str))
|
|
140
|
+
|
|
141
|
+
# Decrypt API key if encrypted
|
|
142
|
+
if api_key.startswith("$encrypted$:"):
|
|
143
|
+
log.debug("Detected encrypted API key in environment variable, attempting to decrypt")
|
|
144
|
+
api_key = decrypt_api_key(api_key)
|
|
145
|
+
|
|
146
|
+
return CredentialProfile(
|
|
147
|
+
name='ENV',
|
|
148
|
+
fqdn=fqdn,
|
|
149
|
+
port=port,
|
|
150
|
+
api_user=api_user,
|
|
151
|
+
api_key=api_key,
|
|
152
|
+
org_id=org_id,
|
|
153
|
+
verify_ssl=verify_ssl,
|
|
154
|
+
originating_file='environment'
|
|
155
|
+
)
|
|
156
|
+
|
|
60
157
|
|
|
61
158
|
CredentialsFileType = Union[CredentialFileEntry | List[CredentialFileEntry]]
|
|
62
159
|
|
|
63
160
|
|
|
64
|
-
def check_profile_json_structure(profile:
|
|
161
|
+
def check_profile_json_structure(profile: CredentialFileEntry) -> None:
|
|
65
162
|
# ensure all fields from CredentialFileEntry are present
|
|
66
163
|
if "name" not in profile or type(profile["name"]) != str:
|
|
67
164
|
raise PyloEx("The profile {} does not contain a name".format(profile))
|
|
@@ -79,7 +176,7 @@ def check_profile_json_structure(profile: Dict) -> None:
|
|
|
79
176
|
raise PyloEx("The profile {} does not contain a verify_ssl".format(profile))
|
|
80
177
|
|
|
81
178
|
|
|
82
|
-
def get_all_credentials_from_file(credential_file: str
|
|
179
|
+
def get_all_credentials_from_file(credential_file: str) -> List[CredentialProfile]:
|
|
83
180
|
log.debug("Loading credentials from file: {}".format(credential_file))
|
|
84
181
|
with open(credential_file, 'r') as f:
|
|
85
182
|
credentials: CredentialsFileType = json.load(f)
|
|
@@ -97,11 +194,26 @@ def get_all_credentials_from_file(credential_file: str ) -> List[CredentialProfi
|
|
|
97
194
|
|
|
98
195
|
def get_credentials_from_file(fqdn_or_profile_name: str = None,
|
|
99
196
|
credential_file: str = None, fail_with_an_exception=True) -> Optional[CredentialProfile]:
|
|
197
|
+
"""
|
|
198
|
+
Get credentials from a file or environment variables.
|
|
199
|
+
|
|
200
|
+
Special profile name 'ENV' will load credentials from environment variables instead of files.
|
|
201
|
+
See CredentialProfile.from_environment_variables() for required environment variables.
|
|
100
202
|
|
|
203
|
+
:param fqdn_or_profile_name: Profile name or FQDN to search for (default: 'default')
|
|
204
|
+
:param credential_file: Specific credential file to search in (optional)
|
|
205
|
+
:param fail_with_an_exception: Raise exception if profile not found (default: True)
|
|
206
|
+
:return: CredentialProfile or None
|
|
207
|
+
"""
|
|
101
208
|
if fqdn_or_profile_name is None:
|
|
102
209
|
log.debug("No fqdn_or_profile_name provided, profile_name=default will be used")
|
|
103
210
|
fqdn_or_profile_name = "default"
|
|
104
211
|
|
|
212
|
+
# Check for special 'ENV' profile name to load from environment variables
|
|
213
|
+
if fqdn_or_profile_name.lower() == 'env':
|
|
214
|
+
log.debug("Loading credentials from environment variables")
|
|
215
|
+
return CredentialProfile.from_environment_variables()
|
|
216
|
+
|
|
105
217
|
credential_files: List[str] = []
|
|
106
218
|
if credential_file is not None:
|
|
107
219
|
credential_files.append(credential_file)
|
|
@@ -170,17 +282,21 @@ def create_credential_in_file(file_full_path: str, data: CredentialFileEntry, ov
|
|
|
170
282
|
credentials: CredentialsFileType = json.load(f)
|
|
171
283
|
if isinstance(credentials, list):
|
|
172
284
|
# check if the profile already exists
|
|
285
|
+
profile_found = False
|
|
173
286
|
for profile in credentials:
|
|
174
287
|
if profile['name'].lower() == data['name'].lower():
|
|
175
288
|
if overwrite_existing_profile:
|
|
176
289
|
# profile is a dict, remove of all its entries
|
|
177
290
|
for key in list(profile.keys()):
|
|
291
|
+
# noinspection PyTypedDict
|
|
178
292
|
del profile[key]
|
|
179
293
|
profile.update(data)
|
|
294
|
+
profile_found = True
|
|
180
295
|
break
|
|
181
296
|
else:
|
|
182
297
|
raise PyloEx("Profile with name {} already exists in file {}".format(data['name'], file_full_path))
|
|
183
|
-
|
|
298
|
+
if not profile_found:
|
|
299
|
+
credentials.append(data)
|
|
184
300
|
else:
|
|
185
301
|
if data['name'].lower() == credentials['name'].lower():
|
|
186
302
|
if overwrite_existing_profile:
|
|
@@ -199,6 +315,44 @@ def create_credential_in_file(file_full_path: str, data: CredentialFileEntry, ov
|
|
|
199
315
|
return file_full_path
|
|
200
316
|
|
|
201
317
|
|
|
318
|
+
def delete_credential_from_file(profile_name: str, file_path: str) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Delete a credential from a file by profile name.
|
|
321
|
+
:param profile_name: Name of the profile to delete
|
|
322
|
+
:param file_path: Path to the credential file
|
|
323
|
+
:return: True if deleted successfully
|
|
324
|
+
"""
|
|
325
|
+
if not os.path.exists(file_path):
|
|
326
|
+
raise PyloEx("Credential file does not exist: {}".format(file_path))
|
|
327
|
+
|
|
328
|
+
with open(file_path, 'r') as f:
|
|
329
|
+
credentials: CredentialsFileType = json.load(f)
|
|
330
|
+
|
|
331
|
+
if isinstance(credentials, list):
|
|
332
|
+
original_len = len(credentials)
|
|
333
|
+
credentials = [c for c in credentials if c['name'].lower() != profile_name.lower()]
|
|
334
|
+
if len(credentials) == original_len:
|
|
335
|
+
raise PyloEx("Profile '{}' not found in file '{}'".format(profile_name, file_path))
|
|
336
|
+
|
|
337
|
+
if len(credentials) == 0:
|
|
338
|
+
# If no credentials left, delete the file
|
|
339
|
+
os.remove(file_path)
|
|
340
|
+
return True
|
|
341
|
+
else:
|
|
342
|
+
if credentials['name'].lower() == profile_name.lower():
|
|
343
|
+
# Single credential in file, delete the file
|
|
344
|
+
os.remove(file_path)
|
|
345
|
+
return True
|
|
346
|
+
else:
|
|
347
|
+
raise PyloEx("Profile '{}' not found in file '{}'".format(profile_name, file_path))
|
|
348
|
+
|
|
349
|
+
# Write the updated credentials back to the file
|
|
350
|
+
with open(file_path, 'w') as f:
|
|
351
|
+
json.dump(credentials, f, indent=4)
|
|
352
|
+
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
|
|
202
356
|
def create_credential_in_default_file(data: CredentialFileEntry) -> str:
|
|
203
357
|
"""
|
|
204
358
|
Create a credential in the default credential file and return the full path to the file
|
|
@@ -324,3 +478,14 @@ def is_encryption_available() -> bool:
|
|
|
324
478
|
return False
|
|
325
479
|
|
|
326
480
|
|
|
481
|
+
def is_env_credentials_available() -> bool:
|
|
482
|
+
"""
|
|
483
|
+
Check if required environment variables for ENV profile are set.
|
|
484
|
+
|
|
485
|
+
:return: True if PYLO_FQDN, PYLO_API_USER, and PYLO_API_KEY are all set, False otherwise
|
|
486
|
+
"""
|
|
487
|
+
return (os.environ.get('PYLO_FQDN') is not None and
|
|
488
|
+
os.environ.get('PYLO_API_USER') is not None and
|
|
489
|
+
os.environ.get('PYLO_API_KEY') is not None)
|
|
490
|
+
|
|
491
|
+
|