diff --git a/docker/rascsi-web/Dockerfile b/docker/rascsi-web/Dockerfile index 3937b3c0..f3f4c367 100644 --- a/docker/rascsi-web/Dockerfile +++ b/docker/rascsi-web/Dockerfile @@ -19,7 +19,10 @@ RUN apt-get update \ RUN groupadd pi RUN useradd --create-home --shell /bin/bash -g pi pi RUN echo "pi ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers -RUN echo "pi:rascsi" | chpasswd +RUN echo "pi:rascsi" | chpasswd + +# Allows custom PATH for mock commands to work when executing with sudo +RUN sed -i 's/^Defaults\tsecure_path/#Defaults\tsecure_path./' /etc/sudoers RUN mkdir /home/pi/shared_files RUN touch /etc/dhcpcd.conf diff --git a/docker/rascsi-web/start.sh b/docker/rascsi-web/start.sh index c282b6d0..19bc0399 100644 --- a/docker/rascsi-web/start.sh +++ b/docker/rascsi-web/start.sh @@ -11,6 +11,9 @@ fi # Start Nginx service nginx +# Use mock commands +export PATH="/home/pi/RASCSI/python/web/mock/bin:$PATH" + # Pass args to web UI start script if [[ $RASCSI_PASSWORD ]]; then /home/pi/RASCSI/python/web/start.sh "$@" --password=$RASCSI_PASSWORD diff --git a/easyinstall.sh b/easyinstall.sh index 52256b5b..2b960a7f 100755 --- a/easyinstall.sh +++ b/easyinstall.sh @@ -626,11 +626,13 @@ function installHfdisk() { # Fetch HFS drivers that the Web Interface uses function fetchHardDiskDrivers() { - if [ ! -d "$BASE/mac-hard-disk-drivers" ]; then + DRIVER_ARCHIVE="mac-hard-disk-drivers" + if [ ! -d "$BASE/$DRIVER_ARCHIVE" ]; then cd "$BASE" || exit 1 - wget -r https://www.dropbox.com/s/gcs4v5pcmk7rxtb/mac-hard-disk-drivers.zip?dl=0 - unzip -d mac-hard-disk-drivers mac-hard-disk-drivers.zip - rm mac-hard-disk-drivers.zip + # -N option overwrites if downloaded file is newer than existing file + wget -N "https://www.dropbox.com/s/gcs4v5pcmk7rxtb/$DRIVER_ARCHIVE.zip?dl=1" -O "$DRIVER_ARCHIVE.zip" + unzip -d "$DRIVER_ARCHIVE" "$DRIVER_ARCHIVE.zip" + rm "$DRIVER_ARCHIVE.zip" fi } diff --git a/python/common/src/rascsi/sys_cmds.py b/python/common/src/rascsi/sys_cmds.py index 0eb5ee64..c2609a43 100644 --- a/python/common/src/rascsi/sys_cmds.py +++ b/python/common/src/rascsi/sys_cmds.py @@ -3,11 +3,12 @@ Module with methods that interact with the Pi system """ import subprocess import logging -from subprocess import run +from subprocess import run, CalledProcessError from shutil import disk_usage from re import findall, match from socket import socket, gethostname, AF_INET, SOCK_DGRAM from pathlib import Path +from platform import uname from rascsi.common_settings import SHELL_ERROR @@ -37,20 +38,6 @@ class SysCmds: logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8")) ra_git_version = "" - try: - os_version = ( - subprocess.run( - ["uname", "--kernel-name", "--kernel-release", "--machine"], - capture_output=True, - check=True, - ) - .stdout.decode("utf-8") - .strip() - ) - except subprocess.CalledProcessError as error: - logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) - os_version = "Unknown OS" - PROC_MODEL_PATH = "/proc/device-tree/model" SYS_VENDOR_PATH = "/sys/devices/virtual/dmi/id/sys_vendor" SYS_PROD_PATH = "/sys/devices/virtual/dmi/id/product_name" @@ -77,7 +64,11 @@ class SysCmds: else: hardware = "Unknown Device" - return {"git": ra_git_version, "env": f"{hardware}, {os_version}" } + env = uname() + return { + "git": ra_git_version, + "env": f"{hardware}, {env.system} {env.release} {env.machine}", + } @staticmethod def running_proc(daemon): @@ -172,6 +163,42 @@ class SysCmds: sock.close() return ip_addr, host + @staticmethod + def get_pretty_host(): + """ + Returns either the pretty hostname if set, or the regular hostname as fallback. + """ + try: + process = run( + ["hostnamectl", "status", "--pretty"], + capture_output=True, + check=True, + ) + pretty_hostname = process.stdout.decode("utf-8").rstrip() + if pretty_hostname: + return pretty_hostname + except CalledProcessError as error: + logging.error(str(error)) + + return gethostname() + + @staticmethod + def set_pretty_host(name): + """ + Set the pretty hostname for the system + """ + try: + process = run( + ["sudo", "hostnamectl", "set-hostname", "--pretty", name], + capture_output=False, + check=True, + ) + except CalledProcessError as error: + logging.error(str(error)) + return False + + return True + @staticmethod def get_logs(lines, scope): """ diff --git a/python/web/mock/bin/hostnamectl b/python/web/mock/bin/hostnamectl new file mode 100755 index 00000000..4cb249dd --- /dev/null +++ b/python/web/mock/bin/hostnamectl @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +TMP_FILE="/tmp/hostnamectl_pretty.tmp" + +if [[ "$1" == "set-hostname" && "$2" == "--pretty" ]]; then + if [[ -z "$3" ]]; then + rm "$TMP_FILE" 2>/dev/null || true + else + echo "$3" > $TMP_FILE + fi + + exit 0 +fi + +if [[ "$1" == "status" ]]; then + cat "$TMP_FILE" 2>/dev/null + exit 0 +fi + +echo "Mock does not recognize: $0 $@" +exit 1 diff --git a/python/web/src/static/themes/modern/style.css b/python/web/src/static/themes/modern/style.css index dbc53dca..301463c3 100644 --- a/python/web/src/static/themes/modern/style.css +++ b/python/web/src/static/themes/modern/style.css @@ -208,12 +208,12 @@ select { */ div.header { display: flex; + align-items: center; } div.header div.title { order: 1; text-align: left; - flex-grow: 1; } div.header div.title h1 { @@ -227,7 +227,18 @@ div.header div.title a { } div.header div.hostname { - display: none; + color: #ccc; + padding: 0 0.5rem; + order: 2; + flex-grow: 1; +} + +div.header div.hostname span { + display: inline-block; + border: 1px solid #ccc; + border-radius: 1rem; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; } div.header div.login-status { @@ -253,9 +264,10 @@ div.header div.authentication-disabled { padding: 0 0.5rem; } -@media (max-width: 820px) { +@media (max-width: 900px) { div.header { min-height: 3.5rem; /* Safari 14 iOS and iPad OS */ + background: var(--dark); } body:not(.logged-in) div.header { @@ -263,10 +275,6 @@ div.header div.authentication-disabled { min-height: 8.875rem; /* Safari 14 iOS and iPad OS */ } - div.header div.title { - background: var(--dark); - } - div.header div.title a { display: block; background: url("/static/logo.png") no-repeat; @@ -326,7 +334,7 @@ div.header div.authentication-disabled { } } -@media (min-width: 821px) { +@media (min-width: 901px) { div.header { background: var(--dark); align-items: center; @@ -509,7 +517,7 @@ section > details ul { border-radius: 0.5rem; } -@media (max-width: 820px) { +@media (max-width: 900px) { section > details summary { font-size: 0.9rem; } @@ -578,7 +586,7 @@ table#attached-devices tr.reserved td { background-color: #ffe9e9; } -@media (max-width: 820px) { +@media (max-width: 900px) { table#attached-devices th.product, table#attached-devices td.product { display: none; @@ -597,7 +605,7 @@ table#attached-devices tr.reserved td { } } -@media (min-width: 821px) { +@media (min-width: 901px) { section#current-config form#config-actions { float: left; height: 2.75rem; @@ -670,7 +678,7 @@ section#files p { margin-top: 1rem; } -@media (max-width: 820px) { +@media (max-width: 900px) { section#files table#images tr th:nth-child(2), section#files table#images tr td:nth-child(2) { display: none; @@ -684,7 +692,7 @@ section#files p { } } -@media (min-width: 821px) { +@media (min-width: 901px) { section#files table#images form.file-copy input[type="submit"], section#files table#images form.file-rename input[type="submit"], section#files table#images form.file-delete input[type="submit"], @@ -740,7 +748,7 @@ section#attach-devices form { display: block; } -@media (max-width: 820px) { +@media (max-width: 900px) { section#attach-devices table tr th:nth-child(2), section#attach-devices table tr td:nth-child(2) { display: none; @@ -779,8 +787,12 @@ section#logging div:first-of-type { Index > Section: System ------------------------------------------------------------------------------ */ -@media (min-width: 821px) { - section#system input[type="submit"] { +section#system div.power-control { + margin-top: 1rem; +} + +@media (min-width: 901px) { + section#system div.power-control input[type="submit"] { background: var(--danger); border-color: var(--danger); color: #fff; diff --git a/python/web/src/templates/base.html b/python/web/src/templates/base.html index 09249238..3a31c9ef 100644 --- a/python/web/src/templates/base.html +++ b/python/web/src/templates/base.html @@ -77,8 +77,7 @@
- {{ _("IP") }}: {{ env["ip_addr"] }} - {{ _("Hostname") }}: {{ env["host"] }} + {{ env['system_name'] }}
@@ -126,10 +125,13 @@ {% endif %}
- {{ _("RaSCSI Reloaded version: ") }}{{ env["version"] }} {{ env["running_env"]["git"][:7] }} + {{ _("RaSCSI Reloaded version:") }} {{ env["version"] }} {{ env["running_env"]["git"][:7] }}
- {{ _("Hardware and OS: ") }}{{ env["running_env"]["env"] }} + {{ _("Hardware and OS:") }} {{ env["running_env"]["env"] }} +
+
+ {{ _("Network Address:") }} {{ env["host"] }} ({{ env["ip_addr"] }})
diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index f749000e..3653af3a 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -726,16 +726,30 @@ {{ _("System Operations") }} +
+
+ + + +
+
+ + +
+
+
-
+
+

diff --git a/python/web/src/web.py b/python/web/src/web.py index 3fbe9f08..77266b56 100644 --- a/python/web/src/web.py +++ b/python/web/src/web.py @@ -94,6 +94,7 @@ def get_env_info(): "logged_in": username and auth_active(AUTH_GROUP)["status"], "ip_addr": ip_addr, "host": host, + "system_name": sys_cmd.get_pretty_host(), "free_disk_space": int(sys_cmd.disk_space()["free"] / 1024 / 1024), "locale": get_locale(), "version": server_info["version"], @@ -797,6 +798,25 @@ def release_id(): return response(error=True, message=process["msg"]) +@APP.route("/sys/rename", methods=["POST"]) +@login_required +def rename_system(): + """ + Changes the hostname of the system + """ + name = str(request.form.get("system_name")) + max_length = 120 + + if len(name) <= max_length: + process = sys_cmd.set_pretty_host(name) + if process: + if name: + return response(message=_("System name changed to '%(name)s'.", name=name)) + return response(message=_("System name reset to default.")) + + return response(error=True, message=_("Failed to change system name.")) + + @APP.route("/sys/reboot", methods=["POST"]) @login_required def restart(): diff --git a/python/web/tests/api/test_settings.py b/python/web/tests/api/test_settings.py index 95e87451..81ac312c 100644 --- a/python/web/tests/api/test_settings.py +++ b/python/web/tests/api/test_settings.py @@ -196,3 +196,49 @@ def test_set_theme_via_query_string(http_client, theme): assert response.status_code == 200 assert response_data["status"] == STATUS_SUCCESS assert response_data["messages"][0]["message"] == f"Theme changed to '{theme}'." + + +# route("/sys/rename", methods=["POST"]) +def test_rename_system(env, http_client): + new_name = "SYSTEM NAME TEST" + + response = http_client.get("/env") + response_data = response.json() + + old_name = response_data["data"]["system_name"] + + response = http_client.post( + "/sys/rename", + data={ + "system_name": new_name, + }, + ) + + response_data = response.json() + + assert response.status_code == 200 + assert response_data["status"] == STATUS_SUCCESS + assert response_data["messages"][0]["message"] == f"System name changed to '{new_name}'." + + response = http_client.get("/env") + response_data = response.json() + + assert response_data["data"]["system_name"] == new_name + + response = http_client.post( + "/sys/rename", + data={ + "system_name": old_name, + }, + ) + + response_data = response.json() + + assert response.status_code == 200 + assert response_data["status"] == STATUS_SUCCESS + assert response_data["messages"][0]["message"] == f"System name changed to '{old_name}'." + + response = http_client.get("/env") + response_data = response.json() + + assert response_data["data"]["system_name"] == old_name