Skip to content

Commit eee65a4

Browse files
Create function, endpoint, frontend implementation for log persistence
Update CHANGELOG.md Lint ctrl.py Add persist_logs bool and integrate throughout the config flow Add many logger.info chunks for testing Add more logic to persist_logs toggle endpoint Change how default value is handled Change default permissions of journald.conf Fix spelling errors, add logger on frontend Move toggle_log_persistance away from FastAPI and towards the Api object Linting Call reinit at end of log toggle, reword models.py description of persist_logs Remove all uses of persist_logs from amplipi core, move to updater side Remove lingering changes from app.py, ctrl.py pylint pylint (2) Finish porting new admin settings tab to updater Change name of Updates tab on settings page Fix spelling in comment in app.py Robustify setPersist function against edge cases, add documentation pylint Add delete logs endpoint, cronjob that calls it at midnight Updates/Admin Change comment on delete_persisted_logs_crontab Update CHANGELOG.md Remove deletion endpoint + supporting files remove optional "value" bool from post persist_logs Fix wording in index.html Change configure.py to only set logs to volatile if the /var/log/journal directory doesn't exist (i.e., only set volatility on first deploy) Reduce verbocity in configure.py Add auto_off_delay feature to admin settings Pylint Pylint Rename attempt_persist_logs_auto_off.py to increment_auto_off.py Change logic paths of POST /settings/persist_logs to avoid interacting with the files more often than necessary PyCodeStyle Update crontab to have a user associated with it remove unused variables from increment_auto_off.py Swap from using subprocess.run and configparser to just responses in increment_auto_off.py Copy files properly in configure.py Cover some oversights in asgi.py Add hoverover tooltip for Auto Off Delay label Make sure to pip install requests in configure.py pylint segregate standard imports from package import in increment_auto_off.py Update styling Move password setting functionality to Admin Settings page, add loading spinner to save log state button update IDs, classnames to use JavaScript naming instead of Python naming ( - instead of _ ) Add new success and failure notification for persist save button Fix formatting for smaller screens on the updater Update CHANGELOG.md Update CHANGELOG.md Fix styling of admin panel Correct error handling, fix styling issue pylint Extend safety net remove pip install requests from configure.py, that should be handled by requirements.txt Update styling, tooltips Gate the delay of increment_auto_off.py behind an if statement Revamp increment_auto_off.py to be more efficient Update verbiage around the updater Update increment_auto_off.py to not run or loop if persistance is disabled Edit github workflows to include more files Add more, better file validation to /persist_logs endpoints Remove looping, add checking to increment_auto_off.py fix typo in python-app.yaml Add docstring to increment_auto_off.py, add same file to github pylinting Reconfigure file validation functions Simplify persistence toggle function Rework increment_auto_off.py to be able to use the backdoor if the frontdoor doesn't work out linting linting (2) Linting (3) Simplify file verification functions Include minor verification in the non-api route of increment_auto_off.py pycodestyle Further robustify auto_off.py and validator functions against direct user interventions pypcodestyle Update password set function to give feedback in the same way as updating persistence and delay
1 parent 739f17d commit eee65a4

12 files changed

Lines changed: 542 additions & 50 deletions

File tree

.github/workflows/python-app.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ jobs:
4141
pylint --exit-zero amplipi --generated-members "signal.Signals,GPIO.*"
4242
pylint -E amplipi --generated-members "signal.Signals,GPIO.*"
4343
pylint -E streams
44+
pylint -E scripts/increment_auto_off.py
4445
- name: Lint with mypy, static type checker
4546
run: |
4647
pip install mypy
4748
mypy amplipi/ --ignore-missing-imports
49+
mypy scripts/increment_auto_off.py --ignore-missing-imports
4850
- name: Test mock using pytest # rpi cannot be tested directly due to hardware...
4951
run: |
5052
pip install pytest pytest-cov

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* Add error handling on browser page for instances where the selected stream isn't browsable
77
* Add scrollbars to tall modals
88
* Change how events are handled with Modals to reduce accidental closures
9+
* Rename Updater to Admin Panel
10+
* Add "Admin Settings" tab to the Updater/Admin Panel
11+
* Add toggleable option to persist system logs to Admin Settings
12+
* Move "Set password" page to be accessible via Admin Settings tab
913
* System
1014
* Make update process properly report errors
1115

amplipi/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def shutdown():
226226
"""
227227
# preemptively save the state (just in case the shutdown procedure doesn't invoke a save)
228228
get_ctrl().save()
229-
# start the shutdown process and returning immediately (hopeully before the shutdown process begins)
229+
# start the shutdown process and returning immediately (hopefully before the shutdown process begins)
230230
Popen('sleep 1 && sudo systemctl poweroff', shell=True)
231231

232232

amplipi/ctrl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def reinit(self, settings: models.AppSettings = models.AppSettings(), change_not
222222
except Exception as exc:
223223
logger.exception("Error setting is_streamer flag: {exc}")
224224

225-
# determine if we're in LMS mode, based on a file
225+
# determine if we're in LMS mode, based on a file existing
226226
lms_mode_path = Path(defaults.USER_CONFIG_DIR, 'lms_mode')
227227
if lms_mode_path.exists():
228228
logger.info("lms mode")

amplipi/updater/asgi.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import shutil
3737
import asyncio
3838

39+
import configparser
40+
3941
# web framework
4042
import requests
4143
from fastapi import FastAPI, Request, File, UploadFile, Depends, APIRouter, Response
@@ -48,7 +50,8 @@
4850
# pylint: disable=no-name-in-module
4951
from pydantic import BaseModel
5052

51-
from ..auth import CookieOrParamAPIKey, router as auth_router, set_password_hash, unset_password_hash, NotAuthenticatedException, not_authenticated_exception_handler, create_access_key
53+
from ..auth import CookieOrParamAPIKey, router as auth_router, set_password_hash, unset_password_hash,\
54+
NotAuthenticatedException, not_authenticated_exception_handler, create_access_key
5255

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

6467

68+
def validate_logging_ini():
69+
"""Fallback in case the ini file or any individual header doesn't exist, set to default settings. Only really comes up during tests."""
70+
tmp = '/tmp/logging.ini.tmp'
71+
ini = '/var/log/logging.ini'
72+
conf = configparser.ConfigParser(strict=False, allow_no_value=True)
73+
74+
with open(tmp, "+w", encoding="utf-8") as file:
75+
if os.path.exists(ini):
76+
conf.read(ini)
77+
else:
78+
conf.read(file)
79+
80+
if not conf.has_section("logging"):
81+
conf.add_section("logging")
82+
83+
if not conf.has_option("logging", "auto_off_delay"):
84+
conf.set("logging", "auto_off_delay", "14")
85+
auto_off_delay = conf.get("logging", "auto_off_delay", fallback="14")
86+
if not auto_off_delay.isdigit() and bool(re.fullmatch(r'\d*\.\d+', auto_off_delay)):
87+
# regex to check decimal state, this would lead to "123.45" and ".45" being true but not "123."
88+
# Exclude anything that isdigit() as to not overwrite valid user settings
89+
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
90+
conf.set("logging", "auto_off_delay", str(rounded))
91+
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
92+
conf.set("logging", "auto_off_delay", "14")
93+
conf.write(file)
94+
95+
subprocess.run(['sudo', 'mv', tmp, ini], check=True)
96+
97+
98+
def validate_journald_conf():
99+
"""Fallback in case the config file or any individual header doesn't exist, set to default settings"""
100+
tmp = '/tmp/journald.conf.tmp'
101+
conf = '/etc/systemd/journald.conf'
102+
confparse = configparser.ConfigParser(strict=False, allow_no_value=True)
103+
104+
with open(tmp, "+w", encoding="utf-8") as file:
105+
if os.path.exists(conf):
106+
confparse.read(conf)
107+
else:
108+
confparse.read(file)
109+
110+
if not confparse.has_section("Journal"):
111+
confparse.add_section("Journal")
112+
113+
if not confparse.has_option("Journal", "Storage") or confparse.get("Journal", "Storage") not in ("volatile", "persistent"):
114+
# 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
115+
# Set to persistent just in case
116+
confparse.set("Journal", "Storage", "persistent")
117+
118+
# Set everything else to default while preserving user settings
119+
if not confparse.has_option("Journal", "SyncIntervalSec"):
120+
confparse.set('Journal', 'SyncIntervalSec', '30s')
121+
if not confparse.has_option("Journal", "SystemMaxUse"):
122+
confparse.set('Journal', 'SystemMaxUse', '64M')
123+
if not confparse.has_option("Journal", "RuntimeMaxUse"):
124+
confparse.set('Journal', 'RuntimeMaxUse', '64M')
125+
if not confparse.has_option("Journal", "ForwardToConsole"):
126+
confparse.set('Journal', 'ForwardToConsole', 'no')
127+
if not confparse.has_option("Journal", "ForwardToWall"):
128+
confparse.set('Journal', 'ForwardToWall', 'no')
129+
130+
confparse.write(file)
131+
subprocess.run(['sudo', 'mv', tmp, conf], check=True)
132+
133+
65134
class ReleaseInfo(BaseModel):
66135
""" Software Release Information """
67136
url: str
@@ -95,6 +164,81 @@ class ReleaseInfo(BaseModel):
95164
logger.exception(f'Error loading identity file: {e}')
96165

97166

167+
class Persist_Logs(BaseModel):
168+
"""Basemodel that consists of a bool and int, used to change different config files around the system via POST /settings/persist_logs"""
169+
persist_logs: bool
170+
auto_off_delay: int
171+
172+
173+
@router.get("/settings/persist_logs")
174+
def get_log_persist_state():
175+
"""
176+
Checks /etc/systemd/journald.conf to find if the current storage setting is persistent and returns a bool
177+
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
178+
"""
179+
validate_journald_conf()
180+
journalconf = configparser.ConfigParser(strict=False, allow_no_value=True)
181+
journalconf.read('/etc/systemd/journald.conf')
182+
183+
validate_logging_ini()
184+
logconf = configparser.ConfigParser(strict=False, allow_no_value=True)
185+
logconf.read('/var/log/logging.ini')
186+
187+
# Fallback set is the default value of the Storage variable under the Journal header of the conf file
188+
# 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)
189+
ret = Persist_Logs(persist_logs=journalconf.get("Journal", "Storage", fallback="volatile") == "persistent", auto_off_delay=logconf.get("logging", "auto_off_delay", fallback="14"),)
190+
return ret
191+
192+
193+
@router.post("/settings/persist_logs")
194+
def toggle_persist_logs(data: Persist_Logs):
195+
"""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"""
196+
try:
197+
# Just in case
198+
validate_logging_ini()
199+
validate_journald_conf()
200+
201+
state = get_log_persist_state()
202+
203+
if state.persist_logs != data.persist_logs:
204+
journalconf = '/etc/systemd/journald.conf'
205+
journaltmp = '/tmp/journald.conf.tmp'
206+
journal = configparser.ConfigParser(strict=False, allow_no_value=True)
207+
journal.read(journalconf)
208+
209+
if not journal.has_section("Journal"):
210+
journal.add_section('Journal')
211+
212+
# goal_value is true if you wish to turn persistent logging on and false if you wish to turn it off
213+
journal.set('Journal', 'Storage', 'persistent' if data.persist_logs else 'volatile')
214+
215+
with open(journaltmp, 'w', encoding="utf-8") as conf_file:
216+
journal.write(conf_file)
217+
218+
subprocess.run(['sudo', 'mv', journaltmp, journalconf], check=True)
219+
subprocess.run(['sudo', 'systemctl', 'restart', 'systemd-journald'], check=True)
220+
logger.info(f"persist_logs set to {data.persist_logs}")
221+
else:
222+
logger.info("persist_logs unchanged")
223+
224+
if state.auto_off_delay != data.auto_off_delay:
225+
logconf = '/var/log/logging.ini'
226+
logtmp = '/tmp/logging.ini.tmp'
227+
log = configparser.ConfigParser(strict=False, allow_no_value=True)
228+
log.read(logconf)
229+
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
230+
with open(logtmp, 'w', encoding='utf-8') as file:
231+
log.write(file)
232+
subprocess.run(['sudo', 'mv', logtmp, logconf], check=True)
233+
logger.info(f"auto_off_delay set to {data.auto_off_delay}")
234+
235+
else:
236+
logger.info("auto_off_delay unchanged")
237+
except Exception as exc:
238+
logger.exception(str(exc))
239+
return 500
240+
241+
98242
@router.get('/update')
99243
def get_index():
100244
""" Get the update website """

amplipi/updater/static/css/styles.css

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
/*
22
A couple styles to make the demo page look good
33
*/
4+
5+
.column-container {
6+
display: flex;
7+
flex-direction: column;
8+
gap: 64px;
9+
}
10+
11+
.column-item {
12+
display: flex;
13+
flex-direction: column;
14+
justify-content: space-around;
15+
gap: 16px;
16+
}
17+
418
body {
519
padding-bottom: 2rem;
620
padding-top: 1rem;
@@ -79,6 +93,15 @@ hr {
7993
background: #4a4a4a;
8094
}
8195

96+
.margin{
97+
margin-left: 0px;
98+
}
99+
@media (min-width: 768px) {
100+
.margin{
101+
margin-left: 10px;
102+
}
103+
}
104+
82105
.fa-circle-notch {
83106
animation: fa-spin 1s infinite linear;
84107
}
@@ -88,3 +111,41 @@ hr {
88111
background-color: #5a6268;
89112
border-color: #545b62;
90113
}
114+
115+
.container {
116+
padding-left: 1rem;
117+
padding-right: 1rem;
118+
}
119+
120+
121+
.set-password-dialog {
122+
color: #aaaaaa !important;
123+
}
124+
125+
/* lifted from https://www.w3schools.com/css/css_tooltip.asp */
126+
127+
.hover-label {
128+
position: relative;
129+
display: inline-block;
130+
border-bottom: 1px dotted black;
131+
}
132+
133+
.hover-label .hover-tooltip{
134+
top: 100%;
135+
left: 50%;
136+
visibility: hidden;
137+
width: 750px;
138+
max-width: 70vw;
139+
background-color: rgba(100, 100, 100, 1);
140+
color: #ffffff;
141+
text-align: center;
142+
padding: 5px 0;
143+
border-radius: 6px;
144+
145+
position: absolute;
146+
z-index: 1;
147+
}
148+
149+
.hover-label:hover .hover-tooltip{
150+
visibility: visible;
151+
}

0 commit comments

Comments
 (0)