Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define HOME_BASEDIR node variable #772

Merged
merged 5 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ is_rootfull = request['is_rootfull']
module_environment = request['environment']
image_url = module_environment['IMAGE_URL']

# Valdate the base homedir path: it must exist.
if 'HOME_BASEDIR' in os.environ and not os.path.isdir(os.environ['HOME_BASEDIR']):
print(agent.SD_ERR + f"[ERROR] The HOME_BASEDIR path, {os.environ['HOME_BASEDIR']} does not exist or is not a directory.", file=sys.stderr)
sys.exit(2)

# Allocate TCP ports
if request['tcp_ports_demand'] > 0:
tcp_ports_range=node.ports_manager.allocate_ports(request['tcp_ports_demand'], module_id, 'tcp')
Expand Down Expand Up @@ -89,8 +94,14 @@ if is_rootfull:
# Start the module agent
agent.run_helper('systemctl', 'enable', '--now', f'agent@{module_id}.service').check_returncode()
else: # rootless
if 'HOME_BASEDIR' in os.environ:
# create the home directory with the custom base path
module_basedir_args = ["-b", os.environ["HOME_BASEDIR"]]
else:
# use default base path (see useradd -D)
module_basedir_args = []
# Create the module dirs structure
agent.run_helper('useradd', '-m', '-k', '/etc/nethserver/skel', '-s', '/bin/bash', module_id).check_returncode()
agent.run_helper('useradd', *module_basedir_args, '-m', '-k', '/etc/nethserver/skel', '-s', '/bin/bash', module_id).check_returncode()
agent.run_helper('chmod', '-c', '0700', os.path.expanduser("~" + module_id)).check_returncode()
os.chdir(os.path.expanduser("~" + module_id) + '/.config')
save_environment(module_environment)
Expand Down
122 changes: 122 additions & 0 deletions core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3

#
# Copyright (C) 2025 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import agent
import sys
import os
import stat
import argparse
import subprocess
import json

def main():
parser = argparse.ArgumentParser(
description="Validate and configure the base path for home directories of NS8 modules.",
epilog="DIR is the base path for new home directories."
)
parser.add_argument(
"-c",
"--check-only",
dest="check_only",
action="store_true",
help="Check only, do not save the configuration.",
)
parser.add_argument(
"DIR",
metavar="DIR",
help="Base path for new home directories.",
)

args = parser.parse_args()
if args.DIR == "" and not args.check_only:
agent.unset_env('HOME_BASEDIR')
print("The base path for home directories has been reset to OS default.")
else:
home_basedir = validate_home_basedir(args.DIR)
if not args.check_only:
store_configuration(home_basedir)
print("The base path for home directories has been updated.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, it is safer if we add an extra flag to reset the configuration of the path to the system one, eg. -r.


def store_configuration(path):
if os.path.isdir("/sys/fs/selinux"):
# On systems with SELinux, configure path as /home equivalent
update_selinux_customization(path)
agent.set_env('HOME_BASEDIR', path)
print("HOME_BASEDIR=" + path)

def update_selinux_customization(path):
ocurrent = subprocess.check_output(['semanage', 'fcontext', '-l', '-C'], text=True)
if not f"\n{path} = /home\n" in ocurrent:
subprocess.check_call(['semanage', 'fcontext', '-a', '-e', '/home', path])
subprocess.check_call(['restorecon', '-v', path])
update_parentdir_selinux_type(path)

def update_parentdir_selinux_type(path):
"""If needed, set home_root_t on parent dir as required by semanage-fcontext manpage."""
parentdir = os.path.dirname(path)
if parentdir != "/":
ols = subprocess.check_output(['ls', '-Zd', parentdir], text=True)
if ":default_t:" in ols:
subprocess.check_call(['semanage', 'fcontext', '-a', '-t', 'home_root_t', parentdir])
subprocess.check_call(['restorecon', '-v', parentdir])

def validate_home_basedir(path):
"""Validate the given path and return it in canonicalized form."""
if os.path.islink(path) or not os.path.isdir(path):
print(f"Error: {path} is not a directory.", file=sys.stderr)
sys.exit(2)
# Canonicalize the path value:
home_basedir = os.path.abspath(path)
if os.path.realpath(home_basedir) != home_basedir:
print(f"Error: {path} contains one or more symlink components.", file=sys.stderr)
sys.exit(2)
check_permissions(home_basedir)
check_unique_mountpoint(home_basedir)
return home_basedir

def get_mountpoint_device(path):
"""Check if path is a mountpoint and return its source device."""
try:
joutput = subprocess.check_output(['findmnt', '--json', '--mountpoint', path])
ofindmnt = json.loads(joutput)
return ofindmnt["filesystems"][0]["source"] # source device path
except subprocess.CalledProcessError:
print(f"Error: {path} is not a filesystem mountpoint.", file=sys.stderr)
sys.exit(2)

def check_unique_mountpoint(path):
"""Check if path is a mountpoint, and its source device is not
mounted elsewhere."""
device = get_mountpoint_device(path)
joutput = subprocess.check_output(['findmnt', '-o', 'TARGET', '--json', '--source', device])
gsanchietti marked this conversation as resolved.
Show resolved Hide resolved
ofindmnt = json.loads(joutput)
for ofs in ofindmnt["filesystems"]:
if ofs['target'] != path:
print(f"Error: device {device} has multiple mountpoints. It is mounted also on {ofs['target']}. Unmount it, persist the change, and retry.", file=sys.stderr)
sys.exit(2)
gsanchietti marked this conversation as resolved.
Show resolved Hide resolved

def check_permissions(dir_path):
"""Checks if the given directory and its ancestors have octal 5
(world-readable and executable) or higher permissions."""
current_path = dir_path
while current_path != "/":
try:
st = os.stat(current_path)
# Extract other permissions (last 3 bits)
other_perms = stat.S_IMODE(st.st_mode) & 0o007
# Check if permissions are at least 5 (r-x)
if other_perms < 0o5:
print(f"Error: invalid permissions on {current_path}. Fix with chmod -c +rx {current_path}, or higher.", file=sys.stderr)
sys.exit(2)
# Move to the parent directory
current_path = os.path.dirname(current_path)
except Exception as ex:
print(f"Error: {current_path}:", ex, file=sys.stderr)
sys.exit(2)

if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions docs/core/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,38 @@ See `/var/lib/nethserver/node/state/coreimage.lst` for a complete list.

Rootless modules are totally contained inside UNIX user home directory, like `/home/trafeik1`.
Rootfull modules are homed under `/var/lib/nethserver/samba1`.

## Custom base path for home directories

NS8 core uses the common `/home` path for users' home directories, but you
can configure a different path if it is a device mount point. Follow these
steps to configure a node's agent for this purpose:

1. Create the alternative base path:

mkdir -m 0755 /home1

2. Mount the device on the new path:

mount /dev/some /home1

To make the mount persistent, either edit `/etc/fstab` or create a
systemd `.mount` unit. Ensure the device is correctly mounted after a
system reboot.

3. Configure the node agent to use `/home1` as base directory for new
modules:

runagent -m node configure-home-basedir /home1

From now on, new module instances will use `/home1` as their base
directory. Existing modules are not affected and will retain their current
home directory.

The `configure-home-basedir` command modifies SELinux configuration. Run
the following command to inspect the current customizations:

semanage fcontext -l -C

Refer to the `semanage-fcontext` man page for additional SELinux-related
information.
3 changes: 0 additions & 3 deletions docs/development_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ nav_order: 3

# Development process

* TOC
{:toc}

All NethServer projects follow a shared development process.

See [NethServer development handbook](https://handbook.nethserver.org/) for a detailed guide on how to contribute to NethServer.
Loading