wasm-cli 0.13.16__py3-none-any.whl → 0.14.1__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.
- wasm/__init__.py +1 -1
- wasm/cli/commands/backup.py +18 -10
- wasm/cli/commands/config.py +96 -0
- wasm/cli/commands/db.py +3 -2
- wasm/cli/commands/health.py +231 -0
- wasm/cli/commands/monitor.py +8 -3
- wasm/cli/commands/setup.py +51 -16
- wasm/cli/commands/store.py +20 -4
- wasm/cli/commands/webapp.py +158 -37
- wasm/cli/interactive.py +3 -2
- wasm/cli/parser.py +99 -18
- wasm/completions/_wasm +469 -32
- wasm/completions/wasm.bash +502 -21
- wasm/completions/wasm.fish +247 -4
- wasm/core/config.py +85 -1
- wasm/core/dependencies.py +51 -17
- wasm/core/exceptions.py +14 -0
- wasm/core/update_checker.py +66 -17
- wasm/core/utils.py +65 -3
- wasm/deployers/base.py +328 -286
- wasm/deployers/helpers/__init__.py +20 -0
- wasm/deployers/helpers/package_manager.py +183 -0
- wasm/deployers/helpers/path_resolver.py +188 -0
- wasm/deployers/helpers/prisma.py +130 -0
- wasm/deployers/vite.py +12 -0
- wasm/main.py +21 -5
- wasm/managers/backup_manager.py +179 -54
- wasm/managers/database/mysql.py +135 -47
- wasm/managers/database/postgres.py +85 -36
- wasm/managers/database/redis.py +14 -10
- wasm/managers/service_manager.py +91 -45
- wasm/monitor/__init__.py +15 -1
- wasm/monitor/ai_analyzer.py +1 -3
- wasm/monitor/process_monitor.py +135 -36
- wasm/monitor/threat_store.py +332 -0
- wasm/web/api/apps.py +6 -4
- wasm/web/api/backups.py +37 -17
- wasm/web/api/monitor.py +164 -10
- wasm/web/api/services.py +31 -25
- wasm/web/jobs.py +1 -1
- wasm/web/static/js/pages/monitor.js +69 -9
- wasm/web/websockets/router.py +8 -6
- wasm_cli-0.14.1.data/data/share/man/man1/wasm.1 +521 -0
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/METADATA +4 -2
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/RECORD +49 -41
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/WHEEL +1 -1
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/entry_points.txt +0 -0
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.1.dist-info}/top_level.txt +0 -0
wasm/__init__.py
CHANGED
wasm/cli/commands/backup.py
CHANGED
|
@@ -105,6 +105,7 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
105
105
|
include_env = not getattr(args, "no_env", False)
|
|
106
106
|
include_node_modules = getattr(args, "include_node_modules", False)
|
|
107
107
|
include_build = getattr(args, "include_build", False)
|
|
108
|
+
include_databases = getattr(args, "include_databases", False)
|
|
108
109
|
tags = getattr(args, "tags", None)
|
|
109
110
|
|
|
110
111
|
if not domain:
|
|
@@ -127,6 +128,7 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
127
128
|
include_env=include_env,
|
|
128
129
|
include_node_modules=include_node_modules,
|
|
129
130
|
include_build=include_build,
|
|
131
|
+
include_databases=include_databases,
|
|
130
132
|
tags=tag_list,
|
|
131
133
|
)
|
|
132
134
|
|
|
@@ -135,7 +137,13 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
135
137
|
logger.info(f" Size: {metadata.size_human}")
|
|
136
138
|
if metadata.git_commit:
|
|
137
139
|
logger.info(f" Commit: {metadata.git_commit} ({metadata.git_branch})")
|
|
138
|
-
|
|
140
|
+
if metadata.database_backups:
|
|
141
|
+
db_count = len(metadata.database_backups)
|
|
142
|
+
logger.info(f" Databases: {db_count} backed up")
|
|
143
|
+
for db_info in metadata.database_backups:
|
|
144
|
+
size_mb = db_info.get("size_bytes", 0) / (1024 * 1024)
|
|
145
|
+
logger.info(f" - {db_info['engine']}/{db_info['name']} ({size_mb:.1f} MB)")
|
|
146
|
+
|
|
139
147
|
return 0
|
|
140
148
|
|
|
141
149
|
except BackupError as e:
|
|
@@ -195,7 +203,7 @@ def _backup_list(args: Namespace, verbose: bool) -> int:
|
|
|
195
203
|
by_domain[backup.domain].append(backup)
|
|
196
204
|
|
|
197
205
|
for dom, dom_backups in by_domain.items():
|
|
198
|
-
logger.info(f"\n
|
|
206
|
+
logger.info(f"\n[{dom}]")
|
|
199
207
|
_print_backup_table(dom_backups, logger, indent=True)
|
|
200
208
|
|
|
201
209
|
return 0
|
|
@@ -226,7 +234,7 @@ def _print_backup_table(backups, logger, indent: bool = False):
|
|
|
226
234
|
desc_str = f" - {backup.description}"
|
|
227
235
|
|
|
228
236
|
logger.info(
|
|
229
|
-
f"{prefix}
|
|
237
|
+
f"{prefix}- {backup.id}: {backup.size_human}, "
|
|
230
238
|
f"{backup.age}{commit_str}{tags_str}{desc_str}"
|
|
231
239
|
)
|
|
232
240
|
|
|
@@ -359,17 +367,17 @@ def _backup_verify(args: Namespace, verbose: bool) -> int:
|
|
|
359
367
|
|
|
360
368
|
if result["valid"]:
|
|
361
369
|
logger.success("Backup is valid")
|
|
362
|
-
if result.get("
|
|
363
|
-
logger.info("
|
|
364
|
-
if result.get("
|
|
365
|
-
logger.info(f"
|
|
370
|
+
if result.get("checksum_ok"):
|
|
371
|
+
logger.info(" [OK] Checksum verified")
|
|
372
|
+
if result.get("files_ok"):
|
|
373
|
+
logger.info(f" [OK] Archive valid ({result.get('file_count', '?')} files)")
|
|
366
374
|
else:
|
|
367
375
|
logger.error("Backup is invalid")
|
|
368
376
|
for err in result["errors"]:
|
|
369
|
-
logger.error(f"
|
|
370
|
-
|
|
377
|
+
logger.error(f" [ERROR] {err}")
|
|
378
|
+
|
|
371
379
|
for warn in result.get("warnings", []):
|
|
372
|
-
logger.warning(f"
|
|
380
|
+
logger.warning(f" [WARN] {warn}")
|
|
373
381
|
|
|
374
382
|
return 0 if result["valid"] else 1
|
|
375
383
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config command handlers for WASM.
|
|
3
|
+
|
|
4
|
+
Commands for managing WASM configuration files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from argparse import Namespace
|
|
8
|
+
|
|
9
|
+
from wasm.core.config import Config, DEFAULT_CONFIG_PATH
|
|
10
|
+
from wasm.core.logger import Logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_config(args: Namespace) -> int:
|
|
14
|
+
"""
|
|
15
|
+
Handle config commands.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
args: Parsed arguments.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Exit code.
|
|
22
|
+
"""
|
|
23
|
+
action = args.action
|
|
24
|
+
|
|
25
|
+
handlers = {
|
|
26
|
+
"upgrade": _handle_upgrade,
|
|
27
|
+
"show": _handle_show,
|
|
28
|
+
"path": _handle_path,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
handler = handlers.get(action)
|
|
32
|
+
if handler:
|
|
33
|
+
return handler(args)
|
|
34
|
+
|
|
35
|
+
# No action specified, show help
|
|
36
|
+
logger = Logger(verbose=getattr(args, "verbose", False))
|
|
37
|
+
logger.info("Usage: wasm config <command>")
|
|
38
|
+
logger.info("")
|
|
39
|
+
logger.info("Commands:")
|
|
40
|
+
logger.info(" upgrade Upgrade config file with new defaults")
|
|
41
|
+
logger.info(" show Show current configuration")
|
|
42
|
+
logger.info(" path Show config file path")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle_upgrade(args: Namespace) -> int:
|
|
47
|
+
"""Handle config upgrade command."""
|
|
48
|
+
quiet = getattr(args, "quiet", False)
|
|
49
|
+
logger = Logger(verbose=getattr(args, "verbose", False))
|
|
50
|
+
|
|
51
|
+
if not quiet:
|
|
52
|
+
logger.info("Upgrading configuration file...")
|
|
53
|
+
|
|
54
|
+
config = Config()
|
|
55
|
+
result = config.upgrade()
|
|
56
|
+
|
|
57
|
+
if "error" in result:
|
|
58
|
+
if not quiet:
|
|
59
|
+
logger.error(f"Failed to upgrade config: {result['error']}")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
if result["upgraded"]:
|
|
63
|
+
if not quiet:
|
|
64
|
+
logger.success(f"Configuration upgraded! Added {len(result['added_keys'])} new option(s):")
|
|
65
|
+
for key in result["added_keys"][:10]: # Show first 10
|
|
66
|
+
logger.info(f" + {key}")
|
|
67
|
+
if len(result["added_keys"]) > 10:
|
|
68
|
+
logger.info(f" ... and {len(result['added_keys']) - 10} more")
|
|
69
|
+
else:
|
|
70
|
+
if not quiet:
|
|
71
|
+
logger.success("Configuration is already up to date")
|
|
72
|
+
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _handle_show(args: Namespace) -> int:
|
|
77
|
+
"""Handle config show command."""
|
|
78
|
+
import yaml
|
|
79
|
+
|
|
80
|
+
logger = Logger(verbose=getattr(args, "verbose", False))
|
|
81
|
+
config = Config()
|
|
82
|
+
|
|
83
|
+
logger.header("Current Configuration")
|
|
84
|
+
print(yaml.dump(config.to_dict(), default_flow_style=False, sort_keys=False))
|
|
85
|
+
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _handle_path(args: Namespace) -> int:
|
|
90
|
+
"""Handle config path command."""
|
|
91
|
+
logger = Logger(verbose=getattr(args, "verbose", False))
|
|
92
|
+
|
|
93
|
+
logger.key_value("Config file", str(DEFAULT_CONFIG_PATH))
|
|
94
|
+
logger.key_value("Exists", "Yes" if DEFAULT_CONFIG_PATH.exists() else "No")
|
|
95
|
+
|
|
96
|
+
return 0
|
wasm/cli/commands/db.py
CHANGED
|
@@ -934,9 +934,10 @@ def _db_connect(args: Namespace, verbose: bool) -> int:
|
|
|
934
934
|
|
|
935
935
|
logger.info(f"Connecting to {manager.DISPLAY_NAME}...")
|
|
936
936
|
logger.info(f"Command: {' '.join(cmd)}")
|
|
937
|
-
|
|
937
|
+
|
|
938
938
|
# Execute interactively
|
|
939
|
-
|
|
939
|
+
result = subprocess.run(cmd)
|
|
940
|
+
return result.returncode
|
|
940
941
|
except Exception as e:
|
|
941
942
|
logger.error(str(e))
|
|
942
943
|
return 1
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Yago López Prado
|
|
2
|
+
# Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
|
|
3
|
+
# https://github.com/Perkybeet/wasm/blob/main/LICENSE
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Health check command for WASM.
|
|
7
|
+
|
|
8
|
+
Provides system-wide health diagnostics.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from argparse import Namespace
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from wasm.core.config import Config
|
|
17
|
+
from wasm.core.logger import Logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _print_status(logger: Logger, key: str, value: str, status: str) -> None:
|
|
21
|
+
"""Print a key-value pair with status indicator."""
|
|
22
|
+
if status == "ok":
|
|
23
|
+
indicator = "\033[32m[OK]\033[0m"
|
|
24
|
+
elif status == "warning":
|
|
25
|
+
indicator = "\033[33m[!]\033[0m"
|
|
26
|
+
elif status == "error":
|
|
27
|
+
indicator = "\033[31m[X]\033[0m"
|
|
28
|
+
else: # info
|
|
29
|
+
indicator = "\033[34m[i]\033[0m"
|
|
30
|
+
|
|
31
|
+
print(f" {indicator} {key}: {value}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def handle_health(args: Namespace) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Handle the health check command.
|
|
37
|
+
|
|
38
|
+
Performs system diagnostics and shows overall health status.
|
|
39
|
+
"""
|
|
40
|
+
logger = Logger(verbose=args.verbose)
|
|
41
|
+
config = Config()
|
|
42
|
+
|
|
43
|
+
logger.header("System Health Check")
|
|
44
|
+
logger.blank()
|
|
45
|
+
|
|
46
|
+
issues = []
|
|
47
|
+
warnings = []
|
|
48
|
+
|
|
49
|
+
# 1. Check disk space
|
|
50
|
+
logger.info("Checking disk space...")
|
|
51
|
+
try:
|
|
52
|
+
apps_dir = config.apps_directory
|
|
53
|
+
if apps_dir.exists():
|
|
54
|
+
stat = shutil.disk_usage(str(apps_dir))
|
|
55
|
+
free_gb = stat.free / (1024 ** 3)
|
|
56
|
+
total_gb = stat.total / (1024 ** 3)
|
|
57
|
+
used_percent = ((stat.total - stat.free) / stat.total) * 100
|
|
58
|
+
|
|
59
|
+
if free_gb < 1.0:
|
|
60
|
+
issues.append(f"Low disk space: {free_gb:.1f}GB free")
|
|
61
|
+
_print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "error")
|
|
62
|
+
elif free_gb < 5.0:
|
|
63
|
+
warnings.append(f"Disk space is getting low: {free_gb:.1f}GB free")
|
|
64
|
+
_print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "warning")
|
|
65
|
+
else:
|
|
66
|
+
_print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "ok")
|
|
67
|
+
else:
|
|
68
|
+
_print_status(logger, "Disk Space", "Apps directory not found", "warning")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
warnings.append(f"Could not check disk space: {e}")
|
|
71
|
+
|
|
72
|
+
# 2. Check web servers
|
|
73
|
+
logger.blank()
|
|
74
|
+
logger.info("Checking web servers...")
|
|
75
|
+
|
|
76
|
+
from wasm.managers.nginx_manager import NginxManager
|
|
77
|
+
from wasm.managers.apache_manager import ApacheManager
|
|
78
|
+
|
|
79
|
+
nginx = NginxManager(verbose=args.verbose)
|
|
80
|
+
apache = ApacheManager(verbose=args.verbose)
|
|
81
|
+
|
|
82
|
+
nginx_installed = nginx.is_installed()
|
|
83
|
+
apache_installed = apache.is_installed()
|
|
84
|
+
|
|
85
|
+
if nginx_installed:
|
|
86
|
+
nginx_status = nginx.get_status()
|
|
87
|
+
if nginx_status.get("active"):
|
|
88
|
+
_print_status(logger, "Nginx", "Running", "ok")
|
|
89
|
+
else:
|
|
90
|
+
issues.append("Nginx is installed but not running")
|
|
91
|
+
_print_status(logger, "Nginx", "Stopped", "error")
|
|
92
|
+
else:
|
|
93
|
+
_print_status(logger, "Nginx", "Not installed", "info")
|
|
94
|
+
|
|
95
|
+
if apache_installed:
|
|
96
|
+
apache_status = apache.get_status()
|
|
97
|
+
if apache_status.get("active"):
|
|
98
|
+
_print_status(logger, "Apache", "Running", "ok")
|
|
99
|
+
else:
|
|
100
|
+
warnings.append("Apache is installed but not running")
|
|
101
|
+
_print_status(logger, "Apache", "Stopped", "warning")
|
|
102
|
+
else:
|
|
103
|
+
_print_status(logger, "Apache", "Not installed", "info")
|
|
104
|
+
|
|
105
|
+
if not nginx_installed and not apache_installed:
|
|
106
|
+
issues.append("No web server installed")
|
|
107
|
+
|
|
108
|
+
# 3. Check deployed applications
|
|
109
|
+
logger.blank()
|
|
110
|
+
logger.info("Checking deployed applications...")
|
|
111
|
+
|
|
112
|
+
from wasm.core.store import get_store
|
|
113
|
+
from wasm.managers.service_manager import ServiceManager
|
|
114
|
+
|
|
115
|
+
store = get_store()
|
|
116
|
+
service_manager = ServiceManager(verbose=args.verbose)
|
|
117
|
+
|
|
118
|
+
apps = store.list_apps()
|
|
119
|
+
apps_running = 0
|
|
120
|
+
apps_stopped = 0
|
|
121
|
+
apps_failed = 0
|
|
122
|
+
|
|
123
|
+
for app in apps:
|
|
124
|
+
try:
|
|
125
|
+
status = service_manager.status(app.domain.replace(".", "-"))
|
|
126
|
+
if status.get("active"):
|
|
127
|
+
apps_running += 1
|
|
128
|
+
else:
|
|
129
|
+
apps_stopped += 1
|
|
130
|
+
warnings.append(f"App '{app.domain}' is not running")
|
|
131
|
+
except Exception:
|
|
132
|
+
apps_failed += 1
|
|
133
|
+
|
|
134
|
+
total_apps = len(apps)
|
|
135
|
+
if total_apps > 0:
|
|
136
|
+
if apps_stopped > 0 or apps_failed > 0:
|
|
137
|
+
_print_status(logger, "Applications", f"{apps_running}/{total_apps} running, {apps_stopped} stopped", "warning")
|
|
138
|
+
else:
|
|
139
|
+
_print_status(logger, "Applications", f"{apps_running}/{total_apps} running", "ok")
|
|
140
|
+
else:
|
|
141
|
+
_print_status(logger, "Applications", "No applications deployed", "info")
|
|
142
|
+
|
|
143
|
+
# 4. Check SSL certificates
|
|
144
|
+
logger.blank()
|
|
145
|
+
logger.info("Checking SSL certificates...")
|
|
146
|
+
|
|
147
|
+
from wasm.managers.cert_manager import CertManager
|
|
148
|
+
cert_manager = CertManager(verbose=args.verbose)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
certs = cert_manager.list_certificates()
|
|
152
|
+
expiring_soon = []
|
|
153
|
+
|
|
154
|
+
for cert in certs:
|
|
155
|
+
if cert.get("expires"):
|
|
156
|
+
try:
|
|
157
|
+
expires = datetime.fromisoformat(cert["expires"].replace("Z", "+00:00"))
|
|
158
|
+
days_left = (expires - datetime.now(expires.tzinfo)).days
|
|
159
|
+
if days_left < 7:
|
|
160
|
+
issues.append(f"Certificate for {cert['domain']} expires in {days_left} days")
|
|
161
|
+
expiring_soon.append(cert["domain"])
|
|
162
|
+
elif days_left < 30:
|
|
163
|
+
warnings.append(f"Certificate for {cert['domain']} expires in {days_left} days")
|
|
164
|
+
expiring_soon.append(cert["domain"])
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
if expiring_soon:
|
|
169
|
+
_print_status(logger, "SSL Certificates", f"{len(certs)} total, {len(expiring_soon)} expiring soon", "warning")
|
|
170
|
+
elif certs:
|
|
171
|
+
_print_status(logger, "SSL Certificates", f"{len(certs)} total, all valid", "ok")
|
|
172
|
+
else:
|
|
173
|
+
_print_status(logger, "SSL Certificates", "None configured", "info")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
_print_status(logger, "SSL Certificates", f"Could not check: {e}", "warning")
|
|
176
|
+
|
|
177
|
+
# 5. Check system resources
|
|
178
|
+
logger.blank()
|
|
179
|
+
logger.info("Checking system resources...")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# Memory check using /proc/meminfo
|
|
183
|
+
with open("/proc/meminfo") as f:
|
|
184
|
+
meminfo = {}
|
|
185
|
+
for line in f:
|
|
186
|
+
parts = line.split(":")
|
|
187
|
+
if len(parts) == 2:
|
|
188
|
+
key = parts[0].strip()
|
|
189
|
+
value = parts[1].strip().split()[0]
|
|
190
|
+
meminfo[key] = int(value)
|
|
191
|
+
|
|
192
|
+
total_mem = meminfo.get("MemTotal", 0) / 1024 / 1024 # GB
|
|
193
|
+
free_mem = (meminfo.get("MemAvailable", 0) or meminfo.get("MemFree", 0)) / 1024 / 1024 # GB
|
|
194
|
+
used_percent = ((total_mem - free_mem) / total_mem) * 100 if total_mem > 0 else 0
|
|
195
|
+
|
|
196
|
+
if used_percent > 90:
|
|
197
|
+
issues.append(f"High memory usage: {used_percent:.0f}%")
|
|
198
|
+
_print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "error")
|
|
199
|
+
elif used_percent > 75:
|
|
200
|
+
warnings.append(f"Memory usage is high: {used_percent:.0f}%")
|
|
201
|
+
_print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "warning")
|
|
202
|
+
else:
|
|
203
|
+
_print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "ok")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
_print_status(logger, "Memory", f"Could not check: {e}", "warning")
|
|
206
|
+
|
|
207
|
+
# Summary
|
|
208
|
+
logger.blank()
|
|
209
|
+
logger.blank()
|
|
210
|
+
|
|
211
|
+
if issues:
|
|
212
|
+
logger.error(f"Health check found {len(issues)} issue(s):")
|
|
213
|
+
for issue in issues:
|
|
214
|
+
logger.error(f" - {issue}")
|
|
215
|
+
logger.blank()
|
|
216
|
+
|
|
217
|
+
if warnings:
|
|
218
|
+
logger.warning(f"Health check found {len(warnings)} warning(s):")
|
|
219
|
+
for warning in warnings:
|
|
220
|
+
logger.warning(f" - {warning}")
|
|
221
|
+
logger.blank()
|
|
222
|
+
|
|
223
|
+
if not issues and not warnings:
|
|
224
|
+
logger.success("All systems healthy!")
|
|
225
|
+
return 0
|
|
226
|
+
elif issues:
|
|
227
|
+
logger.error("System has issues that need attention.")
|
|
228
|
+
return 1
|
|
229
|
+
else:
|
|
230
|
+
logger.warning("System is healthy with minor warnings.")
|
|
231
|
+
return 0
|
wasm/cli/commands/monitor.py
CHANGED
|
@@ -9,6 +9,11 @@ from argparse import Namespace
|
|
|
9
9
|
|
|
10
10
|
from wasm.core.logger import Logger
|
|
11
11
|
from wasm.core.exceptions import WASMError, MonitorError, EmailError
|
|
12
|
+
from wasm.monitor import (
|
|
13
|
+
DEFAULT_SCAN_INTERVAL,
|
|
14
|
+
DEFAULT_CPU_THRESHOLD,
|
|
15
|
+
DEFAULT_MEMORY_THRESHOLD,
|
|
16
|
+
)
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
def handle_monitor(args: Namespace) -> int:
|
|
@@ -348,9 +353,9 @@ def _handle_config(args: Namespace) -> int:
|
|
|
348
353
|
# General settings
|
|
349
354
|
logger.info("")
|
|
350
355
|
logger.key_value("Enabled", str(config.get("monitor.enabled", False)))
|
|
351
|
-
logger.key_value("Scan Interval", f"{config.get('monitor.scan_interval',
|
|
352
|
-
logger.key_value("CPU Threshold", f"{config.get('monitor.cpu_threshold',
|
|
353
|
-
logger.key_value("Memory Threshold", f"{config.get('monitor.memory_threshold',
|
|
356
|
+
logger.key_value("Scan Interval", f"{config.get('monitor.scan_interval', DEFAULT_SCAN_INTERVAL)}s")
|
|
357
|
+
logger.key_value("CPU Threshold", f"{config.get('monitor.cpu_threshold', DEFAULT_CPU_THRESHOLD)}%")
|
|
358
|
+
logger.key_value("Memory Threshold", f"{config.get('monitor.memory_threshold', DEFAULT_MEMORY_THRESHOLD)}%")
|
|
354
359
|
|
|
355
360
|
# Actions
|
|
356
361
|
logger.info("")
|
wasm/cli/commands/setup.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import Dict, List, Optional, Tuple
|
|
|
12
12
|
from wasm.core.logger import Logger
|
|
13
13
|
from wasm.core.exceptions import WASMError
|
|
14
14
|
from wasm.core.config import DEFAULT_APPS_DIR, DEFAULT_LOG_DIR, DEFAULT_CONFIG_PATH
|
|
15
|
-
from wasm.core.utils import command_exists, run_command, run_command_sudo
|
|
15
|
+
from wasm.core.utils import command_exists, run_command, run_command_sudo, run_trusted_installer
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def handle_setup(args: Namespace) -> int:
|
|
@@ -361,20 +361,20 @@ def _handle_init(args: Namespace) -> int:
|
|
|
361
361
|
|
|
362
362
|
if config_choices.get("install_nodejs") and not command_exists("node"):
|
|
363
363
|
logger.substep("Installing Node.js 20.x LTS...")
|
|
364
|
-
|
|
365
|
-
# Install Node.js from NodeSource
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
shell=True
|
|
369
|
-
)
|
|
370
|
-
if result.success:
|
|
371
|
-
result = run_command_sudo(["apt-get", "install", "-y", "nodejs"])
|
|
364
|
+
|
|
365
|
+
# Install Node.js from NodeSource using trusted installer
|
|
366
|
+
try:
|
|
367
|
+
result = run_trusted_installer("https://deb.nodesource.com/setup_20.x")
|
|
372
368
|
if result.success:
|
|
373
|
-
|
|
369
|
+
result = run_command_sudo(["apt-get", "install", "-y", "nodejs"])
|
|
370
|
+
if result.success:
|
|
371
|
+
logger.success("Node.js installed")
|
|
372
|
+
else:
|
|
373
|
+
logger.warning(f"Failed to install Node.js: {result.stderr}")
|
|
374
374
|
else:
|
|
375
|
-
logger.warning(
|
|
376
|
-
|
|
377
|
-
logger.warning("Failed to setup Node.js
|
|
375
|
+
logger.warning("Failed to setup Node.js repository. Please install manually.")
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logger.warning(f"Failed to setup Node.js: {e}")
|
|
378
378
|
|
|
379
379
|
# Install package managers
|
|
380
380
|
selected_pms = config_choices.get("package_managers", ["npm"])
|
|
@@ -394,8 +394,12 @@ def _handle_init(args: Namespace) -> int:
|
|
|
394
394
|
elif pm == "yarn":
|
|
395
395
|
result = run_command_sudo(["npm", "install", "-g", "yarn"])
|
|
396
396
|
elif pm == "bun":
|
|
397
|
-
# Bun has its own installer
|
|
398
|
-
|
|
397
|
+
# Bun has its own trusted installer
|
|
398
|
+
try:
|
|
399
|
+
result = run_trusted_installer("https://bun.sh/install")
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.warning(f"Failed to install bun: {e}")
|
|
402
|
+
continue
|
|
399
403
|
else:
|
|
400
404
|
result = run_command_sudo(["npm", "install", "-g", pm])
|
|
401
405
|
|
|
@@ -467,7 +471,38 @@ def _handle_init(args: Namespace) -> int:
|
|
|
467
471
|
logger.success(f"Created {DEFAULT_CONFIG_PATH}")
|
|
468
472
|
except Exception as e:
|
|
469
473
|
logger.warning(f"Could not save config file: {e}")
|
|
470
|
-
|
|
474
|
+
|
|
475
|
+
# Install man page
|
|
476
|
+
logger.substep("Installing man page...")
|
|
477
|
+
try:
|
|
478
|
+
# Search for man page in multiple locations
|
|
479
|
+
man_source = None
|
|
480
|
+
search_paths = [
|
|
481
|
+
# Development: project root
|
|
482
|
+
Path(__file__).parent.parent.parent.parent.parent / "man" / "wasm.1",
|
|
483
|
+
# Installed via pip with data_files
|
|
484
|
+
Path("/usr/local/share/man/man1/wasm.1"),
|
|
485
|
+
Path("/usr/share/man/man1/wasm.1"),
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
for path in search_paths:
|
|
489
|
+
if path.exists():
|
|
490
|
+
man_source = path
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if man_source and man_source != Path("/usr/share/man/man1/wasm.1"):
|
|
494
|
+
man_dest = Path("/usr/share/man/man1/wasm.1")
|
|
495
|
+
shutil.copy(man_source, man_dest)
|
|
496
|
+
os.chmod(man_dest, 0o644)
|
|
497
|
+
run_command(["mandb", "-q"])
|
|
498
|
+
logger.success("Man page installed (man wasm)")
|
|
499
|
+
elif man_source:
|
|
500
|
+
logger.substep("Man page already installed")
|
|
501
|
+
else:
|
|
502
|
+
logger.debug("Man page source not found, skipping")
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.debug(f"Could not install man page: {e}")
|
|
505
|
+
|
|
471
506
|
# =========================================================================
|
|
472
507
|
# Final Summary
|
|
473
508
|
# =========================================================================
|
wasm/cli/commands/store.py
CHANGED
|
@@ -313,13 +313,29 @@ def _store_import(args: Namespace, verbose: bool) -> int:
|
|
|
313
313
|
else:
|
|
314
314
|
logger.step(2, 3, "No Apache sites found")
|
|
315
315
|
|
|
316
|
-
# 3. Import from systemd services (wasm-*
|
|
316
|
+
# 3. Import from systemd services (both legacy wasm-* and new format)
|
|
317
317
|
logger.step(3, 3, "Scanning systemd services")
|
|
318
318
|
if SYSTEMD_DIR.exists():
|
|
319
|
-
|
|
319
|
+
# Find all potential WASM service files
|
|
320
|
+
service_files = list(SYSTEMD_DIR.glob("wasm-*.service"))
|
|
321
|
+
# Also check for services matching domain pattern (new format)
|
|
322
|
+
for sf in SYSTEMD_DIR.glob("*.service"):
|
|
323
|
+
name = sf.stem
|
|
324
|
+
# Skip if already found as wasm-* or if it's a system service
|
|
325
|
+
if name.startswith("wasm-") or not "-" in name:
|
|
326
|
+
continue
|
|
327
|
+
# Check if it looks like a domain-based name (has hyphen, no @ or other special chars)
|
|
328
|
+
if "@" not in name and name.count("-") >= 1:
|
|
329
|
+
service_files.append(sf)
|
|
330
|
+
|
|
331
|
+
for service_file in service_files:
|
|
320
332
|
# Extract app name from service file name
|
|
321
|
-
service_name = service_file.stem
|
|
322
|
-
|
|
333
|
+
service_name = service_file.stem
|
|
334
|
+
# Handle both legacy (wasm-example-com) and new format (example-com)
|
|
335
|
+
if service_name.startswith("wasm-"):
|
|
336
|
+
app_name = service_name[5:] # Remove wasm- prefix
|
|
337
|
+
else:
|
|
338
|
+
app_name = service_name
|
|
323
339
|
|
|
324
340
|
if store.get_service(app_name):
|
|
325
341
|
continue
|