lnp-devopscli 1.0.0__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/PKG-INFO +2 -1
  2. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/pyproject.toml +2 -1
  3. lnp_devopscli-1.0.3/scripts/dc_cli/groups/test.py +581 -0
  4. lnp_devopscli-1.0.3/scripts/dc_cli/groups/ui.py +171 -0
  5. lnp_devopscli-1.0.3/scripts/dc_cli/ui/__init__.py +18 -0
  6. lnp_devopscli-1.0.3/scripts/dc_cli/ui/dashboard.py +371 -0
  7. lnp_devopscli-1.0.3/scripts/dc_cli/ui/events.py +88 -0
  8. lnp_devopscli-1.0.3/scripts/dc_cli/ui/themes.py +22 -0
  9. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/PKG-INFO +2 -1
  10. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/SOURCES.txt +6 -0
  11. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/requires.txt +1 -0
  12. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/README.md +0 -0
  13. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/__init__.py +0 -0
  14. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/groups/__init__.py +0 -0
  15. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/groups/bw.py +0 -0
  16. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/groups/gl.py +0 -0
  17. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/groups/ws.py +0 -0
  18. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/main.py +0 -0
  19. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/utils/__init__.py +0 -0
  20. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/utils/config.py +0 -0
  21. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/dc_cli/utils/gitlab.py +0 -0
  22. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/dependency_links.txt +0 -0
  23. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/entry_points.txt +0 -0
  24. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/scripts/lnp_devopscli.egg-info/top_level.txt +0 -0
  25. {lnp_devopscli-1.0.0 → lnp_devopscli-1.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lnp-devopscli
3
- Version: 1.0.0
3
+ Version: 1.0.3
4
4
  Summary: DevOps CLI — workspace sync (GDrive, git, GPG, systemd timers) + GitLab/Bitwarden tooling
5
5
  Author-email: Lucas Neves Pires <npires.lucas@gmail.com>
6
6
  License: MIT
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
24
24
  Requires-Dist: click>=8.0
25
25
  Requires-Dist: pyyaml>=6.0
26
26
  Requires-Dist: requests>=2.28
27
+ Requires-Dist: rich>=13.0
27
28
 
28
29
  # lnp-devopscli
29
30
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lnp-devopscli"
7
- version = "1.0.0"
7
+ version = "1.0.3"
8
8
  description = "DevOps CLI — workspace sync (GDrive, git, GPG, systemd timers) + GitLab/Bitwarden tooling"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "click>=8.0",
31
31
  "pyyaml>=6.0",
32
32
  "requests>=2.28",
33
+ "rich>=13.0",
33
34
  ]
34
35
 
35
36
  [project.urls]
@@ -0,0 +1,581 @@
1
+ """Grupo: dc test - Testing infrastructure (Multipass VM)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+
14
+ DEFAULT_VM_NAME = "dev-test"
15
+ DEFAULT_IMAGE = "24.04"
16
+ SNAPPED_SNAPSHOT_NAME = "fresh"
17
+ BOOTSTRAP_URL = (
18
+ "https://gitlab.com/-/snippets/6003334/raw/main/bootstrap.sh"
19
+ )
20
+
21
+
22
+ def _require_multipass() -> None:
23
+ if not shutil.which("multipass"):
24
+ click.echo(
25
+ "[ERRO] multipass nao instalado.\n"
26
+ " Instale com: sudo snap install multipass",
27
+ err=True,
28
+ )
29
+ sys.exit(1)
30
+
31
+
32
+ def _vm_exists(name: str) -> bool:
33
+ result = subprocess.run(
34
+ ["multipass", "info", name],
35
+ capture_output=True,
36
+ text=True,
37
+ )
38
+ return result.returncode == 0
39
+
40
+
41
+ def _vm_state(name: str) -> str:
42
+ """Return: running | stopped | not-found."""
43
+ result = subprocess.run(
44
+ ["multipass", "info", name, "--format", "csv"],
45
+ capture_output=True,
46
+ text=True,
47
+ )
48
+ if result.returncode != 0:
49
+ return "not-found"
50
+ for line in result.stdout.splitlines():
51
+ if line.startswith(name + ","):
52
+ parts = line.split(",")
53
+ if len(parts) > 1:
54
+ return parts[1].lower().strip()
55
+ return "unknown"
56
+
57
+
58
+ def _snapshot_exists(name: str, snapshot: str) -> bool:
59
+ result = subprocess.run(
60
+ ["multipass", "list", "--snapshots", "--format", "csv"],
61
+ capture_output=True,
62
+ text=True,
63
+ )
64
+ if result.returncode != 0:
65
+ return False
66
+ target = f"{name}.{snapshot}"
67
+ for line in result.stdout.splitlines():
68
+ if target in line:
69
+ return True
70
+ return False
71
+
72
+
73
+ @click.group(name="test")
74
+ def test_group():
75
+ """Testing infrastructure (Multipass VM, etc)."""
76
+
77
+
78
+ @test_group.group(name="vm")
79
+ def vm_group():
80
+ """Multipass VM management for isolated testing."""
81
+
82
+
83
+ # ─── vm create ──────────────────────────────────────────────────
84
+
85
+
86
+ @vm_group.command(name="create")
87
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
88
+ @click.option("--cpus", default=2, show_default=True, type=int)
89
+ @click.option("--memory", default="4G", show_default=True)
90
+ @click.option("--disk", default="20G", show_default=True)
91
+ @click.option("--image", default=DEFAULT_IMAGE, show_default=True)
92
+ @click.option(
93
+ "--gui/--no-gui",
94
+ default=False,
95
+ help="Pre-install X11 deps (xauth, firefox) for GUI via SSH -X",
96
+ )
97
+ @click.option(
98
+ "--snapshot/--no-snapshot",
99
+ default=True,
100
+ help="Take snapshot after baseline setup",
101
+ )
102
+ def vm_create_cmd(
103
+ name: str,
104
+ cpus: int,
105
+ memory: str,
106
+ disk: str,
107
+ image: str,
108
+ gui: bool,
109
+ snapshot: bool,
110
+ ) -> None:
111
+ """Create a baseline Ubuntu VM with apt deps pre-installed."""
112
+ _require_multipass()
113
+
114
+ if _vm_exists(name):
115
+ click.echo(f"[INFO] VM '{name}' ja existe. Use 'vm destroy' primeiro.")
116
+ sys.exit(1)
117
+
118
+ click.echo(f"▶ Launching VM '{name}' ({cpus} CPUs, {memory} RAM, {disk} disk)...")
119
+ subprocess.run(
120
+ [
121
+ "multipass",
122
+ "launch",
123
+ "--name",
124
+ name,
125
+ "--cpus",
126
+ str(cpus),
127
+ "--memory",
128
+ memory,
129
+ "--disk",
130
+ disk,
131
+ image,
132
+ ],
133
+ check=True,
134
+ )
135
+
136
+ click.echo("\n▶ Pre-installing apt deps...")
137
+ base_deps = [
138
+ "git",
139
+ "curl",
140
+ "wget",
141
+ "ca-certificates",
142
+ "python3",
143
+ "python3-pip",
144
+ "pipx",
145
+ "rclone",
146
+ "git-crypt",
147
+ "gnupg",
148
+ "libnotify-bin",
149
+ "whiptail",
150
+ "jq",
151
+ "sudo",
152
+ ]
153
+ if gui:
154
+ base_deps += ["xauth", "x11-apps", "firefox"]
155
+ click.echo(" (GUI mode: incluindo xauth + firefox)")
156
+
157
+ apt_cmd = "sudo apt-get update -qq && sudo apt-get install -y " + " ".join(base_deps)
158
+ subprocess.run(["multipass", "exec", name, "--", "bash", "-c", apt_cmd], check=True)
159
+
160
+ click.echo("\n▶ Setup pipx PATH...")
161
+ subprocess.run(
162
+ ["multipass", "exec", name, "--", "bash", "-c", "pipx ensurepath"],
163
+ check=False,
164
+ )
165
+
166
+ if snapshot:
167
+ click.echo(f"\n▶ Stopping VM for snapshot...")
168
+ subprocess.run(["multipass", "stop", name], check=True)
169
+
170
+ click.echo(f"▶ Taking snapshot '{SNAPPED_SNAPSHOT_NAME}'...")
171
+ subprocess.run(
172
+ ["multipass", "snapshot", name, "--name", SNAPPED_SNAPSHOT_NAME],
173
+ check=True,
174
+ )
175
+
176
+ click.echo(f"▶ Starting VM again...")
177
+ subprocess.run(["multipass", "start", name], check=True)
178
+
179
+ click.echo("\n✓ VM pronta!")
180
+ click.echo(f" Nome: {name}")
181
+ click.echo(f" Snapshot: {SNAPPED_SNAPSHOT_NAME}")
182
+ click.echo("\nProximos passos:")
183
+ click.echo(f" devopscli test vm bootstrap --name {name} # roda o bootstrap snippet")
184
+ click.echo(f" devopscli test vm shell --name {name} # abre shell")
185
+ click.echo(f" devopscli test vm reset --name {name} # restaura snapshot")
186
+
187
+
188
+ # ─── vm reset ───────────────────────────────────────────────────
189
+
190
+
191
+ @vm_group.command(name="reset")
192
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
193
+ @click.option(
194
+ "--snapshot",
195
+ default=SNAPPED_SNAPSHOT_NAME,
196
+ show_default=True,
197
+ help="Snapshot name to restore",
198
+ )
199
+ def vm_reset_cmd(name: str, snapshot: str) -> None:
200
+ """Restore VM to a snapshot (default: fresh baseline)."""
201
+ _require_multipass()
202
+
203
+ if not _vm_exists(name):
204
+ click.echo(f"[ERRO] VM '{name}' nao existe", err=True)
205
+ sys.exit(1)
206
+
207
+ if not _snapshot_exists(name, snapshot):
208
+ click.echo(
209
+ f"[ERRO] Snapshot '{name}.{snapshot}' nao existe", err=True
210
+ )
211
+ sys.exit(1)
212
+
213
+ click.echo(f"▶ Stopping VM '{name}'...")
214
+ subprocess.run(["multipass", "stop", name], check=False)
215
+
216
+ click.echo(f"▶ Restoring to snapshot '{snapshot}'...")
217
+ subprocess.run(
218
+ ["multipass", "restore", "--destructive", f"{name}.{snapshot}"],
219
+ check=True,
220
+ )
221
+
222
+ click.echo(f"▶ Starting VM...")
223
+ subprocess.run(["multipass", "start", name], check=True)
224
+
225
+ click.echo(f"✓ VM '{name}' restaurada para snapshot '{snapshot}'")
226
+
227
+
228
+ # ─── vm shell ───────────────────────────────────────────────────
229
+
230
+
231
+ @vm_group.command(name="shell")
232
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
233
+ def vm_shell_cmd(name: str) -> None:
234
+ """Open interactive shell in the VM."""
235
+ _require_multipass()
236
+
237
+ if _vm_state(name) != "running":
238
+ click.echo(f"▶ Starting VM '{name}'...")
239
+ subprocess.run(["multipass", "start", name], check=False)
240
+
241
+ subprocess.run(["multipass", "shell", name])
242
+
243
+
244
+ # ─── vm exec ────────────────────────────────────────────────────
245
+
246
+
247
+ @vm_group.command(name="exec")
248
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
249
+ @click.argument("command", nargs=-1, required=True)
250
+ def vm_exec_cmd(name: str, command: tuple[str, ...]) -> None:
251
+ """Execute a command in the VM."""
252
+ _require_multipass()
253
+
254
+ if _vm_state(name) != "running":
255
+ subprocess.run(["multipass", "start", name], check=False)
256
+
257
+ cmd_str = " ".join(command)
258
+ subprocess.run(["multipass", "exec", name, "--", "bash", "-c", cmd_str])
259
+
260
+
261
+ # ─── vm bootstrap ───────────────────────────────────────────────
262
+
263
+
264
+ @vm_group.command(name="bootstrap")
265
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
266
+ @click.option(
267
+ "--url",
268
+ default=BOOTSTRAP_URL,
269
+ show_default=True,
270
+ help="URL of bootstrap script to execute",
271
+ )
272
+ def vm_bootstrap_cmd(name: str, url: str) -> None:
273
+ """Run the bootstrap snippet inside the VM (full setup)."""
274
+ _require_multipass()
275
+
276
+ if _vm_state(name) != "running":
277
+ subprocess.run(["multipass", "start", name], check=False)
278
+
279
+ click.echo(f"▶ Executing bootstrap in VM '{name}'...")
280
+ click.echo(f" URL: {url}")
281
+ click.echo()
282
+
283
+ subprocess.run(
284
+ [
285
+ "multipass",
286
+ "exec",
287
+ name,
288
+ "--",
289
+ "bash",
290
+ "-c",
291
+ f"bash <(curl -fsSL {url})",
292
+ ]
293
+ )
294
+
295
+
296
+ # ─── vm mount ───────────────────────────────────────────────────
297
+
298
+
299
+ @vm_group.command(name="mount")
300
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
301
+ @click.option(
302
+ "--source",
303
+ default=str(Path.home() / "bin" / "devops-cli"),
304
+ show_default=True,
305
+ help="Local path to mount",
306
+ )
307
+ @click.option(
308
+ "--target",
309
+ default="/home/ubuntu/devops-cli",
310
+ show_default=True,
311
+ help="Path inside VM",
312
+ )
313
+ def vm_mount_cmd(name: str, source: str, target: str) -> None:
314
+ """Mount a local directory inside the VM (for development)."""
315
+ _require_multipass()
316
+
317
+ src_path = Path(source).expanduser().resolve()
318
+ if not src_path.exists():
319
+ click.echo(f"[ERRO] Source path nao existe: {src_path}", err=True)
320
+ sys.exit(1)
321
+
322
+ click.echo(f"▶ Mounting {src_path} -> {name}:{target}")
323
+ subprocess.run(
324
+ ["multipass", "mount", str(src_path), f"{name}:{target}"],
325
+ check=True,
326
+ )
327
+ click.echo(f"✓ Mounted. Dentro da VM, use:")
328
+ click.echo(f" cd {target} && pipx install -e .")
329
+ click.echo(f" # edita no host, testa na VM, sem ciclo de push")
330
+
331
+
332
+ # ─── vm info ────────────────────────────────────────────────────
333
+
334
+
335
+ @vm_group.command(name="info")
336
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
337
+ def vm_info_cmd(name: str) -> None:
338
+ """Show VM status, IP, snapshots."""
339
+ _require_multipass()
340
+ subprocess.run(["multipass", "info", name])
341
+ click.echo()
342
+ click.echo("Snapshots:")
343
+ subprocess.run(
344
+ ["multipass", "list", "--snapshots", "--filter", name],
345
+ check=False,
346
+ )
347
+
348
+
349
+ # ─── vm destroy ─────────────────────────────────────────────────
350
+
351
+
352
+ @vm_group.command(name="destroy")
353
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
354
+ @click.confirmation_option(prompt="Tem certeza? VM e todos snapshots serao deletados.")
355
+ def vm_destroy_cmd(name: str) -> None:
356
+ """Stop and permanently delete VM + snapshots."""
357
+ _require_multipass()
358
+
359
+ subprocess.run(["multipass", "stop", name], check=False)
360
+ subprocess.run(["multipass", "delete", name, "--purge"], check=False)
361
+ click.echo(f"✓ VM '{name}' destroyed.")
362
+
363
+
364
+ # ─── vm ssh ─────────────────────────────────────────────────────
365
+
366
+
367
+ def _get_vm_ip(name: str) -> str | None:
368
+ result = subprocess.run(
369
+ ["multipass", "info", name, "--format", "csv"],
370
+ capture_output=True,
371
+ text=True,
372
+ )
373
+ if result.returncode != 0:
374
+ return None
375
+ for line in result.stdout.splitlines():
376
+ if line.startswith(name + ","):
377
+ parts = line.split(",")
378
+ # CSV: Name,State,Ipv4,Image,Release,Cpu(s),Load,Disk usage,Memory usage,Mounts
379
+ if len(parts) > 2 and parts[2].strip():
380
+ return parts[2].strip()
381
+ return None
382
+
383
+
384
+ def _ensure_ssh_key_in_vm(name: str) -> None:
385
+ """Copia ~/.ssh/id_*.pub do host pra ~/.ssh/authorized_keys da VM."""
386
+ host_pubkeys = list(Path.home().joinpath(".ssh").glob("id_*.pub"))
387
+ if not host_pubkeys:
388
+ click.echo(
389
+ "[WARN] Nenhuma chave publica em ~/.ssh/id_*.pub — gera com ssh-keygen",
390
+ err=True,
391
+ )
392
+ return
393
+ pubkey_content = "\n".join(p.read_text().strip() for p in host_pubkeys)
394
+ subprocess.run(
395
+ ["multipass", "exec", name, "--", "bash", "-c", "mkdir -p ~/.ssh && chmod 700 ~/.ssh"],
396
+ check=False,
397
+ )
398
+ subprocess.run(
399
+ [
400
+ "multipass",
401
+ "exec",
402
+ name,
403
+ "--",
404
+ "bash",
405
+ "-c",
406
+ f"echo '{pubkey_content}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys",
407
+ ],
408
+ check=False,
409
+ )
410
+
411
+
412
+ @vm_group.command(name="ssh")
413
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
414
+ @click.option(
415
+ "--x11/--no-x11",
416
+ default=True,
417
+ help="Enable X11 forwarding (-X) for GUI apps inside VM",
418
+ )
419
+ @click.option(
420
+ "--cmd",
421
+ default=None,
422
+ help="Run a single command via SSH and exit (instead of interactive shell)",
423
+ )
424
+ def vm_ssh_cmd(name: str, x11: bool, cmd: str | None) -> None:
425
+ """SSH to VM with X11 forwarding (for GUI apps like browser, rclone OAuth)."""
426
+ _require_multipass()
427
+
428
+ if _vm_state(name) != "running":
429
+ click.echo(f"▶ Starting VM '{name}'...")
430
+ subprocess.run(["multipass", "start", name], check=False)
431
+ time.sleep(2)
432
+
433
+ ip = _get_vm_ip(name)
434
+ if not ip:
435
+ click.echo(f"[ERRO] Nao consegui pegar IP da VM '{name}'", err=True)
436
+ sys.exit(1)
437
+
438
+ click.echo(f"▶ Setting up SSH key in VM...")
439
+ _ensure_ssh_key_in_vm(name)
440
+
441
+ ssh_args = ["ssh"]
442
+ if x11:
443
+ ssh_args.append("-X")
444
+ click.echo(f" X11 forwarding: ENABLED (apps GUI abrem no host)")
445
+ ssh_args += [
446
+ "-o",
447
+ "StrictHostKeyChecking=no",
448
+ "-o",
449
+ "UserKnownHostsFile=/dev/null",
450
+ "-o",
451
+ "LogLevel=ERROR",
452
+ f"ubuntu@{ip}",
453
+ ]
454
+ if cmd:
455
+ ssh_args.append(cmd)
456
+
457
+ click.echo(f" Connecting to ubuntu@{ip}...")
458
+ click.echo()
459
+ subprocess.run(ssh_args)
460
+
461
+
462
+ # ─── vm rclone-setup ────────────────────────────────────────────
463
+
464
+
465
+ @vm_group.command(name="rclone-setup")
466
+ @click.option("--name", default=DEFAULT_VM_NAME, show_default=True)
467
+ @click.option(
468
+ "--remote",
469
+ default="gdrive",
470
+ show_default=True,
471
+ help="Name of rclone remote to create",
472
+ )
473
+ def vm_rclone_setup_cmd(name: str, remote: str) -> None:
474
+ """Setup rclone OAuth in VM without browser (uses host browser for auth)."""
475
+ _require_multipass()
476
+
477
+ if _vm_state(name) != "running":
478
+ subprocess.run(["multipass", "start", name], check=False)
479
+ time.sleep(2)
480
+
481
+ click.echo("""
482
+ ┌──────────────────────────────────────────────────────────────┐
483
+ │ Rclone OAuth setup (token vem do browser do host) │
484
+ └──────────────────────────────────────────────────────────────┘
485
+
486
+ Vou abrir o browser do HOST agora pra gerar o token Google Drive.
487
+ """)
488
+
489
+ click.echo("▶ Executando 'rclone authorize drive' no host...")
490
+ click.echo(" (cole o token retornado quando solicitado)")
491
+ click.echo()
492
+
493
+ # rclone authorize abre browser no host e imprime token JSON
494
+ result = subprocess.run(
495
+ ["rclone", "authorize", "drive"],
496
+ capture_output=True,
497
+ text=True,
498
+ )
499
+ if result.returncode != 0:
500
+ click.echo(f"[ERRO] rclone authorize falhou: {result.stderr}", err=True)
501
+ sys.exit(1)
502
+
503
+ # Extrair o token JSON da saida
504
+ token = None
505
+ for line in result.stdout.splitlines():
506
+ line = line.strip()
507
+ if line.startswith("{") and "access_token" in line:
508
+ token = line
509
+ break
510
+
511
+ if not token:
512
+ click.echo("[ERRO] Nao consegui extrair token da saida do rclone", err=True)
513
+ click.echo("Saida:")
514
+ click.echo(result.stdout)
515
+ sys.exit(1)
516
+
517
+ click.echo(f"✓ Token gerado.")
518
+ click.echo(f"▶ Importando para VM '{name}' como remote '{remote}'...")
519
+
520
+ rclone_cmd = (
521
+ f"rclone config create {remote} drive "
522
+ f"config_is_local=false "
523
+ f"scope=drive "
524
+ f"token='{token}'"
525
+ )
526
+ subprocess.run(
527
+ ["multipass", "exec", name, "--", "bash", "-c", rclone_cmd],
528
+ check=True,
529
+ )
530
+
531
+ click.echo(f"\n▶ Validando...")
532
+ subprocess.run(
533
+ ["multipass", "exec", name, "--", "rclone", "lsf", f"{remote}:", "--max-depth", "1"],
534
+ check=False,
535
+ )
536
+
537
+ click.echo(f"\n✓ Rclone configurado na VM. Remote: {remote}:")
538
+
539
+
540
+ # ─── vm gui-help ────────────────────────────────────────────────
541
+
542
+
543
+ @vm_group.command(name="gui-help")
544
+ def vm_gui_help_cmd() -> None:
545
+ """Show how to access GUI (X11 forwarding, browser for rclone OAuth)."""
546
+ click.echo("""
547
+ ┌──────────────────────────────────────────────────────────────┐
548
+ │ Acesso a GUI dentro da VM Multipass │
549
+ └──────────────────────────────────────────────────────────────┘
550
+
551
+ Opcao 1 — X11 forwarding via SSH (recomendado)
552
+ ───────────────────────────────────────────────
553
+ Pre-requisito: VM criada com --gui (instala xauth + firefox)
554
+
555
+ # 1. Pegar IP da VM
556
+ devopscli test vm info | grep IPv4
557
+
558
+ # 2. SSH com -X
559
+ ssh -X ubuntu@<IP-da-VM>
560
+
561
+ # 3. Rodar app GUI dentro da VM, janela abre no host
562
+ firefox &
563
+ rclone config # abre browser pra OAuth
564
+
565
+ Opcao 2 — rclone OAuth sem GUI (truque)
566
+ ────────────────────────────────────────
567
+ # No host (sua maquina), gera token:
568
+ rclone authorize "drive"
569
+ # Cola codigo no browser, retorna JSON token
570
+
571
+ # Na VM, importa direto:
572
+ rclone config create gdrive drive \\
573
+ config_is_local=false \\
574
+ token='<json colado>'
575
+
576
+ Opcao 3 — Desktop completo (VNC)
577
+ ─────────────────────────────────
578
+ Mais complexo, raramente necessario. Veja:
579
+ https://multipass.run/docs
580
+
581
+ """)