Get and set pretty hostname to use as system name in Web UI (#997)

- Display the pretty hostname as system name in header
- Move IP and hostname down to the footer
- New endpoint for setting the pretty hostname, plus form field in the Web UI
- (unrelated) Use platform.uname() instead of shell uname
- (unrelated) Better logic for fetching the Mac HD Drivers zip file in easyinstall.sh
This commit is contained in:
Daniel Markstedt 2022-11-20 10:20:32 -08:00 committed by GitHub
parent 2645656199
commit 0d95890887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 42 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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):
"""

21
python/web/mock/bin/hostnamectl Executable file
View File

@ -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

View File

@ -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;

View File

@ -77,8 +77,7 @@
</div>
<div class="hostname">
<span>{{ _("IP") }}: {{ env["ip_addr"] }}</span>
<span>{{ _("Hostname") }}: {{ env["host"] }}</span>
<span>{{ env['system_name'] }}</span>
</div>
</div>
@ -126,10 +125,13 @@
{% endif %}
</div>
<div>
{{ _("RaSCSI Reloaded version: ") }}<b>{{ env["version"] }} <a href="https://github.com/akuker/RASCSI/commit/{{ env["running_env"]["git"] }}" target="_blank">{{ env["running_env"]["git"][:7] }}</a></b>
{{ _("RaSCSI Reloaded version:") }} <b>{{ env["version"] }} <a href="https://github.com/akuker/RASCSI/commit/{{ env["running_env"]["git"] }}" target="_blank">{{ env["running_env"]["git"][:7] }}</a></b>
</div>
<div>
{{ _("Hardware and OS: ") }}{{ env["running_env"]["env"] }}
{{ _("Hardware and OS:") }} {{ env["running_env"]["env"] }}
</div>
<div>
{{ _("Network Address:") }} {{ env["host"] }} ({{ env["ip_addr"] }})
</div>
</div>
</body>

View File

@ -726,16 +726,30 @@
{{ _("System Operations") }}
</summary>
<ul>
<li>{{ _("For System Name we are using the high-level \"pretty\" hostname.") }}</li>
<li>{{ _("IMPORTANT: Always shut down the system before turning off the power. Failing to do so may lead to data loss.") }}</li>
</ul>
</details>
<div>
<form action="/sys/rename" method="post">
<label for="system_name">{{ _("System Name:") }}</label>
<input name="system_name" id="system_name" type="text" maxlength=120 required>
<input type="submit" value="{{ _("Rename") }}">
</form>
<form action="/sys/rename" method="post">
<input name="system_name" type="hidden" value="">
<input type="submit" value="{{ _("Reset") }}">
</form>
</div>
<div class="power-control">
<form action="/sys/reboot" method="post" onclick="if (confirm('{{ _("Reboot the System?") }}')) shutdownNotify('{{ _("Rebooting the system...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Reboot System") }}">
</form>
<form action="/sys/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the System?") }}')) shutdownNotify('{{ _("Shutting down the system...") }}'); else event.preventDefault();">
<form action="/sys/shutdown" method="post" onclick="if (confirm('{{ _("Shut Down the System?") }}')) shutdownNotify('{{ _("Shutting down the system...") }}'); else event.preventDefault();">
<input type="submit" value="{{ _("Shut Down System") }}">
</form>
</div>
</section>
<hr/>

View File

@ -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():

View File

@ -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