BOSWatch 3
Python Script to receive and decode German BOS Information with rtl_fm and multimon-NG
 
Loading...
Searching...
No Matches
install_service Namespace Reference

Data Structures

class  DummyFore
 
class  DummyStyle
 

Functions

 get_lang ()
 
 set_lang (lang)
 
 colorama_auto_install ()
 
 setup_logging (verbose=False, quiet=False)
 
 t (key)
 
 get_user_input (prompt, valid_inputs, max_attempts=3)
 
 list_yaml_files ()
 
 test_yaml_file (file_path)
 
 detect_yaml_type (file_path)
 
 execute (command, dry_run=False)
 
 verify_service (service_path)
 
 install_service (yaml_file, dry_run=False)
 
 remove_service (service_name, dry_run=False)
 
 remove_menu (dry_run=False)
 
 init_language ()
 
 main (dry_run=False)
 

Variables

 BASE_DIR = Path(__file__).resolve().parent
 
str BW_DIR = '/opt/boswatch3'
 
 SERVICE_DIR = Path('/etc/systemd/system')
 
tuple CONFIG_DIR = (BASE_DIR / 'config').resolve()
 
tuple LOG_FILE = (BASE_DIR / 'log' / 'install' / 'service_install.log').resolve()
 
 parent
 
 exist_ok
 
str _lang = 'de'
 
dict TEXT
 
 colorama_available
 
 Fore
 
 Style = DummyStyle()
 
 lang_parser
 
 remaining_argv
 
 parser
 
 action
 
 help
 
 args = parser.parse_args(remaining_argv)
 
 verbose
 
 quiet
 
 dry_run
 

Detailed Description


/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / / __ / / / /__ | | /| / / __ `/ __/ ___/ __ \ /_ < / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / /_____/____//____/ |__/|__/__,_/__/___/_/ /_/ /____/ German BOS Information Script by Bastian Schroll

Function Documentation

◆ get_lang()

install_service.get_lang ( )
39def get_lang():
40 return _lang
41
42

◆ set_lang()

install_service.set_lang (   lang)
43def set_lang(lang):
44 global _lang
45 _lang = lang
46
47
48# === text-dictionary ===

◆ colorama_auto_install()

install_service.colorama_auto_install ( )
    Auto-installs colorama if missing.
    Note: Language detection happens before colorama is available.
155def colorama_auto_install():
156 r"""
157 Auto-installs colorama if missing.
158 Note: Language detection happens before colorama is available.
159 """
160 # recognize language early (before colorama installation)
161 import argparse
162 early_parser = argparse.ArgumentParser(add_help=False)
163 early_parser.add_argument('--lang', '-l', choices=['de', 'en'], default='de')
164 early_args, _ = early_parser.parse_known_args()
165 lang = early_args.lang
166
167 # use text from global TEXT dictionary
168 txt = TEXT[lang]
169
170 try:
171 from colorama import init as colorama_init, Fore, Style
172 colorama_init(autoreset=True)
173 return True, Fore, Style
174 except ImportError:
175 print(txt["colorama_missing"])
176
177 # install Colorama
178 print(txt["colorama_install"])
179 subprocess.run(["sudo", "apt", "install", "-y", "python3-colorama"], check=False)
180
181 # retry importing Colorama
182 try:
183 from colorama import init as colorama_init, Fore, Style
184 colorama_init(autoreset=True)
185 print(txt["colorama_install_ok"])
186 return True, Fore, Style
187 except ImportError:
188 print(txt["colorama_install_fail"])
189 return False, None, None
190
191
192# === import / install colorama ===

◆ setup_logging()

install_service.setup_logging (   verbose = False,
  quiet = False 
)
Setup logging to file and console with colorized output.
209def setup_logging(verbose=False, quiet=False):
210 r"""
211 Setup logging to file and console with colorized output.
212 """
213 log_level = logging.INFO
214 if quiet:
215 log_level = logging.WARNING
216 elif verbose:
217 log_level = logging.DEBUG
218
219 logger = logging.getLogger()
220 logger.setLevel(log_level)
221
222 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
223
224 # File Handler (plain)
225 fh = logging.FileHandler(LOG_FILE)
226 fh.setFormatter(formatter)
227 logger.addHandler(fh)
228
229 # Console Handler (colorized)
230 class ColorFormatter(logging.Formatter):
231 COLORS = {
232 logging.DEBUG: Fore.CYAN,
233 logging.INFO: Fore.GREEN,
234 logging.WARNING: Fore.YELLOW,
235 logging.ERROR: Fore.RED,
236 logging.CRITICAL: Fore.RED + Style.BRIGHT,
237 }
238
239 def format(self, record):
240 color = self.COLORS.get(record.levelno, Fore.RESET)
241 message = super().format(record)
242 return f"{color}{message}{Style.RESET_ALL}"
243
244 ch = logging.StreamHandler(sys.stdout)
245 ch.setFormatter(ColorFormatter('%(levelname)s: %(message)s'))
246 logger.addHandler(ch)
247
248 return logger
249
250

◆ t()

install_service.t (   key)
Translation helper: returns the localized string for the given key.
251def t(key):
252 r"""
253 Translation helper: returns the localized string for the given key.
254 """
255 lang = get_lang()
256 return TEXT.get(lang, TEXT['de']).get(key, key)
257
258

◆ get_user_input()

install_service.get_user_input (   prompt,
  valid_inputs,
  max_attempts = 3 
)
Prompt user for input until a valid input from valid_inputs is entered or max_attempts exceeded.
Raises RuntimeError on failure.
259def get_user_input(prompt, valid_inputs, max_attempts=3):
260 r"""
261 Prompt user for input until a valid input from valid_inputs is entered or max_attempts exceeded.
262 Raises RuntimeError on failure.
263 """
264 attempts = 0
265 while attempts < max_attempts:
266 value = input(prompt).strip().lower()
267 if value in valid_inputs:
268 return value
269 logging.warning(t("invalid_input").format(", ".join(valid_inputs)))
270 attempts += 1
271 raise RuntimeError(t("max_attempts_exceeded"))
272
273

◆ list_yaml_files()

install_service.list_yaml_files ( )
Returns a list of .yaml or .yml files in the config directory.
274def list_yaml_files():
275 r"""
276 Returns a list of .yaml or .yml files in the config directory.
277 """
278 return sorted([f.name for f in CONFIG_DIR.glob("*.y*ml")])
279
280

◆ test_yaml_file()

install_service.test_yaml_file (   file_path)
Tests if YAML file can be loaded without error.
281def test_yaml_file(file_path):
282 r"""
283 Tests if YAML file can be loaded without error.
284 """
285 try:
286 content = file_path.read_text(encoding='utf-8')
287 yaml.safe_load(content)
288 return True
289 except Exception as e:
290 logging.error(t("yaml_error").format(file_path, e))
291 return False
292
293

◆ detect_yaml_type()

install_service.detect_yaml_type (   file_path)
Detects if YAML config is 'client' or 'server' type.
294def detect_yaml_type(file_path):
295 r"""
296 Detects if YAML config is 'client' or 'server' type.
297 """
298 try:
299 with open(file_path, 'r', encoding='utf-8') as f:
300 data = yaml.safe_load(f)
301 if 'client' in data:
302 return 'client'
303 elif 'server' in data:
304 return 'server'
305 else:
306 logging.error(t("unknown_yaml_type").format(os.path.basename(file_path)))
307 return None
308 except Exception as e:
309 logging.error(t("yaml_read_error").format(file_path, e))
310 return None
311
312

◆ execute()

install_service.execute (   command,
  dry_run = False 
)
Executes shell command unless dry_run is True.
313def execute(command, dry_run=False):
314 r"""
315 Executes shell command unless dry_run is True.
316 """
317 logging.debug(f"→ {command}")
318 if not dry_run:
319 subprocess.run(command, shell=True, check=False)
320
321

◆ verify_service()

install_service.verify_service (   service_path)
Runs 'systemd-analyze verify' on the service file and logs warnings/errors.
322def verify_service(service_path):
323 r"""
324 Runs 'systemd-analyze verify' on the service file and logs warnings/errors.
325 """
326 try:
327 result = subprocess.run(
328 ['systemd-analyze', 'verify', service_path],
329 capture_output=True,
330 text=True,
331 timeout=10
332 )
333 if result.returncode != 0 or result.stderr:
334 logging.warning(t("verify_warn").format(result.stderr.strip()))
335 else:
336 logging.debug(t("verify_ok").format(os.path.basename(service_path)))
337 except subprocess.TimeoutExpired:
338 logging.warning(t("verify_timeout").format(os.path.basename(service_path)))
339 except Exception as e:
340 logging.error(t("yaml_error").format(service_path, e))
341
342

◆ install_service()

install_service.install_service (   yaml_file,
  dry_run = False 
)
Creates and installs systemd service based on YAML config.
343def install_service(yaml_file, dry_run=False):
344 r"""
345 Creates and installs systemd service based on YAML config.
346 """
347 yaml_path = CONFIG_DIR / yaml_file
348 yaml_type = detect_yaml_type(yaml_path)
349 if yaml_type == 'server':
350 is_server = True
351 elif yaml_type == 'client':
352 is_server = False
353 else:
354 logging.error(t("unknown_yaml_type").format(yaml_file))
355 return
356
357 service_name = f"bw3_{Path(yaml_file).stem}.service"
358 service_path = SERVICE_DIR / service_name
359
360 if is_server:
361 exec_line = f"/usr/bin/python3 {BW_DIR}/bw_server.py -c {yaml_file}"
362 description = "BOSWatch Server"
363 after = "network-online.target"
364 wants = "Wants=network-online.target"
365 else:
366 exec_line = f"/usr/bin/python3 {BW_DIR}/bw_client.py -c {yaml_file}"
367 description = "BOSWatch Client"
368 after = "network.target"
369 wants = ""
370
371 service_content = f"""[Unit]
372Description={description}
373After={after}
374{wants}
375
376[Service]
377Type=simple
378WorkingDirectory={BW_DIR}
379ExecStart={exec_line}
380Restart=on-abort
381
382[Install]
383WantedBy=multi-user.target
384"""
385
386 logging.info(t("creating_service_file").format(yaml_file, service_name))
387
388 if not dry_run:
389 try:
390 with open(service_path, 'w', encoding='utf-8') as f:
391 f.write(service_content)
392 except IOError as e:
393 logging.error(t("file_write_error").format(service_path, e))
394 return
395 verify_service(service_path)
396
397 execute("systemctl daemon-reload", dry_run=dry_run)
398 execute(f"systemctl enable {service_name}", dry_run=dry_run)
399 execute(f"systemctl start {service_name}", dry_run=dry_run)
400
401 if not dry_run:
402 try:
403 subprocess.run(
404 ["systemctl", "is-active", "--quiet", service_name],
405 check=True,
406 timeout=5
407 )
408 logging.info(t("service_active").format(service_name))
409 except subprocess.CalledProcessError:
410 logging.warning(t("service_inactive").format(service_name))
411 except subprocess.TimeoutExpired:
412 logging.warning(t("status_timeout").format(service_name))
413 else:
414 logging.info(t("dryrun_status_check").format(service_name))
415
416
Definition install_service.py:1

◆ remove_service()

install_service.remove_service (   service_name,
  dry_run = False 
)
Stops, disables and removes the given systemd service.
417def remove_service(service_name, dry_run=False):
418 r"""
419 Stops, disables and removes the given systemd service.
420 """
421 logging.warning(t("removing_service").format(service_name))
422 execute(f"systemctl stop {service_name}", dry_run=dry_run)
423 execute(f"systemctl disable {service_name}", dry_run=dry_run)
424
425 service_path = SERVICE_DIR / service_name
426 if not dry_run and service_path.exists():
427 try:
428 service_path.unlink()
429 logging.info(t("service_deleted").format(service_name))
430 except Exception as e:
431 logging.error(t("file_write_error").format(service_path, e))
432 else:
433 logging.warning(t("service_not_found").format(service_name))
434
435 execute("systemctl daemon-reload", dry_run=dry_run)
436
437

◆ remove_menu()

install_service.remove_menu (   dry_run = False)
Interactive menu to remove services.
438def remove_menu(dry_run=False):
439 r"""
440 Interactive menu to remove services.
441 """
442 while True:
443 services = sorted([
444 f for f in os.listdir(SERVICE_DIR)
445 if f.startswith('bw3_') and f.endswith('.service')
446 ])
447
448 if not services:
449 print(Fore.YELLOW + t("no_services") + Style.RESET_ALL)
450 return
451
452 print(Fore.CYAN + "\n" + t("available_services") + Style.RESET_ALL)
453 for i, s in enumerate(services):
454 print(f" [{i}] {s}")
455 print(" " + t("all"))
456 print(" " + t("exit"))
457
458 try:
459 auswahl = get_user_input(
460 t("remove_prompt"),
461 ['e', 'a'] + [str(i) for i in range(len(services))]
462 )
463 except RuntimeError:
464 logging.error(t("max_attempts_exceeded"))
465 break
466
467 if auswahl == 'e':
468 break
469 elif auswahl == 'a':
470 for s in services:
471 remove_service(s, dry_run=dry_run)
472 # directly continue to the next loop (updated list!)
473 continue
474 else:
475 remove_service(services[int(auswahl)], dry_run=dry_run)
476 # also directly continue to the next loop (updated list!)
477 continue
478
479

◆ init_language()

install_service.init_language ( )
Parses --lang/-l argument early to set language before other parsing.
480def init_language():
481 r"""
482 Parses --lang/-l argument early to set language before other parsing.
483 """
484 lang_parser = argparse.ArgumentParser(add_help=False)
485 lang_parser.add_argument(
486 '--lang', '-l',
487 choices=['de', 'en'],
488 default='de',
489 metavar='LANG',
490 help=TEXT["en"]["help_lang"]
491 )
492 lang_args, remaining_argv = lang_parser.parse_known_args()
493 set_lang(lang_args.lang)
494 return lang_parser, remaining_argv
495
496

◆ main()

install_service.main (   dry_run = False)
main program: install or remove service.
497def main(dry_run=False):
498 r"""
499 main program: install or remove service.
500 """
501 print(Fore.GREEN + Style.BRIGHT + t("script_title") + Style.RESET_ALL)
502 print(t('mode_dry') if dry_run else t('mode_live'))
503 print()
504
505 yaml_files = list_yaml_files()
506 if not yaml_files:
507 print(Fore.RED + t("no_yaml") + Style.RESET_ALL)
508 sys.exit(1)
509
510 print(Fore.GREEN + t("found_yaml").format(len(yaml_files)) + Style.RESET_ALL)
511 for f in yaml_files:
512 file_path = CONFIG_DIR / f
513 valid = test_yaml_file(file_path)
514 status = Fore.GREEN + "✅" if valid else Fore.RED + "❌"
515 print(f" - {f} {status}{Style.RESET_ALL}")
516
517 try:
518 action = get_user_input(t("action_prompt"), ['i', 'r', 'e'])
519 except RuntimeError:
520 logging.error(t("max_retries_exit"))
521 sys.exit(1)
522
523 if action == 'e':
524 sys.exit(0)
525 elif action == 'r':
526 remove_menu(dry_run=dry_run)
527 return
528
529 try:
530 edited = get_user_input(t("edited_prompt"), ['y', 'n'])
531 except RuntimeError:
532 logging.error(t("max_retries_exit"))
533 sys.exit(1)
534
535 if edited == 'n':
536 print(Fore.YELLOW + t("edit_abort") + Style.RESET_ALL)
537 sys.exit(0)
538
539 installed = 0
540 skipped = 0
541
542 for yaml_file in yaml_files:
543 file_path = CONFIG_DIR / yaml_file
544 if not test_yaml_file(file_path):
545 print(Fore.RED + t("skip_invalid_yaml").format(yaml_file) + Style.RESET_ALL)
546 skipped += 1
547 continue
548
549 try:
550 install = get_user_input(t("install_confirm").format(yaml_file), ['y', 'n'])
551 except RuntimeError:
552 logging.error(t("max_retries_skip"))
553 skipped += 1
554 continue
555
556 if install == 'y':
557 install_service(yaml_file, dry_run=dry_run)
558 installed += 1
559 else:
560 logging.info(t("install_skipped").format(yaml_file))
561 skipped += 1
562
563 print()
564 logging.info(t("install_done").format(installed, skipped))
565
566

Variable Documentation

◆ BASE_DIR

install_service.BASE_DIR = Path(__file__).resolve().parent

◆ BW_DIR

str install_service.BW_DIR = '/opt/boswatch3'

◆ SERVICE_DIR

install_service.SERVICE_DIR = Path('/etc/systemd/system')

◆ CONFIG_DIR

tuple install_service.CONFIG_DIR = (BASE_DIR / 'config').resolve()

◆ LOG_FILE

tuple install_service.LOG_FILE = (BASE_DIR / 'log' / 'install' / 'service_install.log').resolve()

◆ parent

install_service.parent

◆ exist_ok

install_service.exist_ok

◆ _lang

str install_service._lang = 'de'
protected

◆ TEXT

dict install_service.TEXT

◆ colorama_available

install_service.colorama_available

◆ Fore

install_service.Fore

◆ Style

install_service.Style = DummyStyle()

◆ lang_parser

install_service.lang_parser

◆ remaining_argv

install_service.remaining_argv

◆ parser

install_service.parser
Initial value:
1= argparse.ArgumentParser(
2 description=t("script_title"),
3 parents=[lang_parser]
4 )

◆ action

install_service.action

◆ help

install_service.help

◆ args

install_service.args = parser.parse_args(remaining_argv)

◆ verbose

install_service.verbose

◆ quiet

install_service.quiet

◆ dry_run

install_service.dry_run