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
2 changes: 2 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ jobs:
pylint --exit-zero amplipi --generated-members "signal.Signals,GPIO.*"
pylint -E amplipi --generated-members "signal.Signals,GPIO.*"
pylint -E streams
pylint -E scripts/increment_auto_off.py
- name: Lint with mypy, static type checker
run: |
pip install mypy
mypy amplipi/ --ignore-missing-imports
mypy scripts/increment_auto_off.py --ignore-missing-imports
- name: Test mock using pytest # rpi cannot be tested directly due to hardware...
run: |
pip install pytest pytest-cov
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
* Add error handling on browser page for instances where the selected stream isn't browsable
* Add scrollbars to tall modals
* Change how events are handled with Modals to reduce accidental closures
* Rename Updater to Admin Panel
* Add "Admin Settings" tab to the Updater/Admin Panel
* Add toggleable option to persist system logs to Admin Settings
* Move "Set password" page to be accessible via Admin Settings tab
* System
* Make update process properly report errors

Expand Down
2 changes: 1 addition & 1 deletion amplipi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def shutdown():
"""
# preemptively save the state (just in case the shutdown procedure doesn't invoke a save)
get_ctrl().save()
# start the shutdown process and returning immediately (hopeully before the shutdown process begins)
# start the shutdown process and returning immediately (hopefully before the shutdown process begins)
Popen('sleep 1 && sudo systemctl poweroff', shell=True)


Expand Down
2 changes: 1 addition & 1 deletion amplipi/ctrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def reinit(self, settings: models.AppSettings = models.AppSettings(), change_not
except Exception as exc:
logger.exception("Error setting is_streamer flag: {exc}")

# determine if we're in LMS mode, based on a file
# determine if we're in LMS mode, based on a file existing
lms_mode_path = Path(defaults.USER_CONFIG_DIR, 'lms_mode')
if lms_mode_path.exists():
logger.info("lms mode")
Expand Down
146 changes: 145 additions & 1 deletion amplipi/updater/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import shutil
import asyncio

import configparser

# web framework
import requests
from fastapi import FastAPI, Request, File, UploadFile, Depends, APIRouter, Response
Expand All @@ -48,7 +50,8 @@
# pylint: disable=no-name-in-module
from pydantic import BaseModel

from ..auth import CookieOrParamAPIKey, router as auth_router, set_password_hash, unset_password_hash, NotAuthenticatedException, not_authenticated_exception_handler, create_access_key
from ..auth import CookieOrParamAPIKey, router as auth_router, set_password_hash, unset_password_hash,\
NotAuthenticatedException, not_authenticated_exception_handler, create_access_key

app = FastAPI()
router = APIRouter(dependencies=[Depends(CookieOrParamAPIKey)])
Expand All @@ -62,6 +65,72 @@
sse_messages: queue.Queue = queue.Queue()


def validate_logging_ini():
"""Fallback in case the ini file or any individual header doesn't exist, set to default settings. Only really comes up during tests."""
tmp = '/tmp/logging.ini.tmp'
ini = '/var/log/logging.ini'
conf = configparser.ConfigParser(strict=False, allow_no_value=True)

with open(tmp, "+w", encoding="utf-8") as file:
if os.path.exists(ini):
conf.read(ini)
else:
conf.read(file)

if not conf.has_section("logging"):
conf.add_section("logging")

if not conf.has_option("logging", "auto_off_delay"):
conf.set("logging", "auto_off_delay", "14")
auto_off_delay = conf.get("logging", "auto_off_delay", fallback="14")
if not auto_off_delay.isdigit() and bool(re.fullmatch(r'\d*\.\d+', auto_off_delay)):
# regex to check decimal state, this would lead to "123.45" and ".45" being true but not "123."
# Exclude anything that isdigit() as to not overwrite valid user settings
rounded = round(float(auto_off_delay)) if round(float(auto_off_delay)) > 0 else 1 # Avoid instances where it could be zero as to not set the "do not deactivate" setting
conf.set("logging", "auto_off_delay", str(rounded))
elif not auto_off_delay.isdigit(): # Cannot be merged with the first check in an OR case as valid regex catches would be intercepted by that
conf.set("logging", "auto_off_delay", "14")
conf.write(file)

subprocess.run(['sudo', 'mv', tmp, ini], check=True)


def validate_journald_conf():
"""Fallback in case the config file or any individual header doesn't exist, set to default settings"""
tmp = '/tmp/journald.conf.tmp'
conf = '/etc/systemd/journald.conf'
confparse = configparser.ConfigParser(strict=False, allow_no_value=True)

with open(tmp, "+w", encoding="utf-8") as file:
if os.path.exists(conf):
confparse.read(conf)
else:
confparse.read(file)

if not confparse.has_section("Journal"):
confparse.add_section("Journal")

if not confparse.has_option("Journal", "Storage") or confparse.get("Journal", "Storage") not in ("volatile", "persistent"):
# While volatile is the system's default, having that value either not exist or be invalid points to there being a system error that caused it to get that way
# Set to persistent just in case
confparse.set("Journal", "Storage", "persistent")

# Set everything else to default while preserving user settings
if not confparse.has_option("Journal", "SyncIntervalSec"):
confparse.set('Journal', 'SyncIntervalSec', '30s')
if not confparse.has_option("Journal", "SystemMaxUse"):
confparse.set('Journal', 'SystemMaxUse', '64M')
if not confparse.has_option("Journal", "RuntimeMaxUse"):
confparse.set('Journal', 'RuntimeMaxUse', '64M')
if not confparse.has_option("Journal", "ForwardToConsole"):
confparse.set('Journal', 'ForwardToConsole', 'no')
if not confparse.has_option("Journal", "ForwardToWall"):
confparse.set('Journal', 'ForwardToWall', 'no')

confparse.write(file)
subprocess.run(['sudo', 'mv', tmp, conf], check=True)


class ReleaseInfo(BaseModel):
""" Software Release Information """
url: str
Expand Down Expand Up @@ -95,6 +164,81 @@ class ReleaseInfo(BaseModel):
logger.exception(f'Error loading identity file: {e}')


class Persist_Logs(BaseModel):
"""Basemodel that consists of a bool and int, used to change different config files around the system via POST /settings/persist_logs"""
persist_logs: bool
auto_off_delay: int


@router.get("/settings/persist_logs")
def get_log_persist_state():
"""
Checks /etc/systemd/journald.conf to find if the current storage setting is persistent and returns a bool
Note that returning false doesn't necessarily mean that logs are set to volatile, and could just mean that the config file is missing the line being read
"""
validate_journald_conf()
journalconf = configparser.ConfigParser(strict=False, allow_no_value=True)
journalconf.read('/etc/systemd/journald.conf')

validate_logging_ini()
logconf = configparser.ConfigParser(strict=False, allow_no_value=True)
logconf.read('/var/log/logging.ini')

# Fallback set is the default value of the Storage variable under the Journal header of the conf file
# Used when the variable cannot be read but the file itself can (implying that the variable is missing, and should be set to a default)
ret = Persist_Logs(persist_logs=journalconf.get("Journal", "Storage", fallback="volatile") == "persistent", auto_off_delay=logconf.get("logging", "auto_off_delay", fallback="14"),)
return ret


@router.post("/settings/persist_logs")
def toggle_persist_logs(data: Persist_Logs):
"""Toggles the option within journald to save logs to memory or storage, and sets the length of time before that setting is reset to volatile"""
try:
# Just in case
validate_logging_ini()
validate_journald_conf()

state = get_log_persist_state()

if state.persist_logs != data.persist_logs:
journalconf = '/etc/systemd/journald.conf'
journaltmp = '/tmp/journald.conf.tmp'
journal = configparser.ConfigParser(strict=False, allow_no_value=True)
journal.read(journalconf)

if not journal.has_section("Journal"):
journal.add_section('Journal')

# goal_value is true if you wish to turn persistent logging on and false if you wish to turn it off
journal.set('Journal', 'Storage', 'persistent' if data.persist_logs else 'volatile')

with open(journaltmp, 'w', encoding="utf-8") as conf_file:
journal.write(conf_file)

subprocess.run(['sudo', 'mv', journaltmp, journalconf], check=True)
subprocess.run(['sudo', 'systemctl', 'restart', 'systemd-journald'], check=True)
logger.info(f"persist_logs set to {data.persist_logs}")
else:
logger.info("persist_logs unchanged")

if state.auto_off_delay != data.auto_off_delay:
logconf = '/var/log/logging.ini'
logtmp = '/tmp/logging.ini.tmp'
log = configparser.ConfigParser(strict=False, allow_no_value=True)
log.read(logconf)
log.set('logging', 'auto_off_delay', f"{data.auto_off_delay}") # Accept auto_off_delay as an int for type checking, parse to str for configParser validity
with open(logtmp, 'w', encoding='utf-8') as file:
log.write(file)
subprocess.run(['sudo', 'mv', logtmp, logconf], check=True)
logger.info(f"auto_off_delay set to {data.auto_off_delay}")

else:
logger.info("auto_off_delay unchanged")
except Exception as exc:
logger.exception(str(exc))
return 500
Comment on lines +237 to +239
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think return 500 is a sensible way to handle things, but it is what we do everywhere else and one of the notes was "do this like everything else" and so I've simply followed the template of the endpoints beneath this



@router.get('/update')
def get_index():
""" Get the update website """
Expand Down
61 changes: 61 additions & 0 deletions amplipi/updater/static/css/styles.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
/*
A couple styles to make the demo page look good
*/

.column-container {
display: flex;
flex-direction: column;
gap: 64px;
}

.column-item {
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 16px;
}

body {
padding-bottom: 2rem;
padding-top: 1rem;
Expand Down Expand Up @@ -79,6 +93,15 @@ hr {
background: #4a4a4a;
}

.margin{
margin-left: 0px;
}
@media (min-width: 768px) {
.margin{
margin-left: 10px;
}
}

.fa-circle-notch {
animation: fa-spin 1s infinite linear;
}
Expand All @@ -88,3 +111,41 @@ hr {
background-color: #5a6268;
border-color: #545b62;
}

.container {
padding-left: 1rem;
padding-right: 1rem;
}


.set-password-dialog {
color: #aaaaaa !important;
}

/* lifted from https://www.w3schools.com/css/css_tooltip.asp */

.hover-label {
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
}

.hover-label .hover-tooltip{
top: 100%;
left: 50%;
visibility: hidden;
width: 750px;
max-width: 70vw;
background-color: rgba(100, 100, 100, 1);
color: #ffffff;
text-align: center;
padding: 5px 0;
border-radius: 6px;

position: absolute;
z-index: 1;
}

.hover-label:hover .hover-tooltip{
visibility: visible;
}
Loading