arcsecond 4.3.3__tar.gz → 4.4.1__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.
- {arcsecond-4.3.3 → arcsecond-4.4.1}/PKG-INFO +1 -1
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cli.py +4 -2
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/docgen.py +60 -1
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/__init__.py +4 -4
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/composedoc.py +6 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/docker/docker-compose.yml +11 -5
- arcsecond-4.3.3/arcsecond/hosting/check.py → arcsecond-4.4.1/arcsecond/hosting/doctor.py +313 -36
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/lifecycle.py +11 -6
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/local.py +121 -190
- arcsecond-4.4.1/arcsecond/hosting/network/__init__.py +215 -0
- arcsecond-4.4.1/arcsecond/hosting/network/guard.py +204 -0
- arcsecond-4.4.1/arcsecond/hosting/network/manifest.toml +649 -0
- arcsecond-4.4.1/arcsecond/hosting/network/outbound.py +247 -0
- arcsecond-4.4.1/arcsecond/hosting/network/probe.py +258 -0
- arcsecond-4.4.1/arcsecond/hosting/network/sheet.py +387 -0
- arcsecond-4.4.1/arcsecond/hosting/skybrightness.py +100 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/stack.py +16 -6
- {arcsecond-4.3.3 → arcsecond-4.4.1}/pyproject.toml +2 -1
- arcsecond-4.4.1/tests/fixtures/docker-compose-6.2.yml +123 -0
- arcsecond-4.4.1/tests/fixtures/docker-compose-6.4.yml +139 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_docgen.py +5 -2
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_composedoc.py +2 -2
- arcsecond-4.3.3/tests/test_hosting_check.py → arcsecond-4.4.1/tests/test_hosting_doctor.py +211 -6
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_lifecycle.py +55 -2
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_local.py +115 -80
- arcsecond-4.4.1/tests/test_hosting_network_guard.py +112 -0
- arcsecond-4.4.1/tests/test_hosting_network_manifest.py +132 -0
- arcsecond-4.4.1/tests/test_hosting_network_outbound.py +158 -0
- arcsecond-4.4.1/tests/test_hosting_network_probe.py +200 -0
- arcsecond-4.4.1/tests/test_hosting_network_sheet.py +123 -0
- arcsecond-4.4.1/tests/test_hosting_skybrightness.py +116 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_stack.py +11 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_token.py +1 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/uv.lock +1 -1
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.docker/Dockerfile_redis +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.flake8 +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.gitattributes +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.github/dependabot.yml +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.github/workflows/tests.yml +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/.gitignore +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/LICENSE +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/Makefile +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/README.md +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/__version__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/alpaca/commands.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/alpaca/dome_probe.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/alpaca/probe.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/alpaca/telescope_probe.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/config.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/constants.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/main.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/api/resources.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/errors.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/backups.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/database.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/token.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/hosting/utils.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/autostart.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/commands.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/detection.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/registry.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/filewatch.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/mdns.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/sources/opencv.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/imagesources/store.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/options.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/arcsecond/targets.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/examples/example_upload_files.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/requirements.txt +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/setup.cfg +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/setup.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/sonar-project.properties +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/test_api.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/test_api_endpoint.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/test_config.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/test_config_pointer.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/api/test_targets.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/conftest.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/fixtures/file1.fits +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_alpaca_probe.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_cli.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_cli_api_commands.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_backups_install_dir.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_database.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_hosting_utils_secret_key.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_autostart.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_commands.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_detection.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_mdns.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_network.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_store.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_imagesources_webcam.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/test_targets_planning.py +0 -0
- {arcsecond-4.3.3 → arcsecond-4.4.1}/tests/utils.py +0 -0
|
@@ -12,8 +12,8 @@ from arcsecond.cloud import (
|
|
|
12
12
|
from arcsecond.docgen import docs
|
|
13
13
|
from arcsecond.hosting import (
|
|
14
14
|
backups,
|
|
15
|
-
check_cmd,
|
|
16
15
|
db,
|
|
16
|
+
doctor_cmd,
|
|
17
17
|
logs,
|
|
18
18
|
restart,
|
|
19
19
|
setup,
|
|
@@ -75,7 +75,9 @@ main.add_command(logs)
|
|
|
75
75
|
main.add_command(update)
|
|
76
76
|
|
|
77
77
|
# Is it reachable from the rest of the observatory, and if not, why not.
|
|
78
|
-
main.add_command(
|
|
78
|
+
main.add_command(doctor_cmd)
|
|
79
|
+
# Its name until 4.4, kept so that scripts and habits go on working.
|
|
80
|
+
main.add_command(doctor_cmd, name="check")
|
|
79
81
|
|
|
80
82
|
# The access token Arcsecond gave the observatory, once per machine.
|
|
81
83
|
main.add_command(token_group)
|
|
@@ -78,7 +78,9 @@ def _visible_commands(
|
|
|
78
78
|
commands = []
|
|
79
79
|
for name in group.list_commands(ctx):
|
|
80
80
|
command = group.get_command(ctx, name)
|
|
81
|
-
|
|
81
|
+
# A command mounted a second time under another name is an alias
|
|
82
|
+
# (`check` for `doctor`): it works, and has no page of its own.
|
|
83
|
+
if command is not None and not command.hidden and command.name == name:
|
|
82
84
|
commands.append((name, command))
|
|
83
85
|
return commands
|
|
84
86
|
|
|
@@ -339,3 +341,60 @@ def docs_compose(out, check):
|
|
|
339
341
|
)
|
|
340
342
|
written = write(files, Path(out))
|
|
341
343
|
click.echo(f"Wrote {len(written)} files to {out}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@docs.command(
|
|
347
|
+
name="network",
|
|
348
|
+
help="Write the one-page network sheet, and the firewall request, from the network manifest.",
|
|
349
|
+
)
|
|
350
|
+
@click.option(
|
|
351
|
+
"--out",
|
|
352
|
+
"out",
|
|
353
|
+
type=click.Path(file_okay=False),
|
|
354
|
+
default=None,
|
|
355
|
+
help="Folder for the documentation page (index.md).",
|
|
356
|
+
)
|
|
357
|
+
@click.option(
|
|
358
|
+
"--static",
|
|
359
|
+
"static",
|
|
360
|
+
type=click.Path(file_okay=False),
|
|
361
|
+
default=None,
|
|
362
|
+
help="Folder for the printable page and the plain-text firewall request, served as they are.",
|
|
363
|
+
)
|
|
364
|
+
@click.option(
|
|
365
|
+
"--check",
|
|
366
|
+
"check",
|
|
367
|
+
is_flag=True,
|
|
368
|
+
help="Compare against the folders instead of writing; exit 1 if either is out of date.",
|
|
369
|
+
)
|
|
370
|
+
def docs_network(out, static, check):
|
|
371
|
+
from arcsecond.hosting import network
|
|
372
|
+
from arcsecond.hosting.network import sheet
|
|
373
|
+
|
|
374
|
+
if not out or not static:
|
|
375
|
+
raise click.UsageError(
|
|
376
|
+
"Give both --out FOLDER (the page) and --static FOLDER (the files to print and paste)."
|
|
377
|
+
)
|
|
378
|
+
manifest = network.load()
|
|
379
|
+
targets = (
|
|
380
|
+
(sheet.generate_page(manifest), Path(out)),
|
|
381
|
+
(sheet.generate_static(manifest), Path(static)),
|
|
382
|
+
)
|
|
383
|
+
if check:
|
|
384
|
+
changed = [
|
|
385
|
+
f"{folder}/{name}"
|
|
386
|
+
for files, folder in targets
|
|
387
|
+
for name in differences(files, folder)
|
|
388
|
+
]
|
|
389
|
+
if changed:
|
|
390
|
+
click.echo("The network sheet is out of date:")
|
|
391
|
+
for name in changed:
|
|
392
|
+
click.echo(f" {name}")
|
|
393
|
+
click.echo(
|
|
394
|
+
f"\nRegenerate it: arcsecond docs network --out {out} --static {static}"
|
|
395
|
+
)
|
|
396
|
+
raise SystemExit(1)
|
|
397
|
+
click.echo("The network sheet is up to date.")
|
|
398
|
+
return
|
|
399
|
+
written = sum(len(write(files, folder)) for files, folder in targets)
|
|
400
|
+
click.echo(f"Wrote {written} files to {out} and {static}")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from .backups import backups
|
|
2
|
+
from .database import db
|
|
2
3
|
|
|
3
|
-
#
|
|
4
|
+
# doctor_cmd, not doctor: `doctor` is also this package's module of that name,
|
|
4
5
|
# and re-exporting the command under it would shadow the module for tests.
|
|
5
|
-
from .
|
|
6
|
-
from .database import db
|
|
6
|
+
from .doctor import doctor_cmd
|
|
7
7
|
from .lifecycle import logs, restart, start, status, stop, update
|
|
8
8
|
from .local import setup
|
|
9
9
|
from .token import token_group
|
|
@@ -11,7 +11,7 @@ from .token import token_group
|
|
|
11
11
|
# Re-exported: the command groups `arcsecond.cli` mounts.
|
|
12
12
|
__all__ = [
|
|
13
13
|
"backups",
|
|
14
|
-
"
|
|
14
|
+
"doctor_cmd",
|
|
15
15
|
"db",
|
|
16
16
|
"setup",
|
|
17
17
|
"token_group",
|
|
@@ -241,6 +241,12 @@ ENV_KEYS: Dict[str, tuple] = {
|
|
|
241
241
|
"Comma-separated origins, scheme included, to trust besides private-network addresses — a public domain, "
|
|
242
242
|
"a Tailscale name. Not needed for a LAN address.",
|
|
243
243
|
),
|
|
244
|
+
"SKY_BRIGHTNESS_GEOTIFF_PATH": (
|
|
245
|
+
SETUP,
|
|
246
|
+
"Where the backend finds the sky-brightness map kept on this machine (a path under `/data`). "
|
|
247
|
+
"Empty: the map is looked up on statics.arcsecond.io instead. Change the answer with "
|
|
248
|
+
"`arcsecond setup --with-sky-map` or `--without-sky-map`.",
|
|
249
|
+
),
|
|
244
250
|
"LIVE_IMAGE_PROXY_URL": (
|
|
245
251
|
BACKEND,
|
|
246
252
|
"Where the backend reaches the live-image proxy for cameras. Default http://host.docker.internal:8765; "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# === Arcsecond.local config. ===
|
|
2
2
|
# *** DO NOT change any name or port number. ***
|
|
3
|
-
# Version 7.
|
|
3
|
+
# Version 7.4
|
|
4
4
|
|
|
5
5
|
services:
|
|
6
6
|
# Database (PostgresQL)
|
|
@@ -33,8 +33,12 @@ services:
|
|
|
33
33
|
container_name: arcsecond-api
|
|
34
34
|
restart: unless-stopped
|
|
35
35
|
entrypoint: /home/app/entrypoint-server.sh
|
|
36
|
+
# This machine only. Browsers and other computers go through the webapp's
|
|
37
|
+
# port, which serves the very same API under /api/; 8800 is for the
|
|
38
|
+
# `arcsecond` command and scripts run on this computer. Published on every
|
|
39
|
+
# interface, it was a second, unneeded door onto the network.
|
|
36
40
|
ports:
|
|
37
|
-
- "8800:8800"
|
|
41
|
+
- "127.0.0.1:8800:8800"
|
|
38
42
|
depends_on:
|
|
39
43
|
- db
|
|
40
44
|
- broker
|
|
@@ -138,7 +142,8 @@ services:
|
|
|
138
142
|
image: ghcr.io/arcsecond-io/arcsecond-service-platesolver-astrometry:latest
|
|
139
143
|
container_name: arcsecond-platesolver
|
|
140
144
|
restart: unless-stopped
|
|
141
|
-
|
|
145
|
+
# No host port. Only the backend calls the solver, over the internal Docker
|
|
146
|
+
# network; it has no authentication and nothing outside the stack needs it.
|
|
142
147
|
|
|
143
148
|
# Arcsecond webapp. Served on 5555 only: a self-hosted install is
|
|
144
149
|
# organisation-based and therefore always a portal, so the second port that
|
|
@@ -172,8 +177,9 @@ services:
|
|
|
172
177
|
|
|
173
178
|
# >>> arcsecond:alerts
|
|
174
179
|
# Arcsecond transient-alerts consumer (optional).
|
|
175
|
-
# Long-lived Kafka client for NASA GCN. Outbound TLS
|
|
176
|
-
#
|
|
180
|
+
# Long-lived Kafka client for NASA GCN. Outbound TLS only — nothing listens:
|
|
181
|
+
# auth.gcn.nasa.gov on 443, then kafka, kafka1, kafka2 and
|
|
182
|
+
# kafka3.gcn.nasa.gov on 9092. Requires GCN_CONSUMER_CLIENT_ID /
|
|
177
183
|
# GCN_CONSUMER_CLIENT_SECRET in .env; idles harmlessly when they are absent.
|
|
178
184
|
# Single instance only — do not scale it.
|
|
179
185
|
alerts:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""`arcsecond
|
|
1
|
+
"""`arcsecond doctor`: is this installation reachable, and if not, why not.
|
|
2
2
|
|
|
3
3
|
The failure this exists for is invisible from where the operator sits: the
|
|
4
4
|
page does not load from the laptop, and nothing anywhere says why. The
|
|
@@ -16,18 +16,32 @@ the rules, not from probing, and says so.
|
|
|
16
16
|
prints it otherwise. It never changes the network profile: that changes what
|
|
17
17
|
every other program on the machine is exposed to, and is the operator's call.
|
|
18
18
|
`--json` is for pasting into a support conversation.
|
|
19
|
+
|
|
20
|
+
It also looks outward. Every destination the network manifest declares
|
|
21
|
+
(`network/manifest.toml`) is probed from this machine — name, connection,
|
|
22
|
+
encrypted handshake, certificate, and the clock against one reference — and
|
|
23
|
+
each failure comes with the sentence to send to whoever runs the firewall.
|
|
24
|
+
The probes carry nothing about the observatory. `--report` writes the whole
|
|
25
|
+
result to a dated file for an administrator's ticket, with the local
|
|
26
|
+
network's addresses left out unless `--show-local` asks for them.
|
|
27
|
+
|
|
28
|
+
Exit code: 0 when everything required works, 1 when only an optional
|
|
29
|
+
destination is out of reach, 2 when something required fails.
|
|
19
30
|
"""
|
|
20
31
|
|
|
21
32
|
import ctypes
|
|
22
33
|
import json
|
|
23
34
|
import os
|
|
24
35
|
import re
|
|
36
|
+
import shutil
|
|
25
37
|
import socket
|
|
26
38
|
import subprocess
|
|
27
39
|
import sys
|
|
28
40
|
import urllib.error
|
|
29
41
|
import urllib.request
|
|
30
42
|
from dataclasses import asdict, dataclass, field
|
|
43
|
+
from datetime import datetime
|
|
44
|
+
from pathlib import Path
|
|
31
45
|
from typing import List, Optional
|
|
32
46
|
|
|
33
47
|
import click
|
|
@@ -240,6 +254,21 @@ class Host:
|
|
|
240
254
|
except (urllib.error.URLError, OSError, ValueError):
|
|
241
255
|
return None
|
|
242
256
|
|
|
257
|
+
def free_bytes(self, path: str) -> Optional[int]:
|
|
258
|
+
try:
|
|
259
|
+
return shutil.disk_usage(path).free
|
|
260
|
+
except OSError:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def host_name(self) -> str:
|
|
264
|
+
return socket.gethostname()
|
|
265
|
+
|
|
266
|
+
def outbound_findings(self) -> List["Finding"]:
|
|
267
|
+
from . import network
|
|
268
|
+
from .network import outbound
|
|
269
|
+
|
|
270
|
+
return outbound.check_destinations(network.load())
|
|
271
|
+
|
|
243
272
|
def is_admin(self) -> bool:
|
|
244
273
|
if self.platform != "win32":
|
|
245
274
|
return os.geteuid() == 0 if hasattr(os, "geteuid") else False
|
|
@@ -439,6 +468,31 @@ def check_ports(host: Host, expect_listening: bool) -> List[Finding]:
|
|
|
439
468
|
data={"bound": []},
|
|
440
469
|
)
|
|
441
470
|
)
|
|
471
|
+
elif port == stack.API_PORT and all(bound_to_loopback(h) for h in hosts):
|
|
472
|
+
# Where the packaged compose file puts it since 7.4: the API port
|
|
473
|
+
# is for this machine, and the network reaches the same API
|
|
474
|
+
# through the web interface's port, under /api/.
|
|
475
|
+
findings.append(
|
|
476
|
+
Finding(
|
|
477
|
+
f"ports.{port}",
|
|
478
|
+
f"Port {port} ({name})",
|
|
479
|
+
OK,
|
|
480
|
+
f"bound to {', '.join(hosts)} — this machine only, as intended",
|
|
481
|
+
data={"bound": hosts},
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
elif port == stack.API_PORT and any(bound_everywhere(h) for h in hosts):
|
|
485
|
+
findings.append(
|
|
486
|
+
Finding(
|
|
487
|
+
f"ports.{port}",
|
|
488
|
+
f"Port {port} ({name})",
|
|
489
|
+
WARN,
|
|
490
|
+
"listening on every interface — the network does not need it: "
|
|
491
|
+
f"other computers reach the API through port {stack.WEB_PORT}, under /api/",
|
|
492
|
+
fix="`arcsecond update` restores the packaged mapping, which keeps this port to this machine",
|
|
493
|
+
data={"bound": hosts},
|
|
494
|
+
)
|
|
495
|
+
)
|
|
442
496
|
elif any(bound_everywhere(h) for h in hosts):
|
|
443
497
|
findings.append(
|
|
444
498
|
Finding(
|
|
@@ -619,6 +673,93 @@ def check_proxy() -> List[Finding]:
|
|
|
619
673
|
return findings
|
|
620
674
|
|
|
621
675
|
|
|
676
|
+
DISK_WARN_GB = 20
|
|
677
|
+
DISK_FAIL_GB = 2
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def check_disk(install, host: "Host") -> Finding:
|
|
681
|
+
"""Images fill a disk quietly, and a full one stops the database."""
|
|
682
|
+
shared = install.read_env("SHARED_DATA_PATH")
|
|
683
|
+
if not shared:
|
|
684
|
+
return Finding("disk", "Disk space", SKIP, "no SHARED_DATA_PATH in .env")
|
|
685
|
+
free = host.free_bytes(shared)
|
|
686
|
+
if free is None:
|
|
687
|
+
return Finding("disk", "Disk space", SKIP, "the data folder does not exist yet")
|
|
688
|
+
gigabytes = free / 1e9
|
|
689
|
+
detail = f"{gigabytes:.0f} GB free where the data is kept"
|
|
690
|
+
if gigabytes < DISK_FAIL_GB:
|
|
691
|
+
return Finding(
|
|
692
|
+
"disk",
|
|
693
|
+
"Disk space",
|
|
694
|
+
FAIL,
|
|
695
|
+
detail,
|
|
696
|
+
fix="Free some space, or move the data folder to a larger disk.",
|
|
697
|
+
)
|
|
698
|
+
if gigabytes < DISK_WARN_GB:
|
|
699
|
+
return Finding("disk", "Disk space", WARN, detail)
|
|
700
|
+
return Finding("disk", "Disk space", OK, detail)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def check_privileges(install, host: "Host") -> Finding:
|
|
704
|
+
"""What the containers are allowed to do on this machine: the question a
|
|
705
|
+
security review asks first."""
|
|
706
|
+
try:
|
|
707
|
+
names = [
|
|
708
|
+
row.get("Name") for row in stack.services_status(install) if row.get("Name")
|
|
709
|
+
]
|
|
710
|
+
except ArcsecondError:
|
|
711
|
+
names = []
|
|
712
|
+
if not names:
|
|
713
|
+
return Finding(
|
|
714
|
+
"privileges", "Container privileges", SKIP, "no container to inspect"
|
|
715
|
+
)
|
|
716
|
+
result = host.run(
|
|
717
|
+
[
|
|
718
|
+
"docker",
|
|
719
|
+
"inspect",
|
|
720
|
+
"--format",
|
|
721
|
+
"{{.Name}}|{{.HostConfig.Privileged}}|{{json .HostConfig.CapAdd}}|{{.HostConfig.NetworkMode}}|{{.Config.User}}",
|
|
722
|
+
*names,
|
|
723
|
+
]
|
|
724
|
+
)
|
|
725
|
+
if result is None or result.returncode != 0:
|
|
726
|
+
return Finding(
|
|
727
|
+
"privileges",
|
|
728
|
+
"Container privileges",
|
|
729
|
+
SKIP,
|
|
730
|
+
"could not inspect the containers",
|
|
731
|
+
)
|
|
732
|
+
rows = [
|
|
733
|
+
line.split("|") for line in result.stdout.splitlines() if line.count("|") == 4
|
|
734
|
+
]
|
|
735
|
+
privileged = [r[0].lstrip("/") for r in rows if r[1] == "true"]
|
|
736
|
+
capabilities = [r[0].lstrip("/") for r in rows if r[2] not in ("null", "[]")]
|
|
737
|
+
on_host_network = [r[0].lstrip("/") for r in rows if r[3] == "host"]
|
|
738
|
+
as_root = [r[0].lstrip("/") for r in rows if r[4] in ("", "0", "root")]
|
|
739
|
+
data = {
|
|
740
|
+
"privileged": privileged,
|
|
741
|
+
"added_capabilities": capabilities,
|
|
742
|
+
"host_network": on_host_network,
|
|
743
|
+
"run_as_root_inside": as_root,
|
|
744
|
+
}
|
|
745
|
+
worries = privileged + capabilities + on_host_network
|
|
746
|
+
if worries:
|
|
747
|
+
return Finding(
|
|
748
|
+
"privileges",
|
|
749
|
+
"Container privileges",
|
|
750
|
+
WARN,
|
|
751
|
+
f"{', '.join(sorted(set(worries)))} run with more than the packaged configuration grants",
|
|
752
|
+
fix="`arcsecond update` restores the packaged configuration",
|
|
753
|
+
data=data,
|
|
754
|
+
)
|
|
755
|
+
detail = f"{len(rows)} containers: none privileged, none on the machine's own network, no added capability"
|
|
756
|
+
if as_root:
|
|
757
|
+
data["note"] = (
|
|
758
|
+
f"{len(as_root)} of them run as root inside their container: {', '.join(as_root)}."
|
|
759
|
+
)
|
|
760
|
+
return Finding("privileges", "Container privileges", OK, detail, data=data)
|
|
761
|
+
|
|
762
|
+
|
|
622
763
|
def check_api_pointer() -> Finding:
|
|
623
764
|
current = ArcsecondConfig.current_api_name()
|
|
624
765
|
if current == LOCAL_API_NAME:
|
|
@@ -645,7 +786,9 @@ def check_api_pointer() -> Finding:
|
|
|
645
786
|
)
|
|
646
787
|
|
|
647
788
|
|
|
648
|
-
def run_checks(
|
|
789
|
+
def run_checks(
|
|
790
|
+
install, host: Host, fix: bool = False, outbound: bool = False
|
|
791
|
+
) -> List[Finding]:
|
|
649
792
|
findings: List[Finding] = []
|
|
650
793
|
docker = check_docker(host)
|
|
651
794
|
findings.append(docker)
|
|
@@ -671,9 +814,45 @@ def run_checks(install, host: Host, fix: bool = False) -> List[Finding]:
|
|
|
671
814
|
|
|
672
815
|
findings += check_proxy()
|
|
673
816
|
findings.append(check_api_pointer())
|
|
817
|
+
findings.append(check_disk(install, host))
|
|
818
|
+
if docker.status == OK:
|
|
819
|
+
findings.append(check_privileges(install, host))
|
|
820
|
+
if outbound:
|
|
821
|
+
findings += host.outbound_findings()
|
|
674
822
|
return findings
|
|
675
823
|
|
|
676
824
|
|
|
825
|
+
def exit_code(findings: List[Finding]) -> int:
|
|
826
|
+
"""0: everything required works. 1: only an optional outside destination
|
|
827
|
+
is out of reach. 2: something required fails."""
|
|
828
|
+
if any(f.status == FAIL for f in findings):
|
|
829
|
+
return 2
|
|
830
|
+
if any(
|
|
831
|
+
f.key.startswith("net.") and f.data.get("reachable") is False for f in findings
|
|
832
|
+
):
|
|
833
|
+
return 1
|
|
834
|
+
return 0
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
_PRIVATE_ADDRESS = re.compile(
|
|
838
|
+
r"\b(?:10\.\d{1,3}|192\.168|172\.(?:1[6-9]|2\d|3[01])|169\.254)\.\d{1,3}\.\d{1,3}\b"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def redact_local(text: str, host_name: Optional[str] = None) -> str:
|
|
843
|
+
"""The same text without what identifies the local network: its private
|
|
844
|
+
addresses and this machine's name. A report leaves the observatory; what
|
|
845
|
+
its network looks like inside is not the recipient's business."""
|
|
846
|
+
text = _PRIVATE_ADDRESS.sub("<local address>", text)
|
|
847
|
+
if host_name:
|
|
848
|
+
for name in {host_name, host_name.split(".")[0]}:
|
|
849
|
+
if len(name) > 2:
|
|
850
|
+
text = re.sub(
|
|
851
|
+
re.escape(name), "<this machine>", text, flags=re.IGNORECASE
|
|
852
|
+
)
|
|
853
|
+
return text
|
|
854
|
+
|
|
855
|
+
|
|
677
856
|
# ---------------------------------------------------------------------------
|
|
678
857
|
# The command
|
|
679
858
|
# ---------------------------------------------------------------------------
|
|
@@ -686,38 +865,108 @@ MARKS = {
|
|
|
686
865
|
}
|
|
687
866
|
|
|
688
867
|
|
|
689
|
-
|
|
690
|
-
|
|
868
|
+
PROMISE = "This check sends nothing about your observatory: it only tests what can be reached."
|
|
869
|
+
|
|
870
|
+
SECTIONS = (
|
|
871
|
+
("This machine", lambda f: not f.key.startswith("net.")),
|
|
872
|
+
("Outside destinations", lambda f: f.key.startswith("net.")),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _finding_lines(f: Finding, width: int) -> List[str]:
|
|
877
|
+
"""One finding: its line, then its remedy and its note when it has them."""
|
|
878
|
+
entry = f" [{f.data['manifest']}]" if f.data.get("manifest") else ""
|
|
879
|
+
lines = [
|
|
880
|
+
f" {MARKS[f.status]} {f.label.ljust(width)} {f.detail}{click.style(entry, dim=True)}"
|
|
881
|
+
]
|
|
882
|
+
if f.fix and f.status in (WARN, FAIL):
|
|
883
|
+
lines += [f" {' ' * width} → {line}" for line in f.fix.splitlines()]
|
|
884
|
+
note = f.data.get("note")
|
|
885
|
+
if note and f.status != SKIP:
|
|
886
|
+
lines.append(click.style(f" {' ' * width} {note}", dim=True))
|
|
887
|
+
return lines
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def render_findings(findings: List[Finding], install) -> str:
|
|
891
|
+
"""The report as text, the same on screen and in a file."""
|
|
892
|
+
out = [PROMISE, "", f"Installation: {install.path}"]
|
|
691
893
|
width = max(len(f.label) for f in findings)
|
|
692
|
-
for
|
|
693
|
-
|
|
694
|
-
if
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
click.echo(click.style(f" {' ' * width} {note}", dim=True))
|
|
894
|
+
for title, belongs in SECTIONS:
|
|
895
|
+
section = [f for f in findings if belongs(f)]
|
|
896
|
+
if not section:
|
|
897
|
+
continue
|
|
898
|
+
out += ["", f"{title}:"]
|
|
899
|
+
for f in section:
|
|
900
|
+
out += _finding_lines(f, width)
|
|
700
901
|
fails = sum(1 for f in findings if f.status == FAIL)
|
|
701
902
|
warns = sum(1 for f in findings if f.status == WARN)
|
|
702
|
-
|
|
903
|
+
out.append("")
|
|
703
904
|
if fails:
|
|
704
|
-
|
|
905
|
+
out.append(
|
|
705
906
|
click.style(f"{fails} problem(s), {warns} warning(s).", fg="red", bold=True)
|
|
706
907
|
)
|
|
707
908
|
elif warns:
|
|
708
|
-
|
|
909
|
+
out.append(click.style(f"No problem, {warns} warning(s).", fg="yellow"))
|
|
709
910
|
else:
|
|
710
|
-
|
|
911
|
+
out.append(click.style("Everything checks out.", fg="green", bold=True))
|
|
912
|
+
|
|
913
|
+
from .network import outbound
|
|
914
|
+
|
|
915
|
+
lost = outbound.unavailable(findings)
|
|
916
|
+
if lost:
|
|
917
|
+
out += [
|
|
918
|
+
"",
|
|
919
|
+
"With the destinations that could not be reached, this installation does without:",
|
|
920
|
+
]
|
|
921
|
+
out += [f" - {line}" for line in dict.fromkeys(lost)]
|
|
711
922
|
lan = next((f for f in findings if f.key == "lan.host"), None)
|
|
712
923
|
if lan and lan.data.get("lan_ipv4"):
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
924
|
+
out += [
|
|
925
|
+
"",
|
|
926
|
+
f"From another computer on this network: http://{lan.data['lan_ipv4']}:{stack.WEB_PORT}",
|
|
927
|
+
]
|
|
928
|
+
return "\n".join(out)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def print_findings(findings: List[Finding], install) -> None:
|
|
932
|
+
click.echo(render_findings(findings, install))
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _payload(findings: List[Finding], install, host: "Host") -> dict:
|
|
936
|
+
return {
|
|
937
|
+
"installation": str(install.path),
|
|
938
|
+
"platform": host.platform,
|
|
939
|
+
"cli": _cli_version(),
|
|
940
|
+
"checked_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
941
|
+
"exit_code": exit_code(findings),
|
|
942
|
+
"findings": [asdict(f) for f in findings],
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def write_report(
|
|
947
|
+
findings: List[Finding], install, host: "Host", folder: Path, show_local: bool
|
|
948
|
+
) -> Path:
|
|
949
|
+
"""One dated text file an administrator can attach to a ticket: the
|
|
950
|
+
readable report, then the same content as JSON."""
|
|
951
|
+
text = click.unstyle(render_findings(findings, install))
|
|
952
|
+
body = json.dumps(_payload(findings, install, host), indent=2)
|
|
953
|
+
stamp = f"{datetime.now().astimezone():%Y-%m-%d %H:%M %Z}"
|
|
954
|
+
content = (
|
|
955
|
+
f"Arcsecond.local doctor report, {stamp}\n\n{text}\n\n"
|
|
956
|
+
f"--- machine-readable ---\n{body}\n"
|
|
957
|
+
)
|
|
958
|
+
if not show_local:
|
|
959
|
+
content = redact_local(content, host.host_name())
|
|
960
|
+
content = content.replace(str(install.path), "<installation folder>")
|
|
961
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
962
|
+
path = folder / f"arcsecond-doctor-{datetime.now():%Y%m%d-%H%M}.txt"
|
|
963
|
+
path.write_text(content, encoding="utf-8")
|
|
964
|
+
return path
|
|
716
965
|
|
|
717
966
|
|
|
718
967
|
@click.command(
|
|
719
|
-
name="
|
|
720
|
-
short_help="Check that the installation works,
|
|
968
|
+
name="doctor",
|
|
969
|
+
short_help="Check that the installation works, is reachable, and can reach what it needs.",
|
|
721
970
|
)
|
|
722
971
|
@dir_option
|
|
723
972
|
@click.option(
|
|
@@ -732,30 +981,58 @@ def print_findings(findings: List[Finding], install) -> None:
|
|
|
732
981
|
help="Apply the remedies this command can apply itself (the Windows firewall rule, "
|
|
733
982
|
"from an Administrator shell). The network profile is never changed.",
|
|
734
983
|
)
|
|
984
|
+
@click.option(
|
|
985
|
+
"--local-only",
|
|
986
|
+
is_flag=True,
|
|
987
|
+
help="Skip the outside destinations: test this machine and nothing beyond it.",
|
|
988
|
+
)
|
|
989
|
+
@click.option(
|
|
990
|
+
"--report",
|
|
991
|
+
"report",
|
|
992
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
993
|
+
default=None,
|
|
994
|
+
is_flag=False,
|
|
995
|
+
flag_value=Path("."),
|
|
996
|
+
help="Also write the result to a dated file, in this folder (default: the current one), "
|
|
997
|
+
"to attach to a ticket. Local addresses and this machine's name are left out.",
|
|
998
|
+
)
|
|
999
|
+
@click.option(
|
|
1000
|
+
"--show-local",
|
|
1001
|
+
is_flag=True,
|
|
1002
|
+
help="With --report: keep the local network's addresses and this machine's name in the file.",
|
|
1003
|
+
)
|
|
735
1004
|
@basic_options
|
|
736
|
-
def
|
|
1005
|
+
def doctor_cmd(directory, as_json, fix, local_only, report, show_local):
|
|
737
1006
|
"""Run every check an installation can run on itself: Docker, the
|
|
738
|
-
configuration files, the containers
|
|
739
|
-
to, the address other computers use,
|
|
740
|
-
and
|
|
1007
|
+
configuration files, the containers and what they are allowed to do, the
|
|
1008
|
+
ports and what they are bound to, the address other computers use, disk
|
|
1009
|
+
space, and on Windows the network profile and the firewall rule.
|
|
1010
|
+
|
|
1011
|
+
Then look outward: every destination Arcsecond.local may contact is
|
|
1012
|
+
tested from this machine (name, connection, encryption, certificate),
|
|
1013
|
+
and this machine's clock is compared with a reference. Nothing about your
|
|
1014
|
+
observatory is sent. Each problem comes with its remedy, or with the
|
|
1015
|
+
sentence to send to whoever runs your network.
|
|
741
1016
|
|
|
742
|
-
Exit code
|
|
1017
|
+
Exit code 0 when everything required works, 1 when only an optional
|
|
1018
|
+
outside destination is out of reach, 2 when something required fails.
|
|
1019
|
+
|
|
1020
|
+
`arcsecond check` is this same command, under its former name.
|
|
743
1021
|
"""
|
|
744
1022
|
install = stack.resolve_install_dir(directory)
|
|
745
1023
|
host = Host()
|
|
746
|
-
findings = run_checks(install, host, fix=fix)
|
|
1024
|
+
findings = run_checks(install, host, fix=fix, outbound=not local_only)
|
|
747
1025
|
if as_json:
|
|
748
|
-
|
|
749
|
-
"installation": str(install.path),
|
|
750
|
-
"platform": host.platform,
|
|
751
|
-
"cli": _cli_version(),
|
|
752
|
-
"findings": [asdict(f) for f in findings],
|
|
753
|
-
}
|
|
754
|
-
click.echo(json.dumps(payload, indent=2))
|
|
1026
|
+
click.echo(json.dumps(_payload(findings, install, host), indent=2))
|
|
755
1027
|
else:
|
|
756
1028
|
print_findings(findings, install)
|
|
757
|
-
if
|
|
758
|
-
|
|
1029
|
+
if report is not None:
|
|
1030
|
+
path = write_report(findings, install, host, report, show_local)
|
|
1031
|
+
if not as_json:
|
|
1032
|
+
click.echo(f"\nReport written to {path}")
|
|
1033
|
+
code = exit_code(findings)
|
|
1034
|
+
if code:
|
|
1035
|
+
raise SystemExit(code)
|
|
759
1036
|
|
|
760
1037
|
|
|
761
1038
|
def _cli_version() -> str:
|
|
@@ -298,8 +298,10 @@ def logs(directory, service, follow, tail):
|
|
|
298
298
|
@dir_option
|
|
299
299
|
@basic_options
|
|
300
300
|
def update(directory):
|
|
301
|
-
"""Bring the installation up to date:
|
|
302
|
-
CLI, download the latest images, and restart what
|
|
301
|
+
"""Bring the installation up to date: rewrite docker-compose.yml from this
|
|
302
|
+
CLI, add what .env lacks, download the latest images, and restart what
|
|
303
|
+
changed. A docker-compose.yml that differed is kept aside as a backup;
|
|
304
|
+
local changes belong in docker-compose.override.yml, which is left alone.
|
|
303
305
|
|
|
304
306
|
Update the CLI itself first, so that the compose file it writes is the
|
|
305
307
|
newest one: pip install --upgrade arcsecond
|
|
@@ -309,13 +311,16 @@ def update(directory):
|
|
|
309
311
|
|
|
310
312
|
click.echo(f"Updating Arcsecond.local in {install.path} ...\n")
|
|
311
313
|
click.echo("Refreshing the configuration files:")
|
|
312
|
-
enabled,
|
|
314
|
+
enabled, to_record = _resolve_optional_services(
|
|
315
|
+
install.env_path, {}, compose_path=install.compose_path
|
|
316
|
+
)
|
|
313
317
|
write_env_file(directory=install.path)
|
|
314
318
|
for name, answer in to_record:
|
|
315
319
|
_record_optional_service_decision(install.env_path, name, answer)
|
|
316
|
-
write_docker_compose_file(
|
|
317
|
-
|
|
318
|
-
|
|
320
|
+
write_docker_compose_file(enabled_services=enabled, directory=install.path)
|
|
321
|
+
# Installations set up before 4.0 never recorded where they are; from now
|
|
322
|
+
# on the other commands find this one from any folder.
|
|
323
|
+
stack.remember_install_dir(install.path)
|
|
319
324
|
|
|
320
325
|
click.echo("\nDownloading the latest images:")
|
|
321
326
|
_streamed(install, "docker compose pull", "pull")
|