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 ADDED
@@ -0,0 +1,3 @@
1
+ """VMware AVI - AVI (NSX ALB) management and AKO Kubernetes operations."""
2
+
3
+ __version__ = "1.4.4"
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
+ )
@@ -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
+ )