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.
Files changed (530) hide show
  1. noust/__init__.py +53 -0
  2. noust/__main__.py +8 -0
  3. noust/central/__init__.py +191 -0
  4. noust/central/setup.py +240 -0
  5. noust/central/unlock.py +283 -0
  6. noust/cli/__init__.py +9 -0
  7. noust/cli/app.py +628 -0
  8. noust/cli/commands/__init__.py +19 -0
  9. noust/cli/commands/app.py +782 -0
  10. noust/cli/commands/backup.py +2294 -0
  11. noust/cli/commands/central.py +417 -0
  12. noust/cli/commands/cert.py +767 -0
  13. noust/cli/commands/config.py +577 -0
  14. noust/cli/commands/cron.py +464 -0
  15. noust/cli/commands/db.py +1877 -0
  16. noust/cli/commands/diagnose.py +94 -0
  17. noust/cli/commands/domain.py +193 -0
  18. noust/cli/commands/env.py +477 -0
  19. noust/cli/commands/fleet.py +344 -0
  20. noust/cli/commands/github.py +189 -0
  21. noust/cli/commands/health.py +202 -0
  22. noust/cli/commands/importer.py +155 -0
  23. noust/cli/commands/migrate_from_wasm.py +126 -0
  24. noust/cli/commands/monitor.py +763 -0
  25. noust/cli/commands/node.py +291 -0
  26. noust/cli/commands/notify.py +88 -0
  27. noust/cli/commands/preview.py +261 -0
  28. noust/cli/commands/recipe.py +112 -0
  29. noust/cli/commands/releases.py +157 -0
  30. noust/cli/commands/service.py +526 -0
  31. noust/cli/commands/sessions.py +120 -0
  32. noust/cli/commands/setup.py +1533 -0
  33. noust/cli/commands/site.py +592 -0
  34. noust/cli/commands/store.py +612 -0
  35. noust/cli/commands/token.py +190 -0
  36. noust/cli/commands/twofa.py +134 -0
  37. noust/cli/commands/version.py +118 -0
  38. noust/cli/commands/web.py +2801 -0
  39. noust/cli/commands/webapp.py +1938 -0
  40. noust/cli/interactive.py +740 -0
  41. noust/cli/panel_links.py +122 -0
  42. noust/cli/prompts.py +181 -0
  43. noust/cli/rename_notice.py +93 -0
  44. noust/cli/web_state.py +48 -0
  45. noust/completions/README.md +20 -0
  46. noust/completions/_noust +40 -0
  47. noust/completions/_wasm +40 -0
  48. noust/completions/noust.bash +28 -0
  49. noust/completions/noust.fish +17 -0
  50. noust/completions/wasm.bash +28 -0
  51. noust/completions/wasm.fish +17 -0
  52. noust/core/__init__.py +40 -0
  53. noust/core/app_state.py +364 -0
  54. noust/core/applock.py +333 -0
  55. noust/core/background.py +181 -0
  56. noust/core/config.py +1938 -0
  57. noust/core/dependencies.py +905 -0
  58. noust/core/deploy_notifications.py +241 -0
  59. noust/core/exceptions.py +404 -0
  60. noust/core/forge_events.py +303 -0
  61. noust/core/fs.py +405 -0
  62. noust/core/logger.py +729 -0
  63. noust/core/messages.py +328 -0
  64. noust/core/migrate_from_wasm.py +1478 -0
  65. noust/core/net.py +251 -0
  66. noust/core/notifier.py +1177 -0
  67. noust/core/package_index.py +909 -0
  68. noust/core/paths.py +438 -0
  69. noust/core/redact.py +296 -0
  70. noust/core/runner.py +1614 -0
  71. noust/core/sealing.py +1157 -0
  72. noust/core/secret_detection.py +483 -0
  73. noust/core/secrets.py +294 -0
  74. noust/core/store.py +4700 -0
  75. noust/core/timeutil.py +67 -0
  76. noust/core/totp.py +176 -0
  77. noust/core/update_checker.py +653 -0
  78. noust/core/utils.py +514 -0
  79. noust/deployers/__init__.py +15 -0
  80. noust/deployers/app_export.py +1503 -0
  81. noust/deployers/auto.py +352 -0
  82. noust/deployers/base.py +2794 -0
  83. noust/deployers/bluegreen.py +1675 -0
  84. noust/deployers/deploy_events.py +186 -0
  85. noust/deployers/docker_compose.py +1950 -0
  86. noust/deployers/domains.py +648 -0
  87. noust/deployers/helpers/__init__.py +27 -0
  88. noust/deployers/helpers/app_env.py +218 -0
  89. noust/deployers/helpers/databases.py +566 -0
  90. noust/deployers/helpers/env_manager.py +825 -0
  91. noust/deployers/helpers/health.py +173 -0
  92. noust/deployers/helpers/health_gate.py +340 -0
  93. noust/deployers/helpers/layout.py +242 -0
  94. noust/deployers/helpers/nginx_config.py +294 -0
  95. noust/deployers/helpers/package_manager.py +249 -0
  96. noust/deployers/helpers/path_resolver.py +186 -0
  97. noust/deployers/helpers/permissions.py +135 -0
  98. noust/deployers/helpers/php_fpm.py +1192 -0
  99. noust/deployers/helpers/preflight.py +200 -0
  100. noust/deployers/helpers/prisma.py +129 -0
  101. noust/deployers/helpers/registration.py +263 -0
  102. noust/deployers/helpers/release_build.py +394 -0
  103. noust/deployers/helpers/summary.py +90 -0
  104. noust/deployers/helpers/target.py +144 -0
  105. noust/deployers/helpers/turbo.py +334 -0
  106. noust/deployers/helpers/workspace.py +424 -0
  107. noust/deployers/importers/__init__.py +154 -0
  108. noust/deployers/importers/base.py +389 -0
  109. noust/deployers/importers/heroku.py +276 -0
  110. noust/deployers/importers/railway.py +236 -0
  111. noust/deployers/importers/render.py +362 -0
  112. noust/deployers/importers/toml_fallback.py +449 -0
  113. noust/deployers/importers/vercel.py +217 -0
  114. noust/deployers/inspect.py +946 -0
  115. noust/deployers/interface.py +240 -0
  116. noust/deployers/lifecycle.py +2614 -0
  117. noust/deployers/logs.py +121 -0
  118. noust/deployers/migrate.py +1499 -0
  119. noust/deployers/monorepo.py +1836 -0
  120. noust/deployers/nextjs.py +166 -0
  121. noust/deployers/nodejs.py +147 -0
  122. noust/deployers/php_fpm.py +1400 -0
  123. noust/deployers/pipeline.py +123 -0
  124. noust/deployers/python.py +281 -0
  125. noust/deployers/recorder.py +763 -0
  126. noust/deployers/registry.py +208 -0
  127. noust/deployers/releases.py +972 -0
  128. noust/deployers/static.py +230 -0
  129. noust/deployers/vite.py +230 -0
  130. noust/fleet/__init__.py +26 -0
  131. noust/fleet/audit.py +68 -0
  132. noust/fleet/authorize.py +948 -0
  133. noust/fleet/client.py +387 -0
  134. noust/fleet/joincode.py +264 -0
  135. noust/fleet/keys.py +363 -0
  136. noust/fleet/models.py +414 -0
  137. noust/fleet/nodes.py +453 -0
  138. noust/fleet/policy.py +47 -0
  139. noust/fleet/status.py +165 -0
  140. noust/fleet/tunnels.py +755 -0
  141. noust/integrations/__init__.py +4 -0
  142. noust/integrations/github/__init__.py +11 -0
  143. noust/integrations/github/app.py +574 -0
  144. noust/integrations/github/client.py +329 -0
  145. noust/integrations/github/comments.py +69 -0
  146. noust/integrations/github/manifest.py +336 -0
  147. noust/integrations/github/service.py +445 -0
  148. noust/integrations/github/statuses.py +357 -0
  149. noust/integrations/github/webhooks.py +156 -0
  150. noust/integrations/hooks_site.py +334 -0
  151. noust/main.py +22 -0
  152. noust/managers/__init__.py +24 -0
  153. noust/managers/apache_manager.py +82 -0
  154. noust/managers/backup_destinations.py +1799 -0
  155. noust/managers/backup_manager.py +3848 -0
  156. noust/managers/backup_scheduler.py +880 -0
  157. noust/managers/base_manager.py +299 -0
  158. noust/managers/cert_manager.py +1383 -0
  159. noust/managers/cron_manager.py +1243 -0
  160. noust/managers/database/__init__.py +31 -0
  161. noust/managers/database/base.py +1524 -0
  162. noust/managers/database/mongodb.py +823 -0
  163. noust/managers/database/mysql.py +1223 -0
  164. noust/managers/database/postgres.py +1720 -0
  165. noust/managers/database/psql_script.py +551 -0
  166. noust/managers/database/redis.py +941 -0
  167. noust/managers/database/registry.py +125 -0
  168. noust/managers/diagnose.py +1148 -0
  169. noust/managers/health.py +443 -0
  170. noust/managers/nginx_manager.py +97 -0
  171. noust/managers/previews.py +2158 -0
  172. noust/managers/service_manager.py +2514 -0
  173. noust/managers/source_manager.py +2995 -0
  174. noust/managers/webserver.py +1840 -0
  175. noust/monitor/__init__.py +106 -0
  176. noust/monitor/email_notifier.py +479 -0
  177. noust/monitor/metrics.py +380 -0
  178. noust/monitor/models.py +184 -0
  179. noust/monitor/observation_store.py +382 -0
  180. noust/monitor/process_monitor.py +1135 -0
  181. noust/monitor/signals.py +229 -0
  182. noust/monitor/timeseries.py +421 -0
  183. noust/py.typed +0 -0
  184. noust/recipes/__init__.py +691 -0
  185. noust/recipes/assets/n8n/package.json +14 -0
  186. noust/recipes/assets/wordpress/wp-config.php +51 -0
  187. noust/recipes/deploy.py +348 -0
  188. noust/recipes/ghost.yaml +6 -0
  189. noust/recipes/n8n.yaml +37 -0
  190. noust/recipes/plausible.yaml +6 -0
  191. noust/recipes/render.py +109 -0
  192. noust/recipes/umami.yaml +32 -0
  193. noust/recipes/uptime-kuma.yaml +25 -0
  194. noust/recipes/wordpress.yaml +62 -0
  195. noust/templates/__init__.py +1 -0
  196. noust/templates/apache/_redirects.j2 +33 -0
  197. noust/templates/apache/proxy.conf.j2 +63 -0
  198. noust/templates/apache/static.conf.j2 +83 -0
  199. noust/templates/nginx/_redirects.j2 +40 -0
  200. noust/templates/nginx/advanced.conf.j2 +118 -0
  201. noust/templates/nginx/fastcgi.conf.j2 +144 -0
  202. noust/templates/nginx/hooks.conf.j2 +71 -0
  203. noust/templates/nginx/monorepo.conf.j2 +144 -0
  204. noust/templates/nginx/proxy.conf.j2 +103 -0
  205. noust/templates/nginx/static.conf.j2 +91 -0
  206. noust/templates/php/pool.conf.j2 +56 -0
  207. noust/templates/systemd/_escape.j2 +21 -0
  208. noust/templates/systemd/app.service.j2 +54 -0
  209. noust/templates/systemd/app@.service.j2 +56 -0
  210. noust/templates/systemd/backup-service.j2 +10 -0
  211. noust/templates/systemd/backup-timer.j2 +14 -0
  212. noust/templates/systemd/cron-service.j2 +28 -0
  213. noust/templates/systemd/cron-timer.j2 +17 -0
  214. noust/templates/systemd/docker-compose.service.j2 +40 -0
  215. noust/templates/systemd/noust-web.service.j2 +61 -0
  216. noust/templates/systemd/preview-sweep.service.j2 +10 -0
  217. noust/templates/systemd/preview-sweep.timer.j2 +13 -0
  218. noust/validators/__init__.py +32 -0
  219. noust/validators/domain.py +182 -0
  220. noust/validators/environment.py +234 -0
  221. noust/validators/health.py +147 -0
  222. noust/validators/names.py +325 -0
  223. noust/validators/port.py +236 -0
  224. noust/validators/source.py +430 -0
  225. noust/validators/ssh.py +431 -0
  226. noust/validators/telegram.py +66 -0
  227. noust/web/__init__.py +15 -0
  228. noust/web/api/__init__.py +7 -0
  229. noust/web/api/app_export.py +376 -0
  230. noust/web/api/apps.py +2318 -0
  231. noust/web/api/audit.py +121 -0
  232. noust/web/api/auth.py +1266 -0
  233. noust/web/api/backup_destinations.py +522 -0
  234. noust/web/api/backup_schedules.py +394 -0
  235. noust/web/api/backups.py +554 -0
  236. noust/web/api/central.py +164 -0
  237. noust/web/api/certs.py +406 -0
  238. noust/web/api/config.py +1175 -0
  239. noust/web/api/cron.py +468 -0
  240. noust/web/api/databases.py +1360 -0
  241. noust/web/api/deployments.py +454 -0
  242. noust/web/api/deps.py +729 -0
  243. noust/web/api/diagnose.py +109 -0
  244. noust/web/api/domains.py +352 -0
  245. noust/web/api/github_hooks.py +270 -0
  246. noust/web/api/hooks.py +751 -0
  247. noust/web/api/integrations.py +341 -0
  248. noust/web/api/jobs.py +627 -0
  249. noust/web/api/metrics.py +114 -0
  250. noust/web/api/monitor.py +804 -0
  251. noust/web/api/node_proxy.py +1276 -0
  252. noust/web/api/nodes.py +391 -0
  253. noust/web/api/openapi.py +175 -0
  254. noust/web/api/platform_proposal.py +97 -0
  255. noust/web/api/previews.py +401 -0
  256. noust/web/api/recipes.py +159 -0
  257. noust/web/api/router.py +134 -0
  258. noust/web/api/services.py +822 -0
  259. noust/web/api/sites.py +666 -0
  260. noust/web/api/system.py +658 -0
  261. noust/web/api/zero_downtime.py +244 -0
  262. noust/web/auth.py +4497 -0
  263. noust/web/events.py +758 -0
  264. noust/web/jobs.py +1782 -0
  265. noust/web/machine.py +374 -0
  266. noust/web/metrics_collector.py +523 -0
  267. noust/web/pydantic_compat.py +132 -0
  268. noust/web/server.py +1990 -0
  269. noust/web/static/apple-touch-icon.png +0 -0
  270. noust/web/static/assets/Announcer-CAC9FfMY.js +1 -0
  271. noust/web/static/assets/AppRowActions-DQ0SWVc4.js +1 -0
  272. noust/web/static/assets/AppStatePill-BTeRDyIc.js +1 -0
  273. noust/web/static/assets/Badge-Dyu2dpxb.js +1 -0
  274. noust/web/static/assets/CSPContext-kKuRERYg.js +1 -0
  275. noust/web/static/assets/Checkbox-CYDZ85SW.js +1 -0
  276. noust/web/static/assets/CommandHint-BTJdbHWz.js +1 -0
  277. noust/web/static/assets/CompositeItem-DKfxp3pP.js +1 -0
  278. noust/web/static/assets/CompositeList-8dlcGwO6.js +1 -0
  279. noust/web/static/assets/ConfirmDialog-D_tKuePS.js +1 -0
  280. noust/web/static/assets/CopyButton-C_-yZ5XH.js +1 -0
  281. noust/web/static/assets/CopyTextButton-B9MzgkJs.js +1 -0
  282. noust/web/static/assets/DangerZone-SVKJeYxf.js +1 -0
  283. noust/web/static/assets/DataTable-DpVhtyFG.js +1 -0
  284. noust/web/static/assets/Dialog-D6SFUvBZ.js +1 -0
  285. noust/web/static/assets/DnsVerdict-Bi9l3zsr.js +1 -0
  286. noust/web/static/assets/Drawer-EqMyFPC7.js +1 -0
  287. noust/web/static/assets/EmptyState-COLuKwtW.js +1 -0
  288. noust/web/static/assets/ErrorBoundary-TRn_G6YZ.js +1 -0
  289. noust/web/static/assets/ExternalLink-COcN2aWC.js +1 -0
  290. noust/web/static/assets/Field-ZUnEqR1A.js +1 -0
  291. noust/web/static/assets/FieldItemContext-CQDQjT1N.js +3 -0
  292. noust/web/static/assets/IconButton-_h955UPm.js +1 -0
  293. noust/web/static/assets/Input-BN-JLZga.js +1 -0
  294. noust/web/static/assets/KeyValueList-1dSLH63w.js +1 -0
  295. noust/web/static/assets/LanguageSwitch-CVeqgnQm.js +1 -0
  296. noust/web/static/assets/LinkTabs-DaOE6MV5.js +1 -0
  297. noust/web/static/assets/LogViewer-CE4YXoNp.js +2 -0
  298. noust/web/static/assets/Logo-B8xCYbX6.js +1 -0
  299. noust/web/static/assets/Match-DjFDQtD2.js +1 -0
  300. noust/web/static/assets/Menu-BDCJy1Lf.js +1 -0
  301. noust/web/static/assets/MenuTrigger-B3htmf9I.js +1 -0
  302. noust/web/static/assets/Mono-bXpUsUCG.js +1 -0
  303. noust/web/static/assets/NeedsAttention-DZtxVk4W.js +1 -0
  304. noust/web/static/assets/PageHeader-feRus_pw.js +1 -0
  305. noust/web/static/assets/Progress-Dng3apNQ.js +1 -0
  306. noust/web/static/assets/QueryState-C9m6Y_Lm.js +1 -0
  307. noust/web/static/assets/ReachabilityPill-CCOo-DQp.js +1 -0
  308. noust/web/static/assets/RelativeTime-gJ9efP4E.js +1 -0
  309. noust/web/static/assets/RollbackDialog-BscXWdT1.js +1 -0
  310. noust/web/static/assets/RowAction-BskZZuRB.js +1 -0
  311. noust/web/static/assets/Section-3R8oFu_f.js +1 -0
  312. noust/web/static/assets/SegmentedControl-B2Hf57R0.js +1 -0
  313. noust/web/static/assets/Select-BCJ2NPIY.js +1 -0
  314. noust/web/static/assets/SettingsForm-yAchxAnx.js +1 -0
  315. noust/web/static/assets/SitesTab-YilI0HYA.js +1 -0
  316. noust/web/static/assets/Skeleton-Ch_u4SzW.js +1 -0
  317. noust/web/static/assets/SourceLink-Bb8xdFhh.js +1 -0
  318. noust/web/static/assets/StatTile-Cm6DDuRb.js +1 -0
  319. noust/web/static/assets/StatusPill-DYUbSHg3.js +1 -0
  320. noust/web/static/assets/Switch-CDmCv6DW.js +1 -0
  321. noust/web/static/assets/Textarea-QZIXXxpc.js +1 -0
  322. noust/web/static/assets/UnlockForm-Dva7gbJ-.js +1 -0
  323. noust/web/static/assets/_console-B5D7QaOX.js +1 -0
  324. noust/web/static/assets/_console-BhpUlgjw.js +1 -0
  325. noust/web/static/assets/_console-d6-YUU06.js +1 -0
  326. noust/web/static/assets/_console-vRGQhCNG.js +2 -0
  327. noust/web/static/assets/_domain-2FThv-3P.js +2 -0
  328. noust/web/static/assets/_domain-Cf7N2h-b.js +1 -0
  329. noust/web/static/assets/_domain-Cm2XEgZo.js +2 -0
  330. noust/web/static/assets/_domain-Mob2qHRX.js +2 -0
  331. noust/web/static/assets/_id-CyG71a9V.js +1 -0
  332. noust/web/static/assets/_id-DGwgnq8W.js +2 -0
  333. noust/web/static/assets/_name-Bkp-LXju.js +2 -0
  334. noust/web/static/assets/_name-Cnt0Hyol.js +2 -0
  335. noust/web/static/assets/_name-D0dcNnCI.js +1 -0
  336. noust/web/static/assets/_name-X5fNzAw0.js +1 -0
  337. noust/web/static/assets/_site-CVmDna7c.js +4 -0
  338. noust/web/static/assets/_site-K-RCUvt2.js +2 -0
  339. noust/web/static/assets/about-BkWWkmWF.js +1 -0
  340. noust/web/static/assets/activity-DGVTacv3.js +2 -0
  341. noust/web/static/assets/activity-PMElGahb.js +2 -0
  342. noust/web/static/assets/apps-DGKx3cZ2.js +1 -0
  343. noust/web/static/assets/apps-DvSylCBB.js +1 -0
  344. noust/web/static/assets/apps-VI42jR__.js +2 -0
  345. noust/web/static/assets/archive-Bypg5U2Q.js +1 -0
  346. noust/web/static/assets/arrow-down-BbB2VImw.js +1 -0
  347. noust/web/static/assets/attention-qT4JHWum.js +2 -0
  348. noust/web/static/assets/backoff-B_sWY3fd.js +1 -0
  349. noust/web/static/assets/backups-Bxm8dJmX.js +2 -0
  350. noust/web/static/assets/backups-DMy9OPAm.js +2 -0
  351. noust/web/static/assets/ban-74715ViC.js +1 -0
  352. noust/web/static/assets/boxes-DFuSL6Pv.js +1 -0
  353. noust/web/static/assets/buildLog-C-Y3fMa1.js +4 -0
  354. noust/web/static/assets/callback-DeFYkBKk.js +1 -0
  355. noust/web/static/assets/central-CLRBQ3eM.js +1 -0
  356. noust/web/static/assets/certificates-_tc3H4RL.js +1 -0
  357. noust/web/static/assets/certs-BIPqV419.js +1 -0
  358. noust/web/static/assets/channelParts-C1IiEb4J.js +1 -0
  359. noust/web/static/assets/chevron-down-BcPVh_7t.js +1 -0
  360. noust/web/static/assets/chevrons-up-down-Csgk-68C.js +1 -0
  361. noust/web/static/assets/circle-alert-BYFNXpJI.js +1 -0
  362. noust/web/static/assets/circle-arrow-up-DnwSPs4W.js +1 -0
  363. noust/web/static/assets/circle-check-B5jBVSLu.js +1 -0
  364. noust/web/static/assets/circle-minus-Dj8jBJe5.js +1 -0
  365. noust/web/static/assets/circle-question-mark-Qps4wKMx.js +1 -0
  366. noust/web/static/assets/clamp-D0kAXd-u.js +1 -0
  367. noust/web/static/assets/clock-CCuYuMHN.js +1 -0
  368. noust/web/static/assets/clock-CNzcajE0.js +1 -0
  369. noust/web/static/assets/cog-BKzOIN61.js +1 -0
  370. noust/web/static/assets/composite-DOieqZRf.js +1 -0
  371. noust/web/static/assets/config-CPas7PXO.js +1 -0
  372. noust/web/static/assets/cron-63dNITsR.js +1 -0
  373. noust/web/static/assets/cron-CsIs9Fyt.js +2 -0
  374. noust/web/static/assets/data-C_B6De_t.js +1 -0
  375. noust/web/static/assets/data-Id7baV8G.js +1 -0
  376. noust/web/static/assets/data-MUeE8W_R.js +1 -0
  377. noust/web/static/assets/database-D7P8Yu7D.js +1 -0
  378. noust/web/static/assets/databases-CXfdJWCt.js +1 -0
  379. noust/web/static/assets/deployments-D1Ox19Ce.js +2 -0
  380. noust/web/static/assets/deployments-D7SUA14-.js +1 -0
  381. noust/web/static/assets/deployments-DET6OZgv.js +1 -0
  382. noust/web/static/assets/diagnose-BHALkj9m.js +2 -0
  383. noust/web/static/assets/diagnose-DP9DHDQa.js +1 -0
  384. noust/web/static/assets/documentTitle-DbZSdOOl.js +1 -0
  385. noust/web/static/assets/domains-C7fOZy6T.js +1 -0
  386. noust/web/static/assets/domains-CUBsfbnl.js +1 -0
  387. noust/web/static/assets/domains-DH308xsX.js +1 -0
  388. noust/web/static/assets/domains-DPogn61p.js +2 -0
  389. noust/web/static/assets/domains-MqwwRmqT.js +2 -0
  390. noust/web/static/assets/download-BM-JGLit.js +1 -0
  391. noust/web/static/assets/elevation-Co1mUqT2.js +1 -0
  392. noust/web/static/assets/ellipsis-DdYsNnJs.js +1 -0
  393. noust/web/static/assets/environment-69kHPcpb.js +2 -0
  394. noust/web/static/assets/environment-CLjMUZnH.js +4 -0
  395. noust/web/static/assets/errors-DB5R-NxL.js +1 -0
  396. noust/web/static/assets/es-BIQ3NJbd.js +1 -0
  397. noust/web/static/assets/events-DV1SYyzJ.js +1 -0
  398. noust/web/static/assets/external-link-NGLHTjPS.js +1 -0
  399. noust/web/static/assets/eye-CBrBUUg5.js +1 -0
  400. noust/web/static/assets/file-cog-B49E2GiF.js +1 -0
  401. noust/web/static/assets/file-x-WyjcRTU0.js +1 -0
  402. noust/web/static/assets/fleet-DMMq-3HF.js +1 -0
  403. noust/web/static/assets/fleet-qVIaFSE3.js +2 -0
  404. noust/web/static/assets/folder-open-Ckk6bll2.js +1 -0
  405. noust/web/static/assets/formErrors-CaUGvCTd.js +1 -0
  406. noust/web/static/assets/format-BVyd6-PE.js +1 -0
  407. noust/web/static/assets/getPseudoElementBounds-BXyRTQl7.js +1 -0
  408. noust/web/static/assets/git-commit-horizontal-CQQys_PZ.js +1 -0
  409. noust/web/static/assets/git-pull-request-BhB7xTV9.js +1 -0
  410. noust/web/static/assets/github-rhfevh1s.js +1 -0
  411. noust/web/static/assets/globe-l3YI3KUl.js +1 -0
  412. noust/web/static/assets/index-B8mkDsgP.js +11 -0
  413. noust/web/static/assets/index-Bf3wMvT_.css +1 -0
  414. noust/web/static/assets/infiniteQueryOptions-Dfvzj6n2.js +1 -0
  415. noust/web/static/assets/info-CUTi4Chn.js +1 -0
  416. noust/web/static/assets/integrations-D4DaKQr5.js +1 -0
  417. noust/web/static/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  418. noust/web/static/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  419. noust/web/static/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  420. noust/web/static/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  421. noust/web/static/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  422. noust/web/static/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  423. noust/web/static/assets/jobs-kAn1wWVK.js +1 -0
  424. noust/web/static/assets/key-round-6IcTXZuR.js +1 -0
  425. noust/web/static/assets/lazyRouteComponent-oeYLxgjX.js +1 -0
  426. noust/web/static/assets/limits-DpW1ccVX.js +1 -0
  427. noust/web/static/assets/lock-B7aBYydo.js +1 -0
  428. noust/web/static/assets/log-out-DnXplwRf.js +1 -0
  429. noust/web/static/assets/login-Beg5MUQR.js +2 -0
  430. noust/web/static/assets/login-DOmEv78o.js +1 -0
  431. noust/web/static/assets/logs-B4zhG2Pk.js +1 -0
  432. noust/web/static/assets/logs-ZUjTSLfY.js +2 -0
  433. noust/web/static/assets/lookups-D7AEwUzB.js +1 -0
  434. noust/web/static/assets/metrics-BR4adl0Z.js +1 -0
  435. noust/web/static/assets/metrics-DGdMwlKO.js +2 -0
  436. noust/web/static/assets/metrics-DzzRPOVx.js +1 -0
  437. noust/web/static/assets/minus-CDe17-bG.js +1 -0
  438. noust/web/static/assets/mona-sans-latin-ext-wdth-normal-CQ1tG6In.woff2 +0 -0
  439. noust/web/static/assets/mona-sans-latin-wdth-normal-BMVx8nn_.woff2 +0 -0
  440. noust/web/static/assets/mona-sans-vietnamese-wdth-normal-Rd0aZl9c.woff2 +0 -0
  441. noust/web/static/assets/monitor-CUYOShol.js +1 -0
  442. noust/web/static/assets/mutation-dkJzcVye.js +1 -0
  443. noust/web/static/assets/names-D39ExiEN.js +1 -0
  444. noust/web/static/assets/nav-C8GxZyac.js +1 -0
  445. noust/web/static/assets/network-BSG1FjG3.js +1 -0
  446. noust/web/static/assets/new-Bdgq5fLm.js +2 -0
  447. noust/web/static/assets/nodeErrors-_pUC78zm.js +1 -0
  448. noust/web/static/assets/notifications-Cxcd3ePM.js +2 -0
  449. noust/web/static/assets/noust-icon-CvSo_g1X.svg +7 -0
  450. noust/web/static/assets/noust-icon-mono-DgxsBjlK.svg +7 -0
  451. noust/web/static/assets/noust-mark--vvPicbB.svg +8 -0
  452. noust/web/static/assets/noust-mark-mono-DgNpATEl.svg +8 -0
  453. noust/web/static/assets/noust-wordmark-TqLHnHrv.svg +10 -0
  454. noust/web/static/assets/noust-wordmark-mono-BlSNg7gr.svg +10 -0
  455. noust/web/static/assets/panel-top-QiWkOEDm.js +1 -0
  456. noust/web/static/assets/pencil-CE418y20.js +1 -0
  457. noust/web/static/assets/play-BTPPX6Gx.js +1 -0
  458. noust/web/static/assets/plus-CHKUeaCi.js +1 -0
  459. noust/web/static/assets/redirect-B4OxYbs2.js +1 -0
  460. noust/web/static/assets/rocket-BMEuDiHj.js +1 -0
  461. noust/web/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  462. noust/web/static/assets/root-DLTE-HSj.js +1 -0
  463. noust/web/static/assets/rotate-ccw-DOOaBthI.js +1 -0
  464. noust/web/static/assets/rotate-ccw-clock-BQqG_1xZ.js +1 -0
  465. noust/web/static/assets/scroll-text-DHqQ7NpH.js +1 -0
  466. noust/web/static/assets/search-ubxk2oKw.js +1 -0
  467. noust/web/static/assets/security-_SRp1dPn.js +5 -0
  468. noust/web/static/assets/serializeValue-CNr88Bzt.js +1 -0
  469. noust/web/static/assets/series-qMpiUCG-.js +3 -0
  470. noust/web/static/assets/server-azZdtSqm.js +1 -0
  471. noust/web/static/assets/servers-CceT5dA-.js +2 -0
  472. noust/web/static/assets/servers-Dw7hO_hH.js +2 -0
  473. noust/web/static/assets/services-BGpz7kMG.js +2 -0
  474. noust/web/static/assets/services-CRksRWSu.js +14 -0
  475. noust/web/static/assets/services-c6-fmTt9.js +1 -0
  476. noust/web/static/assets/settings-BM-98MbS.js +1 -0
  477. noust/web/static/assets/settings-BXvYioUw.js +2 -0
  478. noust/web/static/assets/settings-C13Rtjan.js +1 -0
  479. noust/web/static/assets/settings-DzMoBV6N.js +2 -0
  480. noust/web/static/assets/shield-check-DB7XY_hx.js +1 -0
  481. noust/web/static/assets/shield-off-CfC67J2A.js +1 -0
  482. noust/web/static/assets/sites-DaWcdCEJ.js +1 -0
  483. noust/web/static/assets/sockets-CKdggv7z.js +1 -0
  484. noust/web/static/assets/square-Ct2HPXcA.js +1 -0
  485. noust/web/static/assets/square-terminal-DdDD0Zro.js +1 -0
  486. noust/web/static/assets/status-ctpAj_oh.js +1 -0
  487. noust/web/static/assets/system-_JUIkbxt.js +1 -0
  488. noust/web/static/assets/tabStrip-DSgENPj5.js +1 -0
  489. noust/web/static/assets/theme-D_mPN_Y9.js +1 -0
  490. noust/web/static/assets/toggle-right-Br96epGU.js +1 -0
  491. noust/web/static/assets/tokens-BRUCnpId.js +1 -0
  492. noust/web/static/assets/trash-BNcFmAWf.js +1 -0
  493. noust/web/static/assets/triangle-alert-CKg_efzz.js +1 -0
  494. noust/web/static/assets/undo-2-Da9WuG6E.js +1 -0
  495. noust/web/static/assets/url-Ox09ysXG.js +1 -0
  496. noust/web/static/assets/useAnnounceChange-Bekpdejb.js +1 -0
  497. noust/web/static/assets/useAppActions--yu6mCx-.js +1 -0
  498. noust/web/static/assets/useAriaLabelledBy-BKs0q4yy.js +1 -0
  499. noust/web/static/assets/useBaseUiId-CC6U9MPC.js +1 -0
  500. noust/web/static/assets/useBlocker-Big88cC6.js +1 -0
  501. noust/web/static/assets/useCertificateJobs-DJj5yc57.js +1 -0
  502. noust/web/static/assets/useDeleteApp-DVoqSzzX.js +1 -0
  503. noust/web/static/assets/useInfiniteQuery-C1a8KNZc.js +1 -0
  504. noust/web/static/assets/useLabel-DIsdVo_X.js +1 -0
  505. noust/web/static/assets/useMutation-U4cnOWEg.js +1 -0
  506. noust/web/static/assets/useNode-CWnt8Qg0.js +1 -0
  507. noust/web/static/assets/useOpenInteractionType-CkATH8nH.js +1 -0
  508. noust/web/static/assets/useRegisterFieldControl-Bnbvmayb.js +1 -0
  509. noust/web/static/assets/useRegisteredLabelId-BgUdkgMr.js +1 -0
  510. noust/web/static/assets/useServiceActions-W30A2d-s.js +1 -0
  511. noust/web/static/assets/useSettingsForm-CV-Jv0Da.js +1 -0
  512. noust/web/static/assets/useStateTransitions-DVyXXIl_.js +1 -0
  513. noust/web/static/assets/useSwipeDismiss-lXqFGztq.js +1 -0
  514. noust/web/static/assets/useValueChanged-DbkN6cK5.js +1 -0
  515. noust/web/static/assets/words-kR5gFSKX.js +1 -0
  516. noust/web/static/assets/x-DcoAY9ab.js +1 -0
  517. noust/web/static/favicon-32.png +0 -0
  518. noust/web/static/favicon.svg +7 -0
  519. noust/web/static/icon-192.png +0 -0
  520. noust/web/static/icon-512.png +0 -0
  521. noust/web/static/index.html +82 -0
  522. noust/web/websockets/__init__.py +7 -0
  523. noust/web/websockets/router.py +888 -0
  524. noust-3.0.0.data/data/share/man/man1/noust.1 +2499 -0
  525. noust-3.0.0.dist-info/METADATA +853 -0
  526. noust-3.0.0.dist-info/RECORD +530 -0
  527. noust-3.0.0.dist-info/WHEEL +5 -0
  528. noust-3.0.0.dist-info/entry_points.txt +3 -0
  529. noust-3.0.0.dist-info/licenses/LICENSE +661 -0
  530. 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,8 @@
1
+ """
2
+ Allow running noust as a module: python -m noust
3
+ """
4
+
5
+ from noust.main import cli
6
+
7
+ if __name__ == "__main__":
8
+ cli()
@@ -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