diff --git a/PATHS_LAYOUT.md b/PATHS_LAYOUT.md new file mode 100644 index 0000000..924dd29 --- /dev/null +++ b/PATHS_LAYOUT.md @@ -0,0 +1,65 @@ +# AtomOS filesystem layout (POSIX / immutable-ready) + +Role-named path constants live in [`paths.json`](paths.json) and are loaded by +[`paths.py`](paths.py). + +## Layout summary + +| Role | Location | +|------|----------| +| Image (read-only) | `/usr/libexec/elemento` | +| Config | `/etc/elemento` | +| Persistent state | `/var/lib/elemento` | +| Logs | `/var/log/elemento` | +| Scratch | `/var/tmp/elemento`, `/var/tmp/elemento_exported` | +| Runtime | `/run` (e.g. OpenRC under `/run/openrc`) | + +Container-internal paths (`GUI_APP_DIR` `/opt/app`, `GUI_DAEMONS_DIR` `/opt/daemons`) +and developer-only `HOMEBREW_PREFIX` are unchanged. + +## Host directories to create + +Before services start, ensure these exist with appropriate ownership (via image +`tmpfiles.d` / packaging — not via application code): + +- `/var/log/elemento` +- `/var/lib/elemento` (and children as needed: `exported`, `schedules`, `nucleus`, `docker-dhcp`, `docker-dhcpd`, `tailscale`, `permissions`, `clustering`) +- `/var/tmp/elemento` +- `/var/tmp/elemento_exported` +- `/etc/elemento` (certs, tunnel, maintenance flag as today) + +## Legacy → new symlinks (operators) + +Legacy AtomOS mixed install and state under `/opt/elemento`. After remapping, +**do not** replace the whole tree with a single `/opt/elemento` → `/usr/libexec/elemento` +link if mutable data still lives under the old prefix. Prefer **per-leaf** symlinks +so historical data remains reachable: + +| Legacy | New | +|--------|-----| +| `/opt/elemento/kelvim` | `/usr/libexec/elemento/kelvim` | +| `/opt/elemento/venv` | `/usr/libexec/elemento/venv` | +| `/opt/elemento/elemento-monorepo-server` | `/usr/libexec/elemento/elemento-monorepo-server` | +| `/opt/elemento/hugepage_resizer.sh` | `/usr/libexec/elemento/hugepage_resizer.sh` | +| `/opt/elemento/docker-dhcp` | `/var/lib/elemento/docker-dhcp` | +| `/opt/elemento/docker-dhcpd` | `/var/lib/elemento/docker-dhcpd` | +| `/opt/elemento/tailscale` | `/var/lib/elemento/tailscale` | +| `/opt/elemento/.nucleus` | `/var/lib/elemento/nucleus` | +| `/var/elemento/exported` | `/var/lib/elemento/exported` | +| `/var/elemento/schedules` | `/var/lib/elemento/schedules` | +| `/etc/elemento/permissions` | `/var/lib/elemento/permissions` | +| `/etc/elemento/clustering` | `/var/lib/elemento/clustering` | +| `/tmp/elemento` | `/var/tmp/elemento` | +| `/tmp/elemento_exported` | `/var/tmp/elemento_exported` | +| CWD-relative `logs/` (optional) | `/var/log/elemento` | + +Example (illustrative; run as root on the host, after moving or seeding data into the new locations): + +```bash +ln -sfn /usr/libexec/elemento/kelvim /opt/elemento/kelvim +ln -sfn /var/lib/elemento/docker-dhcpd /opt/elemento/docker-dhcpd +ln -sfn /var/log/elemento /path/to/old/cwd/logs # only if needed +``` + +Symlink creation is an **operator / image** responsibility; AtomOS application code +does not create these links. diff --git a/paths.json b/paths.json new file mode 100644 index 0000000..f924b53 --- /dev/null +++ b/paths.json @@ -0,0 +1,56 @@ +{ + "HOST_ETC" : "/etc", + "CONFIG_ROOT" : "/etc/elemento", + "CERTS_DIR" : "/etc/elemento/certs", + "MAINTENANCE_FLAG" : "/etc/elemento/maintenance_mode", + "TUNNEL_DIR" : "/etc/elemento/tunnel", + + "HOST_HOME" : "/home", + + "HOST_MNT" : "/mnt", + "VOLUME_MOUNT_GLOB_ONE" : "/mnt/*", + "VOLUME_MOUNT_GLOB_ANY" : "/mnt/**", + "BRICKS_MOUNT" : "/mnt/bricks", + "VAULT_MOUNT" : "/mnt/elemento-vault", + "REPOSITORY_MOUNT" : "/mnt/repository", + + "HOST_OPT" : "/opt", + "GUI_APP_DIR" : "/opt/app", + "GUI_DAEMONS_DIR" : "/opt/daemons", + "HOMEBREW_PREFIX" : "/opt/homebrew", + + "HOST_RUN" : "/run", + "OPENRC_RUN_DIR" : "/run/openrc", + + "HOST_TMP" : "/tmp", + + "HOST_USR" : "/usr", + "USR_BIN" : "/usr/bin", + "USR_LIBEXEC" : "/usr/libexec", + "INSTALL_ROOT" : "/usr/libexec/elemento", + "MONOREPO_ROOT" : "/usr/libexec/elemento/elemento-monorepo-server", + "HUGEPAGE_RESIZER" : "/usr/libexec/elemento/hugepage_resizer.sh", + "KELVIM_DIR" : "/usr/libexec/elemento/kelvim", + "EXPORTER_SCRIPTS_DIR" : "/usr/libexec/elemento/scripts", + "VENV_DIR" : "/usr/libexec/elemento/venv", + "USR_LOCAL" : "/usr/local", + "USR_SHARE" : "/usr/share", + + "HOST_VAR" : "/var", + "HOST_VAR_LIB" : "/var/lib", + "STATE_ROOT" : "/var/lib/elemento", + "CLUSTERING_DIR" : "/var/lib/elemento/clustering", + "DHCP_DATA_DIR" : "/var/lib/elemento/docker-dhcp", + "DHCPD_DATA_DIR" : "/var/lib/elemento/docker-dhcpd", + "EXPORTED_LINKS_DIR" : "/var/lib/elemento/exported", + "NUCLEUS_DIR" : "/var/lib/elemento/nucleus", + "PERMISSIONS_DIR" : "/var/lib/elemento/permissions", + "SCHEDULES_DIR" : "/var/lib/elemento/schedules", + "TAILSCALE_DIR" : "/var/lib/elemento/tailscale", + "HOST_VAR_LOG" : "/var/log", + "APP_LOG_DIR" : "/var/log/elemento", + "SYSTEM_LOG_DIR" : "/var/log/elemento", + "HOST_VAR_RUN" : "/var/run", + "SCRATCH_DIR" : "/var/tmp/elemento", + "EXPORT_SCRATCH_DIR" : "/var/tmp/elemento_exported" +} diff --git a/paths.py b/paths.py new file mode 100644 index 0000000..ba09516 --- /dev/null +++ b/paths.py @@ -0,0 +1,70 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # +# # Author: Elemento srl # +# # Contributors are mentioned in the code where appropriate. # +# # Permission to use and modify this software and its documentation strictly # +# # for personal purposes is hereby granted without fee, # +# # provided that the above copyright notice appears in all copies # +# # and that both the copyright notice and this permission notice appear in the # +# # supporting documentation. # +# # Modifications to this work are allowed for personal use. # +# # Such modifications have to be licensed under a # +# # Creative Commons BY-NC-ND 4.0 International License available at # +# # http://creativecommons.org/licenses/by-nc-nd/4.0/ and have to be made # +# # available to the Elemento user community # +# # through the original distribution channels. # +# # The authors make no claims about the suitability # +# # of this software for any purpose. # +# # It is provided "as is" without express or implied warranty. # +# #******************************************************************************# +# +# #------------------------------------------------------------------------------# +# #elemento-monorepo-server # +# #Authors: # +# #- Gabriele Gaetano Fronze' (gfronze at elemento.cloud) # +# #------------------------------------------------------------------------------# +# +# Central filesystem path prefixes for AtomOS. +# Names describe role/purpose; values live in paths.json +# (immutable-ready FHS layout: image under /usr/libexec/elemento, +# state/logs/scratch under /var, config under /etc/elemento). +# + +from os.path import join, dirname, basename +from json import load + +data = open(join(dirname(__file__), basename(__file__).replace('.py', '.json').replace('.jsonc', '.json'))) +data_json = load(data) +globals().update(data_json) +del data_json +del data + + +def tailscale_bridge_dir(bridge_name: str) -> str: + """Per-bridge Tailscale state/compose directory.""" + return join(TAILSCALE_DIR, bridge_name) + + +def certs_for_addr(addr: str) -> str: + """Client certificate path for a peer address.""" + return join(CERTS_DIR, f"{addr}.crt") + + +def schedule_dir(frequency: str) -> str: + """Backup schedule scripts directory for a frequency.""" + return join(SCHEDULES_DIR, frequency) + + +def scratch_pool_dir(pool_name: str) -> str: + """Per-pool scratch mount directory.""" + return join(SCRATCH_DIR, pool_name) + + +def log_file(name: str) -> str: + """Absolute application log file path under APP_LOG_DIR (/var/log/elemento).""" + return join(APP_LOG_DIR, name) + + +def monorepo_server_dir(server_name: str) -> str: + """Packaged server directory under the monorepo install root.""" + return join(MONOREPO_ROOT, server_name) diff --git a/test.py b/test.py index a62a1f7..3f9c76a 100644 --- a/test.py +++ b/test.py @@ -5,7 +5,9 @@ # fmt: off import networking import restkeys +import paths if __name__ == "__main__": print(networking.RETRIEVE_VMS_SERVER_PORT) print(restkeys.UNREGISTER_API_KEY) + print(paths.CERTS_DIR) diff --git a/test_paths.py b/test_paths.py new file mode 100644 index 0000000..a0199af --- /dev/null +++ b/test_paths.py @@ -0,0 +1,105 @@ +# #******************************************************************************# +# # Copyright(c) 2019-2026, Elemento srl, All rights reserved # +# #******************************************************************************# +# +# Frozen-layout tests for paths.json / paths.py. +# Source of truth: immutable-ready FHS AtomOS layout (absolute path strings below). +# + +import json +import unittest +from os.path import dirname, join + +import paths + +# Frozen layout. Update together with paths.json when the on-disk contract changes. +FROZEN_LAYOUT = { + "HOST_ETC": "/etc", + "CONFIG_ROOT": "/etc/elemento", + "CERTS_DIR": "/etc/elemento/certs", + "MAINTENANCE_FLAG": "/etc/elemento/maintenance_mode", + "TUNNEL_DIR": "/etc/elemento/tunnel", + "HOST_HOME": "/home", + "HOST_MNT": "/mnt", + "VOLUME_MOUNT_GLOB_ONE": "/mnt/*", + "VOLUME_MOUNT_GLOB_ANY": "/mnt/**", + "BRICKS_MOUNT": "/mnt/bricks", + "VAULT_MOUNT": "/mnt/elemento-vault", + "REPOSITORY_MOUNT": "/mnt/repository", + "HOST_OPT": "/opt", + "GUI_APP_DIR": "/opt/app", + "GUI_DAEMONS_DIR": "/opt/daemons", + "HOMEBREW_PREFIX": "/opt/homebrew", + "HOST_RUN": "/run", + "OPENRC_RUN_DIR": "/run/openrc", + "HOST_TMP": "/tmp", + "HOST_USR": "/usr", + "USR_BIN": "/usr/bin", + "USR_LIBEXEC": "/usr/libexec", + "INSTALL_ROOT": "/usr/libexec/elemento", + "MONOREPO_ROOT": "/usr/libexec/elemento/elemento-monorepo-server", + "HUGEPAGE_RESIZER": "/usr/libexec/elemento/hugepage_resizer.sh", + "KELVIM_DIR": "/usr/libexec/elemento/kelvim", + "EXPORTER_SCRIPTS_DIR": "/usr/libexec/elemento/scripts", + "VENV_DIR": "/usr/libexec/elemento/venv", + "USR_LOCAL": "/usr/local", + "USR_SHARE": "/usr/share", + "HOST_VAR": "/var", + "HOST_VAR_LIB": "/var/lib", + "STATE_ROOT": "/var/lib/elemento", + "CLUSTERING_DIR": "/var/lib/elemento/clustering", + "DHCP_DATA_DIR": "/var/lib/elemento/docker-dhcp", + "DHCPD_DATA_DIR": "/var/lib/elemento/docker-dhcpd", + "EXPORTED_LINKS_DIR": "/var/lib/elemento/exported", + "NUCLEUS_DIR": "/var/lib/elemento/nucleus", + "PERMISSIONS_DIR": "/var/lib/elemento/permissions", + "SCHEDULES_DIR": "/var/lib/elemento/schedules", + "TAILSCALE_DIR": "/var/lib/elemento/tailscale", + "HOST_VAR_LOG": "/var/log", + "APP_LOG_DIR": "/var/log/elemento", + "SYSTEM_LOG_DIR": "/var/log/elemento", + "HOST_VAR_RUN": "/var/run", + "SCRATCH_DIR": "/var/tmp/elemento", + "EXPORT_SCRATCH_DIR": "/var/tmp/elemento_exported", +} + + +class TestPathsFrozenLayout(unittest.TestCase): + def test_module_attrs_match_frozen_layout(self): + for key, expected in FROZEN_LAYOUT.items(): + with self.subTest(key=key): + self.assertTrue(hasattr(paths, key), f"missing attribute {key}") + self.assertEqual(getattr(paths, key), expected) + + def test_json_matches_frozen_layout(self): + json_path = join(dirname(__file__), "paths.json") + with open(json_path, encoding="utf-8") as fh: + data = json.load(fh) + self.assertEqual(data, FROZEN_LAYOUT) + + def test_helpers_match_frozen_layout(self): + self.assertEqual( + paths.certs_for_addr("1.2.3.4"), + "/etc/elemento/certs/1.2.3.4.crt", + ) + self.assertEqual( + paths.tailscale_bridge_dir("br0"), + "/var/lib/elemento/tailscale/br0", + ) + self.assertEqual( + paths.schedule_dir("daily"), + "/var/lib/elemento/schedules/daily", + ) + self.assertEqual( + paths.scratch_pool_dir("pool"), + "/var/tmp/elemento/pool", + ) + self.assertEqual(paths.log_file("x.log"), "/var/log/elemento/x.log") + self.assertEqual( + paths.monorepo_server_dir("elemento-matcher-server"), + "/usr/libexec/elemento/elemento-monorepo-server/elemento-matcher-server", + ) + + +if __name__ == "__main__": + unittest.main()