Skip to content
Merged
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
7 changes: 7 additions & 0 deletions docs/network_file_sharing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Network File Sharing

The Network File Sharing page manages Samba shares and Samba service status.
It also includes the server name. This is the name you'll use to find this
server on your network. Scheduled task alert emails include it in the subject.

SimpleSaferServer now distinguishes between two kinds of Samba shares:

Expand All @@ -16,9 +18,14 @@ That ownership split matters because SimpleSaferServer only edits shares that it
- **Edit Share**: updates a SimpleSaferServer-managed share
- **Delete Share**: removes a SimpleSaferServer-managed share
- **Unmanaged share warning**: appears when SimpleSaferServer detects other non-system Samba shares in `smb.conf`
- **Server Name**: changes the OS hostname, SimpleSaferServer's stored server name,
and the local hostname entry used by the server itself
- **Restart Services**: restarts `smbd` and `nmbd`
Restarting Samba disconnects anyone who is currently connected to a share, so active file copies or other SMB activity will drop.

Changing the server name also restarts Samba discovery/services so the new name is
advertised without rebooting. Connected file-sharing clients may need to reconnect.

If unmanaged shares are detected, the page shows a small warning button with the count.
That button opens a modal that:

Expand Down
5 changes: 4 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ That split is important because the safety checks are different.
## Step 1: Create Admin Account

- Enter the admin username.
- Enter the server name.
- Enter the server name. This is the name you use to find the server on your
network, and it appears in alert emails.
- Server names may contain letters, numbers, and hyphens. They cannot contain
spaces or start/end with a hyphen.
- Enter and confirm the password.
- On success, the wizard logs the user in and moves to the next step.
- That first account becomes the initial administrator for the web UI.
Expand Down
5 changes: 5 additions & 0 deletions docs/uninstall.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ leaves unmanaged or legacy untagged Samba share blocks in `/etc/samba/smb.conf`
If SimpleSaferServer configured Ubuntu Pro Livepatch, uninstall warns that Ubuntu Pro and Livepatch
state are retained. Review that host-level subscription/security state manually after uninstall.

If SimpleSaferServer changed the server hostname, uninstall warns with the original,
last SimpleSaferServer-applied, and current hostnames when available. The uninstaller
does not automatically change the hostname or `/etc/hosts` back because those are
host-level identity settings that may still be in use after the app is removed.

This process is irreversible. Back up anything important before uninstalling.
19 changes: 19 additions & 0 deletions notes/0027-cloud-backup-setup-service-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Cloud Backup Setup Service Refactor

## Context

The cloud backup management page already uses `CloudBackupService`, but the setup wizard still
implemented MEGA password obscuring, temporary rclone config generation, folder listing, folder
creation, and credential persistence inside the route module.

## Decision

Keep the existing setup wizard endpoint URLs and request field names as onboarding compatibility
wrappers. Delegate the actual cloud-backup behavior to `CloudBackupService` so onboarding and
post-setup management use the same rclone and configuration rules.

## Follow-up Checks

- Setup wizard tests should verify the legacy setup payloads are mapped to the shared service.
- Cloud backup service tests remain the behavior source for rclone command shape, timeouts,
credential write ordering, and advanced rclone persistence.
27 changes: 27 additions & 0 deletions notes/0028-server-identity-hostname.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Server Identity Hostname

## Context

The setup wizard collected `system.server_name`, but that value was only stored in
SimpleSaferServer config and used by alert email scripts. Operators expected the
same name to also be the server name they connect to on the network.

## Decision

Server-name changes now go through a shared server-identity service. The service
validates one simple hostname format, updates `/etc/hosts`, applies the OS
hostname, persists SimpleSaferServer config, and restarts Samba discovery/services
when changed after setup.

The original hostname is recorded once so uninstall can warn operators that the
host-level name was changed by SimpleSaferServer. Uninstall intentionally leaves
the current hostname and `/etc/hosts` in place because they are host identity, not
app-owned files.

## Follow-up Checks

- Setup and File Sharing should keep using the shared validation and helper text.
- If hostname policy changes, update the Python service, setup JavaScript, and
File Sharing JavaScript together.
- `uninstall.sh` should continue warning about managed hostname metadata before
deleting `/etc/SimpleSaferServer`.
36 changes: 36 additions & 0 deletions simple_safer_server/adapters/server_identity_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Optional

from simple_safer_server.adapters.command_runner import CommandRunner

SERVER_IDENTITY_TIMEOUT_SECONDS = 30


class ServerIdentityCommandAdapter:
"""Wrap host identity commands behind one injectable boundary."""

def __init__(self, command_runner: Optional[CommandRunner] = None) -> None:
self._command_runner = command_runner or CommandRunner()

def current_hostname(self) -> str:
result = self._command_runner.run(
["hostname"],
capture_output=True,
text=True,
check=True,
timeout=SERVER_IDENTITY_TIMEOUT_SECONDS,
)
return result.stdout.strip()

def set_hostname(self, hostname: str) -> None:
self._command_runner.run(
["hostnamectl", "set-hostname", hostname],
check=True,
timeout=SERVER_IDENTITY_TIMEOUT_SECONDS,
)

def restart_unit(self, unit_name: str) -> None:
self._command_runner.run(
["systemctl", "restart", unit_name],
check=True,
timeout=SERVER_IDENTITY_TIMEOUT_SECONDS,
)
8 changes: 8 additions & 0 deletions simple_safer_server/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from simple_safer_server.routes.cloud_backup import cloud_backup as cloud_backup_routes
from simple_safer_server.routes.ddns import ddns as ddns_routes
from simple_safer_server.routes.drive_health import drive_health as drive_health_routes
from simple_safer_server.routes.server_identity import server_identity as server_identity_routes
from simple_safer_server.routes.setup_wizard import setup
from simple_safer_server.routes.smb import smb as smb_routes
from simple_safer_server.routes.storage import storage as storage_routes
Expand All @@ -36,6 +37,7 @@
from simple_safer_server.services.ddns_service import DdnsService
from simple_safer_server.services.drive_health import DriveHealthSummaryService
from simple_safer_server.services.runtime import get_fake_state, get_flask_secret_key, get_runtime
from simple_safer_server.services.server_identity import ServerIdentityService
from simple_safer_server.services.smb_manager import SMB_DOCS_URL, SMBManager
from simple_safer_server.services.storage_service import StorageService
from simple_safer_server.services.system_updates import SystemUpdatesManager
Expand Down Expand Up @@ -136,6 +138,10 @@ def create_app() -> Tuple[Flask, SocketIO]:
config_manager=config_manager,
system_utils=system_utils,
)
server_identity_service = ServerIdentityService(
config_manager=config_manager,
runtime=runtime,
)
storage_service = StorageService(
runtime=runtime,
fake_state=fake_state,
Expand All @@ -156,6 +162,7 @@ def create_app() -> Tuple[Flask, SocketIO]:
ddns_service=ddns_service,
cloud_backup_service=cloud_backup_service,
alerts_service=alerts_service,
server_identity_service=server_identity_service,
storage_service=storage_service,
drive_health_summary_service=drive_health_summary_service,
)
Expand All @@ -166,6 +173,7 @@ def create_app() -> Tuple[Flask, SocketIO]:
app.register_blueprint(cloud_backup_routes)
app.register_blueprint(system_updates_routes)
app.register_blueprint(alerts_routes)
app.register_blueprint(server_identity_routes)
app.register_blueprint(smb_routes)
app.register_blueprint(users_routes)
app.register_blueprint(storage_routes)
Expand Down
50 changes: 50 additions & 0 deletions simple_safer_server/routes/server_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Any

from flask import Blueprint, current_app

from simple_safer_server.services.server_identity import ServerIdentityError
from simple_safer_server.services.user_manager import api_admin_required
from simple_safer_server.web.api import json_data, json_problem, json_request_data
from simple_safer_server.web.problems import OperationProblem, ValidationProblem

server_identity = Blueprint("server_identity_routes", __name__)


def _get_services() -> Any:
"""Return app-level services registered during Flask startup."""
return current_app.extensions["simple_safer_server"]


@server_identity.route("/api/server_identity", methods=["GET"])
@api_admin_required
def api_get_server_identity():
try:
return json_data(_get_services().server_identity_service.current_identity())
except Exception:
current_app.logger.exception("Failed to read server identity")
return json_problem(OperationProblem("Failed to read server name."))


@server_identity.route("/api/server_identity", methods=["PUT"])
@api_admin_required
def api_update_server_identity():
data = json_request_data()
try:
result = _get_services().server_identity_service.update_server_name(
data.get("server_name"),
restart_samba=True,
)
message = "Server name updated."
if result.warning:
message = f"{message} {result.warning}"
return json_data(result, message=message)
except ServerIdentityError as exc:
return json_problem(ValidationProblem(str(exc), slug="server-identity-validation-error"))
except Exception:
current_app.logger.exception("Failed to update server identity")
return json_problem(
OperationProblem(
"Failed to update server name.",
slug="server-identity-operation-failed",
)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading