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.
- illumio_pylo/API/APIConnector.py +82 -97
- illumio_pylo/API/CredentialsManager.py +38 -0
- illumio_pylo/Helpers/exports.py +1 -1
- illumio_pylo/__init__.py +1 -1
- illumio_pylo/cli/commands/credential_manager.py +379 -4
- illumio_pylo/cli/commands/label_delete_unused.py +0 -3
- illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +449 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +168 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +430 -0
- illumio_pylo/cli/commands/ven_duplicate_remover.py +79 -59
- 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.12.dist-info}/METADATA +2 -1
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.12.dist-info}/RECORD +17 -14
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.12.dist-info}/WHEEL +1 -1
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.12.dist-info}/licenses/LICENSE +0 -0
- {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.12.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
illumio_pylo/utilities/cli.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|