noust 3.0.0__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.
- noust/__init__.py +53 -0
- noust/__main__.py +8 -0
- noust/central/__init__.py +191 -0
- noust/central/setup.py +240 -0
- noust/central/unlock.py +283 -0
- noust/cli/__init__.py +9 -0
- noust/cli/app.py +628 -0
- noust/cli/commands/__init__.py +19 -0
- noust/cli/commands/app.py +782 -0
- noust/cli/commands/backup.py +2294 -0
- noust/cli/commands/central.py +417 -0
- noust/cli/commands/cert.py +767 -0
- noust/cli/commands/config.py +577 -0
- noust/cli/commands/cron.py +464 -0
- noust/cli/commands/db.py +1877 -0
- noust/cli/commands/diagnose.py +94 -0
- noust/cli/commands/domain.py +193 -0
- noust/cli/commands/env.py +477 -0
- noust/cli/commands/fleet.py +344 -0
- noust/cli/commands/github.py +189 -0
- noust/cli/commands/health.py +202 -0
- noust/cli/commands/importer.py +155 -0
- noust/cli/commands/migrate_from_wasm.py +126 -0
- noust/cli/commands/monitor.py +763 -0
- noust/cli/commands/node.py +291 -0
- noust/cli/commands/notify.py +88 -0
- noust/cli/commands/preview.py +261 -0
- noust/cli/commands/recipe.py +112 -0
- noust/cli/commands/releases.py +157 -0
- noust/cli/commands/service.py +526 -0
- noust/cli/commands/sessions.py +120 -0
- noust/cli/commands/setup.py +1533 -0
- noust/cli/commands/site.py +592 -0
- noust/cli/commands/store.py +612 -0
- noust/cli/commands/token.py +190 -0
- noust/cli/commands/twofa.py +134 -0
- noust/cli/commands/version.py +118 -0
- noust/cli/commands/web.py +2801 -0
- noust/cli/commands/webapp.py +1938 -0
- noust/cli/interactive.py +740 -0
- noust/cli/panel_links.py +122 -0
- noust/cli/prompts.py +181 -0
- noust/cli/rename_notice.py +93 -0
- noust/cli/web_state.py +48 -0
- noust/completions/README.md +20 -0
- noust/completions/_noust +40 -0
- noust/completions/_wasm +40 -0
- noust/completions/noust.bash +28 -0
- noust/completions/noust.fish +17 -0
- noust/completions/wasm.bash +28 -0
- noust/completions/wasm.fish +17 -0
- noust/core/__init__.py +40 -0
- noust/core/app_state.py +364 -0
- noust/core/applock.py +333 -0
- noust/core/background.py +181 -0
- noust/core/config.py +1938 -0
- noust/core/dependencies.py +905 -0
- noust/core/deploy_notifications.py +241 -0
- noust/core/exceptions.py +404 -0
- noust/core/forge_events.py +303 -0
- noust/core/fs.py +405 -0
- noust/core/logger.py +729 -0
- noust/core/messages.py +328 -0
- noust/core/migrate_from_wasm.py +1478 -0
- noust/core/net.py +251 -0
- noust/core/notifier.py +1177 -0
- noust/core/package_index.py +909 -0
- noust/core/paths.py +438 -0
- noust/core/redact.py +296 -0
- noust/core/runner.py +1614 -0
- noust/core/sealing.py +1157 -0
- noust/core/secret_detection.py +483 -0
- noust/core/secrets.py +294 -0
- noust/core/store.py +4700 -0
- noust/core/timeutil.py +67 -0
- noust/core/totp.py +176 -0
- noust/core/update_checker.py +653 -0
- noust/core/utils.py +514 -0
- noust/deployers/__init__.py +15 -0
- noust/deployers/app_export.py +1503 -0
- noust/deployers/auto.py +352 -0
- noust/deployers/base.py +2794 -0
- noust/deployers/bluegreen.py +1675 -0
- noust/deployers/deploy_events.py +186 -0
- noust/deployers/docker_compose.py +1950 -0
- noust/deployers/domains.py +648 -0
- noust/deployers/helpers/__init__.py +27 -0
- noust/deployers/helpers/app_env.py +218 -0
- noust/deployers/helpers/databases.py +566 -0
- noust/deployers/helpers/env_manager.py +825 -0
- noust/deployers/helpers/health.py +173 -0
- noust/deployers/helpers/health_gate.py +340 -0
- noust/deployers/helpers/layout.py +242 -0
- noust/deployers/helpers/nginx_config.py +294 -0
- noust/deployers/helpers/package_manager.py +249 -0
- noust/deployers/helpers/path_resolver.py +186 -0
- noust/deployers/helpers/permissions.py +135 -0
- noust/deployers/helpers/php_fpm.py +1192 -0
- noust/deployers/helpers/preflight.py +200 -0
- noust/deployers/helpers/prisma.py +129 -0
- noust/deployers/helpers/registration.py +263 -0
- noust/deployers/helpers/release_build.py +394 -0
- noust/deployers/helpers/summary.py +90 -0
- noust/deployers/helpers/target.py +144 -0
- noust/deployers/helpers/turbo.py +334 -0
- noust/deployers/helpers/workspace.py +424 -0
- noust/deployers/importers/__init__.py +154 -0
- noust/deployers/importers/base.py +389 -0
- noust/deployers/importers/heroku.py +276 -0
- noust/deployers/importers/railway.py +236 -0
- noust/deployers/importers/render.py +362 -0
- noust/deployers/importers/toml_fallback.py +449 -0
- noust/deployers/importers/vercel.py +217 -0
- noust/deployers/inspect.py +946 -0
- noust/deployers/interface.py +240 -0
- noust/deployers/lifecycle.py +2614 -0
- noust/deployers/logs.py +121 -0
- noust/deployers/migrate.py +1499 -0
- noust/deployers/monorepo.py +1836 -0
- noust/deployers/nextjs.py +166 -0
- noust/deployers/nodejs.py +147 -0
- noust/deployers/php_fpm.py +1400 -0
- noust/deployers/pipeline.py +123 -0
- noust/deployers/python.py +281 -0
- noust/deployers/recorder.py +763 -0
- noust/deployers/registry.py +208 -0
- noust/deployers/releases.py +972 -0
- noust/deployers/static.py +230 -0
- noust/deployers/vite.py +230 -0
- noust/fleet/__init__.py +26 -0
- noust/fleet/audit.py +68 -0
- noust/fleet/authorize.py +948 -0
- noust/fleet/client.py +387 -0
- noust/fleet/joincode.py +264 -0
- noust/fleet/keys.py +363 -0
- noust/fleet/models.py +414 -0
- noust/fleet/nodes.py +453 -0
- noust/fleet/policy.py +47 -0
- noust/fleet/status.py +165 -0
- noust/fleet/tunnels.py +755 -0
- noust/integrations/__init__.py +4 -0
- noust/integrations/github/__init__.py +11 -0
- noust/integrations/github/app.py +574 -0
- noust/integrations/github/client.py +329 -0
- noust/integrations/github/comments.py +69 -0
- noust/integrations/github/manifest.py +336 -0
- noust/integrations/github/service.py +445 -0
- noust/integrations/github/statuses.py +357 -0
- noust/integrations/github/webhooks.py +156 -0
- noust/integrations/hooks_site.py +334 -0
- noust/main.py +22 -0
- noust/managers/__init__.py +24 -0
- noust/managers/apache_manager.py +82 -0
- noust/managers/backup_destinations.py +1799 -0
- noust/managers/backup_manager.py +3848 -0
- noust/managers/backup_scheduler.py +880 -0
- noust/managers/base_manager.py +299 -0
- noust/managers/cert_manager.py +1383 -0
- noust/managers/cron_manager.py +1243 -0
- noust/managers/database/__init__.py +31 -0
- noust/managers/database/base.py +1524 -0
- noust/managers/database/mongodb.py +823 -0
- noust/managers/database/mysql.py +1223 -0
- noust/managers/database/postgres.py +1720 -0
- noust/managers/database/psql_script.py +551 -0
- noust/managers/database/redis.py +941 -0
- noust/managers/database/registry.py +125 -0
- noust/managers/diagnose.py +1148 -0
- noust/managers/health.py +443 -0
- noust/managers/nginx_manager.py +97 -0
- noust/managers/previews.py +2158 -0
- noust/managers/service_manager.py +2514 -0
- noust/managers/source_manager.py +2995 -0
- noust/managers/webserver.py +1840 -0
- noust/monitor/__init__.py +106 -0
- noust/monitor/email_notifier.py +479 -0
- noust/monitor/metrics.py +380 -0
- noust/monitor/models.py +184 -0
- noust/monitor/observation_store.py +382 -0
- noust/monitor/process_monitor.py +1135 -0
- noust/monitor/signals.py +229 -0
- noust/monitor/timeseries.py +421 -0
- noust/py.typed +0 -0
- noust/recipes/__init__.py +691 -0
- noust/recipes/assets/n8n/package.json +14 -0
- noust/recipes/assets/wordpress/wp-config.php +51 -0
- noust/recipes/deploy.py +348 -0
- noust/recipes/ghost.yaml +6 -0
- noust/recipes/n8n.yaml +37 -0
- noust/recipes/plausible.yaml +6 -0
- noust/recipes/render.py +109 -0
- noust/recipes/umami.yaml +32 -0
- noust/recipes/uptime-kuma.yaml +25 -0
- noust/recipes/wordpress.yaml +62 -0
- noust/templates/__init__.py +1 -0
- noust/templates/apache/_redirects.j2 +33 -0
- noust/templates/apache/proxy.conf.j2 +63 -0
- noust/templates/apache/static.conf.j2 +83 -0
- noust/templates/nginx/_redirects.j2 +40 -0
- noust/templates/nginx/advanced.conf.j2 +118 -0
- noust/templates/nginx/fastcgi.conf.j2 +144 -0
- noust/templates/nginx/hooks.conf.j2 +71 -0
- noust/templates/nginx/monorepo.conf.j2 +144 -0
- noust/templates/nginx/proxy.conf.j2 +103 -0
- noust/templates/nginx/static.conf.j2 +91 -0
- noust/templates/php/pool.conf.j2 +56 -0
- noust/templates/systemd/_escape.j2 +21 -0
- noust/templates/systemd/app.service.j2 +54 -0
- noust/templates/systemd/app@.service.j2 +56 -0
- noust/templates/systemd/backup-service.j2 +10 -0
- noust/templates/systemd/backup-timer.j2 +14 -0
- noust/templates/systemd/cron-service.j2 +28 -0
- noust/templates/systemd/cron-timer.j2 +17 -0
- noust/templates/systemd/docker-compose.service.j2 +40 -0
- noust/templates/systemd/noust-web.service.j2 +61 -0
- noust/templates/systemd/preview-sweep.service.j2 +10 -0
- noust/templates/systemd/preview-sweep.timer.j2 +13 -0
- noust/validators/__init__.py +32 -0
- noust/validators/domain.py +182 -0
- noust/validators/environment.py +234 -0
- noust/validators/health.py +147 -0
- noust/validators/names.py +325 -0
- noust/validators/port.py +236 -0
- noust/validators/source.py +430 -0
- noust/validators/ssh.py +431 -0
- noust/validators/telegram.py +66 -0
- noust/web/__init__.py +15 -0
- noust/web/api/__init__.py +7 -0
- noust/web/api/app_export.py +376 -0
- noust/web/api/apps.py +2318 -0
- noust/web/api/audit.py +121 -0
- noust/web/api/auth.py +1266 -0
- noust/web/api/backup_destinations.py +522 -0
- noust/web/api/backup_schedules.py +394 -0
- noust/web/api/backups.py +554 -0
- noust/web/api/central.py +164 -0
- noust/web/api/certs.py +406 -0
- noust/web/api/config.py +1175 -0
- noust/web/api/cron.py +468 -0
- noust/web/api/databases.py +1360 -0
- noust/web/api/deployments.py +454 -0
- noust/web/api/deps.py +729 -0
- noust/web/api/diagnose.py +109 -0
- noust/web/api/domains.py +352 -0
- noust/web/api/github_hooks.py +270 -0
- noust/web/api/hooks.py +751 -0
- noust/web/api/integrations.py +341 -0
- noust/web/api/jobs.py +627 -0
- noust/web/api/metrics.py +114 -0
- noust/web/api/monitor.py +804 -0
- noust/web/api/node_proxy.py +1276 -0
- noust/web/api/nodes.py +391 -0
- noust/web/api/openapi.py +175 -0
- noust/web/api/platform_proposal.py +97 -0
- noust/web/api/previews.py +401 -0
- noust/web/api/recipes.py +159 -0
- noust/web/api/router.py +134 -0
- noust/web/api/services.py +822 -0
- noust/web/api/sites.py +666 -0
- noust/web/api/system.py +658 -0
- noust/web/api/zero_downtime.py +244 -0
- noust/web/auth.py +4497 -0
- noust/web/events.py +758 -0
- noust/web/jobs.py +1782 -0
- noust/web/machine.py +374 -0
- noust/web/metrics_collector.py +523 -0
- noust/web/pydantic_compat.py +132 -0
- noust/web/server.py +1990 -0
- noust/web/static/apple-touch-icon.png +0 -0
- noust/web/static/assets/Announcer-CAC9FfMY.js +1 -0
- noust/web/static/assets/AppRowActions-DQ0SWVc4.js +1 -0
- noust/web/static/assets/AppStatePill-BTeRDyIc.js +1 -0
- noust/web/static/assets/Badge-Dyu2dpxb.js +1 -0
- noust/web/static/assets/CSPContext-kKuRERYg.js +1 -0
- noust/web/static/assets/Checkbox-CYDZ85SW.js +1 -0
- noust/web/static/assets/CommandHint-BTJdbHWz.js +1 -0
- noust/web/static/assets/CompositeItem-DKfxp3pP.js +1 -0
- noust/web/static/assets/CompositeList-8dlcGwO6.js +1 -0
- noust/web/static/assets/ConfirmDialog-D_tKuePS.js +1 -0
- noust/web/static/assets/CopyButton-C_-yZ5XH.js +1 -0
- noust/web/static/assets/CopyTextButton-B9MzgkJs.js +1 -0
- noust/web/static/assets/DangerZone-SVKJeYxf.js +1 -0
- noust/web/static/assets/DataTable-DpVhtyFG.js +1 -0
- noust/web/static/assets/Dialog-D6SFUvBZ.js +1 -0
- noust/web/static/assets/DnsVerdict-Bi9l3zsr.js +1 -0
- noust/web/static/assets/Drawer-EqMyFPC7.js +1 -0
- noust/web/static/assets/EmptyState-COLuKwtW.js +1 -0
- noust/web/static/assets/ErrorBoundary-TRn_G6YZ.js +1 -0
- noust/web/static/assets/ExternalLink-COcN2aWC.js +1 -0
- noust/web/static/assets/Field-ZUnEqR1A.js +1 -0
- noust/web/static/assets/FieldItemContext-CQDQjT1N.js +3 -0
- noust/web/static/assets/IconButton-_h955UPm.js +1 -0
- noust/web/static/assets/Input-BN-JLZga.js +1 -0
- noust/web/static/assets/KeyValueList-1dSLH63w.js +1 -0
- noust/web/static/assets/LanguageSwitch-CVeqgnQm.js +1 -0
- noust/web/static/assets/LinkTabs-DaOE6MV5.js +1 -0
- noust/web/static/assets/LogViewer-CE4YXoNp.js +2 -0
- noust/web/static/assets/Logo-B8xCYbX6.js +1 -0
- noust/web/static/assets/Match-DjFDQtD2.js +1 -0
- noust/web/static/assets/Menu-BDCJy1Lf.js +1 -0
- noust/web/static/assets/MenuTrigger-B3htmf9I.js +1 -0
- noust/web/static/assets/Mono-bXpUsUCG.js +1 -0
- noust/web/static/assets/NeedsAttention-DZtxVk4W.js +1 -0
- noust/web/static/assets/PageHeader-feRus_pw.js +1 -0
- noust/web/static/assets/Progress-Dng3apNQ.js +1 -0
- noust/web/static/assets/QueryState-C9m6Y_Lm.js +1 -0
- noust/web/static/assets/ReachabilityPill-CCOo-DQp.js +1 -0
- noust/web/static/assets/RelativeTime-gJ9efP4E.js +1 -0
- noust/web/static/assets/RollbackDialog-BscXWdT1.js +1 -0
- noust/web/static/assets/RowAction-BskZZuRB.js +1 -0
- noust/web/static/assets/Section-3R8oFu_f.js +1 -0
- noust/web/static/assets/SegmentedControl-B2Hf57R0.js +1 -0
- noust/web/static/assets/Select-BCJ2NPIY.js +1 -0
- noust/web/static/assets/SettingsForm-yAchxAnx.js +1 -0
- noust/web/static/assets/SitesTab-YilI0HYA.js +1 -0
- noust/web/static/assets/Skeleton-Ch_u4SzW.js +1 -0
- noust/web/static/assets/SourceLink-Bb8xdFhh.js +1 -0
- noust/web/static/assets/StatTile-Cm6DDuRb.js +1 -0
- noust/web/static/assets/StatusPill-DYUbSHg3.js +1 -0
- noust/web/static/assets/Switch-CDmCv6DW.js +1 -0
- noust/web/static/assets/Textarea-QZIXXxpc.js +1 -0
- noust/web/static/assets/UnlockForm-Dva7gbJ-.js +1 -0
- noust/web/static/assets/_console-B5D7QaOX.js +1 -0
- noust/web/static/assets/_console-BhpUlgjw.js +1 -0
- noust/web/static/assets/_console-d6-YUU06.js +1 -0
- noust/web/static/assets/_console-vRGQhCNG.js +2 -0
- noust/web/static/assets/_domain-2FThv-3P.js +2 -0
- noust/web/static/assets/_domain-Cf7N2h-b.js +1 -0
- noust/web/static/assets/_domain-Cm2XEgZo.js +2 -0
- noust/web/static/assets/_domain-Mob2qHRX.js +2 -0
- noust/web/static/assets/_id-CyG71a9V.js +1 -0
- noust/web/static/assets/_id-DGwgnq8W.js +2 -0
- noust/web/static/assets/_name-Bkp-LXju.js +2 -0
- noust/web/static/assets/_name-Cnt0Hyol.js +2 -0
- noust/web/static/assets/_name-D0dcNnCI.js +1 -0
- noust/web/static/assets/_name-X5fNzAw0.js +1 -0
- noust/web/static/assets/_site-CVmDna7c.js +4 -0
- noust/web/static/assets/_site-K-RCUvt2.js +2 -0
- noust/web/static/assets/about-BkWWkmWF.js +1 -0
- noust/web/static/assets/activity-DGVTacv3.js +2 -0
- noust/web/static/assets/activity-PMElGahb.js +2 -0
- noust/web/static/assets/apps-DGKx3cZ2.js +1 -0
- noust/web/static/assets/apps-DvSylCBB.js +1 -0
- noust/web/static/assets/apps-VI42jR__.js +2 -0
- noust/web/static/assets/archive-Bypg5U2Q.js +1 -0
- noust/web/static/assets/arrow-down-BbB2VImw.js +1 -0
- noust/web/static/assets/attention-qT4JHWum.js +2 -0
- noust/web/static/assets/backoff-B_sWY3fd.js +1 -0
- noust/web/static/assets/backups-Bxm8dJmX.js +2 -0
- noust/web/static/assets/backups-DMy9OPAm.js +2 -0
- noust/web/static/assets/ban-74715ViC.js +1 -0
- noust/web/static/assets/boxes-DFuSL6Pv.js +1 -0
- noust/web/static/assets/buildLog-C-Y3fMa1.js +4 -0
- noust/web/static/assets/callback-DeFYkBKk.js +1 -0
- noust/web/static/assets/central-CLRBQ3eM.js +1 -0
- noust/web/static/assets/certificates-_tc3H4RL.js +1 -0
- noust/web/static/assets/certs-BIPqV419.js +1 -0
- noust/web/static/assets/channelParts-C1IiEb4J.js +1 -0
- noust/web/static/assets/chevron-down-BcPVh_7t.js +1 -0
- noust/web/static/assets/chevrons-up-down-Csgk-68C.js +1 -0
- noust/web/static/assets/circle-alert-BYFNXpJI.js +1 -0
- noust/web/static/assets/circle-arrow-up-DnwSPs4W.js +1 -0
- noust/web/static/assets/circle-check-B5jBVSLu.js +1 -0
- noust/web/static/assets/circle-minus-Dj8jBJe5.js +1 -0
- noust/web/static/assets/circle-question-mark-Qps4wKMx.js +1 -0
- noust/web/static/assets/clamp-D0kAXd-u.js +1 -0
- noust/web/static/assets/clock-CCuYuMHN.js +1 -0
- noust/web/static/assets/clock-CNzcajE0.js +1 -0
- noust/web/static/assets/cog-BKzOIN61.js +1 -0
- noust/web/static/assets/composite-DOieqZRf.js +1 -0
- noust/web/static/assets/config-CPas7PXO.js +1 -0
- noust/web/static/assets/cron-63dNITsR.js +1 -0
- noust/web/static/assets/cron-CsIs9Fyt.js +2 -0
- noust/web/static/assets/data-C_B6De_t.js +1 -0
- noust/web/static/assets/data-Id7baV8G.js +1 -0
- noust/web/static/assets/data-MUeE8W_R.js +1 -0
- noust/web/static/assets/database-D7P8Yu7D.js +1 -0
- noust/web/static/assets/databases-CXfdJWCt.js +1 -0
- noust/web/static/assets/deployments-D1Ox19Ce.js +2 -0
- noust/web/static/assets/deployments-D7SUA14-.js +1 -0
- noust/web/static/assets/deployments-DET6OZgv.js +1 -0
- noust/web/static/assets/diagnose-BHALkj9m.js +2 -0
- noust/web/static/assets/diagnose-DP9DHDQa.js +1 -0
- noust/web/static/assets/documentTitle-DbZSdOOl.js +1 -0
- noust/web/static/assets/domains-C7fOZy6T.js +1 -0
- noust/web/static/assets/domains-CUBsfbnl.js +1 -0
- noust/web/static/assets/domains-DH308xsX.js +1 -0
- noust/web/static/assets/domains-DPogn61p.js +2 -0
- noust/web/static/assets/domains-MqwwRmqT.js +2 -0
- noust/web/static/assets/download-BM-JGLit.js +1 -0
- noust/web/static/assets/elevation-Co1mUqT2.js +1 -0
- noust/web/static/assets/ellipsis-DdYsNnJs.js +1 -0
- noust/web/static/assets/environment-69kHPcpb.js +2 -0
- noust/web/static/assets/environment-CLjMUZnH.js +4 -0
- noust/web/static/assets/errors-DB5R-NxL.js +1 -0
- noust/web/static/assets/es-BIQ3NJbd.js +1 -0
- noust/web/static/assets/events-DV1SYyzJ.js +1 -0
- noust/web/static/assets/external-link-NGLHTjPS.js +1 -0
- noust/web/static/assets/eye-CBrBUUg5.js +1 -0
- noust/web/static/assets/file-cog-B49E2GiF.js +1 -0
- noust/web/static/assets/file-x-WyjcRTU0.js +1 -0
- noust/web/static/assets/fleet-DMMq-3HF.js +1 -0
- noust/web/static/assets/fleet-qVIaFSE3.js +2 -0
- noust/web/static/assets/folder-open-Ckk6bll2.js +1 -0
- noust/web/static/assets/formErrors-CaUGvCTd.js +1 -0
- noust/web/static/assets/format-BVyd6-PE.js +1 -0
- noust/web/static/assets/getPseudoElementBounds-BXyRTQl7.js +1 -0
- noust/web/static/assets/git-commit-horizontal-CQQys_PZ.js +1 -0
- noust/web/static/assets/git-pull-request-BhB7xTV9.js +1 -0
- noust/web/static/assets/github-rhfevh1s.js +1 -0
- noust/web/static/assets/globe-l3YI3KUl.js +1 -0
- noust/web/static/assets/index-B8mkDsgP.js +11 -0
- noust/web/static/assets/index-Bf3wMvT_.css +1 -0
- noust/web/static/assets/infiniteQueryOptions-Dfvzj6n2.js +1 -0
- noust/web/static/assets/info-CUTi4Chn.js +1 -0
- noust/web/static/assets/integrations-D4DaKQr5.js +1 -0
- noust/web/static/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- noust/web/static/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- noust/web/static/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- noust/web/static/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- noust/web/static/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- noust/web/static/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- noust/web/static/assets/jobs-kAn1wWVK.js +1 -0
- noust/web/static/assets/key-round-6IcTXZuR.js +1 -0
- noust/web/static/assets/lazyRouteComponent-oeYLxgjX.js +1 -0
- noust/web/static/assets/limits-DpW1ccVX.js +1 -0
- noust/web/static/assets/lock-B7aBYydo.js +1 -0
- noust/web/static/assets/log-out-DnXplwRf.js +1 -0
- noust/web/static/assets/login-Beg5MUQR.js +2 -0
- noust/web/static/assets/login-DOmEv78o.js +1 -0
- noust/web/static/assets/logs-B4zhG2Pk.js +1 -0
- noust/web/static/assets/logs-ZUjTSLfY.js +2 -0
- noust/web/static/assets/lookups-D7AEwUzB.js +1 -0
- noust/web/static/assets/metrics-BR4adl0Z.js +1 -0
- noust/web/static/assets/metrics-DGdMwlKO.js +2 -0
- noust/web/static/assets/metrics-DzzRPOVx.js +1 -0
- noust/web/static/assets/minus-CDe17-bG.js +1 -0
- noust/web/static/assets/mona-sans-latin-ext-wdth-normal-CQ1tG6In.woff2 +0 -0
- noust/web/static/assets/mona-sans-latin-wdth-normal-BMVx8nn_.woff2 +0 -0
- noust/web/static/assets/mona-sans-vietnamese-wdth-normal-Rd0aZl9c.woff2 +0 -0
- noust/web/static/assets/monitor-CUYOShol.js +1 -0
- noust/web/static/assets/mutation-dkJzcVye.js +1 -0
- noust/web/static/assets/names-D39ExiEN.js +1 -0
- noust/web/static/assets/nav-C8GxZyac.js +1 -0
- noust/web/static/assets/network-BSG1FjG3.js +1 -0
- noust/web/static/assets/new-Bdgq5fLm.js +2 -0
- noust/web/static/assets/nodeErrors-_pUC78zm.js +1 -0
- noust/web/static/assets/notifications-Cxcd3ePM.js +2 -0
- noust/web/static/assets/noust-icon-CvSo_g1X.svg +7 -0
- noust/web/static/assets/noust-icon-mono-DgxsBjlK.svg +7 -0
- noust/web/static/assets/noust-mark--vvPicbB.svg +8 -0
- noust/web/static/assets/noust-mark-mono-DgNpATEl.svg +8 -0
- noust/web/static/assets/noust-wordmark-TqLHnHrv.svg +10 -0
- noust/web/static/assets/noust-wordmark-mono-BlSNg7gr.svg +10 -0
- noust/web/static/assets/panel-top-QiWkOEDm.js +1 -0
- noust/web/static/assets/pencil-CE418y20.js +1 -0
- noust/web/static/assets/play-BTPPX6Gx.js +1 -0
- noust/web/static/assets/plus-CHKUeaCi.js +1 -0
- noust/web/static/assets/redirect-B4OxYbs2.js +1 -0
- noust/web/static/assets/rocket-BMEuDiHj.js +1 -0
- noust/web/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- noust/web/static/assets/root-DLTE-HSj.js +1 -0
- noust/web/static/assets/rotate-ccw-DOOaBthI.js +1 -0
- noust/web/static/assets/rotate-ccw-clock-BQqG_1xZ.js +1 -0
- noust/web/static/assets/scroll-text-DHqQ7NpH.js +1 -0
- noust/web/static/assets/search-ubxk2oKw.js +1 -0
- noust/web/static/assets/security-_SRp1dPn.js +5 -0
- noust/web/static/assets/serializeValue-CNr88Bzt.js +1 -0
- noust/web/static/assets/series-qMpiUCG-.js +3 -0
- noust/web/static/assets/server-azZdtSqm.js +1 -0
- noust/web/static/assets/servers-CceT5dA-.js +2 -0
- noust/web/static/assets/servers-Dw7hO_hH.js +2 -0
- noust/web/static/assets/services-BGpz7kMG.js +2 -0
- noust/web/static/assets/services-CRksRWSu.js +14 -0
- noust/web/static/assets/services-c6-fmTt9.js +1 -0
- noust/web/static/assets/settings-BM-98MbS.js +1 -0
- noust/web/static/assets/settings-BXvYioUw.js +2 -0
- noust/web/static/assets/settings-C13Rtjan.js +1 -0
- noust/web/static/assets/settings-DzMoBV6N.js +2 -0
- noust/web/static/assets/shield-check-DB7XY_hx.js +1 -0
- noust/web/static/assets/shield-off-CfC67J2A.js +1 -0
- noust/web/static/assets/sites-DaWcdCEJ.js +1 -0
- noust/web/static/assets/sockets-CKdggv7z.js +1 -0
- noust/web/static/assets/square-Ct2HPXcA.js +1 -0
- noust/web/static/assets/square-terminal-DdDD0Zro.js +1 -0
- noust/web/static/assets/status-ctpAj_oh.js +1 -0
- noust/web/static/assets/system-_JUIkbxt.js +1 -0
- noust/web/static/assets/tabStrip-DSgENPj5.js +1 -0
- noust/web/static/assets/theme-D_mPN_Y9.js +1 -0
- noust/web/static/assets/toggle-right-Br96epGU.js +1 -0
- noust/web/static/assets/tokens-BRUCnpId.js +1 -0
- noust/web/static/assets/trash-BNcFmAWf.js +1 -0
- noust/web/static/assets/triangle-alert-CKg_efzz.js +1 -0
- noust/web/static/assets/undo-2-Da9WuG6E.js +1 -0
- noust/web/static/assets/url-Ox09ysXG.js +1 -0
- noust/web/static/assets/useAnnounceChange-Bekpdejb.js +1 -0
- noust/web/static/assets/useAppActions--yu6mCx-.js +1 -0
- noust/web/static/assets/useAriaLabelledBy-BKs0q4yy.js +1 -0
- noust/web/static/assets/useBaseUiId-CC6U9MPC.js +1 -0
- noust/web/static/assets/useBlocker-Big88cC6.js +1 -0
- noust/web/static/assets/useCertificateJobs-DJj5yc57.js +1 -0
- noust/web/static/assets/useDeleteApp-DVoqSzzX.js +1 -0
- noust/web/static/assets/useInfiniteQuery-C1a8KNZc.js +1 -0
- noust/web/static/assets/useLabel-DIsdVo_X.js +1 -0
- noust/web/static/assets/useMutation-U4cnOWEg.js +1 -0
- noust/web/static/assets/useNode-CWnt8Qg0.js +1 -0
- noust/web/static/assets/useOpenInteractionType-CkATH8nH.js +1 -0
- noust/web/static/assets/useRegisterFieldControl-Bnbvmayb.js +1 -0
- noust/web/static/assets/useRegisteredLabelId-BgUdkgMr.js +1 -0
- noust/web/static/assets/useServiceActions-W30A2d-s.js +1 -0
- noust/web/static/assets/useSettingsForm-CV-Jv0Da.js +1 -0
- noust/web/static/assets/useStateTransitions-DVyXXIl_.js +1 -0
- noust/web/static/assets/useSwipeDismiss-lXqFGztq.js +1 -0
- noust/web/static/assets/useValueChanged-DbkN6cK5.js +1 -0
- noust/web/static/assets/words-kR5gFSKX.js +1 -0
- noust/web/static/assets/x-DcoAY9ab.js +1 -0
- noust/web/static/favicon-32.png +0 -0
- noust/web/static/favicon.svg +7 -0
- noust/web/static/icon-192.png +0 -0
- noust/web/static/icon-512.png +0 -0
- noust/web/static/index.html +82 -0
- noust/web/websockets/__init__.py +7 -0
- noust/web/websockets/router.py +888 -0
- noust-3.0.0.data/data/share/man/man1/noust.1 +2499 -0
- noust-3.0.0.dist-info/METADATA +853 -0
- noust-3.0.0.dist-info/RECORD +530 -0
- noust-3.0.0.dist-info/WHEEL +5 -0
- noust-3.0.0.dist-info/entry_points.txt +3 -0
- noust-3.0.0.dist-info/licenses/LICENSE +661 -0
- noust-3.0.0.dist-info/top_level.txt +1 -0
noust/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Yago López Prado
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Noust - deploy and manage web applications on Linux servers.
|
|
6
|
+
|
|
7
|
+
Called WASM (Web App System Management) until 3.0.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError
|
|
11
|
+
from importlib.metadata import version as _installed_version
|
|
12
|
+
|
|
13
|
+
#: Fallback used when running from a source tree that was never installed, such
|
|
14
|
+
#: as an OBS build directory. The single source of truth is the ``version``
|
|
15
|
+
#: field of pyproject.toml; ``scripts/release.py`` keeps this literal and the
|
|
16
|
+
#: distribution packaging files in step with it.
|
|
17
|
+
_FALLBACK_VERSION = "3.0.0"
|
|
18
|
+
|
|
19
|
+
#: The distribution names this package has been published under: ``noust``
|
|
20
|
+
#: from 3.0, ``wasm-cli`` before (and as the transitional package after).
|
|
21
|
+
_DISTRIBUTIONS = ("noust", "wasm-cli")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_version() -> str:
|
|
25
|
+
"""
|
|
26
|
+
Read the installed version, whichever name the distribution has.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The version of the first distribution found, else the fallback.
|
|
30
|
+
"""
|
|
31
|
+
for distribution in _DISTRIBUTIONS:
|
|
32
|
+
try:
|
|
33
|
+
return _installed_version(distribution)
|
|
34
|
+
except PackageNotFoundError:
|
|
35
|
+
continue
|
|
36
|
+
return _FALLBACK_VERSION # pragma: no cover - only hit in uninstalled trees
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__version__ = _read_version()
|
|
40
|
+
|
|
41
|
+
__author__ = "Yago López Prado"
|
|
42
|
+
__license__ = "AGPL-3.0-or-later"
|
|
43
|
+
|
|
44
|
+
from noust.core.config import Config
|
|
45
|
+
from noust.core.exceptions import NoustError
|
|
46
|
+
from noust.core.logger import Logger
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"Config",
|
|
50
|
+
"Logger",
|
|
51
|
+
"NoustError",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
noust/__main__.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Yago Lopez Prado
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The central: a Noust whose job is to manage other servers.
|
|
6
|
+
|
|
7
|
+
Every Noust can manage a fleet; what this package adds is the *role* that
|
|
8
|
+
says whether it also deploys applications itself (``central.role``):
|
|
9
|
+
|
|
10
|
+
- ``server`` (the default): the Noust every release has been. It deploys and
|
|
11
|
+
serves applications, and may also manage other servers.
|
|
12
|
+
- ``hub``: a central with no local deployments - the container on a NAS, or a
|
|
13
|
+
small VPS whose only job is the fleet. Applications, sites, certificates,
|
|
14
|
+
databases and the rest of what needs nginx and systemd *here* are refused
|
|
15
|
+
with a message instead of failing half-way on a missing nginx; the
|
|
16
|
+
console hides them.
|
|
17
|
+
|
|
18
|
+
:func:`role` is the one reading of the setting. :func:`require_server_role`
|
|
19
|
+
is the guard a feature calls where it starts, and :func:`central_info` is
|
|
20
|
+
what the API hands the console (``central`` in the session payload) so it
|
|
21
|
+
can hide what a hub does not do and show the unlock form while the secrets
|
|
22
|
+
are sealed and locked.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
from noust.core.config import Config
|
|
30
|
+
from noust.core.exceptions import ConfigError, NoustError
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
#: A Noust that deploys applications here.
|
|
36
|
+
ROLE_SERVER = "server"
|
|
37
|
+
#: A central with no local deployments.
|
|
38
|
+
ROLE_HUB = "hub"
|
|
39
|
+
#: Every role, the default first.
|
|
40
|
+
ROLES: tuple[str, ...] = (ROLE_SERVER, ROLE_HUB)
|
|
41
|
+
|
|
42
|
+
#: CLI commands a hub refuses, and the feature each belongs to, as the
|
|
43
|
+
#: refusal names it. Everything not listed (the console, tokens, sessions,
|
|
44
|
+
#: two-factor, configuration, notifications, the fleet and the central
|
|
45
|
+
#: itself) works the same on both roles.
|
|
46
|
+
HUB_REFUSED_COMMANDS: dict[str, str] = {
|
|
47
|
+
**dict.fromkeys(
|
|
48
|
+
("create", "delete", "update", "start", "stop", "restart", "status", "logs", "list"),
|
|
49
|
+
"Applications",
|
|
50
|
+
),
|
|
51
|
+
"app": "Applications",
|
|
52
|
+
"releases": "Releases",
|
|
53
|
+
"preview": "Preview environments",
|
|
54
|
+
"recipe": "Recipes",
|
|
55
|
+
"import": "Importing applications",
|
|
56
|
+
"env": "Application environments",
|
|
57
|
+
"github": "GitHub deploys",
|
|
58
|
+
"site": "Sites",
|
|
59
|
+
"domain": "Domains",
|
|
60
|
+
"cert": "Certificates",
|
|
61
|
+
"db": "Databases",
|
|
62
|
+
"backup": "Application backups",
|
|
63
|
+
"rollback": "Rollbacks",
|
|
64
|
+
"cron": "Scheduled jobs",
|
|
65
|
+
"service": "Services",
|
|
66
|
+
"monitor": "The process monitor",
|
|
67
|
+
"diagnose": "Application diagnosis",
|
|
68
|
+
"health": "The server health report",
|
|
69
|
+
"setup": "Server setup (nginx, runtimes, certbot)",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RoleError(NoustError):
|
|
74
|
+
"""A feature was asked of a Noust whose role does not have it."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def role(config: Config | None = None) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Return this Noust's role.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
config: The configuration; the process-wide one by default.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
``"server"`` or ``"hub"``.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ConfigError: When ``central.role`` (or ``NOUST_CENTRAL_ROLE``, which
|
|
89
|
+
overrides it) holds anything else: guessing a role would either
|
|
90
|
+
refuse everything or let a hub try to run nginx.
|
|
91
|
+
"""
|
|
92
|
+
value = str((config or Config()).get("central.role", ROLE_SERVER) or ROLE_SERVER)
|
|
93
|
+
normalised = value.strip().lower()
|
|
94
|
+
if normalised not in ROLES:
|
|
95
|
+
raise ConfigError(
|
|
96
|
+
f"central.role must be one of {', '.join(ROLES)}, not {value!r}",
|
|
97
|
+
details=(
|
|
98
|
+
"Fix it with 'noust config set central.role server' (or hub), or correct "
|
|
99
|
+
"NOUST_CENTRAL_ROLE in the environment."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
return normalised
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_hub(config: Config | None = None) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Report whether this Noust is a hub.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config: The configuration; the process-wide one by default.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True when ``central.role`` is ``hub``.
|
|
114
|
+
"""
|
|
115
|
+
return role(config) == ROLE_HUB
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def require_server_role(feature: str, config: Config | None = None) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Refuse a local-deployment feature on a hub.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
feature: What was asked for, as a label ("Applications").
|
|
124
|
+
config: The configuration; the process-wide one by default.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
RoleError: When this Noust is a hub.
|
|
128
|
+
"""
|
|
129
|
+
if not is_hub(config):
|
|
130
|
+
return
|
|
131
|
+
raise RoleError(
|
|
132
|
+
f"{feature}: not available on this central, which is a hub",
|
|
133
|
+
details=(
|
|
134
|
+
"A hub (central.role = hub) manages other servers and deploys nothing "
|
|
135
|
+
"itself. Pick the server in the console, or run the command against a node. "
|
|
136
|
+
"On a machine that should also deploy, set 'noust config set central.role "
|
|
137
|
+
"server'; a container cannot, it has no nginx or systemd."
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def refuse_command_on_hub(name: str, ctx: click.Context | None = None) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Refuse a CLI command a hub does not have.
|
|
145
|
+
|
|
146
|
+
Called by the root command group when it resolves a subcommand, so every
|
|
147
|
+
command is covered by one check rather than by one per command.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: The command's canonical name.
|
|
151
|
+
ctx: The root context. A command aimed at another server (a ``node``
|
|
152
|
+
parameter, when the fleet adds one) runs there, not here, and is
|
|
153
|
+
not refused.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
RoleError: When this Noust is a hub and the command deploys locally.
|
|
157
|
+
"""
|
|
158
|
+
feature = HUB_REFUSED_COMMANDS.get(name)
|
|
159
|
+
if feature is None:
|
|
160
|
+
return
|
|
161
|
+
if ctx is not None and ctx.params.get("node"):
|
|
162
|
+
return
|
|
163
|
+
require_server_role(feature)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def central_info(config: Config | None = None) -> dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Describe the central for the console: its role and its secrets' state.
|
|
169
|
+
|
|
170
|
+
This is the payload the API exposes as ``central`` (in the session the
|
|
171
|
+
console reads after sign-in), so the console can hide what a hub does not
|
|
172
|
+
do and show the unlock form while the secrets are sealed and locked.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
config: The configuration; the process-wide one by default.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
``{"role": "server" | "hub", "sealed": bool, "locked": bool}``;
|
|
179
|
+
``locked`` is True only while the store is sealed and this process
|
|
180
|
+
has not been given the passphrase.
|
|
181
|
+
"""
|
|
182
|
+
from noust.core import sealing
|
|
183
|
+
from noust.core.secrets import secrets_dir
|
|
184
|
+
|
|
185
|
+
root = secrets_dir()
|
|
186
|
+
sealed = sealing.is_sealed(root)
|
|
187
|
+
return {
|
|
188
|
+
"role": role(config),
|
|
189
|
+
"sealed": sealed,
|
|
190
|
+
"locked": sealed and not sealing.is_unlocked(root),
|
|
191
|
+
}
|
noust/central/setup.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Yago Lopez Prado
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
What ``noust central run`` does before it serves: the data directory, the
|
|
6
|
+
console's first credential, who may connect, and the facts ``noust central
|
|
7
|
+
status`` reports.
|
|
8
|
+
|
|
9
|
+
The first start is decided by the one fact that marks it: no master token
|
|
10
|
+
has ever been issued. That start issues one and the caller prints it; every
|
|
11
|
+
later start issues none, so the token appears once in ``docker logs`` and
|
|
12
|
+
never again (only its hash is kept, as on any Noust).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import binascii
|
|
19
|
+
import hashlib
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sqlite3
|
|
23
|
+
from collections.abc import Mapping, Sequence
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from noust.core import paths
|
|
28
|
+
from noust.core.config import Config
|
|
29
|
+
from noust.core.exceptions import ConfigError
|
|
30
|
+
from noust.core.fs import SECRET_DIR_MODE, FileSystem, get_fs
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from noust.web.auth import TokenManager
|
|
34
|
+
|
|
35
|
+
#: Who may reach a central's console when nothing else is said: this
|
|
36
|
+
#: machine, the private IPv4 ranges (RFC 1918) and IPv6 unique local
|
|
37
|
+
#: addresses. A NAS console answers the LAN, and a port forwarded to it by
|
|
38
|
+
#: mistake still answers nobody on the internet.
|
|
39
|
+
DEFAULT_ALLOWLIST: tuple[str, ...] = (
|
|
40
|
+
"127.0.0.0/8",
|
|
41
|
+
"::1/128",
|
|
42
|
+
"10.0.0.0/8",
|
|
43
|
+
"172.16.0.0/12",
|
|
44
|
+
"192.168.0.0/16",
|
|
45
|
+
"fc00::/7",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
#: The environment variable that overrides the allowlist, entries separated
|
|
49
|
+
#: by spaces or commas.
|
|
50
|
+
ALLOW_IP_ENV = "NOUST_ALLOW_IP"
|
|
51
|
+
#: A certificate pair the operator brings, instead of the self-signed one.
|
|
52
|
+
TLS_CERT_ENV = "NOUST_TLS_CERT"
|
|
53
|
+
TLS_KEY_ENV = "NOUST_TLS_KEY"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def data_directories() -> tuple[Path, ...]:
|
|
57
|
+
"""
|
|
58
|
+
Return the directories a central keeps its data in.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Under ``NOUST_DATA_DIR`` when it is set; otherwise the system
|
|
62
|
+
configuration and state directories.
|
|
63
|
+
"""
|
|
64
|
+
if paths.DATA_DIR is not None:
|
|
65
|
+
return paths.data_layout(paths.DATA_DIR).all()
|
|
66
|
+
return (paths.config_dir(), paths.state_dir())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ensure_data_dir(
|
|
70
|
+
directories: Sequence[Path] | None = None,
|
|
71
|
+
*,
|
|
72
|
+
root: Path | None = None,
|
|
73
|
+
fs: FileSystem | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Create the data directories, owner-only, and check they can be written.
|
|
77
|
+
|
|
78
|
+
The store falls back to a per-user location when its directory is not
|
|
79
|
+
there, so the directories are created before anything opens it: a
|
|
80
|
+
central whose store landed outside the volume would lose its fleet on
|
|
81
|
+
the next image update.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
directories: What to create; :func:`data_directories` by default.
|
|
85
|
+
root: The data directory itself, checked for writability first;
|
|
86
|
+
``NOUST_DATA_DIR`` by default.
|
|
87
|
+
fs: The filesystem seam; the process-wide one by default.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ConfigError: When the data directory cannot be written by this
|
|
91
|
+
user, which on a bind mount means it belongs to someone else.
|
|
92
|
+
"""
|
|
93
|
+
fs = fs or get_fs()
|
|
94
|
+
root = root if root is not None else paths.DATA_DIR
|
|
95
|
+
if root is not None and not (root.is_dir() and os.access(root, os.W_OK)):
|
|
96
|
+
raise ConfigError(
|
|
97
|
+
f"The data directory {root} is not writable by uid {os.getuid()}",
|
|
98
|
+
details=(
|
|
99
|
+
"A folder bind-mounted from the host must belong to this user first: "
|
|
100
|
+
f"chown -R {os.getuid()}:{os.getgid()} <the folder on the host>. "
|
|
101
|
+
"A named Docker volume is created with the right owner."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
for directory in directories if directories is not None else data_directories():
|
|
105
|
+
try:
|
|
106
|
+
fs.make_dir(directory, mode=SECRET_DIR_MODE, parents=True)
|
|
107
|
+
except PermissionError as exc:
|
|
108
|
+
raise ConfigError(
|
|
109
|
+
f"Cannot create {directory} as uid {os.getuid()}",
|
|
110
|
+
details=(
|
|
111
|
+
f"Run as root, or set {paths.DATA_DIR_ENV} to a directory this user "
|
|
112
|
+
"owns, as the container image does (/data)."
|
|
113
|
+
),
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def allowlist(environ: Mapping[str, str] | None = None, config: Config | None = None) -> list[str]:
|
|
118
|
+
"""
|
|
119
|
+
Decide who may connect to the central's console.
|
|
120
|
+
|
|
121
|
+
Precedence: ``NOUST_ALLOW_IP``, then ``web.ip_whitelist`` in config.yaml,
|
|
122
|
+
then :data:`DEFAULT_ALLOWLIST`. There is no "anyone" by omission: an
|
|
123
|
+
operator who wants it says ``0.0.0.0/0 ::/0`` in so many words.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
environ: The environment; the process's own by default.
|
|
127
|
+
config: The configuration; the process-wide one by default.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Addresses and networks, in the form ``web.ip_whitelist`` takes.
|
|
131
|
+
"""
|
|
132
|
+
env = os.environ if environ is None else environ
|
|
133
|
+
raw = env.get(ALLOW_IP_ENV, "")
|
|
134
|
+
entries = [entry for entry in re.split(r"[\s,]+", raw) if entry]
|
|
135
|
+
if entries:
|
|
136
|
+
return entries
|
|
137
|
+
configured = [str(entry) for entry in (config or Config()).get("web.ip_whitelist") or []]
|
|
138
|
+
return configured or list(DEFAULT_ALLOWLIST)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def operator_tls_pair(environ: Mapping[str, str] | None = None) -> tuple[str, str] | None:
|
|
142
|
+
"""
|
|
143
|
+
Return the certificate pair the operator brought, if any.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
environ: The environment; the process's own by default.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
``(certificate, key)``, or None to mint a self-signed pair.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ConfigError: When only one of the two is set.
|
|
153
|
+
"""
|
|
154
|
+
env = os.environ if environ is None else environ
|
|
155
|
+
cert = env.get(TLS_CERT_ENV, "").strip()
|
|
156
|
+
key = env.get(TLS_KEY_ENV, "").strip()
|
|
157
|
+
if not cert and not key:
|
|
158
|
+
return None
|
|
159
|
+
if not cert or not key:
|
|
160
|
+
missing = TLS_KEY_ENV if cert else TLS_CERT_ENV
|
|
161
|
+
raise ConfigError(
|
|
162
|
+
"A TLS certificate and its private key travel together",
|
|
163
|
+
details=f"Set {missing} as well, or neither to use the self-signed certificate.",
|
|
164
|
+
)
|
|
165
|
+
return cert, key
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def issue_first_token(manager: TokenManager) -> str | None:
|
|
169
|
+
"""
|
|
170
|
+
Issue the console's first master token, if none was ever issued.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
manager: The console's token manager.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The token, to be shown once; None on every later start.
|
|
177
|
+
"""
|
|
178
|
+
if manager.current_master_generation() is not None:
|
|
179
|
+
return None
|
|
180
|
+
return manager.generate_master_token()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def certificate_fingerprint(path: Path) -> str | None:
|
|
184
|
+
"""
|
|
185
|
+
Compute the SHA-256 fingerprint of a PEM certificate.
|
|
186
|
+
|
|
187
|
+
It is what a browser shows for a self-signed certificate, so the
|
|
188
|
+
operator can check that the one it warns about is this central's.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
path: The certificate file.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
``AB:CD:...``, or None when the file is missing or holds no
|
|
195
|
+
certificate.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
text = path.read_text(encoding="ascii")
|
|
199
|
+
except (OSError, UnicodeDecodeError):
|
|
200
|
+
return None
|
|
201
|
+
match = re.search(
|
|
202
|
+
r"-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----", text, flags=re.DOTALL
|
|
203
|
+
)
|
|
204
|
+
if match is None:
|
|
205
|
+
return None
|
|
206
|
+
try:
|
|
207
|
+
der = base64.b64decode("".join(match.group(1).split()), validate=True)
|
|
208
|
+
except (binascii.Error, ValueError):
|
|
209
|
+
return None
|
|
210
|
+
digest = hashlib.sha256(der).hexdigest().upper()
|
|
211
|
+
return ":".join(digest[index : index + 2] for index in range(0, len(digest), 2))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def count_nodes(db_path: Path) -> int | None:
|
|
215
|
+
"""
|
|
216
|
+
Count the servers the central manages.
|
|
217
|
+
|
|
218
|
+
Read-only, on a connection of its own: a status report must not create,
|
|
219
|
+
migrate or lock the store.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
db_path: The store's database file.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The number of nodes, or None when there is no store or no nodes
|
|
226
|
+
table in it yet.
|
|
227
|
+
"""
|
|
228
|
+
if not db_path.is_file():
|
|
229
|
+
return None
|
|
230
|
+
try:
|
|
231
|
+
connection = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5)
|
|
232
|
+
except sqlite3.Error:
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
row = connection.execute("SELECT COUNT(*) FROM nodes").fetchone()
|
|
236
|
+
except sqlite3.Error:
|
|
237
|
+
return None
|
|
238
|
+
finally:
|
|
239
|
+
connection.close()
|
|
240
|
+
return int(row[0]) if row else 0
|