clonebox 0.1.25__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.
clonebox/detector.py ADDED
@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SystemDetector - Detects running services, applications and important paths.
4
+ """
5
+
6
+ import os
7
+ import pwd
8
+ import subprocess
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ import psutil
13
+
14
+
15
+ @dataclass
16
+ class DetectedService:
17
+ """A detected systemd service."""
18
+
19
+ name: str
20
+ status: str # running, stopped, failed
21
+ description: str = ""
22
+ enabled: bool = False
23
+
24
+
25
+ @dataclass
26
+ class DetectedApplication:
27
+ """A detected running application."""
28
+
29
+ name: str
30
+ pid: int
31
+ cmdline: str
32
+ exe: str
33
+ working_dir: str = ""
34
+ memory_mb: float = 0.0
35
+
36
+
37
+ @dataclass
38
+ class DetectedPath:
39
+ """A detected important path."""
40
+
41
+ path: str
42
+ type: str # config, data, project, home
43
+ size_mb: float = 0.0
44
+ description: str = ""
45
+
46
+
47
+ @dataclass
48
+ class SystemSnapshot:
49
+ """Complete snapshot of detected system state."""
50
+
51
+ services: list = field(default_factory=list)
52
+ applications: list = field(default_factory=list)
53
+ paths: list = field(default_factory=list)
54
+
55
+ @property
56
+ def running_services(self) -> list:
57
+ return [s for s in self.services if s.status == "running"]
58
+
59
+ @property
60
+ def running_apps(self) -> list:
61
+ return self.applications
62
+
63
+
64
+ class SystemDetector:
65
+ """Detects running services, applications and important paths on the system."""
66
+
67
+ # Services that should NOT be cloned to VM (host-specific, hardware-dependent, or hypervisor services)
68
+ VM_EXCLUDED_SERVICES = {
69
+ # Hypervisor/virtualization - no nested virt needed
70
+ "libvirtd",
71
+ "virtlogd",
72
+ "libvirt-guests",
73
+ "qemu-guest-agent", # Host-side, VM has its own
74
+ # Hardware-specific
75
+ "bluetooth",
76
+ "bluez",
77
+ "upower",
78
+ "thermald",
79
+ "tlp",
80
+ "power-profiles-daemon",
81
+ # Display manager (VM has its own)
82
+ "gdm",
83
+ "gdm3",
84
+ "sddm",
85
+ "lightdm",
86
+ # Snap-based duplicates (prefer APT versions)
87
+ "snap.cups.cups-browsed",
88
+ "snap.cups.cupsd",
89
+ # Network hardware
90
+ "ModemManager",
91
+ "wpa_supplicant",
92
+ # Host-specific desktop
93
+ "accounts-daemon",
94
+ "colord",
95
+ "switcheroo-control",
96
+ }
97
+
98
+ # Common development/server services to look for
99
+ INTERESTING_SERVICES = [
100
+ "docker",
101
+ "containerd",
102
+ "podman",
103
+ "nginx",
104
+ "apache2",
105
+ "httpd",
106
+ "caddy",
107
+ "postgresql",
108
+ "mysql",
109
+ "mariadb",
110
+ "mongodb",
111
+ "redis",
112
+ "memcached",
113
+ "elasticsearch",
114
+ "kibana",
115
+ "grafana",
116
+ "prometheus",
117
+ "jenkins",
118
+ "gitlab-runner",
119
+ "sshd",
120
+ "rsync",
121
+ "rabbitmq-server",
122
+ "kafka",
123
+ "nodejs",
124
+ "pm2",
125
+ "supervisor",
126
+ "systemd-resolved",
127
+ "cups",
128
+ "bluetooth",
129
+ "NetworkManager",
130
+ "libvirtd",
131
+ "virtlogd",
132
+ ]
133
+
134
+ # Interesting process names
135
+ INTERESTING_PROCESSES = [
136
+ "python",
137
+ "python3",
138
+ "node",
139
+ "npm",
140
+ "yarn",
141
+ "pnpm",
142
+ "java",
143
+ "gradle",
144
+ "mvn",
145
+ "go",
146
+ "cargo",
147
+ "rustc",
148
+ "docker",
149
+ "docker-compose",
150
+ "podman",
151
+ "nginx",
152
+ "apache",
153
+ "httpd",
154
+ "postgres",
155
+ "mysql",
156
+ "mongod",
157
+ "redis-server",
158
+ "code",
159
+ "code-server",
160
+ "cursor",
161
+ "vim",
162
+ "nvim",
163
+ "emacs",
164
+ "firefox",
165
+ "chrome",
166
+ "chromium",
167
+ "jupyter",
168
+ "jupyter-lab",
169
+ "gunicorn",
170
+ "uvicorn",
171
+ "flask",
172
+ "django",
173
+ "webpack",
174
+ "vite",
175
+ "esbuild",
176
+ "tmux",
177
+ "screen",
178
+ # IDEs and desktop apps
179
+ "pycharm",
180
+ "idea",
181
+ "webstorm",
182
+ "phpstorm",
183
+ "goland",
184
+ "clion",
185
+ "rider",
186
+ "datagrip",
187
+ "sublime",
188
+ "atom",
189
+ "slack",
190
+ "discord",
191
+ "telegram",
192
+ "spotify",
193
+ "vlc",
194
+ "gimp",
195
+ "inkscape",
196
+ "blender",
197
+ "obs",
198
+ "postman",
199
+ "insomnia",
200
+ "dbeaver",
201
+ ]
202
+
203
+ # Map process/service names to Ubuntu packages or snap packages
204
+ # Format: "process_name": ("package_name", "install_type") where install_type is "apt" or "snap"
205
+ APP_TO_PACKAGE_MAP = {
206
+ "python": ("python3", "apt"),
207
+ "python3": ("python3", "apt"),
208
+ "pip": ("python3-pip", "apt"),
209
+ "node": ("nodejs", "apt"),
210
+ "npm": ("npm", "apt"),
211
+ "yarn": ("yarnpkg", "apt"),
212
+ "docker": ("docker.io", "apt"),
213
+ "dockerd": ("docker.io", "apt"),
214
+ "docker-compose": ("docker-compose", "apt"),
215
+ "podman": ("podman", "apt"),
216
+ "nginx": ("nginx", "apt"),
217
+ "apache2": ("apache2", "apt"),
218
+ "httpd": ("apache2", "apt"),
219
+ "postgres": ("postgresql", "apt"),
220
+ "postgresql": ("postgresql", "apt"),
221
+ "mysql": ("mysql-server", "apt"),
222
+ "mysqld": ("mysql-server", "apt"),
223
+ "mongod": ("mongodb", "apt"),
224
+ "mongodb": ("mongodb", "apt"),
225
+ "redis-server": ("redis-server", "apt"),
226
+ "redis": ("redis-server", "apt"),
227
+ "vim": ("vim", "apt"),
228
+ "nvim": ("neovim", "apt"),
229
+ "emacs": ("emacs", "apt"),
230
+ "firefox": ("firefox", "apt"),
231
+ "chromium": ("chromium-browser", "apt"),
232
+ "jupyter": ("jupyter-notebook", "apt"),
233
+ "jupyter-lab": ("jupyterlab", "apt"),
234
+ "gunicorn": ("gunicorn", "apt"),
235
+ "uvicorn": ("uvicorn", "apt"),
236
+ "tmux": ("tmux", "apt"),
237
+ "screen": ("screen", "apt"),
238
+ "git": ("git", "apt"),
239
+ "curl": ("curl", "apt"),
240
+ "wget": ("wget", "apt"),
241
+ "ssh": ("openssh-client", "apt"),
242
+ "sshd": ("openssh-server", "apt"),
243
+ "go": ("golang", "apt"),
244
+ "cargo": ("cargo", "apt"),
245
+ "rustc": ("rustc", "apt"),
246
+ "java": ("default-jdk", "apt"),
247
+ "gradle": ("gradle", "apt"),
248
+ "mvn": ("maven", "apt"),
249
+ # Popular desktop apps (snap packages)
250
+ "chrome": ("chromium", "snap"),
251
+ "google-chrome": ("chromium", "snap"),
252
+ "pycharm": ("pycharm-community", "snap"),
253
+ "idea": ("intellij-idea-community", "snap"),
254
+ "code": ("code", "snap"),
255
+ "vscode": ("code", "snap"),
256
+ "slack": ("slack", "snap"),
257
+ "discord": ("discord", "snap"),
258
+ "spotify": ("spotify", "snap"),
259
+ "vlc": ("vlc", "apt"),
260
+ "gimp": ("gimp", "apt"),
261
+ "inkscape": ("inkscape", "apt"),
262
+ "blender": ("blender", "apt"),
263
+ "obs": ("obs-studio", "apt"),
264
+ "telegram": ("telegram-desktop", "snap"),
265
+ "postman": ("postman", "snap"),
266
+ "insomnia": ("insomnia", "snap"),
267
+ "dbeaver": ("dbeaver-ce", "snap"),
268
+ "sublime": ("sublime-text", "snap"),
269
+ "atom": ("atom", "snap"),
270
+ }
271
+
272
+ # Map applications to their config/data directories for complete cloning
273
+ # These directories contain user settings, extensions, profiles, credentials
274
+ APP_DATA_DIRS = {
275
+ # Browsers - profiles, extensions, bookmarks, passwords
276
+ "chrome": [".config/google-chrome", ".config/chromium"],
277
+ "chromium": [".config/chromium"],
278
+ "firefox": [
279
+ "snap/firefox/common/.mozilla/firefox",
280
+ "snap/firefox/common/.cache/mozilla/firefox",
281
+ ".mozilla/firefox",
282
+ ".cache/mozilla/firefox",
283
+ ],
284
+
285
+ # IDEs and editors - settings, extensions, projects history
286
+ "code": [".config/Code", ".vscode", ".vscode-server"],
287
+ "vscode": [".config/Code", ".vscode", ".vscode-server"],
288
+ "pycharm": [
289
+ "snap/pycharm-community/common/.config/JetBrains",
290
+ "snap/pycharm-community/common/.local/share/JetBrains",
291
+ "snap/pycharm-community/common/.cache/JetBrains",
292
+ ".config/JetBrains",
293
+ ".local/share/JetBrains",
294
+ ".cache/JetBrains",
295
+ ],
296
+ "idea": [".config/JetBrains", ".local/share/JetBrains"],
297
+ "webstorm": [".config/JetBrains", ".local/share/JetBrains"],
298
+ "goland": [".config/JetBrains", ".local/share/JetBrains"],
299
+ "sublime": [".config/sublime-text", ".config/sublime-text-3"],
300
+ "atom": [".atom"],
301
+ "vim": [".vim", ".vimrc", ".config/nvim"],
302
+ "nvim": [".config/nvim", ".local/share/nvim"],
303
+ "emacs": [".emacs.d", ".emacs"],
304
+ "cursor": [".config/Cursor", ".cursor"],
305
+
306
+ # Development tools
307
+ "docker": [".docker"],
308
+ "git": [".gitconfig", ".git-credentials", ".config/git"],
309
+ "npm": [".npm", ".npmrc"],
310
+ "yarn": [".yarn", ".yarnrc"],
311
+ "pip": [".pip", ".config/pip"],
312
+ "cargo": [".cargo"],
313
+ "rustup": [".rustup"],
314
+ "go": [".go", "go"],
315
+ "gradle": [".gradle"],
316
+ "maven": [".m2"],
317
+
318
+ # Python environments
319
+ "python": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
320
+ "python3": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
321
+ "conda": [".conda", "anaconda3", "miniconda3"],
322
+
323
+ # Node.js
324
+ "node": [".nvm", ".node", ".npm"],
325
+
326
+ # Databases
327
+ "postgres": [".pgpass", ".psqlrc", ".psql_history"],
328
+ "mysql": [".my.cnf", ".mysql_history"],
329
+ "mongodb": [".mongorc.js", ".dbshell"],
330
+ "redis": [".rediscli_history"],
331
+
332
+ # Communication apps
333
+ "slack": [".config/Slack"],
334
+ "discord": [".config/discord"],
335
+ "telegram": [".local/share/TelegramDesktop"],
336
+ "teams": [".config/Microsoft/Microsoft Teams"],
337
+
338
+ # Other tools
339
+ "postman": [".config/Postman"],
340
+ "insomnia": [".config/Insomnia"],
341
+ "dbeaver": [".local/share/DBeaverData"],
342
+ "ssh": [".ssh"],
343
+ "gpg": [".gnupg"],
344
+ "aws": [".aws"],
345
+ "gcloud": [".config/gcloud"],
346
+ "kubectl": [".kube"],
347
+ "terraform": [".terraform.d"],
348
+ "ansible": [".ansible"],
349
+
350
+ # General app data
351
+ "spotify": [".config/spotify"],
352
+ "vlc": [".config/vlc"],
353
+ "gimp": [".config/GIMP", ".gimp-2.10"],
354
+ "obs": [".config/obs-studio"],
355
+ }
356
+
357
+ def __init__(self):
358
+ self.user = pwd.getpwuid(os.getuid()).pw_name
359
+ self.home = Path.home()
360
+
361
+ def detect_app_data_dirs(self, applications: list) -> list:
362
+ """Detect config/data directories for running applications.
363
+
364
+ Returns list of paths that contain user data needed by running apps.
365
+ """
366
+ app_data_paths = []
367
+ seen_paths = set()
368
+
369
+ matched_patterns = set()
370
+
371
+ for app in applications:
372
+ app_name = app.name.lower()
373
+
374
+ for pattern in self.APP_DATA_DIRS:
375
+ if pattern in app_name:
376
+ matched_patterns.add(pattern)
377
+
378
+ for pattern in ("firefox", "chrome", "chromium", "pycharm"):
379
+ matched_patterns.add(pattern)
380
+
381
+ for pattern in sorted(matched_patterns):
382
+ dirs = self.APP_DATA_DIRS.get(pattern, [])
383
+ if not dirs:
384
+ continue
385
+
386
+ snap_dirs = [d for d in dirs if d.startswith("snap/")]
387
+ preferred_dirs = snap_dirs if any((self.home / d).exists() for d in snap_dirs) else dirs
388
+
389
+ for dir_name in preferred_dirs:
390
+ full_path = self.home / dir_name
391
+ if full_path.exists() and str(full_path) not in seen_paths:
392
+ seen_paths.add(str(full_path))
393
+ try:
394
+ size = self._get_dir_size(full_path, max_depth=2)
395
+ except Exception:
396
+ size = 0
397
+ app_data_paths.append({
398
+ "path": str(full_path),
399
+ "app": pattern,
400
+ "type": "app_data",
401
+ "size_mb": round(size / 1024 / 1024, 1),
402
+ })
403
+
404
+ return app_data_paths
405
+
406
+ def detect_all(self) -> SystemSnapshot:
407
+ """Detect all services, applications and paths."""
408
+ return SystemSnapshot(
409
+ services=self.detect_services(),
410
+ applications=self.detect_applications(),
411
+ paths=self.detect_paths(),
412
+ )
413
+
414
+ def detect_services(self) -> list:
415
+ """Detect systemd services."""
416
+ services = []
417
+
418
+ try:
419
+ # Get all services
420
+ result = subprocess.run(
421
+ ["systemctl", "list-units", "--type=service", "--all", "--no-pager", "--plain"],
422
+ capture_output=True,
423
+ text=True,
424
+ timeout=10,
425
+ )
426
+
427
+ for line in result.stdout.strip().split("\n")[1:]: # Skip header
428
+ parts = line.split()
429
+ if len(parts) >= 4:
430
+ name = parts[0].replace(".service", "")
431
+
432
+ # Filter to interesting services
433
+ if any(
434
+ interesting in name.lower() for interesting in self.INTERESTING_SERVICES
435
+ ):
436
+ status = "running" if parts[3] == "running" else parts[3]
437
+
438
+ # Get description
439
+ desc = " ".join(parts[4:]) if len(parts) > 4 else ""
440
+
441
+ # Check if enabled
442
+ enabled = False
443
+ try:
444
+ en_result = subprocess.run(
445
+ ["systemctl", "is-enabled", name],
446
+ capture_output=True,
447
+ text=True,
448
+ timeout=5,
449
+ )
450
+ enabled = en_result.stdout.strip() == "enabled"
451
+ except:
452
+ pass
453
+
454
+ services.append(
455
+ DetectedService(
456
+ name=name, status=status, description=desc, enabled=enabled
457
+ )
458
+ )
459
+ except Exception:
460
+ pass
461
+
462
+ return services
463
+
464
+ def detect_applications(self) -> list:
465
+ """Detect running applications/processes."""
466
+ applications = []
467
+ seen_names = set()
468
+
469
+ for proc in psutil.process_iter(["pid", "name", "cmdline", "exe", "cwd", "memory_info"]):
470
+ try:
471
+ info = proc.info
472
+ name = info["name"] or ""
473
+
474
+ # Filter to interesting processes
475
+ if not any(
476
+ interesting in name.lower() for interesting in self.INTERESTING_PROCESSES
477
+ ):
478
+ continue
479
+
480
+ # Skip duplicates by name (keep first)
481
+ if name in seen_names:
482
+ continue
483
+ seen_names.add(name)
484
+
485
+ cmdline = " ".join(info["cmdline"] or [])[:200]
486
+ exe = info["exe"] or ""
487
+ cwd = info["cwd"] or ""
488
+ mem = (info["memory_info"].rss / 1024 / 1024) if info["memory_info"] else 0
489
+
490
+ applications.append(
491
+ DetectedApplication(
492
+ name=name,
493
+ pid=info["pid"],
494
+ cmdline=cmdline,
495
+ exe=exe,
496
+ working_dir=cwd,
497
+ memory_mb=round(mem, 1),
498
+ )
499
+ )
500
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
501
+ continue
502
+
503
+ # Sort by memory usage
504
+ applications.sort(key=lambda x: x.memory_mb, reverse=True)
505
+ return applications
506
+
507
+ def detect_paths(self) -> list:
508
+ """Detect important paths (projects, configs, data)."""
509
+ paths = []
510
+
511
+ # User home subdirectories
512
+ important_home_dirs = [
513
+ ("projects", "project"),
514
+ ("workspace", "project"),
515
+ ("code", "project"),
516
+ ("dev", "project"),
517
+ ("work", "project"),
518
+ ("repos", "project"),
519
+ ("github", "project"),
520
+ ("gitlab", "project"),
521
+ (".config", "config"),
522
+ (".local/share", "data"),
523
+ (".ssh", "config"),
524
+ (".docker", "config"),
525
+ (".kube", "config"),
526
+ (".npm", "config"),
527
+ (".cargo", "config"),
528
+ (".rustup", "data"),
529
+ (".pyenv", "data"),
530
+ (".nvm", "data"),
531
+ (".vscode", "config"),
532
+ ("Documents", "data"),
533
+ ("Downloads", "data"),
534
+ ]
535
+
536
+ for dirname, path_type in important_home_dirs:
537
+ full_path = self.home / dirname
538
+ if full_path.exists() and full_path.is_dir():
539
+ size = self._get_dir_size(full_path, max_depth=1)
540
+ paths.append(
541
+ DetectedPath(
542
+ path=str(full_path),
543
+ type=path_type,
544
+ size_mb=round(size / 1024 / 1024, 1),
545
+ description=f"User {dirname}",
546
+ )
547
+ )
548
+
549
+ # System paths that might be interesting
550
+ system_paths = [
551
+ ("/var/www", "data", "Web server root"),
552
+ ("/var/lib/docker", "data", "Docker data"),
553
+ ("/var/lib/postgresql", "data", "PostgreSQL data"),
554
+ ("/var/lib/mysql", "data", "MySQL data"),
555
+ ("/opt", "data", "Optional software"),
556
+ ("/etc/nginx", "config", "Nginx config"),
557
+ ("/etc/apache2", "config", "Apache config"),
558
+ ]
559
+
560
+ for path, path_type, desc in system_paths:
561
+ p = Path(path)
562
+ if p.exists():
563
+ size = self._get_dir_size(p, max_depth=1)
564
+ paths.append(
565
+ DetectedPath(
566
+ path=path,
567
+ type=path_type,
568
+ size_mb=round(size / 1024 / 1024, 1),
569
+ description=desc,
570
+ )
571
+ )
572
+
573
+ # Detect project directories (with .git, package.json, etc.)
574
+ project_markers = [
575
+ ".git",
576
+ "package.json",
577
+ "Cargo.toml",
578
+ "go.mod",
579
+ "pyproject.toml",
580
+ "setup.py",
581
+ ]
582
+ for search_dir in [
583
+ self.home / "projects",
584
+ self.home / "code",
585
+ self.home / "github",
586
+ self.home,
587
+ ]:
588
+ if search_dir.exists():
589
+ for item in search_dir.iterdir():
590
+ if item.is_dir() and not item.name.startswith("."):
591
+ for marker in project_markers:
592
+ if (item / marker).exists():
593
+ size = self._get_dir_size(item, max_depth=2)
594
+ if str(item) not in [p.path for p in paths]:
595
+ paths.append(
596
+ DetectedPath(
597
+ path=str(item),
598
+ type="project",
599
+ size_mb=round(size / 1024 / 1024, 1),
600
+ description=f"Project ({marker})",
601
+ )
602
+ )
603
+ break
604
+
605
+ # Sort by type then path
606
+ paths.sort(key=lambda x: (x.type, x.path))
607
+ return paths
608
+
609
+ def _get_dir_size(self, path: Path, max_depth: int = 2) -> int:
610
+ """Get approximate directory size in bytes."""
611
+ total = 0
612
+ if not path.exists():
613
+ return 0
614
+ try:
615
+ for item in path.iterdir():
616
+ if item.is_file():
617
+ try:
618
+ total += item.stat().st_size
619
+ except:
620
+ pass
621
+ elif item.is_dir() and max_depth > 0 and not item.is_symlink():
622
+ total += self._get_dir_size(item, max_depth - 1)
623
+ except (PermissionError, FileNotFoundError, OSError):
624
+ pass
625
+ return total
626
+
627
+ def detect_docker_containers(self) -> list:
628
+ """Detect running Docker containers."""
629
+ containers = []
630
+ try:
631
+ result = subprocess.run(
632
+ ["docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Status}}"],
633
+ capture_output=True,
634
+ text=True,
635
+ timeout=10,
636
+ )
637
+ for line in result.stdout.strip().split("\n"):
638
+ if line:
639
+ parts = line.split("\t")
640
+ if len(parts) >= 3:
641
+ containers.append({"name": parts[0], "image": parts[1], "status": parts[2]})
642
+ except:
643
+ pass
644
+ return containers
645
+
646
+ def suggest_packages_for_apps(self, applications: list) -> dict:
647
+ """Suggest packages based on detected applications.
648
+
649
+ Returns:
650
+ dict with 'apt' and 'snap' keys containing lists of packages
651
+ """
652
+ apt_packages = set()
653
+ snap_packages = set()
654
+
655
+ for app in applications:
656
+ app_name = app.name.lower()
657
+ for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
658
+ if key in app_name:
659
+ if install_type == "snap":
660
+ snap_packages.add(package)
661
+ else:
662
+ apt_packages.add(package)
663
+ break
664
+
665
+ return {
666
+ "apt": sorted(list(apt_packages)),
667
+ "snap": sorted(list(snap_packages))
668
+ }
669
+
670
+ def suggest_packages_for_services(self, services: list) -> dict:
671
+ """Suggest packages based on detected services.
672
+
673
+ Returns:
674
+ dict with 'apt' and 'snap' keys containing lists of packages
675
+ """
676
+ apt_packages = set()
677
+ snap_packages = set()
678
+
679
+ for service in services:
680
+ service_name = service.name.lower()
681
+ for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
682
+ if key in service_name:
683
+ if install_type == "snap":
684
+ snap_packages.add(package)
685
+ else:
686
+ apt_packages.add(package)
687
+ break
688
+
689
+ return {
690
+ "apt": sorted(list(apt_packages)),
691
+ "snap": sorted(list(snap_packages))
692
+ }
693
+
694
+ def get_system_info(self) -> dict:
695
+ """Get basic system information."""
696
+ return {
697
+ "hostname": os.uname().nodename,
698
+ "user": self.user,
699
+ "home": str(self.home),
700
+ "cpu_count": psutil.cpu_count(),
701
+ "memory_total_gb": round(psutil.virtual_memory().total / 1024 / 1024 / 1024, 1),
702
+ "memory_available_gb": round(psutil.virtual_memory().available / 1024 / 1024 / 1024, 1),
703
+ "disk_total_gb": round(psutil.disk_usage("/").total / 1024 / 1024 / 1024, 1),
704
+ "disk_free_gb": round(psutil.disk_usage("/").free / 1024 / 1024 / 1024, 1),
705
+ }