illumio-pylo 0.3.11__py3-none-any.whl → 0.3.12__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.
@@ -0,0 +1,430 @@
1
+ /* Base styles */
2
+ * {
3
+ box-sizing: border-box;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
10
+ background-color: #f5f7fa;
11
+ color: #333;
12
+ line-height: 1.6;
13
+ }
14
+
15
+ .container {
16
+ max-width: 1200px;
17
+ margin: 0 auto;
18
+ padding: 20px;
19
+ }
20
+
21
+ /* Header */
22
+ header {
23
+ text-align: center;
24
+ margin-bottom: 30px;
25
+ padding: 20px;
26
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
27
+ border-radius: 10px;
28
+ color: white;
29
+ }
30
+
31
+ header h1 {
32
+ font-size: 2rem;
33
+ margin-bottom: 5px;
34
+ }
35
+
36
+ header .subtitle {
37
+ opacity: 0.9;
38
+ font-size: 1rem;
39
+ }
40
+
41
+ /* Buttons */
42
+ .btn {
43
+ padding: 10px 20px;
44
+ border: none;
45
+ border-radius: 5px;
46
+ cursor: pointer;
47
+ font-size: 0.95rem;
48
+ font-weight: 500;
49
+ transition: all 0.2s ease;
50
+ }
51
+
52
+ .btn-primary {
53
+ background-color: #667eea;
54
+ color: white;
55
+ }
56
+
57
+ .btn-primary:hover {
58
+ background-color: #5a6fd6;
59
+ }
60
+
61
+ .btn-secondary {
62
+ background-color: #e2e8f0;
63
+ color: #4a5568;
64
+ }
65
+
66
+ .btn-secondary:hover {
67
+ background-color: #cbd5e0;
68
+ }
69
+
70
+ .btn-danger {
71
+ background-color: #e53e3e;
72
+ color: white;
73
+ }
74
+
75
+ .btn-danger:hover {
76
+ background-color: #c53030;
77
+ }
78
+
79
+ .btn-success {
80
+ background-color: #38a169;
81
+ color: white;
82
+ }
83
+
84
+ .btn-success:hover {
85
+ background-color: #2f855a;
86
+ }
87
+
88
+ .btn-small {
89
+ padding: 5px 10px;
90
+ font-size: 0.85rem;
91
+ }
92
+
93
+ /* Section styles */
94
+ section {
95
+ background: white;
96
+ border-radius: 10px;
97
+ padding: 25px;
98
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
99
+ }
100
+
101
+ .section-header {
102
+ display: flex;
103
+ justify-content: space-between;
104
+ align-items: center;
105
+ margin-bottom: 20px;
106
+ }
107
+
108
+ .section-header h2 {
109
+ font-size: 1.3rem;
110
+ color: #2d3748;
111
+ }
112
+
113
+ /* Table styles */
114
+ table {
115
+ width: 100%;
116
+ border-collapse: collapse;
117
+ }
118
+
119
+ th, td {
120
+ padding: 12px 15px;
121
+ text-align: left;
122
+ border-bottom: 1px solid #e2e8f0;
123
+ }
124
+
125
+ th {
126
+ background-color: #f7fafc;
127
+ font-weight: 600;
128
+ color: #4a5568;
129
+ font-size: 0.85rem;
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.5px;
132
+ }
133
+
134
+ tr:hover {
135
+ background-color: #f7fafc;
136
+ }
137
+
138
+ td {
139
+ font-size: 0.95rem;
140
+ }
141
+
142
+ .actions-cell {
143
+ white-space: nowrap;
144
+ }
145
+
146
+ .actions-cell .btn {
147
+ margin-right: 5px;
148
+ }
149
+
150
+ .actions-cell .btn:last-child {
151
+ margin-right: 0;
152
+ }
153
+
154
+ /* Status indicators */
155
+ .status-yes {
156
+ color: #38a169;
157
+ font-weight: 500;
158
+ }
159
+
160
+ .status-no {
161
+ color: #e53e3e;
162
+ font-weight: 500;
163
+ }
164
+
165
+ /* Loading and empty states */
166
+ .loading, .empty-state {
167
+ text-align: center;
168
+ padding: 40px;
169
+ color: #718096;
170
+ }
171
+
172
+ .loading::before {
173
+ content: '⏳ ';
174
+ }
175
+
176
+ .empty-state {
177
+ background-color: #f7fafc;
178
+ border-radius: 8px;
179
+ }
180
+
181
+ /* Notifications */
182
+ .notification {
183
+ padding: 15px 20px;
184
+ border-radius: 8px;
185
+ margin-bottom: 20px;
186
+ font-weight: 500;
187
+ }
188
+
189
+ .notification.success {
190
+ background-color: #c6f6d5;
191
+ color: #276749;
192
+ border: 1px solid #9ae6b4;
193
+ }
194
+
195
+ .notification.error {
196
+ background-color: #fed7d7;
197
+ color: #c53030;
198
+ border: 1px solid #feb2b2;
199
+ }
200
+
201
+ .notification.info {
202
+ background-color: #bee3f8;
203
+ color: #2b6cb0;
204
+ border: 1px solid #90cdf4;
205
+ }
206
+
207
+ /* Modal */
208
+ .modal {
209
+ position: fixed;
210
+ top: 0;
211
+ left: 0;
212
+ width: 100%;
213
+ height: 100%;
214
+ background-color: rgba(0, 0, 0, 0.5);
215
+ display: flex;
216
+ justify-content: center;
217
+ align-items: center;
218
+ z-index: 1000;
219
+ }
220
+
221
+ .modal-content {
222
+ background: white;
223
+ border-radius: 12px;
224
+ width: 90%;
225
+ max-width: 550px;
226
+ max-height: 90vh;
227
+ overflow-y: auto;
228
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
229
+ }
230
+
231
+ .modal-small {
232
+ max-width: 400px;
233
+ }
234
+
235
+ .modal-header {
236
+ display: flex;
237
+ justify-content: space-between;
238
+ align-items: center;
239
+ padding: 20px 25px;
240
+ border-bottom: 1px solid #e2e8f0;
241
+ }
242
+
243
+ .modal-header h2 {
244
+ font-size: 1.2rem;
245
+ color: #2d3748;
246
+ }
247
+
248
+ .btn-close {
249
+ background: none;
250
+ border: none;
251
+ font-size: 1.5rem;
252
+ color: #a0aec0;
253
+ cursor: pointer;
254
+ line-height: 1;
255
+ }
256
+
257
+ .btn-close:hover {
258
+ color: #4a5568;
259
+ }
260
+
261
+ /* Form styles */
262
+ form {
263
+ padding: 25px;
264
+ }
265
+
266
+ .form-group {
267
+ margin-bottom: 20px;
268
+ }
269
+
270
+ .form-group label {
271
+ display: block;
272
+ margin-bottom: 6px;
273
+ font-weight: 500;
274
+ color: #4a5568;
275
+ font-size: 0.95rem;
276
+ }
277
+
278
+ .form-group input[type="text"],
279
+ .form-group input[type="number"],
280
+ .form-group input[type="password"],
281
+ .form-group select {
282
+ width: 100%;
283
+ padding: 10px 12px;
284
+ border: 1px solid #e2e8f0;
285
+ border-radius: 6px;
286
+ font-size: 1rem;
287
+ transition: border-color 0.2s;
288
+ }
289
+
290
+ .form-group input:focus,
291
+ .form-group select:focus {
292
+ outline: none;
293
+ border-color: #667eea;
294
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
295
+ }
296
+
297
+ .form-group small {
298
+ display: block;
299
+ margin-top: 5px;
300
+ color: #718096;
301
+ font-size: 0.85rem;
302
+ }
303
+
304
+ .form-row {
305
+ display: flex;
306
+ gap: 15px;
307
+ }
308
+
309
+ .form-row .form-group {
310
+ flex: 1;
311
+ }
312
+
313
+ .checkbox-group label {
314
+ display: flex;
315
+ align-items: center;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .checkbox-group input[type="checkbox"] {
320
+ margin-right: 10px;
321
+ width: 18px;
322
+ height: 18px;
323
+ }
324
+
325
+ .form-section {
326
+ background-color: #f7fafc;
327
+ padding: 15px;
328
+ border-radius: 8px;
329
+ margin-bottom: 20px;
330
+ }
331
+
332
+ .form-section h3 {
333
+ font-size: 1rem;
334
+ color: #4a5568;
335
+ margin-bottom: 15px;
336
+ }
337
+
338
+ .form-actions {
339
+ display: flex;
340
+ justify-content: flex-end;
341
+ gap: 10px;
342
+ padding-top: 15px;
343
+ border-top: 1px solid #e2e8f0;
344
+ }
345
+
346
+ /* Test result */
347
+ .test-result {
348
+ padding: 25px;
349
+ text-align: center;
350
+ }
351
+
352
+ .test-result.success {
353
+ color: #276749;
354
+ }
355
+
356
+ .test-result.success::before {
357
+ content: '✅ ';
358
+ font-size: 2rem;
359
+ display: block;
360
+ margin-bottom: 10px;
361
+ }
362
+
363
+ .test-result.error {
364
+ color: #c53030;
365
+ }
366
+
367
+ .test-result.error::before {
368
+ content: '❌ ';
369
+ font-size: 2rem;
370
+ display: block;
371
+ margin-bottom: 10px;
372
+ }
373
+
374
+ .test-result.loading::before {
375
+ content: '🔄 ';
376
+ font-size: 2rem;
377
+ display: block;
378
+ margin-bottom: 10px;
379
+ animation: spin 1s linear infinite;
380
+ }
381
+
382
+ @keyframes spin {
383
+ from { transform: rotate(0deg); }
384
+ to { transform: rotate(360deg); }
385
+ }
386
+
387
+ /* Utility classes */
388
+ .hidden {
389
+ display: none !important;
390
+ }
391
+
392
+ /* Responsive */
393
+ @media (max-width: 768px) {
394
+ .form-row {
395
+ flex-direction: column;
396
+ gap: 0;
397
+ }
398
+
399
+ table {
400
+ font-size: 0.85rem;
401
+ }
402
+
403
+ th, td {
404
+ padding: 8px 10px;
405
+ }
406
+
407
+ .section-header {
408
+ flex-direction: column;
409
+ gap: 15px;
410
+ align-items: flex-start;
411
+ }
412
+ }
413
+
414
+ /* Delete confirmation modal */
415
+ .delete-confirmation {
416
+ padding: 25px;
417
+ text-align: center;
418
+ }
419
+
420
+ .delete-confirmation p {
421
+ margin-bottom: 10px;
422
+ font-size: 1rem;
423
+ }
424
+
425
+ .delete-confirmation .warning-text {
426
+ color: #c53030;
427
+ font-size: 0.9rem;
428
+ font-style: italic;
429
+ }
430
+
@@ -2,15 +2,16 @@ from typing import Dict, List, Literal, Optional
2
2
  import datetime
3
3
  import click
4
4
  import argparse
5
+ import os
5
6
 
6
7
  import illumio_pylo as pylo
7
- from illumio_pylo import ExcelHeader, nice_json
8
+ from illumio_pylo import ExcelHeader
8
9
 
9
10
  from .utils.misc import make_filename_with_timestamp
10
11
  from . import Command
11
12
 
12
13
  command_name = 'ven-duplicate-remover'
13
- objects_load_filter = ['labels']
14
+ objects_load_filter = ['labels', 'workloads']
14
15
 
15
16
 
16
17
  def fill_parser(parser: argparse.ArgumentParser):
@@ -37,8 +38,18 @@ def fill_parser(parser: argparse.ArgumentParser):
37
38
  parser.add_argument('--limit-number-of-deleted-workloads', '-l', type=int, default=None,
38
39
  help='Limit the number of workloads to be deleted, for a limited test run for example.')
39
40
 
41
+ # New option: don't delete if labels mismatch across duplicates
42
+ parser.add_argument('--do-not-delete-if-labels-mismatch', action='store_true',
43
+ help='Do not delete workloads for a duplicated hostname if the workloads do not all have the same set of labels')
44
+
45
+ # New option: ignore PCE online status and allow online workloads to be considered for deletion
46
+ parser.add_argument('--ignore-pce-online-status', action='store_true',
47
+ help='Bypass the logic that keeps online workloads; when set online workloads will be treated like offline ones for deletion decisions')
48
+
40
49
  parser.add_argument('--output-dir', '-o', type=str, required=False, default="output",
41
50
  help='Directory where to write the report file(s)')
51
+ parser.add_argument('--output-filename', type=str, default=None,
52
+ help='Write report to the specified file (or basename) instead of using the default timestamped filename. If multiple formats are requested, the provided path\'s extension will be replaced/added per format.')
42
53
 
43
54
 
44
55
  def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
@@ -55,11 +66,16 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
55
66
  arg_do_not_delete_if_last_heartbeat_is_more_recent_than = args['do_not_delete_if_last_heartbeat_is_more_recent_than']
56
67
  arg_override_pce_offline_timer_to = args['override_pce_offline_timer_to']
57
68
  arg_limit_number_of_deleted_workloads = args['limit_number_of_deleted_workloads']
58
- arg_report_output_dir: str = args['output_dir']
59
-
60
- output_file_prefix = make_filename_with_timestamp('ven-duplicate-removal_', arg_report_output_dir)
61
- output_file_csv = output_file_prefix + '.csv'
62
- output_file_excel = output_file_prefix + '.xlsx'
69
+ arg_ignore_pce_online_status = args['ignore_pce_online_status'] is True
70
+ arg_do_not_delete_if_labels_mismatch = args['do_not_delete_if_labels_mismatch'] is True
71
+ arg_report_output_dir: str = args['output_dir']
72
+
73
+ # Determine output filename behavior: user provided filename/basename or use timestamped prefix
74
+ arg_output_filename: Optional[str] = args.get('output_filename')
75
+ if arg_output_filename is None:
76
+ output_file_prefix = make_filename_with_timestamp('ven-duplicate-removal_', arg_report_output_dir)
77
+ else:
78
+ output_file_prefix = None
63
79
 
64
80
  csv_report_headers = pylo.ExcelHeaderSet([
65
81
  ExcelHeader(name='name', max_width=40),
@@ -88,47 +104,11 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
88
104
  raise pylo.PyloEx("Cannot find label '{}' in the PCE".format(label_name))
89
105
  filter_labels.append(label)
90
106
 
91
- # <editor-fold desc="Download workloads from PCE">
92
- if not pce_cache_was_used:
93
- print("* Downloading Workloads data from the PCE (it may take moment for large amounts of workloads) ... ", flush=True, end='')
94
- if args['filter_label'] is None:
95
- workloads_json = org.connector.objects_workload_get(async_mode=True, max_results=1000000)
96
- else:
97
- filter_labels_list_of_list: List[List[pylo.Label]] = []
98
- # convert filter_labels dict to an array of arrays
99
- for label_type, label_list in org.LabelStore.Utils.list_to_dict_by_type(filter_labels).items():
100
- filter_labels_list_of_list.append(label_list)
101
-
102
- # convert filter_labels_list_of_list to a matrix of all possibilities
103
- # example: [[a,b],[c,d]] becomes [[a,c],[a,d],[b,c],[b,d]]
104
- filter_labels_matrix = [[]]
105
- for label_list in filter_labels_list_of_list:
106
- new_matrix = []
107
- for label in label_list:
108
- for row in filter_labels_matrix:
109
- new_row = row.copy()
110
- new_row.append(label.href)
111
- new_matrix.append(new_row)
112
- filter_labels_matrix = new_matrix
113
-
114
- workloads_json = org.connector.objects_workload_get(async_mode=False, max_results=1000000, filter_by_label=filter_labels_matrix)
115
-
116
- org.WorkloadStore.load_workloads_from_json(workloads_json)
117
-
118
- print("OK! {} workloads loaded".format(org.WorkloadStore.count_workloads()))
119
- # </editor-fold>
120
-
121
- all_workloads: List[pylo.Workload] # the list of all workloads to be processed
122
-
123
- if pce_cache_was_used:
124
- # if some filters were used, let's apply them now
125
- print("* Filtering workloads loaded from cache based on their labels... ", end='', flush=True)
126
- # if some label filters were used, we will apply them at later stage
127
- all_workloads: List[pylo.Workload] = list((org.WorkloadStore.find_workloads_matching_all_labels(filter_labels)).values())
128
- print("OK! {} workloads left after filtering".format(len(all_workloads)))
129
- else:
130
- # filter was already applied during the download from the PCE
131
- all_workloads = org.WorkloadStore.workloads
107
+ # if some filters were used, let's apply them now
108
+ print("* Filtering workloads loaded based on their labels... ", end='', flush=True)
109
+ # if some label filters were used, we will apply them at later stage
110
+ all_workloads: List[pylo.Workload] = list((org.WorkloadStore.find_workloads_matching_all_labels(filter_labels)).values())
111
+ print("OK! {} workloads left after filtering".format(len(all_workloads)))
132
112
 
133
113
  def add_workload_to_report(workload: pylo.Workload, action: str):
134
114
  url_link_to_pce = workload.get_pce_ui_url()
@@ -147,9 +127,8 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
147
127
 
148
128
  sheet.add_line_from_object(new_row)
149
129
 
150
- duplicated_hostnames = DuplicateRecordManager(arg_override_pce_offline_timer_to)
151
-
152
130
  print(" * Looking for VEN with duplicated hostname(s)")
131
+ duplicated_hostnames = DuplicateRecordManager(arg_override_pce_offline_timer_to)
153
132
 
154
133
  for workload in all_workloads:
155
134
  if workload.deleted:
@@ -161,10 +140,12 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
161
140
 
162
141
  print(" * Found {} duplicated hostnames".format(duplicated_hostnames.count_duplicates()))
163
142
 
164
- delete_tracker = org.connector.new_tracker_workload_multi_delete()
143
+ delete_tracker = org.connector.new_tracker_workload_multi_delete() # tracker to handle deletions, it will be executed later
165
144
 
145
+ # Process each duplicated hostname record
166
146
  for dup_hostname, dup_record in duplicated_hostnames._records.items():
167
- if not dup_record.has_duplicates():
147
+
148
+ if not dup_record.has_duplicates(): # no duplicates, skip
168
149
  continue
169
150
 
170
151
  print(" - hostname '{}' has duplicates. ({} online, {} offline, {} unmanaged)".format(dup_hostname,
@@ -172,13 +153,27 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
172
153
  len(dup_record.offline),
173
154
  len(dup_record.unmanaged)))
174
155
 
156
+ # If the new flag was passed, ensure all workloads under this duplicate record have identical labels
157
+ if arg_do_not_delete_if_labels_mismatch:
158
+ label_strings = set()
159
+ for wkl in dup_record.all:
160
+ # Use workload.get_labels_str() to produce a stable representation across label types
161
+ lbl_str = wkl.get_labels_str()
162
+ label_strings.add(lbl_str)
163
+
164
+ if len(label_strings) > 1:
165
+ print(" - IGNORED: workloads for hostname '{}' have mismatching labels".format(dup_hostname))
166
+ for wkl in dup_record.all:
167
+ add_workload_to_report(wkl, "ignored (labels mismatch)")
168
+ continue
169
+
175
170
  if not dup_record.has_no_managed_workloads():
176
171
  latest_created_workload = dup_record.find_latest_managed_created_at()
177
172
  latest_heartbeat_workload = dup_record.find_latest_heartbeat()
178
173
 
179
174
  print(" - Latest created at {} and latest heartbeat at {}".format(latest_created_workload.created_at, latest_heartbeat_workload.ven_agent.get_last_heartbeat_date()))
180
175
 
181
- if dup_record.count_online() == 0:
176
+ if not arg_ignore_pce_online_status and dup_record.count_online() == 0:
182
177
  print(" - IGNORED: there is no VEN online")
183
178
  for wkl in dup_record.offline:
184
179
  add_workload_to_report(wkl, "ignored (no VEN online)")
@@ -188,10 +183,18 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
188
183
  print(" - WARNING: there are more than 1 VEN online")
189
184
 
190
185
  # Don't delete online workloads but still show them in the report
191
- for wkl in dup_record.online:
192
- add_workload_to_report(wkl, "ignored (VEN is online)")
186
+ if not arg_ignore_pce_online_status:
187
+ for wkl in dup_record.online:
188
+ add_workload_to_report(wkl, "ignored (VEN is online)")
189
+
190
+ # Build the list of candidate workloads to consider for deletion. If --ignore-pce-online-status
191
+ # is passed, include online workloads among the candidates.
192
+ if arg_ignore_pce_online_status:
193
+ deletion_candidates = list(dup_record.offline) + list(dup_record.online)
194
+ else:
195
+ deletion_candidates = list(dup_record.offline)
193
196
 
194
- for wkl in dup_record.offline:
197
+ for wkl in deletion_candidates:
195
198
  if arg_do_not_delete_the_most_recent_workload and wkl is latest_created_workload:
196
199
  print(" - IGNORED: wkl {}/{} is the most recent".format(wkl.get_name_stripped_fqdn(), wkl.href))
197
200
  add_workload_to_report(wkl, "ignored (it is the most recently created)")
@@ -207,7 +210,7 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
207
210
  add_workload_to_report(wkl, "ignored (limit of {} workloads to be deleted was reached)".format(arg_limit_number_of_deleted_workloads))
208
211
  else:
209
212
  delete_tracker.add_workload(wkl)
210
- print(" - added offline wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
213
+ print(" - added wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
211
214
 
212
215
  for wkl in dup_record.unmanaged:
213
216
  if arg_limit_number_of_deleted_workloads is not None and delete_tracker.count_entries() >= arg_limit_number_of_deleted_workloads:
@@ -257,7 +260,8 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
257
260
  for wkl in delete_tracker.workloads:
258
261
  add_workload_to_report(wkl, "TO BE DELETED (aborted by user)")
259
262
  else:
260
- print(" * Executing deletion requests ... ".format(output_file_csv), end='', flush=True)
263
+ # execute deletions
264
+ print(" * Executing deletion requests ... ", end='', flush=True)
261
265
  delete_tracker.execute(unpair_agents=True)
262
266
  print("DONE")
263
267
 
@@ -277,13 +281,29 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
277
281
  for wkl in delete_tracker.workloads:
278
282
  add_workload_to_report(wkl, "TO BE DELETED (no confirm option used)")
279
283
 
284
+ # if report is not empty, write it to disk
280
285
  if sheet.lines_count() >= 1:
281
286
  if len(report_wanted_format) < 1:
282
287
  print(" * No report format was specified, no report will be generated")
283
288
  else:
284
289
  sheet.reorder_lines(['hostname']) # sort by hostname for better readability
285
290
  for report_format in report_wanted_format:
286
- output_filename = output_file_prefix + '.' + report_format
291
+ # Choose output filename depending on whether user provided --output-filename
292
+ if arg_output_filename is None:
293
+ output_filename = output_file_prefix + '.' + report_format
294
+ else:
295
+ # If only one format requested, use the provided filename as-is
296
+ if len(report_wanted_format) == 1:
297
+ output_filename = arg_output_filename
298
+ else:
299
+ base = os.path.splitext(arg_output_filename)[0]
300
+ output_filename = base + '.' + report_format
301
+
302
+ # Ensure parent directory exists
303
+ output_directory = os.path.dirname(output_filename)
304
+ if output_directory:
305
+ os.makedirs(output_directory, exist_ok=True)
306
+
287
307
  print(" * Writing report file '{}' ... ".format(output_filename), end='', flush=True)
288
308
  if report_format == 'csv':
289
309
  sheet.write_to_csv(output_filename)
@@ -4,7 +4,10 @@ import os
4
4
  import sys
5
5
 
6
6
  # in case user wants to run this utility while having a version of pylo already installed
7
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
7
+ if __name__ == "__main__":
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
9
+ pass
10
+
8
11
  import illumio_pylo.cli
9
12
 
10
13
  illumio_pylo.cli.run()
@@ -4,7 +4,11 @@ import argparse
4
4
  import math
5
5
  from datetime import datetime
6
6
 
7
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
7
+ # in case user wants to run this utility while having a version of pylo already installed
8
+ if __name__ == "__main__":
9
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
10
+ pass
11
+
8
12
  import illumio_pylo as pylo
9
13
 
10
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: illumio_pylo
3
- Version: 0.3.11
3
+ Version: 0.3.12
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