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.
Files changed (30) hide show
  1. illumio_pylo/API/APIConnector.py +138 -106
  2. illumio_pylo/API/CredentialsManager.py +168 -3
  3. illumio_pylo/API/Explorer.py +619 -14
  4. illumio_pylo/API/JsonPayloadTypes.py +64 -4
  5. illumio_pylo/FilterQuery.py +892 -0
  6. illumio_pylo/Helpers/exports.py +1 -1
  7. illumio_pylo/LabelCommon.py +13 -3
  8. illumio_pylo/LabelDimension.py +109 -0
  9. illumio_pylo/LabelStore.py +97 -38
  10. illumio_pylo/WorkloadStore.py +58 -0
  11. illumio_pylo/__init__.py +9 -3
  12. illumio_pylo/cli/__init__.py +5 -2
  13. illumio_pylo/cli/commands/__init__.py +1 -0
  14. illumio_pylo/cli/commands/credential_manager.py +555 -4
  15. illumio_pylo/cli/commands/label_delete_unused.py +0 -3
  16. illumio_pylo/cli/commands/traffic_export.py +358 -0
  17. illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +638 -0
  18. illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +217 -0
  19. illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +581 -0
  20. illumio_pylo/cli/commands/update_pce_objects_cache.py +1 -2
  21. illumio_pylo/cli/commands/ven_duplicate_remover.py +79 -59
  22. illumio_pylo/cli/commands/workload_export.py +29 -0
  23. illumio_pylo/utilities/cli.py +4 -1
  24. illumio_pylo/utilities/health_monitoring.py +5 -1
  25. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/METADATA +2 -1
  26. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/RECORD +29 -24
  27. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/WHEEL +1 -1
  28. illumio_pylo/Query.py +0 -331
  29. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/licenses/LICENSE +0 -0
  30. {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: Dict) -> None:
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 ) -> List[CredentialProfile]:
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
- credentials.append(data)
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
+