vmware-avi 1.4.4__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.
- vmware_avi/__init__.py +3 -0
- vmware_avi/_safety.py +17 -0
- vmware_avi/cli.py +394 -0
- vmware_avi/config.py +140 -0
- vmware_avi/connection.py +81 -0
- vmware_avi/doctor.py +135 -0
- vmware_avi/k8s_connection.py +61 -0
- vmware_avi/notify/__init__.py +1 -0
- vmware_avi/notify/audit.py +41 -0
- vmware_avi/ops/__init__.py +1 -0
- vmware_avi/ops/ako_config.py +70 -0
- vmware_avi/ops/ako_ingress.py +172 -0
- vmware_avi/ops/ako_multi_cluster.py +81 -0
- vmware_avi/ops/ako_pod.py +136 -0
- vmware_avi/ops/ako_sync.py +99 -0
- vmware_avi/ops/analytics.py +74 -0
- vmware_avi/ops/pool_mgmt.py +76 -0
- vmware_avi/ops/se_mgmt.py +63 -0
- vmware_avi/ops/ssl_mgmt.py +85 -0
- vmware_avi/ops/vs_mgmt.py +95 -0
- vmware_avi-1.4.4.dist-info/METADATA +490 -0
- vmware_avi-1.4.4.dist-info/RECORD +24 -0
- vmware_avi-1.4.4.dist-info/WHEEL +4 -0
- vmware_avi-1.4.4.dist-info/entry_points.txt +3 -0
vmware_avi/__init__.py
ADDED
vmware_avi/_safety.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Safety utilities for destructive operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def double_confirm(action: str) -> bool:
|
|
11
|
+
"""Require double confirmation for destructive operations."""
|
|
12
|
+
console.print(f"\n[bold red]WARNING: {action}[/bold red]")
|
|
13
|
+
first = console.input(" Are you sure? (yes/no): ").strip().lower()
|
|
14
|
+
if first != "yes":
|
|
15
|
+
return False
|
|
16
|
+
second = console.input(" Confirm again to proceed (yes/no): ").strip().lower()
|
|
17
|
+
return second == "yes"
|
vmware_avi/cli.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""CLI entry point for vmware-avi.
|
|
2
|
+
|
|
3
|
+
Typer-based CLI with subcommands for VS, Pool, SSL, SE, Analytics, and AKO operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="vmware-avi",
|
|
17
|
+
help="AVI (NSX ALB) management and AKO Kubernetes operations.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# --- Sub-apps ---
|
|
24
|
+
|
|
25
|
+
vs_app = typer.Typer(help="Virtual Service management", no_args_is_help=True)
|
|
26
|
+
pool_app = typer.Typer(help="Pool member management", no_args_is_help=True)
|
|
27
|
+
ssl_app = typer.Typer(help="SSL certificate management", no_args_is_help=True)
|
|
28
|
+
se_app = typer.Typer(help="Service Engine management", no_args_is_help=True)
|
|
29
|
+
ako_app = typer.Typer(help="AKO (Avi Kubernetes Operator) operations", no_args_is_help=True)
|
|
30
|
+
|
|
31
|
+
app.add_typer(vs_app, name="vs")
|
|
32
|
+
app.add_typer(pool_app, name="pool")
|
|
33
|
+
app.add_typer(ssl_app, name="ssl")
|
|
34
|
+
app.add_typer(se_app, name="se")
|
|
35
|
+
app.add_typer(ako_app, name="ako")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- Global commands ---
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def doctor() -> None:
|
|
43
|
+
"""Run environment diagnostics."""
|
|
44
|
+
from vmware_avi.doctor import run_doctor
|
|
45
|
+
|
|
46
|
+
ok = run_doctor()
|
|
47
|
+
raise SystemExit(0 if ok else 1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def init() -> None:
|
|
52
|
+
"""Generate config.yaml and .env templates."""
|
|
53
|
+
from vmware_avi.config import CONFIG_DIR, CONFIG_FILE, ENV_FILE
|
|
54
|
+
|
|
55
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
if not CONFIG_FILE.exists():
|
|
58
|
+
template = Path(__file__).parent.parent / "config.example.yaml"
|
|
59
|
+
if template.exists():
|
|
60
|
+
CONFIG_FILE.write_text(template.read_text())
|
|
61
|
+
else:
|
|
62
|
+
CONFIG_FILE.write_text(
|
|
63
|
+
"controllers:\n"
|
|
64
|
+
" - name: my-avi\n"
|
|
65
|
+
" host: avi-controller.example.com\n"
|
|
66
|
+
" username: admin\n"
|
|
67
|
+
" api_version: '22.1.4'\n"
|
|
68
|
+
" tenant: admin\n\n"
|
|
69
|
+
"default_controller: my-avi\n\n"
|
|
70
|
+
"ako:\n"
|
|
71
|
+
" kubeconfig: ~/.kube/config\n"
|
|
72
|
+
" namespace: avi-system\n"
|
|
73
|
+
)
|
|
74
|
+
console.print(f"[green]Created:[/green] {CONFIG_FILE}")
|
|
75
|
+
else:
|
|
76
|
+
console.print(f"[yellow]Exists:[/yellow] {CONFIG_FILE}")
|
|
77
|
+
|
|
78
|
+
if not ENV_FILE.exists():
|
|
79
|
+
ENV_FILE.write_text("MY_AVI_PASSWORD=changeme\n")
|
|
80
|
+
ENV_FILE.chmod(0o600)
|
|
81
|
+
console.print(f"[green]Created:[/green] {ENV_FILE} (chmod 600)")
|
|
82
|
+
else:
|
|
83
|
+
console.print(f"[yellow]Exists:[/yellow] {ENV_FILE}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("config")
|
|
87
|
+
def config_show() -> None:
|
|
88
|
+
"""Show current configuration (passwords masked)."""
|
|
89
|
+
from vmware_avi.config import load_config
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
cfg = load_config()
|
|
93
|
+
except FileNotFoundError as exc:
|
|
94
|
+
console.print(f"[red]{exc}[/red]")
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
|
|
97
|
+
console.print("\n[bold]Controllers:[/bold]")
|
|
98
|
+
for c in cfg.controllers:
|
|
99
|
+
console.print(f" {c.name}: {c.host} (user={c.username}, tenant={c.tenant})")
|
|
100
|
+
|
|
101
|
+
console.print(f"\n[bold]Default controller:[/bold] {cfg.default_controller or '(first)'}")
|
|
102
|
+
console.print(f"\n[bold]AKO:[/bold]")
|
|
103
|
+
console.print(f" kubeconfig: {cfg.ako.kubeconfig}")
|
|
104
|
+
console.print(f" default_context: {cfg.ako.default_context or '(current)'}")
|
|
105
|
+
console.print(f" namespace: {cfg.ako.namespace}")
|
|
106
|
+
console.print()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- VS commands ---
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@vs_app.command("list")
|
|
113
|
+
def vs_list(
|
|
114
|
+
controller: str | None = typer.Option(None, help="Controller name"),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""List all Virtual Services."""
|
|
117
|
+
from vmware_avi.ops.vs_mgmt import list_virtual_services
|
|
118
|
+
|
|
119
|
+
list_virtual_services(controller)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@vs_app.command("status")
|
|
123
|
+
def vs_status(name: str = typer.Argument(help="Virtual Service name")) -> None:
|
|
124
|
+
"""Show Virtual Service status details."""
|
|
125
|
+
from vmware_avi.ops.vs_mgmt import show_vs_status
|
|
126
|
+
|
|
127
|
+
show_vs_status(name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@vs_app.command("enable")
|
|
131
|
+
def vs_enable(name: str = typer.Argument(help="Virtual Service name")) -> None:
|
|
132
|
+
"""Enable a Virtual Service."""
|
|
133
|
+
from vmware_avi.ops.vs_mgmt import toggle_vs
|
|
134
|
+
|
|
135
|
+
toggle_vs(name, enable=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@vs_app.command("disable")
|
|
139
|
+
def vs_disable(name: str = typer.Argument(help="Virtual Service name")) -> None:
|
|
140
|
+
"""Disable a Virtual Service (requires confirmation)."""
|
|
141
|
+
from vmware_avi.ops.vs_mgmt import toggle_vs
|
|
142
|
+
|
|
143
|
+
toggle_vs(name, enable=False)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --- Pool commands ---
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pool_app.command("members")
|
|
150
|
+
def pool_members(pool: str = typer.Argument(help="Pool name")) -> None:
|
|
151
|
+
"""List pool members and health status."""
|
|
152
|
+
from vmware_avi.ops.pool_mgmt import list_pool_members
|
|
153
|
+
|
|
154
|
+
list_pool_members(pool)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pool_app.command("enable")
|
|
158
|
+
def pool_enable(
|
|
159
|
+
pool: str = typer.Argument(help="Pool name"),
|
|
160
|
+
server: str = typer.Argument(help="Server IP"),
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Enable a pool member (restore traffic)."""
|
|
163
|
+
from vmware_avi.ops.pool_mgmt import toggle_pool_member
|
|
164
|
+
|
|
165
|
+
toggle_pool_member(pool, server, enable=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pool_app.command("disable")
|
|
169
|
+
def pool_disable(
|
|
170
|
+
pool: str = typer.Argument(help="Pool name"),
|
|
171
|
+
server: str = typer.Argument(help="Server IP"),
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Disable a pool member (graceful drain, requires confirmation)."""
|
|
174
|
+
from vmware_avi.ops.pool_mgmt import toggle_pool_member
|
|
175
|
+
|
|
176
|
+
toggle_pool_member(pool, server, enable=False)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# --- SSL commands ---
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@ssl_app.command("list")
|
|
183
|
+
def ssl_list_cmd() -> None:
|
|
184
|
+
"""List all SSL certificates."""
|
|
185
|
+
from vmware_avi.ops.ssl_mgmt import list_certificates
|
|
186
|
+
|
|
187
|
+
list_certificates()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@ssl_app.command("expiry")
|
|
191
|
+
def ssl_expiry(
|
|
192
|
+
days: int = typer.Option(30, help="Show certs expiring within N days"),
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Check SSL certificate expiry."""
|
|
195
|
+
from vmware_avi.ops.ssl_mgmt import check_expiry
|
|
196
|
+
|
|
197
|
+
check_expiry(days)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# --- SE commands ---
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@se_app.command("list")
|
|
204
|
+
def se_list_cmd() -> None:
|
|
205
|
+
"""List all Service Engines."""
|
|
206
|
+
from vmware_avi.ops.se_mgmt import list_service_engines
|
|
207
|
+
|
|
208
|
+
list_service_engines()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@se_app.command("health")
|
|
212
|
+
def se_health() -> None:
|
|
213
|
+
"""Check Service Engine health."""
|
|
214
|
+
from vmware_avi.ops.se_mgmt import check_se_health
|
|
215
|
+
|
|
216
|
+
check_se_health()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --- Analytics commands ---
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("analytics")
|
|
223
|
+
def analytics_cmd(vs_name: str = typer.Argument(help="Virtual Service name")) -> None:
|
|
224
|
+
"""Show VS analytics (throughput, latency, errors)."""
|
|
225
|
+
from vmware_avi.ops.analytics import show_analytics
|
|
226
|
+
|
|
227
|
+
show_analytics(vs_name)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("logs")
|
|
231
|
+
def logs_cmd(
|
|
232
|
+
vs_name: str = typer.Argument(help="Virtual Service name"),
|
|
233
|
+
since: str = typer.Option("1h", help="Time range (e.g., 1h, 30m)"),
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Show VS request error logs."""
|
|
236
|
+
from vmware_avi.ops.analytics import show_error_logs
|
|
237
|
+
|
|
238
|
+
show_error_logs(vs_name, since)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- AKO commands ---
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@ako_app.command("status")
|
|
245
|
+
def ako_status(
|
|
246
|
+
context: str | None = typer.Option(None, help="K8s context"),
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Check AKO pod status."""
|
|
249
|
+
from vmware_avi.ops.ako_pod import check_ako_status
|
|
250
|
+
|
|
251
|
+
check_ako_status(context)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@ako_app.command("logs")
|
|
255
|
+
def ako_logs(
|
|
256
|
+
tail: int = typer.Option(100, help="Number of lines"),
|
|
257
|
+
since: str = typer.Option("", help="Time range (e.g., 30m, 1h)"),
|
|
258
|
+
context: str | None = typer.Option(None, help="K8s context"),
|
|
259
|
+
) -> None:
|
|
260
|
+
"""View AKO pod logs."""
|
|
261
|
+
from vmware_avi.ops.ako_pod import view_ako_logs
|
|
262
|
+
|
|
263
|
+
view_ako_logs(tail, since, context)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@ako_app.command("restart")
|
|
267
|
+
def ako_restart(
|
|
268
|
+
context: str | None = typer.Option(None, help="K8s context"),
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Restart AKO pod (requires confirmation)."""
|
|
271
|
+
from vmware_avi.ops.ako_pod import restart_ako
|
|
272
|
+
|
|
273
|
+
restart_ako(context)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@ako_app.command("version")
|
|
277
|
+
def ako_version(
|
|
278
|
+
context: str | None = typer.Option(None, help="K8s context"),
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Show AKO version info."""
|
|
281
|
+
from vmware_avi.ops.ako_pod import show_ako_version
|
|
282
|
+
|
|
283
|
+
show_ako_version(context)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# --- AKO config sub-commands (nested under ako) ---
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@ako_app.command("config-show")
|
|
290
|
+
def ako_config_show_cmd() -> None:
|
|
291
|
+
"""Show current AKO values.yaml."""
|
|
292
|
+
from vmware_avi.ops.ako_config import show_ako_config
|
|
293
|
+
|
|
294
|
+
show_ako_config()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@ako_app.command("config-diff")
|
|
298
|
+
def ako_config_diff_cmd() -> None:
|
|
299
|
+
"""Show pending Helm changes (diff)."""
|
|
300
|
+
from vmware_avi.ops.ako_config import diff_ako_config
|
|
301
|
+
|
|
302
|
+
diff_ako_config()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@ako_app.command("config-upgrade")
|
|
306
|
+
def ako_config_upgrade_cmd(
|
|
307
|
+
dry_run: bool = typer.Option(True, help="Preview only (default: true)"),
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Helm upgrade AKO (requires confirmation)."""
|
|
310
|
+
from vmware_avi.ops.ako_config import upgrade_ako
|
|
311
|
+
|
|
312
|
+
upgrade_ako(dry_run)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --- AKO ingress sub-commands ---
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@ako_app.command("ingress-check")
|
|
319
|
+
def ako_ingress_check_cmd(
|
|
320
|
+
namespace: str = typer.Argument(help="Namespace to check"),
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Validate Ingress annotations in a namespace."""
|
|
323
|
+
from vmware_avi.ops.ako_ingress import check_ingress_annotations
|
|
324
|
+
|
|
325
|
+
check_ingress_annotations(namespace)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@ako_app.command("ingress-map")
|
|
329
|
+
def ako_ingress_map_cmd() -> None:
|
|
330
|
+
"""Show Ingress to VS mapping."""
|
|
331
|
+
from vmware_avi.ops.ako_ingress import show_ingress_map
|
|
332
|
+
|
|
333
|
+
show_ingress_map()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@ako_app.command("ingress-diagnose")
|
|
337
|
+
def ako_ingress_diagnose_cmd(
|
|
338
|
+
name: str = typer.Argument(help="Ingress name"),
|
|
339
|
+
namespace: str = typer.Option("default", help="Namespace"),
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Diagnose why an Ingress has no VS."""
|
|
342
|
+
from vmware_avi.ops.ako_ingress import diagnose_ingress
|
|
343
|
+
|
|
344
|
+
diagnose_ingress(name, namespace)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- AKO sync sub-commands ---
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@ako_app.command("sync-status")
|
|
351
|
+
def ako_sync_status_cmd() -> None:
|
|
352
|
+
"""Check K8s-Controller sync status."""
|
|
353
|
+
from vmware_avi.ops.ako_sync import check_sync_status
|
|
354
|
+
|
|
355
|
+
check_sync_status()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@ako_app.command("sync-diff")
|
|
359
|
+
def ako_sync_diff_cmd() -> None:
|
|
360
|
+
"""Show K8s-Controller inconsistencies."""
|
|
361
|
+
from vmware_avi.ops.ako_sync import show_sync_diff
|
|
362
|
+
|
|
363
|
+
show_sync_diff()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@ako_app.command("sync-force")
|
|
367
|
+
def ako_sync_force_cmd() -> None:
|
|
368
|
+
"""Force AKO resync (requires confirmation)."""
|
|
369
|
+
from vmware_avi.ops.ako_sync import force_resync
|
|
370
|
+
|
|
371
|
+
force_resync()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# --- AKO multi-cluster ---
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@ako_app.command("clusters")
|
|
378
|
+
def ako_clusters_cmd() -> None:
|
|
379
|
+
"""List all clusters with AKO deployed."""
|
|
380
|
+
from vmware_avi.ops.ako_multi_cluster import list_clusters
|
|
381
|
+
|
|
382
|
+
list_clusters()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@ako_app.command("amko-status")
|
|
386
|
+
def ako_amko_status_cmd() -> None:
|
|
387
|
+
"""Show AMKO (multi-cluster GSLB) status."""
|
|
388
|
+
from vmware_avi.ops.ako_multi_cluster import show_amko_status
|
|
389
|
+
|
|
390
|
+
show_amko_status()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
app()
|
vmware_avi/config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Configuration management for VMware AVI.
|
|
2
|
+
|
|
3
|
+
Loads AVI Controller targets and AKO settings from YAML config + environment variables.
|
|
4
|
+
Passwords are NEVER stored in config files — always via environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import stat
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
|
|
18
|
+
CONFIG_DIR = Path.home() / ".vmware-avi"
|
|
19
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
20
|
+
ENV_FILE = CONFIG_DIR / ".env"
|
|
21
|
+
|
|
22
|
+
_log = logging.getLogger("vmware-avi.config")
|
|
23
|
+
|
|
24
|
+
load_dotenv(ENV_FILE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_env_permissions() -> None:
|
|
28
|
+
"""Warn if .env file has permissions wider than owner-only (600)."""
|
|
29
|
+
if not ENV_FILE.exists():
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
mode = ENV_FILE.stat().st_mode
|
|
33
|
+
if mode & (stat.S_IRWXG | stat.S_IRWXO):
|
|
34
|
+
_log.warning(
|
|
35
|
+
"Security warning: %s has permissions %s (should be 600). "
|
|
36
|
+
"Run: chmod 600 %s",
|
|
37
|
+
ENV_FILE,
|
|
38
|
+
oct(stat.S_IMODE(mode)),
|
|
39
|
+
ENV_FILE,
|
|
40
|
+
)
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_check_env_permissions()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ControllerConfig:
|
|
50
|
+
"""An AVI Controller connection target."""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
host: str
|
|
54
|
+
username: str = "admin"
|
|
55
|
+
api_version: str = "22.1.4"
|
|
56
|
+
tenant: str = "admin"
|
|
57
|
+
port: int = 443
|
|
58
|
+
verify_ssl: bool = True
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def password(self) -> str:
|
|
62
|
+
env_key = f"{self.name.upper().replace('-', '_')}_PASSWORD"
|
|
63
|
+
pw = os.environ.get(env_key, "")
|
|
64
|
+
if not pw:
|
|
65
|
+
raise OSError(
|
|
66
|
+
f"Password not found. Set environment variable: {env_key}"
|
|
67
|
+
)
|
|
68
|
+
return pw
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class AkoConfig:
|
|
73
|
+
"""AKO (Avi Kubernetes Operator) connection settings."""
|
|
74
|
+
|
|
75
|
+
kubeconfig: str = str(Path.home() / ".kube" / "config")
|
|
76
|
+
default_context: str = ""
|
|
77
|
+
namespace: str = "avi-system"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class AppConfig:
|
|
82
|
+
"""Top-level application config."""
|
|
83
|
+
|
|
84
|
+
controllers: tuple[ControllerConfig, ...] = ()
|
|
85
|
+
default_controller: str = ""
|
|
86
|
+
ako: AkoConfig = field(default_factory=AkoConfig)
|
|
87
|
+
|
|
88
|
+
def get_controller(self, name: str) -> ControllerConfig:
|
|
89
|
+
for c in self.controllers:
|
|
90
|
+
if c.name == name:
|
|
91
|
+
return c
|
|
92
|
+
available = ", ".join(c.name for c in self.controllers)
|
|
93
|
+
raise KeyError(f"Controller '{name}' not found. Available: {available}")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def active_controller(self) -> ControllerConfig:
|
|
97
|
+
if self.default_controller:
|
|
98
|
+
return self.get_controller(self.default_controller)
|
|
99
|
+
if not self.controllers:
|
|
100
|
+
raise ValueError("No controllers configured. Check config.yaml")
|
|
101
|
+
return self.controllers[0]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_config(config_path: Path | None = None) -> AppConfig:
|
|
105
|
+
"""Load config from YAML file, with env var overrides for passwords."""
|
|
106
|
+
path = config_path or CONFIG_FILE
|
|
107
|
+
if not path.exists():
|
|
108
|
+
raise FileNotFoundError(
|
|
109
|
+
f"Config file not found: {path}\n"
|
|
110
|
+
f"Copy config.example.yaml to {CONFIG_FILE} and edit it."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with open(path) as f:
|
|
114
|
+
raw = yaml.safe_load(f) or {}
|
|
115
|
+
|
|
116
|
+
controllers = tuple(
|
|
117
|
+
ControllerConfig(
|
|
118
|
+
name=c["name"],
|
|
119
|
+
host=c["host"],
|
|
120
|
+
username=c.get("username", "admin"),
|
|
121
|
+
api_version=c.get("api_version", "22.1.4"),
|
|
122
|
+
tenant=c.get("tenant", "admin"),
|
|
123
|
+
port=c.get("port", 443),
|
|
124
|
+
verify_ssl=c.get("verify_ssl", True),
|
|
125
|
+
)
|
|
126
|
+
for c in raw.get("controllers", [])
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
ako_raw = raw.get("ako", {})
|
|
130
|
+
ako = AkoConfig(
|
|
131
|
+
kubeconfig=ako_raw.get("kubeconfig", str(Path.home() / ".kube" / "config")),
|
|
132
|
+
default_context=ako_raw.get("default_context", ""),
|
|
133
|
+
namespace=ako_raw.get("namespace", "avi-system"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return AppConfig(
|
|
137
|
+
controllers=controllers,
|
|
138
|
+
default_controller=raw.get("default_controller", ""),
|
|
139
|
+
ako=ako,
|
|
140
|
+
)
|
vmware_avi/connection.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""AVI Controller connection management via avisdk.
|
|
2
|
+
|
|
3
|
+
Handles multi-controller connections with session reuse.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from avi.sdk.avi_api import ApiSession
|
|
13
|
+
|
|
14
|
+
from vmware_avi.config import AppConfig, ControllerConfig, load_config
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("vmware-avi.connection")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AviConnectionManager:
|
|
20
|
+
"""Manages connections to multiple AVI Controllers."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: AppConfig) -> None:
|
|
23
|
+
self._config = config
|
|
24
|
+
self._sessions: dict[str, ApiSession] = {}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_config(cls, config: AppConfig | None = None) -> AviConnectionManager:
|
|
28
|
+
cfg = config or load_config()
|
|
29
|
+
return cls(cfg)
|
|
30
|
+
|
|
31
|
+
def connect(self, controller_name: str | None = None) -> ApiSession:
|
|
32
|
+
"""Connect to a controller by name, or the active controller."""
|
|
33
|
+
ctrl = (
|
|
34
|
+
self._config.get_controller(controller_name)
|
|
35
|
+
if controller_name
|
|
36
|
+
else self._config.active_controller
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if ctrl.name in self._sessions:
|
|
40
|
+
session = self._sessions[ctrl.name]
|
|
41
|
+
try:
|
|
42
|
+
session.get("cluster/runtime")
|
|
43
|
+
return session
|
|
44
|
+
except Exception:
|
|
45
|
+
del self._sessions[ctrl.name]
|
|
46
|
+
|
|
47
|
+
session = self._create_session(ctrl)
|
|
48
|
+
self._sessions[ctrl.name] = session
|
|
49
|
+
return session
|
|
50
|
+
|
|
51
|
+
def disconnect(self, controller_name: str) -> None:
|
|
52
|
+
if controller_name in self._sessions:
|
|
53
|
+
try:
|
|
54
|
+
self._sessions[controller_name].delete("logout")
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
del self._sessions[controller_name]
|
|
58
|
+
|
|
59
|
+
def disconnect_all(self) -> None:
|
|
60
|
+
for name in list(self._sessions):
|
|
61
|
+
self.disconnect(name)
|
|
62
|
+
|
|
63
|
+
def list_controllers(self) -> list[str]:
|
|
64
|
+
return [c.name for c in self._config.controllers]
|
|
65
|
+
|
|
66
|
+
def list_connected(self) -> list[str]:
|
|
67
|
+
return list(self._sessions.keys())
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _create_session(ctrl: ControllerConfig) -> ApiSession:
|
|
71
|
+
from avi.sdk.avi_api import ApiSession
|
|
72
|
+
|
|
73
|
+
_log.info("Connecting to AVI Controller: %s (%s)", ctrl.name, ctrl.host)
|
|
74
|
+
return ApiSession.get_session(
|
|
75
|
+
controller_ip=ctrl.host,
|
|
76
|
+
username=ctrl.username,
|
|
77
|
+
password=ctrl.password,
|
|
78
|
+
api_version=ctrl.api_version,
|
|
79
|
+
tenant=ctrl.tenant,
|
|
80
|
+
port=ctrl.port,
|
|
81
|
+
)
|