wasm-cli 1.0.2__tar.gz → 1.1.0__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.
- {wasm_cli-1.0.2/src/wasm_cli.egg-info → wasm_cli-1.1.0}/PKG-INFO +1 -1
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/pyproject.toml +1 -1
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/setup.py +1 -1
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/__init__.py +1 -1
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/health.py +26 -35
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/web.py +47 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/webapp.py +31 -16
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/interactive.py +5 -3
- wasm_cli-1.1.0/src/wasm/core/app_state.py +205 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/logger.py +206 -33
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/service_manager.py +8 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/SOURCES.txt +1 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/LICENSE +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/MANIFEST.in +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/README.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/MONITOR.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/assets/logo.png +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/superpowers/specs/2026-08-12-wasm-panel-design-direction.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/superpowers/specs/2026-08-12-wasm-v1-refactor-design.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/man/wasm.1 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/setup.cfg +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/__main__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/app.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/backup.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/cert.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/db.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/env.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/monitor.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/prompts.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/README.md +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/config.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/dependencies.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/exceptions.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/fs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/runner.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/store.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/utils.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/auto.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/base.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/docker_compose.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/env_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/health.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/nginx_config.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/preflight.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/registration.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/summary.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/turbo.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/workspace.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/interface.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/monorepo.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/pipeline.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/python.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/registry.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/main.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/backup_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/backup_scheduler.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/base_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/cert_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/postgres.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/redis.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/nginx_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/webserver.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/metrics.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/models.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/observation_store.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/process_monitor.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/signals.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/advanced.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/_escape.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/backup-service.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/environment.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/names.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/port.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/source.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/apps.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/auth.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/deps.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/router.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/services.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/sites.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/auth.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/jobs.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/server.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/app.css +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/logo.webp +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/panel.js +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/alpine.min.js +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-600-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/htmx.min.js +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/uplot.min.css +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/uplot.min.js +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/xterm.css +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/xterm.js +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/_macros.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/base.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/fragments/machine.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/login.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/activity.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/app.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/backups.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/certificates.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/dashboard.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/missing.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/resources.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/settings.html +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/context.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/machine.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/rendering.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/resources.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/router.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/websockets/router.py +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wasm-cli"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Web App System Management - Deploy and manage web applications on Linux servers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "WASM-NCSAL" }
|
|
@@ -9,7 +9,7 @@ from setuptools import setup, find_packages
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="wasm-cli",
|
|
12
|
-
version="1.0
|
|
12
|
+
version="1.1.0",
|
|
13
13
|
description="Web App System Management - Deploy and manage web applications on Linux servers",
|
|
14
14
|
author="Yago López Prado",
|
|
15
15
|
author_email="yago.lopez.adeje@gmail.com",
|
|
@@ -15,7 +15,7 @@ from importlib.metadata import version as _installed_version
|
|
|
15
15
|
#: as an OBS build directory. The single source of truth is the ``version``
|
|
16
16
|
#: field of pyproject.toml; ``scripts/release.py`` keeps this literal and the
|
|
17
17
|
#: distribution packaging files in step with it.
|
|
18
|
-
_FALLBACK_VERSION = "1.0
|
|
18
|
+
_FALLBACK_VERSION = "1.1.0"
|
|
19
19
|
|
|
20
20
|
try:
|
|
21
21
|
__version__ = _installed_version("wasm-cli")
|
|
@@ -28,11 +28,11 @@ from datetime import datetime
|
|
|
28
28
|
import click
|
|
29
29
|
|
|
30
30
|
from wasm.cli.app import Context, pass_context
|
|
31
|
+
from wasm.core.app_state import RUNNING, STATIC, resolve_states
|
|
31
32
|
from wasm.core.config import Config
|
|
32
|
-
from wasm.core.exceptions import
|
|
33
|
+
from wasm.core.exceptions import WASMError
|
|
33
34
|
from wasm.core.logger import Logger
|
|
34
35
|
from wasm.core.store import get_store
|
|
35
|
-
from wasm.core.utils import domain_to_app_name
|
|
36
36
|
from wasm.managers.apache_manager import ApacheManager
|
|
37
37
|
from wasm.managers.cert_manager import CertificateInfo, CertManager
|
|
38
38
|
from wasm.managers.nginx_manager import NginxManager
|
|
@@ -50,21 +50,14 @@ def _print_status(logger: Logger, key: str, value: str, status: str) -> None:
|
|
|
50
50
|
Print a key-value pair with status indicator.
|
|
51
51
|
|
|
52
52
|
Args:
|
|
53
|
-
logger: Logger of the current command
|
|
53
|
+
logger: Logger of the current command. Writing through it is what makes
|
|
54
|
+
``wasm --no-color health`` colourless; the escape codes used to be
|
|
55
|
+
written to stdout directly, so the flag did nothing here.
|
|
54
56
|
key: Name of the checked item.
|
|
55
57
|
value: Human readable result.
|
|
56
58
|
status: One of "ok", "warning", "error" or "info".
|
|
57
59
|
"""
|
|
58
|
-
|
|
59
|
-
indicator = "\033[32m[OK]\033[0m"
|
|
60
|
-
elif status == "warning":
|
|
61
|
-
indicator = "\033[33m[!]\033[0m"
|
|
62
|
-
elif status == "error":
|
|
63
|
-
indicator = "\033[31m[X]\033[0m"
|
|
64
|
-
else: # info
|
|
65
|
-
indicator = "\033[34m[i]\033[0m"
|
|
66
|
-
|
|
67
|
-
print(f" {indicator} {key}: {value}")
|
|
60
|
+
logger.check(key, value, status)
|
|
68
61
|
|
|
69
62
|
|
|
70
63
|
def _days_until(expiry: str) -> int | None:
|
|
@@ -222,37 +215,35 @@ def run_health_check(verbose: bool = False) -> int:
|
|
|
222
215
|
service_manager = ServiceManager(verbose=verbose)
|
|
223
216
|
|
|
224
217
|
apps = store.list_apps()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if status.get("active"):
|
|
240
|
-
apps_running += 1
|
|
241
|
-
else:
|
|
242
|
-
apps_stopped += 1
|
|
243
|
-
warnings.append(f"App '{app.domain}' is not running")
|
|
218
|
+
|
|
219
|
+
# The same resolver `wasm list` uses. When these two commands each decided
|
|
220
|
+
# for themselves what "running" meant, list reported fifteen applications
|
|
221
|
+
# running while this reported seven stopped, and five of the seven were
|
|
222
|
+
# static sites that have no service to run in the first place.
|
|
223
|
+
states = resolve_states(apps, service_manager)
|
|
224
|
+
|
|
225
|
+
apps_running = sum(1 for s in states.values() if s.label == RUNNING)
|
|
226
|
+
apps_static = sum(1 for s in states.values() if s.label == STATIC)
|
|
227
|
+
unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
|
|
228
|
+
|
|
229
|
+
for domain, current in unhealthy:
|
|
230
|
+
warnings.append(f"App '{domain}' - {current.detail or current.label.lower()}")
|
|
244
231
|
|
|
245
232
|
total_apps = len(apps)
|
|
233
|
+
served = apps_running + apps_static
|
|
246
234
|
if total_apps > 0:
|
|
247
|
-
|
|
235
|
+
summary = f"{served}/{total_apps} serving"
|
|
236
|
+
if apps_static:
|
|
237
|
+
summary += f" ({apps_static} static)"
|
|
238
|
+
if unhealthy:
|
|
248
239
|
_print_status(
|
|
249
240
|
logger,
|
|
250
241
|
"Applications",
|
|
251
|
-
f"{
|
|
242
|
+
f"{summary}, {len(unhealthy)} need attention",
|
|
252
243
|
"warning",
|
|
253
244
|
)
|
|
254
245
|
else:
|
|
255
|
-
_print_status(logger, "Applications",
|
|
246
|
+
_print_status(logger, "Applications", summary, "ok")
|
|
256
247
|
else:
|
|
257
248
|
_print_status(logger, "Applications", "No applications deployed", "info")
|
|
258
249
|
|
|
@@ -708,6 +708,38 @@ def _prompt_install(missing_apt: list[str], missing_pip: list[str], verbose: boo
|
|
|
708
708
|
# ---------------------------------------------------------------------------
|
|
709
709
|
|
|
710
710
|
|
|
711
|
+
def _port_in_use(host: str, port: int) -> bool:
|
|
712
|
+
"""
|
|
713
|
+
Ask whether the panel's address is already taken.
|
|
714
|
+
|
|
715
|
+
Binding is the only reliable answer: a connection attempt says nothing
|
|
716
|
+
about a socket bound with no backlog, and reading ``ss`` output means
|
|
717
|
+
parsing a different format on every distribution.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
host: Address the panel would bind.
|
|
721
|
+
port: Port the panel would bind.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
True when binding would fail because something holds the address.
|
|
725
|
+
"""
|
|
726
|
+
family = socket.AF_INET6 if ":" in _strip_brackets(host) else socket.AF_INET
|
|
727
|
+
probe = socket.socket(family, socket.SOCK_STREAM)
|
|
728
|
+
try:
|
|
729
|
+
# Deliberately not SO_REUSEADDR: with it the bind succeeds against a
|
|
730
|
+
# socket in TIME_WAIT, which is the case this is meant to catch.
|
|
731
|
+
probe.bind((_strip_brackets(host), port))
|
|
732
|
+
except OSError:
|
|
733
|
+
return True
|
|
734
|
+
except (ValueError, socket.gaierror):
|
|
735
|
+
# An address this process cannot even parse is not this check's problem;
|
|
736
|
+
# the bind that follows will report it properly.
|
|
737
|
+
return False
|
|
738
|
+
finally:
|
|
739
|
+
probe.close()
|
|
740
|
+
return False
|
|
741
|
+
|
|
742
|
+
|
|
711
743
|
def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> int:
|
|
712
744
|
"""
|
|
713
745
|
Start the panel, refusing an unsafe exposure first.
|
|
@@ -773,6 +805,21 @@ def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> in
|
|
|
773
805
|
# Process not running, remove stale PID file
|
|
774
806
|
get_fs().remove(pid_file, missing_ok=True)
|
|
775
807
|
|
|
808
|
+
# The PID file only catches a panel this machine still has a record of. A
|
|
809
|
+
# panel whose PID file was removed, or anything else on the port, walked
|
|
810
|
+
# past that check: the token was printed and then uvicorn failed with a
|
|
811
|
+
# bare OSError, leaving an operator holding a credential for a server that
|
|
812
|
+
# never started.
|
|
813
|
+
if not dry_run and _port_in_use(host, config.port):
|
|
814
|
+
logger.error(f"Something is already listening on {host}:{config.port}")
|
|
815
|
+
logger.info("If it is a panel this machine has forgotten about:")
|
|
816
|
+
logger.info(" wasm web stop")
|
|
817
|
+
logger.info("To see what holds the port:")
|
|
818
|
+
logger.info(f" ss -ltnp 'sport = :{config.port}'")
|
|
819
|
+
logger.info("Or serve somewhere else:")
|
|
820
|
+
logger.info(f" wasm web start --port {config.port + 1}")
|
|
821
|
+
return 1
|
|
822
|
+
|
|
776
823
|
if config.ip_whitelist:
|
|
777
824
|
logger.info(f"Only these clients may connect: {', '.join(config.ip_whitelist)}")
|
|
778
825
|
if config.trusted_proxies:
|
|
@@ -33,15 +33,16 @@ from typing import Any, TypeVar
|
|
|
33
33
|
import click
|
|
34
34
|
|
|
35
35
|
from wasm.cli.app import Context, enable_dry_run, pass_context
|
|
36
|
+
from wasm.core.app_state import RUNNING, STATIC, resolve_states
|
|
36
37
|
from wasm.core.config import Config
|
|
37
38
|
from wasm.core.dependencies import check_deployment_ready
|
|
38
39
|
from wasm.core.exceptions import DeploymentError, ServiceError, WASMError
|
|
39
|
-
from wasm.core.logger import Logger, set_colors_disabled
|
|
40
|
+
from wasm.core.logger import Logger, set_colors_disabled, state, styled
|
|
40
41
|
from wasm.core.runner import (
|
|
41
42
|
CommandResult,
|
|
42
43
|
get_runner,
|
|
43
44
|
)
|
|
44
|
-
from wasm.core.store import
|
|
45
|
+
from wasm.core.store import get_store
|
|
45
46
|
from wasm.core.utils import domain_to_app_name, remove_directory
|
|
46
47
|
from wasm.deployers import detect_app_type, get_deployer
|
|
47
48
|
from wasm.deployers.docker_compose import DockerComposeDeployer
|
|
@@ -464,30 +465,44 @@ def _list_apps(logger: Logger) -> int:
|
|
|
464
465
|
logger.info(" wasm deploy -d example.com -s https://github.com/user/repo")
|
|
465
466
|
return 0
|
|
466
467
|
|
|
468
|
+
# Asked of systemd and of the port, not read from the status column. That
|
|
469
|
+
# column is written at deploy time and never again, so it called everything
|
|
470
|
+
# Running while health, which did ask, reported half of them stopped.
|
|
471
|
+
states = resolve_states(apps, ServiceManager(verbose=logger.verbose))
|
|
472
|
+
|
|
467
473
|
headers = ["Domain", "Type", "Status", "Port", "SSL"]
|
|
468
474
|
rows = []
|
|
469
475
|
|
|
470
|
-
labels = {
|
|
471
|
-
AppStatus.RUNNING.value: "Running",
|
|
472
|
-
AppStatus.STOPPED.value: "Stopped",
|
|
473
|
-
AppStatus.DEPLOYING.value: "Deploying",
|
|
474
|
-
AppStatus.FAILED.value: "Failed",
|
|
475
|
-
}
|
|
476
|
-
|
|
477
476
|
for app in apps:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
477
|
+
current = states[app.domain]
|
|
478
|
+
rows.append(
|
|
479
|
+
[
|
|
480
|
+
styled(app.domain, "bold"),
|
|
481
|
+
app.app_type,
|
|
482
|
+
state(current.label),
|
|
483
|
+
styled(app.port, "") if app.port else state("static"),
|
|
484
|
+
state("yes") if app.ssl_enabled else state("no"),
|
|
485
|
+
]
|
|
486
|
+
)
|
|
481
487
|
|
|
482
|
-
|
|
488
|
+
logger.table(headers, rows, justify=["left", "left", "left", "right", "left"])
|
|
483
489
|
|
|
484
|
-
|
|
490
|
+
running = sum(1 for s in states.values() if s.label == RUNNING)
|
|
491
|
+
static = sum(1 for s in states.values() if s.label == STATIC)
|
|
492
|
+
unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
|
|
485
493
|
|
|
486
494
|
logger.blank()
|
|
487
|
-
running = sum(1 for a in apps if a.status == AppStatus.RUNNING.value)
|
|
488
|
-
static = sum(1 for a in apps if a.is_static)
|
|
489
495
|
logger.info(f"Total: {len(apps)} apps ({running} running, {static} static)")
|
|
490
496
|
|
|
497
|
+
# The reason belongs next to the list. Making the operator run a second
|
|
498
|
+
# command to find out why something says Stopped is how the contradiction
|
|
499
|
+
# between these two commands went unnoticed for as long as it did.
|
|
500
|
+
if unhealthy:
|
|
501
|
+
logger.blank()
|
|
502
|
+
logger.warning(f"{len(unhealthy)} need attention:")
|
|
503
|
+
for domain, current in unhealthy:
|
|
504
|
+
logger.list_item(f"{domain} - {current.detail or current.label.lower()}")
|
|
505
|
+
|
|
491
506
|
return 0
|
|
492
507
|
|
|
493
508
|
|
|
@@ -34,9 +34,11 @@ class InteractiveMode:
|
|
|
34
34
|
raise WASMError(
|
|
35
35
|
"Interactive mode needs questionary, which is missing",
|
|
36
36
|
details=(
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
37
|
+
"Every other command works without it. To get it:\n"
|
|
38
|
+
" apt install python3-questionary Debian 13, Ubuntu 24.04+\n"
|
|
39
|
+
" dnf install python3-questionary Fedora 42+\n"
|
|
40
|
+
" pip install questionary anywhere else\n"
|
|
41
|
+
"It is not packaged for Debian 12, Ubuntu 22.04 or Leap 15."
|
|
40
42
|
),
|
|
41
43
|
)
|
|
42
44
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Yago Lopez Prado
|
|
2
|
+
# Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
|
|
3
|
+
# https://github.com/Perkybeet/wasm/blob/main/LICENSE
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
One answer to the question "is this application actually working".
|
|
7
|
+
|
|
8
|
+
``wasm list`` printed the status column from the database and ``wasm health``
|
|
9
|
+
asked systemd, so on the same machine at the same moment list called fifteen
|
|
10
|
+
applications Running while health reported seven of them stopped. Nothing ever
|
|
11
|
+
wrote to that column after a deploy, so list was reading a value that had been
|
|
12
|
+
true once.
|
|
13
|
+
|
|
14
|
+
Asking systemd instead is necessary but not sufficient. A unit is active while
|
|
15
|
+
the process exists, which includes a service that accepts no connections and
|
|
16
|
+
one systemd is restarting every few seconds because it crashes on startup. Both
|
|
17
|
+
report ``active`` if you catch them between restarts. So the state here is
|
|
18
|
+
decided from four signals:
|
|
19
|
+
|
|
20
|
+
- what systemd says the unit is doing (ActiveState and SubState),
|
|
21
|
+
- how many times it has been restarted (NRestarts),
|
|
22
|
+
- whether anything answers on the port the application is supposed to serve,
|
|
23
|
+
- and whether the application is static, in which case there is no unit at all
|
|
24
|
+
and the web server serves the files directly.
|
|
25
|
+
|
|
26
|
+
The last one is why health warned about five sites that were fine: a static
|
|
27
|
+
site has no systemd unit, so querying one always says "not running".
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import socket
|
|
33
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
from wasm.core.exceptions import ValidationError, WASMError
|
|
38
|
+
from wasm.core.utils import domain_to_app_name
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING: # pragma: no cover - imported for types only
|
|
41
|
+
from wasm.core.store import App
|
|
42
|
+
from wasm.managers.service_manager import ServiceManager
|
|
43
|
+
|
|
44
|
+
#: How long to wait for a port to accept a connection. The probe runs against
|
|
45
|
+
#: loopback, where anything listening answers immediately; a longer wait would
|
|
46
|
+
#: only make `wasm list` slower on the applications that are already broken.
|
|
47
|
+
PROBE_TIMEOUT = 0.4
|
|
48
|
+
|
|
49
|
+
#: Labels. These are what the Status column shows, and core.logger.STATE_STYLES
|
|
50
|
+
#: gives each of them its colour.
|
|
51
|
+
RUNNING = "Running"
|
|
52
|
+
RESTARTING = "Restarting"
|
|
53
|
+
NOT_RESPONDING = "No answer"
|
|
54
|
+
STOPPED = "Stopped"
|
|
55
|
+
FAILED = "Failed"
|
|
56
|
+
STATIC = "Static"
|
|
57
|
+
UNKNOWN = "Unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class AppState:
|
|
62
|
+
"""
|
|
63
|
+
What is true about one application right now.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
label: One word for the Status column.
|
|
67
|
+
healthy: Whether this needs an operator's attention. Static sites are
|
|
68
|
+
healthy: there is nothing to run.
|
|
69
|
+
detail: Why, in a sentence, when the answer is not simply "it works".
|
|
70
|
+
Empty when there is nothing to explain.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
label: str
|
|
74
|
+
healthy: bool
|
|
75
|
+
detail: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def port_answers(port: int, host: str = "127.0.0.1", timeout: float = PROBE_TIMEOUT) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Ask whether anything accepts a connection on a port.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
port: The port the application should be serving.
|
|
84
|
+
host: Where to look, loopback by default.
|
|
85
|
+
timeout: Seconds to wait.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True when the connection is accepted.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
92
|
+
return True
|
|
93
|
+
except (OSError, ValueError):
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _restart_count(status: dict[str, Any]) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Read NRestarts out of a status dictionary.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
status: What ServiceManager.get_status returned.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The restart count, and 0 when systemd did not report one.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
return int(str(status.get("restarts", "0")).strip() or 0)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_state(
|
|
114
|
+
app: App,
|
|
115
|
+
service_manager: ServiceManager,
|
|
116
|
+
*,
|
|
117
|
+
probe: bool = True,
|
|
118
|
+
) -> AppState:
|
|
119
|
+
"""
|
|
120
|
+
Work out what is true about one application.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
app: The application record.
|
|
124
|
+
service_manager: Used to ask systemd about the unit.
|
|
125
|
+
probe: Whether to check that the port answers. Turn it off where the
|
|
126
|
+
extra connection is not wanted; the systemd signals still apply.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The state, ready to be shown or counted.
|
|
130
|
+
"""
|
|
131
|
+
if app.is_static:
|
|
132
|
+
return AppState(STATIC, healthy=True, detail="served directly by the web server")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
status = service_manager.get_status(domain_to_app_name(app.domain))
|
|
136
|
+
except (WASMError, ValidationError) as error:
|
|
137
|
+
return AppState(UNKNOWN, healthy=False, detail=str(error))
|
|
138
|
+
|
|
139
|
+
active_state = str(status.get("active_state", "")).strip()
|
|
140
|
+
sub_state = str(status.get("sub_state", "")).strip()
|
|
141
|
+
restarts = _restart_count(status)
|
|
142
|
+
|
|
143
|
+
if not status.get("exists", True) and not status.get("active"):
|
|
144
|
+
return AppState(STOPPED, healthy=False, detail="there is no systemd unit for it")
|
|
145
|
+
|
|
146
|
+
if active_state == "failed" or sub_state == "failed":
|
|
147
|
+
reason = str(status.get("result", "")).strip()
|
|
148
|
+
detail = f"systemd gave up on it ({reason})" if reason else "systemd gave up on it"
|
|
149
|
+
return AppState(FAILED, healthy=False, detail=detail)
|
|
150
|
+
|
|
151
|
+
# A crash loop: systemd keeps starting it and it keeps dying. Between two
|
|
152
|
+
# restarts the unit reads as active, which is how this got reported as
|
|
153
|
+
# Running for as long as it did.
|
|
154
|
+
if sub_state == "auto-restart" or active_state == "activating":
|
|
155
|
+
detail = f"restarted {restarts} times; check the logs with wasm service logs {app.domain}"
|
|
156
|
+
return AppState(RESTARTING, healthy=False, detail=detail)
|
|
157
|
+
|
|
158
|
+
if not status.get("active"):
|
|
159
|
+
return AppState(STOPPED, healthy=False, detail="the unit is not running")
|
|
160
|
+
|
|
161
|
+
# systemd is satisfied. That only means a process exists, so ask the
|
|
162
|
+
# application itself.
|
|
163
|
+
if probe and app.port and not port_answers(app.port):
|
|
164
|
+
return AppState(
|
|
165
|
+
NOT_RESPONDING,
|
|
166
|
+
healthy=False,
|
|
167
|
+
detail=f"the unit is up but nothing accepts connections on port {app.port}",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if restarts:
|
|
171
|
+
return AppState(RUNNING, healthy=True, detail=f"answering, after {restarts} restarts")
|
|
172
|
+
|
|
173
|
+
return AppState(RUNNING, healthy=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def resolve_states(
|
|
177
|
+
apps: list[App],
|
|
178
|
+
service_manager: ServiceManager,
|
|
179
|
+
*,
|
|
180
|
+
probe: bool = True,
|
|
181
|
+
) -> dict[str, AppState]:
|
|
182
|
+
"""
|
|
183
|
+
Resolve several applications at once.
|
|
184
|
+
|
|
185
|
+
Each one costs three systemctl calls and possibly a connection attempt, so
|
|
186
|
+
fifteen applications in sequence is a visible pause before `wasm list`
|
|
187
|
+
prints anything. They are independent, so they run together.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
apps: The applications to resolve.
|
|
191
|
+
service_manager: Used to ask systemd about the units.
|
|
192
|
+
probe: Whether to check that the ports answer.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The state of each application, keyed by domain.
|
|
196
|
+
"""
|
|
197
|
+
if not apps:
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
def one(app: App) -> tuple[str, AppState]:
|
|
201
|
+
return app.domain, resolve_state(app, service_manager, probe=probe)
|
|
202
|
+
|
|
203
|
+
workers = min(8, len(apps))
|
|
204
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
205
|
+
return dict(pool.map(one, apps))
|