Compare commits

...

6 Commits

Author SHA1 Message Date
Pascal Vizeli
ece40008c7 Logging in local timezone (#2971)
* Logging in local timezone

* fix convert

* Apply suggestions from code review

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-06-21 13:42:39 +02:00
Pascal Vizeli
0177b38ded Support HOT/COLD snapshots for Add-ons (#2943)
* Support HOT/COLD snapshots for Add-ons

* Apply suggestions from code review

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Add warning

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-06-21 09:07:54 +02:00
Stefan Agner
16f2f63081 Allow downgrade to "hassos" from "haos" (#2970)
* Add "os_name" as possible URL variable

* Replace "os_name" for downgrades to OS versions before 6.0
2021-06-19 18:50:54 +02:00
Pascal Vizeli
5f376c2a27 Using images data from version file (#2969)
* Using images data from version file

* fix tests
2021-06-18 22:59:11 +02:00
dependabot[bot]
90a6f109ee Bump gitpython from 3.1.17 to 3.1.18 (#2967)
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.17 to 3.1.18.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.17...3.1.18)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-18 13:32:53 +02:00
dependabot[bot]
e6fd0ef5dc Bump actions/upload-artifact from 2.2.3 to 2.2.4 (#2965)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-17 09:49:28 +02:00
14 changed files with 182 additions and 39 deletions

View File

@@ -394,7 +394,7 @@ jobs:
-o console_output_style=count \
tests
- name: Upload coverage artifact
uses: actions/upload-artifact@v2.2.3
uses: actions/upload-artifact@v2.2.4
with:
name: coverage-${{ matrix.python-version }}
path: .coverage

View File

@@ -11,7 +11,7 @@ cpe==1.2.1
cryptography==3.4.6
debugpy==1.3.0
docker==5.0.0
gitpython==3.1.17
gitpython==3.1.18
jinja2==3.0.1
pulsectl==21.5.18
pyudev==0.22.0

View File

@@ -65,6 +65,7 @@ from ..utils import check_port
from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file
from ..utils.tar import atomic_contents_add, secure_path
from .const import SnapshotAddonMode
from .model import AddonModel, Data
from .options import AddonOptions
from .utils import remove_data
@@ -695,6 +696,8 @@ class Addon(AddonModel):
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
"""Snapshot state of an add-on."""
is_running = await self.is_running()
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
temp_path = Path(temp)
@@ -744,8 +747,15 @@ class Addon(AddonModel):
arcname="data",
)
if self.snapshot_pre is not None:
if (
is_running
and self.snapshot_mode == SnapshotAddonMode.HOT
and self.snapshot_pre is not None
):
await self._snapshot_command(self.snapshot_pre)
elif is_running and self.snapshot_mode == SnapshotAddonMode.COLD:
_LOGGER.info("Shutdown add-on %s for cold snapshot", self.slug)
await self.instance.stop()
try:
_LOGGER.info("Building snapshot for add-on %s", self.slug)
@@ -754,8 +764,15 @@ class Addon(AddonModel):
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
raise AddonsError() from err
finally:
if self.snapshot_post is not None:
if (
is_running
and self.snapshot_mode == SnapshotAddonMode.HOT
and self.snapshot_post is not None
):
await self._snapshot_command(self.snapshot_post)
elif is_running and self.snapshot_mode is SnapshotAddonMode.COLD:
_LOGGER.info("Starting add-on %s again", self.slug)
await self.start()
_LOGGER.info("Finish snapshot for addon %s", self.slug)

View File

@@ -0,0 +1,12 @@
"""Add-on static data."""
from enum import Enum
class SnapshotAddonMode(str, Enum):
"""Snapshot mode of an Add-on."""
HOT = "hot"
COLD = "cold"
ATTR_SNAPSHOT = "snapshot"

View File

@@ -5,6 +5,8 @@ from typing import Any, Awaitable, Dict, List, Optional
from awesomeversion import AwesomeVersion, AwesomeVersionException
from supervisor.addons.const import SnapshotAddonMode
from ..const import (
ATTR_ADVANCED,
ATTR_APPARMOR,
@@ -76,6 +78,7 @@ from ..const import (
)
from ..coresys import CoreSys, CoreSysAttributes
from ..docker.const import Capabilities
from .const import ATTR_SNAPSHOT
from .options import AddonOptions, UiOptions
from .validate import RE_SERVICE, RE_VOLUME
@@ -370,6 +373,11 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return post-snapshot command."""
return self.data.get(ATTR_SNAPSHOT_POST)
@property
def snapshot_mode(self) -> SnapshotAddonMode:
"""Return if snapshot is hot/cold."""
return self.data[ATTR_SNAPSHOT]
@property
def default_init(self) -> bool:
"""Return True if the add-on have no own init."""

View File

@@ -7,6 +7,8 @@ import uuid
import voluptuous as vol
from supervisor.addons.const import SnapshotAddonMode
from ..const import (
ARCH_ALL,
ATTR_ACCESS_TOKEN,
@@ -107,6 +109,7 @@ from ..validate import (
uuid_match,
version_tag,
)
from .const import ATTR_SNAPSHOT
from .options import RE_SCHEMA_ELEMENT
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -161,6 +164,14 @@ def _warn_addon_config(config: Dict[str, Any]):
name,
)
if config.get(ATTR_SNAPSHOT, SnapshotAddonMode.HOT) == SnapshotAddonMode.COLD and (
config.get(ATTR_SNAPSHOT_POST) or config.get(ATTR_SNAPSHOT_PRE)
):
_LOGGER.warning(
"Add-on which only support COLD backups trying to use post/pre commands. Please report this to the maintainer of %s",
name,
)
return config
@@ -284,6 +295,9 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
vol.Optional(ATTR_SNAPSHOT_PRE): str,
vol.Optional(ATTR_SNAPSHOT_POST): str,
vol.Optional(ATTR_SNAPSHOT, default=SnapshotAddonMode.HOT): vol.Coerce(
SnapshotAddonMode
),
vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
vol.Schema(

View File

@@ -88,23 +88,39 @@ async def initialize_coresys() -> CoreSys:
setup_diagnostics(coresys)
# bootstrap config
initialize_system_data(coresys)
initialize_system(coresys)
# Set Machine/Host ID
if MACHINE_ID.exists():
coresys.machine_id = MACHINE_ID.read_text().strip()
# Check if ENV is in development mode
if coresys.dev:
_LOGGER.warning("Environment variable 'SUPERVISOR_DEV' is set")
coresys.config.logging = LogLevel.DEBUG
coresys.config.debug = True
coresys.updater.channel = UpdateChannel.DEV
coresys.security.content_trust = False
else:
coresys.config.modify_log_level()
# Convert datetime
logging.Formatter.converter = lambda *args: coresys.now().timetuple()
# Set machine type
if os.environ.get(ENV_SUPERVISOR_MACHINE):
coresys.machine = os.environ[ENV_SUPERVISOR_MACHINE]
elif os.environ.get(ENV_HOMEASSISTANT_REPOSITORY):
coresys.machine = os.environ[ENV_HOMEASSISTANT_REPOSITORY][14:-14]
_LOGGER.warning(
"Missing SUPERVISOR_MACHINE environment variable. Fallback to deprecated extraction!"
)
_LOGGER.info("Seting up coresys for machine: %s", coresys.machine)
return coresys
def initialize_system_data(coresys: CoreSys) -> None:
def initialize_system(coresys: CoreSys) -> None:
"""Set up the default configuration and create folders."""
config = coresys.config
@@ -179,17 +195,6 @@ def initialize_system_data(coresys: CoreSys) -> None:
_LOGGER.debug("Creating Supervisor media folder at '%s'", config.path_media)
config.path_media.mkdir()
# Update log level
coresys.config.modify_log_level()
# Check if ENV is in development mode
if coresys.dev:
_LOGGER.warning("Environment variables 'SUPERVISOR_DEV' is set")
coresys.config.logging = LogLevel.DEBUG
coresys.config.debug = True
coresys.updater.channel = UpdateChannel.DEV
coresys.security.content_trust = False
def migrate_system_env(coresys: CoreSys) -> None:
"""Cleanup some stuff after update."""

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from datetime import datetime
import logging
import os
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar
@@ -12,6 +13,7 @@ import sentry_sdk
from .config import CoreConfig
from .const import ENV_SUPERVISOR_DEV
from .docker import DockerAPI
from .utils.dt import UTC, get_time_zone
if TYPE_CHECKING:
from .addons import AddonManager
@@ -466,6 +468,24 @@ class CoreSys:
raise RuntimeError("Machine-ID type already set!")
self._machine_id = value
def now(self) -> datetime:
"""Return now in local timezone."""
return datetime.now(get_time_zone(self.timezone) or UTC)
def run_in_executor(
self, funct: Callable[..., T], *args: Any
) -> Coroutine[Any, Any, T]:
"""Add an job to the executor pool."""
return self.loop.run_in_executor(None, funct, *args)
def create_task(self, coroutine: Coroutine) -> asyncio.Task:
"""Create an async task."""
return self.loop.create_task(coroutine)
def capture_exception(self, err: Exception) -> None:
"""Capture a exception."""
sentry_sdk.capture_exception(err)
class CoreSysAttributes:
"""Inherit basic CoreSysAttributes."""
@@ -622,16 +642,20 @@ class CoreSysAttributes:
"""Return Job manager object."""
return self.coresys.jobs
def now(self) -> datetime:
"""Return now in local timezone."""
return self.coresys.now()
def sys_run_in_executor(
self, funct: Callable[..., T], *args: Any
) -> Coroutine[Any, Any, T]:
"""Add an job to the executor pool."""
return self.sys_loop.run_in_executor(None, funct, *args)
return self.coresys.run_in_executor(funct, *args)
def sys_create_task(self, coroutine: Coroutine) -> asyncio.Task:
"""Create an async task."""
return self.sys_loop.create_task(coroutine)
return self.coresys.create_task(coroutine)
def sys_capture_exception(self, err: Exception) -> None:
"""Capture a exception."""
sentry_sdk.capture_exception(err)
self.coresys.capture_exception(err)

View File

@@ -26,6 +26,7 @@ class HassOS(CoreSysAttributes):
self._available: bool = False
self._version: Optional[AwesomeVersion] = None
self._board: Optional[str] = None
self._os_name: Optional[str] = None
@property
def available(self) -> bool:
@@ -55,18 +56,30 @@ class HassOS(CoreSysAttributes):
"""Return board name."""
return self._board
@property
def os_name(self) -> Optional[str]:
"""Return OS name."""
return self._os_name
def _get_download_url(self, version: AwesomeVersion) -> str:
raw_url = self.sys_updater.ota_url
if raw_url is None:
raise HassOSUpdateError("Don't have an URL for OTA updates!", _LOGGER.error)
update_board = self.board
update_os_name = self.os_name
# OS version 6 and later renamed intel-nuc to generic-x86-64...
if update_board == "intel-nuc" and version >= 6.0:
update_board = "generic-x86-64"
url = raw_url.format(version=str(version), board=update_board)
# The OS name used to be hassos before renaming to haos...
if version < 6.0:
update_os_name = "hassos"
url = raw_url.format(
version=str(version), board=update_board, os_name=update_os_name
)
return url
async def _download_raucb(self, url: str, raucb: Path) -> None:
@@ -115,13 +128,13 @@ class HassOS(CoreSysAttributes):
except NotImplementedError:
_LOGGER.info("No Home Assistant Operating System found")
return
else:
self._available = True
self.sys_host.supported_features.cache_clear()
# Store meta data
self._available = True
self.sys_host.supported_features.cache_clear()
self._version = AwesomeVersion(cpe.get_version()[0])
self._board = cpe.get_target_hardware()[0]
self._os_name = cpe.get_product()[0]
await self.sys_dbus.rauc.update()

View File

@@ -256,13 +256,13 @@ class Updater(FileConfiguration, CoreSysAttributes):
self._data[ATTR_MULTICAST] = AwesomeVersion(data["multicast"])
# Update images for that versions
self._data[ATTR_IMAGE][ATTR_HOMEASSISTANT] = data["image"]["core"]
self._data[ATTR_IMAGE][ATTR_SUPERVISOR] = data["image"]["supervisor"]
self._data[ATTR_IMAGE][ATTR_AUDIO] = data["image"]["audio"]
self._data[ATTR_IMAGE][ATTR_CLI] = data["image"]["cli"]
self._data[ATTR_IMAGE][ATTR_DNS] = data["image"]["dns"]
self._data[ATTR_IMAGE][ATTR_OBSERVER] = data["image"]["observer"]
self._data[ATTR_IMAGE][ATTR_MULTICAST] = data["image"]["multicast"]
self._data[ATTR_IMAGE][ATTR_HOMEASSISTANT] = data["images"]["core"]
self._data[ATTR_IMAGE][ATTR_SUPERVISOR] = data["images"]["supervisor"]
self._data[ATTR_IMAGE][ATTR_AUDIO] = data["images"]["audio"]
self._data[ATTR_IMAGE][ATTR_CLI] = data["images"]["cli"]
self._data[ATTR_IMAGE][ATTR_DNS] = data["images"]["dns"]
self._data[ATTR_IMAGE][ATTR_OBSERVER] = data["images"]["observer"]
self._data[ATTR_IMAGE][ATTR_MULTICAST] = data["images"]["multicast"]
except KeyError as err:
raise UpdaterError(

View File

@@ -126,7 +126,7 @@ async def network_manager(dbus) -> NetworkManager:
@pytest.fixture
async def coresys(loop, docker, network_manager, aiohttp_client) -> CoreSys:
"""Create a CoreSys Mock."""
with patch("supervisor.bootstrap.initialize_system_data"), patch(
with patch("supervisor.bootstrap.initialize_system"), patch(
"supervisor.bootstrap.setup_diagnostics"
):
coresys_obj = await initialize_coresys()

View File

@@ -1,6 +1,9 @@
"""Testing handling with CoreState."""
from datetime import timedelta
from supervisor.coresys import CoreSys
from supervisor.utils.dt import utcnow
async def test_timezone(run_dir, coresys: CoreSys):
@@ -15,3 +18,14 @@ async def test_timezone(run_dir, coresys: CoreSys):
coresys.config.timezone = "Europe/Zurich"
assert coresys.timezone == "Europe/Zurich"
def test_now(coresys: CoreSys):
"""Test datetime now with local time."""
coresys.config.timezone = "Europe/Zurich"
zurich = coresys.now()
utc = utcnow()
assert zurich != utc
assert zurich - utc <= timedelta(hours=2)

View File

@@ -20,3 +20,39 @@ async def test_ota_url_generic_x86_64_rename(coresys: CoreSys) -> None:
url = coresys.updater.ota_url.format(version=str(version6), board="generic-x86-64")
assert coresys.hassos._get_download_url(version6) == url
def test_ota_url_os_name(coresys: CoreSys) -> None:
"""Test download URL generated with os_name."""
board = "generic-x86-64"
os_name = "haos"
versionstr = "6.0"
url = "https://github.com/home-assistant/operating-system/releases/download/{version}/{os_name}_{board}-{version}.raucb"
url_formatted = url.format(version=versionstr, board=board, os_name=os_name)
coresys.hassos._board = board
coresys.hassos._os_name = os_name
coresys.updater._data = {"ota": url}
url = coresys.hassos._get_download_url(AwesomeVersion(versionstr))
assert url == url_formatted
def test_ota_url_os_name_rel_5_downgrade(coresys: CoreSys) -> None:
"""Test download URL generated with os_name."""
board = "generic-x86-64"
versionstr = "5.9"
# On downgrade below 6.0 we need to use hassos as os_name.
url = "https://github.com/home-assistant/operating-system/releases/download/{version}/{os_name}_{board}-{version}.raucb"
url_formatted = url.format(version=versionstr, board=board, os_name="hassos")
coresys.hassos._board = board
coresys.hassos._os_name = "haos"
coresys.updater._data = {"ota": url}
url = coresys.hassos._get_download_url(AwesomeVersion(versionstr))
assert url == url_formatted

View File

@@ -26,25 +26,25 @@ async def test_fetch_versions(coresys: CoreSys) -> None:
assert coresys.updater.version_multicast == data["multicast"]
assert coresys.updater.version_observer == data["observer"]
assert coresys.updater.image_homeassistant == data["image"]["core"].format(
assert coresys.updater.image_homeassistant == data["images"]["core"].format(
machine=coresys.machine
)
assert coresys.updater.image_supervisor == data["image"]["supervisor"].format(
assert coresys.updater.image_supervisor == data["images"]["supervisor"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_cli == data["image"]["cli"].format(
assert coresys.updater.image_cli == data["images"]["cli"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_audio == data["image"]["audio"].format(
assert coresys.updater.image_audio == data["images"]["audio"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_dns == data["image"]["dns"].format(
assert coresys.updater.image_dns == data["images"]["dns"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_observer == data["image"]["observer"].format(
assert coresys.updater.image_observer == data["images"]["observer"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_multicast == data["image"]["multicast"].format(
assert coresys.updater.image_multicast == data["images"]["multicast"].format(
arch=coresys.arch.supervisor
)