Skip to content

Commit b4e63d6

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
1 parent dfe9110 commit b4e63d6

10 files changed

Lines changed: 383 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
* Web App
55
* Ensure that abnormally-shaped album art is still horizontally centered
66
* Add error handling on browser page for instances where the selected stream isn't browsable
7+
* Rename Updater to Updater/Admin Control Panel
8+
* Add "Admin Settings" tab to the Updater/Admin Control Panel
9+
* Add toggleable option to persist system logs to Admin Settings
10+
* Move "Set password" page to be accessible via Admin Settings tab
711
* System
812
* Make update process properly report errors
913

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: 104 additions & 0 deletions
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
@@ -62,6 +64,21 @@
6264
sse_messages: queue.Queue = queue.Queue()
6365

6466

67+
def create_logging_ini():
68+
"""Fallback in case the ini doesn't exist, set to default settings. Only really comes up during github tests."""
69+
ini = '/var/log/logging.ini'
70+
tmp = '/tmp/logging.ini.tmp'
71+
72+
if not os.path.exists(ini):
73+
conf = configparser.ConfigParser(strict=False, allow_no_value=True)
74+
with open(tmp, "+w", encoding="utf-8") as file:
75+
conf.read(file)
76+
conf.add_section("logging")
77+
conf.set("logging", "auto_off_delay", "14")
78+
conf.write(file)
79+
subprocess.run(['sudo', 'mv', tmp, ini], check=True)
80+
81+
6582
class ReleaseInfo(BaseModel):
6683
""" Software Release Information """
6784
url: str
@@ -95,6 +112,93 @@ class ReleaseInfo(BaseModel):
95112
logger.exception(f'Error loading identity file: {e}')
96113

97114

115+
class Persist_Logs(BaseModel):
116+
"""Basemodel that consists of a bool and int, used to change different config files around the system via POST /settings/persist_logs"""
117+
persist_logs: bool
118+
auto_off_delay: int
119+
120+
121+
@router.get("/settings/persist_logs")
122+
def get_log_persist_state():
123+
"""
124+
Checks /etc/systemd/journald.conf to find if the current storage setting is persistent and returns a bool
125+
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
126+
"""
127+
create_logging_ini()
128+
129+
journalconf = configparser.ConfigParser(strict=False, allow_no_value=True)
130+
journalconf.read('/etc/systemd/journald.conf')
131+
132+
logconf = configparser.ConfigParser(strict=False, allow_no_value=True)
133+
logconf.read('/var/log/logging.ini')
134+
135+
# Fallback set is the default value of the Storage variable under the Journal header of the conf file
136+
# 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)
137+
ret = Persist_Logs(persist_logs=journalconf.get("Journal", "Storage", fallback="volatile") == "persistent", auto_off_delay=logconf.get("logging", "auto_off_delay", fallback="14"),)
138+
return ret
139+
140+
141+
@router.post("/settings/persist_logs")
142+
def toggle_persist_logs(data: Persist_Logs):
143+
"""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"""
144+
try:
145+
create_logging_ini() # Just in case
146+
state = get_log_persist_state()
147+
148+
if state.persist_logs != data.persist_logs:
149+
journalconf = '/etc/systemd/journald.conf'
150+
journaltmp = '/tmp/journald.conf.tmp'
151+
journal = configparser.ConfigParser(strict=False, allow_no_value=True)
152+
journal.read(journalconf)
153+
154+
if not journal.has_section("Journal"):
155+
journal.add_section('Journal')
156+
157+
# goal_value is true if you wish to turn persistent logging on and false if you wish to turn it off
158+
if data.persist_logs: # Set persist
159+
journal.set('Journal', 'Storage', 'persistent')
160+
journal.set('Journal', 'SyncIntervalSec', '30s')
161+
journal.set('Journal', 'SystemMaxUse', '64M')
162+
163+
journal.remove_option('Journal', 'RuntimeMaxUse')
164+
journal.remove_option('Journal', 'ForwardToConsole')
165+
journal.remove_option('Journal', 'ForwardToWall')
166+
else: # Reset config to default as seen in configure.py
167+
journal.set('Journal', 'Storage', 'volatile')
168+
journal.set('Journal', 'RuntimeMaxUse', '64M')
169+
journal.set('Journal', 'ForwardToConsole', 'no')
170+
journal.set('Journal', 'ForwardToWall', 'no')
171+
172+
journal.remove_option('Journal', 'SyncIntervalSec')
173+
journal.remove_option('Journal', 'SystemMaxUse')
174+
175+
with open(journaltmp, 'w', encoding="utf-8") as conf_file:
176+
journal.write(conf_file)
177+
178+
subprocess.run(['sudo', 'mv', journaltmp, journalconf], check=True)
179+
subprocess.run(['sudo', 'systemctl', 'restart', 'systemd-journald'], check=True)
180+
logger.info(f"persist_logs set to {data.persist_logs}")
181+
else:
182+
logger.info("persist_logs unchanged")
183+
184+
if state.auto_off_delay != data.auto_off_delay:
185+
logconf = '/var/log/logging.ini'
186+
logtmp = '/tmp/logging.ini.tmp'
187+
log = configparser.ConfigParser(strict=False, allow_no_value=True)
188+
log.read(logconf)
189+
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
190+
with open(logtmp, 'w', encoding='utf-8') as file:
191+
log.write(file)
192+
subprocess.run(['sudo', 'mv', logtmp, logconf], check=True)
193+
logger.info(f"auto_off_delay set to {data.auto_off_delay}")
194+
195+
else:
196+
logger.info("auto_off_delay unchanged")
197+
except Exception as exc:
198+
logger.exception(str(exc))
199+
return 500
200+
201+
98202
@router.get('/update')
99203
def get_index():
100204
""" 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)