Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions PATHS_LAYOUT.md
Original file line number Diff line number Diff line change
@@ -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.
56 changes: 56 additions & 0 deletions paths.json
Original file line number Diff line number Diff line change
@@ -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"
}
70 changes: 70 additions & 0 deletions paths.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
105 changes: 105 additions & 0 deletions test_paths.py
Original file line number Diff line number Diff line change
@@ -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()