Skip to content

Commit

Permalink
add backend support for safe mode
Browse files Browse the repository at this point in the history
  • Loading branch information
roflcoopter committed Mar 15, 2024
1 parent c9fcea2 commit 727fc01
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 7 deletions.
8 changes: 8 additions & 0 deletions viseron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sqlalchemy import insert

from viseron.components import (
CriticalComponentsConfigStore,
get_component,
setup_component,
setup_components,
Expand Down Expand Up @@ -174,6 +175,11 @@ def setup_viseron() -> Viseron:
setup_domains(vis)
vis.setup()

if vis.safe_mode:
LOGGER.warning("Viseron is running in safe mode")
else:
vis.critical_components_config_store.save(config)

end = timer()
LOGGER.info("Viseron initialized in %.1f seconds", end - start)
return vis
Expand Down Expand Up @@ -209,6 +215,8 @@ def __init__(self) -> None:

self.storage: Storage | None = None

self.critical_components_config_store = CriticalComponentsConfigStore(self)
self.safe_mode = False
self.exit_code = 0

def register_signal_handler(self, viseron_signal, callback):
Expand Down
69 changes: 69 additions & 0 deletions viseron/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from viseron.events import EventData
from viseron.exceptions import ComponentNotReady, DomainNotReady
from viseron.helpers.storage import Storage

if TYPE_CHECKING:
from viseron import Viseron
Expand Down Expand Up @@ -79,6 +80,9 @@ class EventDomanSetupStatusData(EventData, DomainToSetup):
CORE_COMPONENTS = {"data_stream"}
# Default components are always loaded even if they are not present in config
DEFAULT_COMPONENTS = {"webserver", "storage"}
# Critical components are required for Viseron to function properly
# If one of these components fail to load, Viseron will activate safe mode
CRITICAL_COMPONENTS = LOGGING_COMPONENTS | CORE_COMPONENTS | DEFAULT_COMPONENTS

DOMAIN_SETUP_LOCK = threading.Lock()

Expand Down Expand Up @@ -676,6 +680,62 @@ def setup_domains(vis: Viseron) -> None:
future.result()


STORAGE_KEY = "critical_components_config"


class CriticalComponentsConfigStore:
"""Storage for critical components config.
Used to store the last known good config for critical components.
"""

def __init__(self, vis) -> None:
self._vis = vis
self._store = Storage(vis, STORAGE_KEY)

def load(self) -> dict[str, Any]:
"""Load config."""
return self._store.load()

def save(self, config: dict[str, Any]) -> None:
"""Save config.
Extracts only the critical components from the config.
"""
critical_components_config = {
component: config[component]
for component in CRITICAL_COMPONENTS
if component in config
}
self._store.save(critical_components_config)


def activate_safe_mode(vis: Viseron) -> None:
"""Activate safe mode."""
vis.safe_mode = True
# Get the last known good config
critical_components_config = vis.critical_components_config_store.load()
if not critical_components_config:
LOGGER.warning(
"No last known good config for critical components found, "
"running with default config"
)
critical_components_config = {}

loaded_set = set(vis.data[LOADED])
# Setup logger first
for component in LOGGING_COMPONENTS - loaded_set:
setup_component(vis, get_component(vis, component, critical_components_config))

# Setup core components
for component in CORE_COMPONENTS - loaded_set:
setup_component(vis, get_component(vis, component, critical_components_config))

# Setup default components
for component in DEFAULT_COMPONENTS - loaded_set:
setup_component(vis, get_component(vis, component, critical_components_config))


def setup_components(vis: Viseron, config: dict[str, Any]) -> None:
"""Set up configured components."""
components_in_config = {key.split(" ")[0] for key in config}
Expand All @@ -691,6 +751,15 @@ def setup_components(vis: Viseron, config: dict[str, Any]) -> None:
for component in DEFAULT_COMPONENTS:
setup_component(vis, get_component(vis, component, config))

if vis.safe_mode:
return

# If any of the critical components failed to load, we activate safe mode
if any(component in vis.data[FAILED] for component in CRITICAL_COMPONENTS):
LOGGER.warning("Critical components failed to load. Activating safe mode")
activate_safe_mode(vis)
return

# Setup components in parallel
setup_threads = []
for component in (
Expand Down
4 changes: 2 additions & 2 deletions viseron/components/webserver/websocket_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def handle_message(self, message) -> None:
if await self.run_in_executor(self.handle_auth, message):
LOGGER.debug("Authentication successful.")
self._waiting_for_auth = False
await self.async_send_message(auth_ok_message())
await self.async_send_message(auth_ok_message(self.vis))
return
LOGGER.warning("Authentication failed.")
await self.async_send_message(
Expand Down Expand Up @@ -199,7 +199,7 @@ def open(self, *_args: str, **_kwargs: str) -> None:
IOLoop.current().spawn_callback(self.send_message, auth_required_message())
else:
IOLoop.current().spawn_callback(
self.send_message, auth_not_required_message()
self.send_message, auth_not_required_message(self.vis)
)
self._waiting_for_auth = False
IOLoop.current().spawn_callback(self._write_message)
Expand Down
25 changes: 20 additions & 5 deletions viseron/components/webserver/websocket_api/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from viseron.helpers.json import JSONEncoder

if TYPE_CHECKING:
from viseron import Event
from viseron import Event, Viseron

LOGGER = logging.getLogger(__name__)

Expand All @@ -37,6 +37,13 @@
)


def system_information(vis: Viseron) -> dict[str, Any]:
"""Return system information."""
return {
"safe_mode": vis.safe_mode,
}


def message_to_json(message: dict[str, Any]) -> str:
"""Serialize a websocket message to json."""
try:
Expand All @@ -52,19 +59,27 @@ def message_to_json(message: dict[str, Any]) -> str:
)


def auth_ok_message() -> dict[str, str]:
def auth_ok_message(vis: Viseron) -> dict[str, Any]:
"""Return an auth_ok message."""
return {"type": TYPE_AUTH_OK}
return {
"type": TYPE_AUTH_OK,
"message": "Authentication successful.",
"system_information": system_information(vis),
}


def auth_required_message() -> dict[str, str]:
"""Return an auth_required message."""
return {"type": TYPE_AUTH_REQUIRED, "message": "Authentication required."}


def auth_not_required_message() -> dict[str, str]:
def auth_not_required_message(vis: Viseron) -> dict[str, Any]:
"""Return an auth_not_required message."""
return {"type": TYPE_AUTH_NOT_REQUIRED, "message": "Authentication not required."}
return {
"type": TYPE_AUTH_NOT_REQUIRED,
"message": "Authentication not required.",
"system_information": system_information(vis),
}


def auth_failed_message(message: str) -> dict[str, str]:
Expand Down

0 comments on commit 727fc01

Please sign in to comment.