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
4 changes: 2 additions & 2 deletions supervisor/addons/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
ATTR_JOURNALD,
ATTR_KERNEL_MODULES,
ATTR_LEGACY,
ATTR_LOCATON,
ATTR_LOCATION,
ATTR_MACHINE,
ATTR_MAP,
ATTR_NAME,
Expand Down Expand Up @@ -581,7 +581,7 @@ def map_volumes(self) -> dict[MappingType, FolderMapping]:
@property
def path_location(self) -> Path:
"""Return path to this add-on."""
return Path(self.data[ATTR_LOCATON])
return Path(self.data[ATTR_LOCATION])

@property
def path_icon(self) -> Path:
Expand Down
4 changes: 2 additions & 2 deletions supervisor/addons/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
ATTR_KERNEL_MODULES,
ATTR_LABELS,
ATTR_LEGACY,
ATTR_LOCATON,
ATTR_LOCATION,
ATTR_MACHINE,
ATTR_MAP,
ATTR_NAME,
Expand Down Expand Up @@ -483,7 +483,7 @@ def _migrate(config: dict[str, Any]):
_migrate_addon_config(),
_SCHEMA_ADDON_CONFIG.extend(
{
vol.Required(ATTR_LOCATON): str,
vol.Required(ATTR_LOCATION): str,
vol.Required(ATTR_REPOSITORY): str,
vol.Required(ATTR_TRANSLATIONS, default=dict): {
str: SCHEMA_ADDON_TRANSLATIONS
Expand Down
78 changes: 56 additions & 22 deletions supervisor/api/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import voluptuous as vol

from ..backups.backup import Backup
from ..backups.const import LOCATION_CLOUD_BACKUP
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT, days_until_stale
from ..const import (
ATTR_ADDONS,
Expand All @@ -22,10 +23,12 @@
ATTR_CONTENT,
ATTR_DATE,
ATTR_DAYS_UNTIL_STALE,
ATTR_FILENAME,
ATTR_FOLDERS,
ATTR_HOMEASSISTANT,
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
ATTR_LOCATON,
ATTR_JOB_ID,
ATTR_LOCATION,
ATTR_NAME,
ATTR_PASSWORD,
ATTR_PROTECTED,
Expand All @@ -36,20 +39,22 @@
ATTR_TIMEOUT,
ATTR_TYPE,
ATTR_VERSION,
REQUEST_FROM,
BusEvent,
CoreState,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..exceptions import APIError, APIForbidden
from ..jobs import JobSchedulerOptions
from ..mounts.const import MountUsage
from ..resolution.const import UnhealthyReason
from .const import ATTR_BACKGROUND, ATTR_JOB_ID, CONTENT_TYPE_TAR
from .const import ATTR_BACKGROUND, ATTR_LOCATIONS, CONTENT_TYPE_TAR
from .utils import api_process, api_validate

_LOGGER: logging.Logger = logging.getLogger(__name__)

RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
RE_BACKUP_FILENAME = re.compile(r"^[^\\\/]+\.tar$")

# Backwards compatible
# Remove: 2022.08
Expand All @@ -76,7 +81,7 @@
vol.Optional(ATTR_NAME): str,
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
vol.Optional(ATTR_LOCATON): vol.Maybe(str),
vol.Optional(ATTR_LOCATION): vol.Maybe(str),
vol.Optional(ATTR_HOMEASSISTANT_EXCLUDE_DATABASE): vol.Boolean(),
vol.Optional(ATTR_BACKGROUND, default=False): vol.Boolean(),
}
Expand All @@ -101,6 +106,12 @@
vol.Optional(ATTR_TIMEOUT): vol.All(int, vol.Range(min=1)),
}
)
SCHEMA_RELOAD = vol.Schema(
{
vol.Inclusive(ATTR_LOCATION, "file"): vol.Maybe(str),
vol.Inclusive(ATTR_FILENAME, "file"): vol.Match(RE_BACKUP_FILENAME),
}
)


class APIBackups(CoreSysAttributes):
Expand All @@ -122,7 +133,8 @@ def _list_backups(self):
ATTR_DATE: backup.date,
ATTR_TYPE: backup.sys_type,
ATTR_SIZE: backup.size,
ATTR_LOCATON: backup.location,
ATTR_LOCATION: backup.location,
ATTR_LOCATIONS: backup.locations,
ATTR_PROTECTED: backup.protected,
ATTR_COMPRESSED: backup.compressed,
ATTR_CONTENT: {
Expand All @@ -132,6 +144,7 @@ def _list_backups(self):
},
}
for backup in self.sys_backups.list_backups
if backup.location != LOCATION_CLOUD_BACKUP
]

@api_process
Expand Down Expand Up @@ -164,10 +177,13 @@ async def options(self, request):
self.sys_backups.save_data()

@api_process
async def reload(self, _):
async def reload(self, request: web.Request):
"""Reload backup list."""
await asyncio.shield(self.sys_backups.reload())
return True
body = await api_validate(SCHEMA_RELOAD, request)
self._validate_cloud_backup_location(request, body.get(ATTR_LOCATION))
backup = self._location_to_mount(body)

return await asyncio.shield(self.sys_backups.reload(**backup))

@api_process
async def backup_info(self, request):
Expand Down Expand Up @@ -195,7 +211,8 @@ async def backup_info(self, request):
ATTR_PROTECTED: backup.protected,
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
ATTR_HOMEASSISTANT: backup.homeassistant_version,
ATTR_LOCATON: backup.location,
ATTR_LOCATION: backup.location,
ATTR_LOCATIONS: backup.locations,
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: backup.repositories,
ATTR_FOLDERS: backup.folders,
Expand All @@ -204,17 +221,29 @@ async def backup_info(self, request):

def _location_to_mount(self, body: dict[str, Any]) -> dict[str, Any]:
"""Change location field to mount if necessary."""
if not body.get(ATTR_LOCATON):
if not body.get(ATTR_LOCATION) or body[ATTR_LOCATION] == LOCATION_CLOUD_BACKUP:
return body

body[ATTR_LOCATON] = self.sys_mounts.get(body[ATTR_LOCATON])
if body[ATTR_LOCATON].usage != MountUsage.BACKUP:
body[ATTR_LOCATION] = self.sys_mounts.get(body[ATTR_LOCATION])
if body[ATTR_LOCATION].usage != MountUsage.BACKUP:
raise APIError(
f"Mount {body[ATTR_LOCATON].name} is not used for backups, cannot backup to there"
f"Mount {body[ATTR_LOCATION].name} is not used for backups, cannot backup to there"
)

return body

def _validate_cloud_backup_location(
self, request: web.Request, location: str | None
) -> None:
"""Cloud backup location is only available to Home Assistant."""
if (
location == LOCATION_CLOUD_BACKUP
and request.get(REQUEST_FROM) != self.sys_homeassistant
):
raise APIForbidden(
f"Location {LOCATION_CLOUD_BACKUP} is only available for Home Assistant"
)

async def _background_backup_task(
self, backup_method: Callable, *args, **kwargs
) -> tuple[asyncio.Task, str]:
Expand Down Expand Up @@ -246,9 +275,10 @@ async def release_on_freeze(new_state: CoreState):
self.sys_bus.remove_listener(listener)

@api_process
async def backup_full(self, request):
async def backup_full(self, request: web.Request):
"""Create full backup."""
body = await api_validate(SCHEMA_BACKUP_FULL, request)
self._validate_cloud_backup_location(request, body.get(ATTR_LOCATION))
background = body.pop(ATTR_BACKGROUND)
backup_task, job_id = await self._background_backup_task(
self.sys_backups.do_backup_full, **self._location_to_mount(body)
Expand All @@ -266,9 +296,10 @@ async def backup_full(self, request):
)

@api_process
async def backup_partial(self, request):
async def backup_partial(self, request: web.Request):
"""Create a partial backup."""
body = await api_validate(SCHEMA_BACKUP_PARTIAL, request)
self._validate_cloud_backup_location(request, body.get(ATTR_LOCATION))
background = body.pop(ATTR_BACKGROUND)
backup_task, job_id = await self._background_backup_task(
self.sys_backups.do_backup_partial, **self._location_to_mount(body)
Expand All @@ -286,9 +317,10 @@ async def backup_partial(self, request):
)

@api_process
async def restore_full(self, request):
async def restore_full(self, request: web.Request):
"""Full restore of a backup."""
backup = self._extract_slug(request)
self._validate_cloud_backup_location(request, backup.location)
body = await api_validate(SCHEMA_RESTORE_FULL, request)
background = body.pop(ATTR_BACKGROUND)
restore_task, job_id = await self._background_backup_task(
Expand All @@ -303,9 +335,10 @@ async def restore_full(self, request):
)

@api_process
async def restore_partial(self, request):
async def restore_partial(self, request: web.Request):
"""Partial restore a backup."""
backup = self._extract_slug(request)
self._validate_cloud_backup_location(request, backup.location)
body = await api_validate(SCHEMA_RESTORE_PARTIAL, request)
background = body.pop(ATTR_BACKGROUND)
restore_task, job_id = await self._background_backup_task(
Expand All @@ -320,23 +353,24 @@ async def restore_partial(self, request):
)

@api_process
async def freeze(self, request):
async def freeze(self, request: web.Request):
"""Initiate manual freeze for external backup."""
body = await api_validate(SCHEMA_FREEZE, request)
await asyncio.shield(self.sys_backups.freeze_all(**body))

@api_process
async def thaw(self, request):
async def thaw(self, request: web.Request):
"""Begin thaw after manual freeze."""
await self.sys_backups.thaw_all()

@api_process
async def remove(self, request):
async def remove(self, request: web.Request):
"""Remove a backup."""
backup = self._extract_slug(request)
self._validate_cloud_backup_location(request, backup.location)
return self.sys_backups.remove(backup)

async def download(self, request):
async def download(self, request: web.Request):
"""Download a backup file."""
backup = self._extract_slug(request)

Expand All @@ -349,7 +383,7 @@ async def download(self, request):
return response

@api_process
async def upload(self, request):
async def upload(self, request: web.Request):
"""Upload a backup file."""
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp_dir:
tar_file = Path(temp_dir, "backup.tar")
Expand Down
3 changes: 2 additions & 1 deletion supervisor/api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
ATTR_IDENTIFIERS = "identifiers"
ATTR_IS_ACTIVE = "is_active"
ATTR_IS_OWNER = "is_owner"
ATTR_JOB_ID = "job_id"
ATTR_JOBS = "jobs"
ATTR_LLMNR = "llmnr"
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
ATTR_LOCAL_ONLY = "local_only"
ATTR_LOCATIONS = "locations"
ATTR_MDNS = "mdns"
ATTR_MODEL = "model"
ATTR_MOUNTS = "mounts"
Expand All @@ -68,6 +68,7 @@
ATTR_USAGE = "usage"
ATTR_USE_NTP = "use_ntp"
ATTR_USERS = "users"
ATTR_USER_PATH = "user_path"
ATTR_VENDOR = "vendor"
ATTR_VIRTUALIZATION = "virtualization"

Expand Down
8 changes: 6 additions & 2 deletions supervisor/api/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ..mounts.const import ATTR_DEFAULT_BACKUP_MOUNT, MountUsage
from ..mounts.mount import Mount
from ..mounts.validate import SCHEMA_MOUNT_CONFIG
from .const import ATTR_MOUNTS
from .const import ATTR_MOUNTS, ATTR_USER_PATH
from .utils import api_process, api_validate

SCHEMA_OPTIONS = vol.Schema(
Expand All @@ -32,7 +32,11 @@ async def info(self, request: web.Request) -> dict[str, Any]:
if self.sys_mounts.default_backup_mount
else None,
ATTR_MOUNTS: [
mount.to_dict() | {ATTR_STATE: mount.state}
mount.to_dict()
| {
ATTR_STATE: mount.state,
ATTR_USER_PATH: mount.container_where.as_posix(),
}
for mount in self.sys_mounts.mounts
],
}
Expand Down
Loading