Refacture version handling with AwesomeVersion (#2392)

* Refacture version handling with AwesomeVersion

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* next

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* next

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* next

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* v20.12.3

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* next

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* next

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* fix exception

* fix schema

* cleanup plugins

* fix tests

* fix attach

* fix TypeError

* fix issue with compairing

* make lint happy

* Update supervisor/homeassistant/__init__.py

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

* Update tests/test_validate.py

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

* Update supervisor/docker/__init__.py

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

* Update supervisor/supervisor.py

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

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
Pascal Vizeli 2020-12-31 17:09:33 +01:00 committed by GitHub
parent 32fb550969
commit 97c35de49a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 331 additions and 378 deletions

View File

@ -2,6 +2,7 @@ aiohttp==3.7.3
async_timeout==3.0.1
atomicwrites==1.4.0
attrs==20.3.0
awesomeversion==20.12.4
brotli==1.0.9
cchardet==2.1.7
colorlog==4.6.2
@ -17,4 +18,4 @@ pytz==2020.5
pyudev==0.22.0
ruamel.yaml==0.15.100
sentry-sdk==0.19.5
voluptuous==0.12.1
voluptuous==0.12.1

View File

@ -101,7 +101,7 @@ class Addon(AddonModel):
async def load(self) -> None:
"""Async initialize of object."""
with suppress(DockerError):
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
# Evaluate state
if await self.instance.is_running():

View File

@ -4,6 +4,8 @@ from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Dict
from awesomeversion import AwesomeVersion
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
from ..coresys import CoreSys, CoreSysAttributes
from ..utils.json import JsonConfig
@ -46,11 +48,11 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
"""Return additional Docker build arguments."""
return self._data[ATTR_ARGS]
def get_docker_args(self, version):
def get_docker_args(self, version: AwesomeVersion):
"""Create a dict with Docker build arguments."""
args = {
"path": str(self.addon.path_location),
"tag": f"{self.addon.image}:{version}",
"tag": f"{self.addon.image}:{version!s}",
"pull": True,
"forcerm": True,
"squash": self.squash,

View File

@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Awaitable, Dict, List, Optional
from packaging import version as pkg_version
from awesomeversion import AwesomeVersion, AwesomeVersionException
import voluptuous as vol
from ..const import (
@ -183,12 +183,12 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_REPOSITORY]
@property
def latest_version(self) -> str:
def latest_version(self) -> AwesomeVersion:
"""Return latest version of add-on."""
return self.data[ATTR_VERSION]
@property
def version(self) -> Optional[str]:
def version(self) -> AwesomeVersion:
"""Return version of add-on."""
return self.data[ATTR_VERSION]
@ -554,15 +554,10 @@ class AddonModel(CoreSysAttributes, ABC):
return False
# Home Assistant
version = config.get(ATTR_HOMEASSISTANT)
if version is None or self.sys_homeassistant.version is None:
return True
version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
try:
return pkg_version.parse(
self.sys_homeassistant.version
) >= pkg_version.parse(version)
except pkg_version.InvalidVersion:
return self.sys_homeassistant.version >= version
except (AwesomeVersionException, TypeError):
return True
def _image(self, config) -> str:

View File

@ -93,6 +93,7 @@ from ..const import (
from ..coresys import CoreSys
from ..discovery.validate import valid_discovery_service
from ..validate import (
docker_image,
docker_ports,
docker_ports_description,
network_port,
@ -144,7 +145,6 @@ _SCHEMA_LENGTH_PARTS = (
"p_max",
)
RE_DOCKER_IMAGE = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
RE_DOCKER_IMAGE_BUILD = re.compile(
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
)
@ -186,7 +186,7 @@ def _simple_startup(value) -> str:
SCHEMA_ADDON_CONFIG = vol.Schema(
{
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.All(version_tag, str),
vol.Required(ATTR_VERSION): version_tag,
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
@ -213,7 +213,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): vol.Coerce(str),
vol.Optional(ATTR_PANEL_TITLE): vol.Coerce(str),
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(version_tag),
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
@ -267,7 +267,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
),
False,
),
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
vol.Optional(ATTR_IMAGE): docker_image,
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
vol.Coerce(int), vol.Range(min=10, max=300)
),
@ -294,8 +294,8 @@ SCHEMA_BUILD_CONFIG = vol.Schema(
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema(
{
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
vol.Required(ATTR_VERSION): version_tag,
vol.Optional(ATTR_IMAGE): docker_image,
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
vol.Optional(ATTR_ACCESS_TOKEN): token,
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): vol.Coerce(

View File

@ -19,6 +19,7 @@ from ..const import (
)
from ..exceptions import APIError, APIForbidden, DockerAPIError, HassioError
from ..utils import check_exception_chain, get_message_from_exception_chain
from ..utils.json import JSONEncoder
from ..utils.log_format import format_message
@ -112,12 +113,16 @@ def api_return_error(
JSON_MESSAGE: message or "Unknown error, see supervisor",
},
status=400,
dumps=lambda x: json.dumps(x, cls=JSONEncoder),
)
def api_return_ok(data: Optional[Dict[str, Any]] = None) -> web.Response:
"""Return an API ok answer."""
return web.json_response({JSON_RESULT: RESULT_OK, JSON_DATA: data or {}})
return web.json_response(
{JSON_RESULT: RESULT_OK, JSON_DATA: data or {}},
dumps=lambda x: json.dumps(x, cls=JSONEncoder),
)
async def api_validate(

View File

@ -5,6 +5,8 @@ import os
from pathlib import Path, PurePath
from typing import List, Optional
from awesomeversion import AwesomeVersion
from .const import (
ATTR_ADDONS_CUSTOM_LIST,
ATTR_DEBUG,
@ -64,12 +66,12 @@ class CoreConfig(JsonConfig):
self._data[ATTR_TIMEZONE] = value
@property
def version(self) -> str:
def version(self) -> AwesomeVersion:
"""Return config version."""
return self._data[ATTR_VERSION]
@version.setter
def version(self, value: str) -> None:
def version(self, value: AwesomeVersion) -> None:
"""Set config version."""
self._data[ATTR_VERSION] = value

View File

@ -14,6 +14,7 @@ from .exceptions import (
HomeAssistantError,
SupervisorUpdateError,
)
from .homeassistant.core import LANDINGPAGE
from .resolution.const import ContextType, IssueType, UnhealthyReason
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -221,7 +222,7 @@ class Core(CoreSysAttributes):
await self.sys_tasks.load()
# If landingpage / run upgrade in background
if self.sys_homeassistant.version == "landingpage":
if self.sys_homeassistant.version == LANDINGPAGE:
self.sys_create_task(self.sys_homeassistant.core.install())
# Start observe the host Hardware

View File

@ -6,8 +6,8 @@ from pathlib import Path
from typing import Any, Dict, Optional
import attr
from awesomeversion import AwesomeVersion
import docker
from packaging import version as pkg_version
import requests
from ..const import (
@ -40,22 +40,21 @@ class CommandReturn:
class DockerInfo:
"""Return docker information."""
version: str = attr.ib()
version: AwesomeVersion = attr.ib()
storage: str = attr.ib()
logging: str = attr.ib()
@staticmethod
def new(data: Dict[str, Any]):
"""Create a object from docker info."""
return DockerInfo(data["ServerVersion"], data["Driver"], data["LoggingDriver"])
return DockerInfo(
AwesomeVersion(data["ServerVersion"]), data["Driver"], data["LoggingDriver"]
)
@property
def supported_version(self) -> bool:
"""Return true, if docker version is supported."""
version_local = pkg_version.parse(self.version)
version_min = pkg_version.parse(MIN_SUPPORTED_DOCKER)
return version_local >= version_min
return self.version >= MIN_SUPPORTED_DOCKER
@property
def inside_lxc(self) -> bool:
@ -114,7 +113,7 @@ class DockerAPI:
def run(
self,
image: str,
version: str = "latest",
tag: str = "latest",
dns: bool = True,
ipv4: Optional[IPv4Address] = None,
**kwargs: Any,
@ -140,7 +139,7 @@ class DockerAPI:
# Create container
try:
container = self.docker.containers.create(
f"{image}:{version}", use_config_proxy=False, **kwargs
f"{image}:{tag}", use_config_proxy=False, **kwargs
)
except docker.errors.NotFound as err:
_LOGGER.error("Image %s not exists for %s", image, name)
@ -195,7 +194,7 @@ class DockerAPI:
def run_command(
self,
image: str,
version: str = "latest",
tag: str = "latest",
command: Optional[str] = None,
**kwargs: Any,
) -> CommandReturn:
@ -210,7 +209,7 @@ class DockerAPI:
container = None
try:
container = self.docker.containers.run(
f"{image}:{version}",
f"{image}:{tag}",
command=command,
network=self.network.name,
use_config_proxy=False,

View File

@ -8,6 +8,7 @@ import os
from pathlib import Path
from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Union
from awesomeversion import AwesomeVersion
import docker
import requests
@ -72,7 +73,7 @@ class DockerAddon(DockerInterface):
return self.addon.timeout
@property
def version(self) -> str:
def version(self) -> AwesomeVersion:
"""Return version of Docker image."""
if self.addon.legacy:
return self.addon.version
@ -355,7 +356,7 @@ class DockerAddon(DockerInterface):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.addon.version,
tag=self.addon.version.string,
name=self.name,
hostname=self.addon.hostname,
detach=True,
@ -390,37 +391,37 @@ class DockerAddon(DockerInterface):
self.sys_capture_exception(err)
def _install(
self, tag: str, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
) -> None:
"""Pull Docker image or build it.
Need run inside executor.
"""
if self.addon.need_build:
self._build(tag)
self._build(version)
else:
super()._install(tag, image, latest)
super()._install(version, image, latest)
def _build(self, tag: str) -> None:
def _build(self, version: AwesomeVersion) -> None:
"""Build a Docker container.
Need run inside executor.
"""
build_env = AddonBuild(self.coresys, self.addon)
_LOGGER.info("Starting build for %s:%s", self.image, tag)
_LOGGER.info("Starting build for %s:%s", self.image, version)
try:
image, log = self.sys_docker.images.build(
use_config_proxy=False, **build_env.get_docker_args(tag)
use_config_proxy=False, **build_env.get_docker_args(version)
)
_LOGGER.debug("Build %s:%s done: %s", self.image, tag, log)
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
# Update meta data
self._meta = image.attrs
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't build %s:%s: %s", self.image, tag, err)
_LOGGER.error("Can't build %s:%s: %s", self.image, version, err)
if hasattr(err, "build_log"):
log = "\n".join(
[
@ -432,7 +433,7 @@ class DockerAddon(DockerInterface):
_LOGGER.error("Build log: \n%s", log)
raise DockerError() from err
_LOGGER.info("Build %s:%s done", self.image, tag)
_LOGGER.info("Build %s:%s done", self.image, version)
@process_lock
def export_image(self, tar_file: Path) -> Awaitable[None]:

View File

@ -59,7 +59,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_plugins.audio.version,
tag=self.sys_plugins.audio.version.string,
init=False,
ipv4=self.sys_docker.network.audio,
name=self.name,

View File

@ -39,7 +39,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
self.image,
entrypoint=["/init"],
command=["/bin/bash", "-c", "sleep infinity"],
version=self.sys_plugins.cli.version,
tag=self.sys_plugins.cli.version.string,
init=False,
ipv4=self.sys_docker.network.cli,
name=self.name,

View File

@ -37,7 +37,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_plugins.dns.version,
tag=self.sys_plugins.dns.version.string,
init=False,
dns=False,
ipv4=self.sys_docker.network.dns,

View File

@ -107,7 +107,7 @@ class DockerHomeAssistant(DockerInterface):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_homeassistant.version,
tag=self.sys_homeassistant.version.string,
name=self.name,
hostname=self.name,
detach=True,

View File

@ -5,8 +5,9 @@ import logging
import re
from typing import Any, Awaitable, Dict, List, Optional
from awesomeversion import AwesomeVersion
from awesomeversion.strategy import AwesomeVersionStrategy
import docker
from packaging import version as pkg_version
import requests
from . import CommandReturn
@ -76,9 +77,11 @@ class DockerInterface(CoreSysAttributes):
return None
@property
def version(self) -> Optional[str]:
def version(self) -> Optional[AwesomeVersion]:
"""Return version of Docker image."""
return self.meta_labels.get(LABEL_VERSION)
if LABEL_VERSION not in self.meta_labels:
return None
return AwesomeVersion(self.meta_labels[LABEL_VERSION])
@property
def arch(self) -> Optional[str]:
@ -90,11 +93,6 @@ class DockerInterface(CoreSysAttributes):
"""Return True if a task is in progress."""
return self.lock.locked()
@process_lock
def install(self, tag: str, image: Optional[str] = None, latest: bool = False):
"""Pull docker image."""
return self.sys_run_in_executor(self._install, tag, image, latest)
def _get_credentials(self, image: str) -> dict:
"""Return a dictionay with credentials for docker login."""
registry = None
@ -135,8 +133,15 @@ class DockerInterface(CoreSysAttributes):
self.sys_docker.docker.login(**credentials)
@process_lock
def install(
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
):
"""Pull docker image."""
return self.sys_run_in_executor(self._install, version, image, latest)
def _install(
self, tag: str, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
) -> None:
"""Pull Docker image.
@ -144,18 +149,20 @@ class DockerInterface(CoreSysAttributes):
"""
image = image or self.image
_LOGGER.info("Downloading docker image %s with tag %s.", image, tag)
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
try:
if self.sys_docker.config.registries:
# Try login if we have defined credentials
self._docker_login(image)
docker_image = self.sys_docker.images.pull(f"{image}:{tag}")
docker_image = self.sys_docker.images.pull(f"{image}:{version!s}")
if latest:
_LOGGER.info("Tagging image %s with version %s as latest", image, tag)
_LOGGER.info(
"Tagging image %s with version %s as latest", image, version
)
docker_image.tag(image, tag="latest")
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", image, tag, err)
_LOGGER.error("Can't install %s:%s -> %s.", image, version, err)
if err.status_code == 429:
self.sys_resolution.create_issue(
IssueType.DOCKER_RATELIMIT,
@ -168,7 +175,7 @@ class DockerInterface(CoreSysAttributes):
)
raise DockerError() from err
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Unknown error with %s:%s -> %s", image, tag, err)
_LOGGER.error("Unknown error with %s:%s -> %s", image, version, err)
self.sys_capture_exception(err)
raise DockerError() from err
else:
@ -184,7 +191,7 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor.
"""
with suppress(docker.errors.DockerException, requests.RequestException):
self.sys_docker.images.get(f"{self.image}:{self.version}")
self.sys_docker.images.get(f"{self.image}:{self.version!s}")
return True
return False
@ -212,11 +219,11 @@ class DockerInterface(CoreSysAttributes):
return docker_container.status == "running"
@process_lock
def attach(self, tag: str):
def attach(self, version: AwesomeVersion):
"""Attach to running Docker container."""
return self.sys_run_in_executor(self._attach, tag)
return self.sys_run_in_executor(self._attach, version)
def _attach(self, tag: str) -> None:
def _attach(self, version: AwesomeVersion) -> None:
"""Attach to running docker container.
Need run inside executor.
@ -226,7 +233,9 @@ class DockerInterface(CoreSysAttributes):
with suppress(docker.errors.DockerException, requests.RequestException):
if not self._meta and self.image:
self._meta = self.sys_docker.images.get(f"{self.image}:{tag}").attrs
self._meta = self.sys_docker.images.get(
f"{self.image}:{version!s}"
).attrs
# Successfull?
if not self._meta:
@ -317,7 +326,7 @@ class DockerInterface(CoreSysAttributes):
with suppress(docker.errors.ImageNotFound):
self.sys_docker.images.remove(
image=f"{self.image}:{self.version}", force=True
image=f"{self.image}:{self.version!s}", force=True
)
except (docker.errors.DockerException, requests.RequestException) as err:
@ -328,13 +337,13 @@ class DockerInterface(CoreSysAttributes):
@process_lock
def update(
self, tag: str, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
) -> Awaitable[None]:
"""Update a Docker image."""
return self.sys_run_in_executor(self._update, tag, image, latest)
return self.sys_run_in_executor(self._update, version, image, latest)
def _update(
self, tag: str, image: Optional[str] = None, latest: bool = False
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
) -> None:
"""Update a docker image.
@ -343,11 +352,11 @@ class DockerInterface(CoreSysAttributes):
image = image or self.image
_LOGGER.info(
"Updating image %s:%s to %s:%s", self.image, self.version, image, tag
"Updating image %s:%s to %s:%s", self.image, self.version, image, version
)
# Update docker image
self._install(tag, image=image, latest=latest)
self._install(version, image=image, latest=latest)
# Stop container & cleanup
with suppress(DockerError):
@ -388,7 +397,7 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor.
"""
try:
origin = self.sys_docker.images.get(f"{self.image}:{self.version}")
origin = self.sys_docker.images.get(f"{self.image}:{self.version!s}")
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.warning("Can't find %s for cleanup", self.image)
raise DockerError() from err
@ -504,23 +513,21 @@ class DockerInterface(CoreSysAttributes):
# Check return value
return int(docker_container.attrs["State"]["ExitCode"]) != 0
def get_latest_version(self) -> Awaitable[str]:
def get_latest_version(self) -> Awaitable[AwesomeVersion]:
"""Return latest version of local image."""
return self.sys_run_in_executor(self._get_latest_version)
def _get_latest_version(self) -> str:
def _get_latest_version(self) -> AwesomeVersion:
"""Return latest version of local image.
Need run inside executor.
"""
available_version: List[str] = []
available_version: List[AwesomeVersion] = []
try:
for image in self.sys_docker.images.list(self.image):
for tag in image.tags:
version = tag.partition(":")[2]
try:
pkg_version.parse(version)
except (TypeError, pkg_version.InvalidVersion):
version = AwesomeVersion(tag.partition(":")[2])
if version.strategy == AwesomeVersionStrategy.UNKNOWN:
continue
available_version.append(version)
@ -537,5 +544,5 @@ class DockerInterface(CoreSysAttributes):
_LOGGER.info("Found %s versions: %s", self.image, available_version)
# Sort version and return latest version
available_version.sort(key=pkg_version.parse, reverse=True)
available_version.sort(reverse=True)
return available_version[0]

View File

@ -37,7 +37,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_plugins.multicast.version,
tag=self.sys_plugins.multicast.version.string,
init=False,
name=self.name,
hostname=self.name.replace("_", "-"),

View File

@ -38,7 +38,7 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_plugins.observer.version,
tag=self.sys_plugins.observer.version.string,
init=False,
ipv4=self.sys_docker.network.observer,
name=self.name,

View File

@ -4,6 +4,7 @@ import logging
import os
from typing import Awaitable
from awesomeversion.awesomeversion import AwesomeVersion
import docker
import requests
@ -32,7 +33,7 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
"""Return True if the container run with Privileged."""
return self.meta_host.get("Privileged", False)
def _attach(self, tag: str) -> None:
def _attach(self, version: AwesomeVersion) -> None:
"""Attach to running docker container.
Need run inside executor.
@ -73,24 +74,24 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
try:
docker_container = self.sys_docker.containers.get(self.name)
docker_container.image.tag(self.image, tag=self.version)
docker_container.image.tag(self.image, tag=self.version.string)
docker_container.image.tag(self.image, tag="latest")
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't retag Supervisor version: %s", err)
raise DockerError() from err
def update_start_tag(self, image: str, version: str) -> Awaitable[None]:
def update_start_tag(self, image: str, version: AwesomeVersion) -> Awaitable[None]:
"""Update start tag to new version."""
return self.sys_run_in_executor(self._update_start_tag, image, version)
def _update_start_tag(self, image: str, version: str) -> None:
def _update_start_tag(self, image: str, version: AwesomeVersion) -> None:
"""Update start tag to new version.
Need run inside executor.
"""
try:
docker_container = self.sys_docker.containers.get(self.name)
docker_image = self.sys_docker.images.get(f"{image}:{version}")
docker_image = self.sys_docker.images.get(f"{image}:{version!s}")
# Find start tag
for tag in docker_container.image.tags:

View File

@ -5,8 +5,8 @@ from pathlib import Path
from typing import Awaitable, Optional
import aiohttp
from awesomeversion import AwesomeVersion, AwesomeVersionException
from cpe import CPE
from packaging.version import parse as pkg_parse
from .coresys import CoreSys, CoreSysAttributes
from .dbus.rauc import RaucState
@ -24,7 +24,7 @@ class HassOS(CoreSysAttributes):
self.coresys: CoreSys = coresys
self.lock: asyncio.Lock = asyncio.Lock()
self._available: bool = False
self._version: Optional[str] = None
self._version: Optional[AwesomeVersion] = None
self._board: Optional[str] = None
@property
@ -33,12 +33,12 @@ class HassOS(CoreSysAttributes):
return self._available
@property
def version(self) -> Optional[str]:
def version(self) -> Optional[AwesomeVersion]:
"""Return version of HassOS."""
return self._version
@property
def latest_version(self) -> str:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return version of HassOS."""
return self.sys_updater.version_hassos
@ -46,8 +46,8 @@ class HassOS(CoreSysAttributes):
def need_update(self) -> bool:
"""Return true if a HassOS update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return self.version < self.latest_version
except (AwesomeVersionException, TypeError):
return False
@property
@ -61,16 +61,16 @@ class HassOS(CoreSysAttributes):
_LOGGER.error("No Home Assistant Operating System available")
raise HassOSNotSupportedError()
async def _download_raucb(self, version: str) -> Path:
async def _download_raucb(self, version: AwesomeVersion) -> Path:
"""Download rauc bundle (OTA) from github."""
raw_url = self.sys_updater.ota_url
if raw_url is None:
_LOGGER.error("Don't have an URL for OTA updates!")
raise HassOSNotSupportedError()
url = raw_url.format(version=version, board=self.board)
url = raw_url.format(version=version.string, board=self.board)
_LOGGER.info("Fetch OTA update from %s", url)
raucb = Path(self.sys_config.path_tmp, f"hassos-{version}.raucb")
raucb = Path(self.sys_config.path_tmp, f"hassos-{version.string}.raucb")
try:
timeout = aiohttp.ClientTimeout(total=60 * 60, connect=180)
async with self.sys_websession.get(url, timeout=timeout) as request:
@ -113,7 +113,7 @@ class HassOS(CoreSysAttributes):
self.sys_host.supported_features.cache_clear()
# Store meta data
self._version = cpe.get_version()[0]
self._version = AwesomeVersion(cpe.get_version()[0])
self._board = cpe.get_target_hardware()[0]
await self.sys_dbus.rauc.update()
@ -135,7 +135,7 @@ class HassOS(CoreSysAttributes):
return self.sys_host.services.restart("hassos-config.service")
@process_lock
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update HassOS system."""
version = version or self.latest_version

View File

@ -7,6 +7,8 @@ import shutil
from typing import Optional
from uuid import UUID
from awesomeversion import AwesomeVersion, AwesomeVersionException
from ..const import (
ATTR_ACCESS_TOKEN,
ATTR_AUDIO_INPUT,
@ -126,7 +128,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._data[ATTR_WAIT_BOOT] = value
@property
def latest_version(self) -> str:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return last available version of Home Assistant."""
return self.sys_updater.version_homeassistant
@ -143,12 +145,12 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._data[ATTR_IMAGE] = value
@property
def version(self) -> Optional[str]:
def version(self) -> Optional[AwesomeVersion]:
"""Return version of local version."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
def version(self, value: AwesomeVersion) -> None:
"""Set installed version."""
self._data[ATTR_VERSION] = value
@ -220,9 +222,10 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
@property
def need_update(self) -> bool:
"""Return true if a Home Assistant update is available."""
if not self.latest_version:
try:
return self.version != self.latest_version
except (AwesomeVersionException, TypeError):
return False
return self.version != self.latest_version
async def load(self) -> None:
"""Prepare Home Assistant object."""

View File

@ -10,7 +10,7 @@ import time
from typing import Awaitable, Optional
import attr
from packaging import version as pkg_version
from awesomeversion import AwesomeVersion, AwesomeVersionException
from ..coresys import CoreSys, CoreSysAttributes
from ..docker.homeassistant import DockerHomeAssistant
@ -30,7 +30,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
LANDINGPAGE: str = "landingpage"
LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage")
@attr.s(frozen=True)
@ -65,7 +65,7 @@ class HomeAssistantCore(CoreSysAttributes):
await self.instance.get_latest_version()
)
await self.instance.attach(tag=self.sys_homeassistant.version)
await self.instance.attach(version=self.sys_homeassistant.version)
except DockerError:
_LOGGER.info(
"No Home Assistant Docker image %s found.", self.sys_homeassistant.image
@ -122,11 +122,11 @@ class HomeAssistantCore(CoreSysAttributes):
if not self.sys_homeassistant.latest_version:
await self.sys_updater.reload()
tag = self.sys_homeassistant.latest_version
if tag:
if self.sys_homeassistant.latest_version:
try:
await self.instance.update(
tag, image=self.sys_updater.image_homeassistant
self.sys_homeassistant.latest_version,
image=self.sys_updater.image_homeassistant,
)
break
except DockerError:
@ -162,7 +162,7 @@ class HomeAssistantCore(CoreSysAttributes):
],
on_condition=HomeAssistantJobError,
)
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update HomeAssistant version."""
version = version or self.sys_homeassistant.latest_version
old_image = self.sys_homeassistant.image
@ -175,7 +175,7 @@ class HomeAssistantCore(CoreSysAttributes):
return
# process an update
async def _update(to_version: str) -> None:
async def _update(to_version: AwesomeVersion) -> None:
"""Run Home Assistant update."""
_LOGGER.info("Updating Home Assistant to version %s", to_version)
try:
@ -348,7 +348,7 @@ class HomeAssistantCore(CoreSysAttributes):
_LOGGER.info("Home Assistant config is valid")
return ConfigResult(True, log)
async def _block_till_run(self, version: str) -> None:
async def _block_till_run(self, version: AwesomeVersion) -> None:
"""Block until Home-Assistant is booting up or startup timeout."""
# Skip landingpage
if version == LANDINGPAGE:
@ -358,9 +358,9 @@ class HomeAssistantCore(CoreSysAttributes):
# Manage timeouts
timeout: bool = True
start_time = time.monotonic()
with suppress(pkg_version.InvalidVersion):
with suppress(AwesomeVersionException):
# Version provide early stage UI
if pkg_version.parse(version) >= pkg_version.parse("0.112.0"):
if version >= AwesomeVersion("0.112.0"):
_LOGGER.debug("Disable startup timeouts - early UI")
timeout = False

View File

@ -5,11 +5,11 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError
from ..resolution.const import ContextType, IssueType, SuggestionType
from .audio import Audio
from .cli import HaCli
from .dns import CoreDNS
from .multicast import Multicast
from .observer import Observer
from .audio import PluginAudio
from .cli import PluginCli
from .dns import PluginDns
from .multicast import PluginMulticast
from .observer import PluginObserver
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -21,34 +21,34 @@ class PluginManager(CoreSysAttributes):
"""Initialize plugin manager."""
self.coresys: CoreSys = coresys
self._cli: HaCli = HaCli(coresys)
self._dns: CoreDNS = CoreDNS(coresys)
self._audio: Audio = Audio(coresys)
self._observer: Observer = Observer(coresys)
self._multicast: Multicast = Multicast(coresys)
self._cli: PluginCli = PluginCli(coresys)
self._dns: PluginDns = PluginDns(coresys)
self._audio: PluginAudio = PluginAudio(coresys)
self._observer: PluginObserver = PluginObserver(coresys)
self._multicast: PluginMulticast = PluginMulticast(coresys)
@property
def cli(self) -> HaCli:
def cli(self) -> PluginCli:
"""Return cli handler."""
return self._cli
@property
def dns(self) -> CoreDNS:
def dns(self) -> PluginDns:
"""Return dns handler."""
return self._dns
@property
def audio(self) -> Audio:
def audio(self) -> PluginAudio:
"""Return audio handler."""
return self._audio
@property
def observer(self) -> Observer:
def observer(self) -> PluginObserver:
"""Return observer handler."""
return self._observer
@property
def multicast(self) -> Multicast:
def multicast(self) -> PluginMulticast:
"""Return multicast handler."""
return self._multicast

View File

@ -9,15 +9,14 @@ from pathlib import Path, PurePath
import shutil
from typing import Awaitable, Optional
from awesomeversion import AwesomeVersion
import jinja2
from packaging.version import parse as pkg_parse
from ..const import ATTR_IMAGE, ATTR_VERSION
from ..coresys import CoreSys, CoreSysAttributes
from ..coresys import CoreSys
from ..docker.audio import DockerAudio
from ..docker.stats import DockerStats
from ..exceptions import AudioError, AudioUpdateError, DockerError
from ..utils.json import JsonConfig
from .base import PluginBase
from .const import FILE_HASSIO_AUDIO
from .validate import SCHEMA_AUDIO_CONFIG
@ -27,14 +26,13 @@ PULSE_CLIENT_TMPL: Path = Path(__file__).parents[1].joinpath("data/pulse-client.
ASOUND_TMPL: Path = Path(__file__).parents[1].joinpath("data/asound.tmpl")
class Audio(JsonConfig, CoreSysAttributes):
class PluginAudio(PluginBase):
"""Home Assistant core object for handle audio."""
slug: str = "audio"
def __init__(self, coresys: CoreSys):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_AUDIO, SCHEMA_AUDIO_CONFIG)
self.slug = "audio"
self.coresys: CoreSys = coresys
self.instance: DockerAudio = DockerAudio(coresys)
self.client_template: Optional[jinja2.Template] = None
@ -50,29 +48,7 @@ class Audio(JsonConfig, CoreSysAttributes):
return self.sys_config.path_extern_audio.joinpath("asound")
@property
def version(self) -> Optional[str]:
"""Return current version of Audio."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set current version of Audio."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of Audio."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-audio"
@image.setter
def image(self, value: str) -> None:
"""Return current image of Audio."""
self._data[ATTR_IMAGE] = value
@property
def latest_version(self) -> Optional[str]:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return latest version of Audio."""
return self.sys_updater.version_audio
@ -81,14 +57,6 @@ class Audio(JsonConfig, CoreSysAttributes):
"""Return True if a task is in progress."""
return self.instance.in_progress
@property
def need_update(self) -> bool:
"""Return True if an update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return False
async def load(self) -> None:
"""Load Audio setup."""
# Initialize Client Template
@ -103,7 +71,7 @@ class Audio(JsonConfig, CoreSysAttributes):
if not self.version:
self.version = await self.instance.get_latest_version()
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image)

View File

@ -0,0 +1,66 @@
"""Supervisor plugins base class."""
from abc import ABC, abstractmethod, abstractproperty
from typing import Optional
from awesomeversion import AwesomeVersion, AwesomeVersionException
from ..const import ATTR_IMAGE, ATTR_VERSION
from ..coresys import CoreSysAttributes
from ..utils.json import JsonConfig
class PluginBase(ABC, JsonConfig, CoreSysAttributes):
"""Base class for plugins."""
slug: str = ""
@property
def version(self) -> Optional[AwesomeVersion]:
"""Return current version of the plugin."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: AwesomeVersion) -> None:
"""Set current version of the plugin."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of plugin."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-{self.slug}"
@image.setter
def image(self, value: str) -> None:
"""Return current image of the plugin."""
self._data[ATTR_IMAGE] = value
@property
@abstractproperty
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return latest version of the plugin."""
@property
def need_update(self) -> bool:
"""Return True if an update is available."""
try:
return self.version < self.latest_version
except (AwesomeVersionException, TypeError):
return False
@abstractmethod
async def load(self) -> None:
"""Load system plugin."""
@abstractmethod
async def install(self) -> None:
"""Install system plugin."""
@abstractmethod
async def update(self, version: Optional[str] = None) -> None:
"""Update system plugin."""
@abstractmethod
async def repair(self) -> None:
"""Repair system plugin."""

View File

@ -8,66 +8,35 @@ import logging
import secrets
from typing import Awaitable, Optional
from packaging.version import parse as pkg_parse
from awesomeversion import AwesomeVersion
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_VERSION
from ..coresys import CoreSys, CoreSysAttributes
from ..const import ATTR_ACCESS_TOKEN
from ..coresys import CoreSys
from ..docker.cli import DockerCli
from ..docker.stats import DockerStats
from ..exceptions import CliError, CliUpdateError, DockerError
from ..utils.json import JsonConfig
from .base import PluginBase
from .const import FILE_HASSIO_CLI
from .validate import SCHEMA_CLI_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__)
class HaCli(CoreSysAttributes, JsonConfig):
class PluginCli(PluginBase):
"""HA cli interface inside supervisor."""
slug: str = "cli"
def __init__(self, coresys: CoreSys):
"""Initialize cli handler."""
super().__init__(FILE_HASSIO_CLI, SCHEMA_CLI_CONFIG)
self.slug = "cli"
self.coresys: CoreSys = coresys
self.instance: DockerCli = DockerCli(coresys)
@property
def version(self) -> Optional[str]:
"""Return version of cli."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set current version of cli."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of cli."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
@image.setter
def image(self, value: str) -> None:
"""Return current image of cli."""
self._data[ATTR_IMAGE] = value
@property
def latest_version(self) -> str:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return version of latest cli."""
return self.sys_updater.version_cli
@property
def need_update(self) -> bool:
"""Return true if a cli update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return False
@property
def supervisor_token(self) -> str:
"""Return an access token for the Supervisor API."""
@ -86,7 +55,7 @@ class HaCli(CoreSysAttributes, JsonConfig):
if not self.version:
self.version = await self.instance.get_latest_version()
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.info("No cli plugin Docker image %s found.", self.instance.image)
@ -126,7 +95,7 @@ class HaCli(CoreSysAttributes, JsonConfig):
self.image = self.sys_updater.image_cli
self.save_data()
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update local HA cli."""
version = version or self.latest_version
old_image = self.image

View File

@ -10,18 +10,19 @@ from pathlib import Path
from typing import Awaitable, List, Optional
import attr
from awesomeversion import AwesomeVersion
import jinja2
from packaging.version import parse as pkg_parse
import voluptuous as vol
from ..const import ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, LogLevel
from ..coresys import CoreSys, CoreSysAttributes
from ..const import ATTR_SERVERS, DNS_SUFFIX, LogLevel
from ..coresys import CoreSys
from ..docker.dns import DockerDNS
from ..docker.stats import DockerStats
from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerError, JsonFileError
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.json import JsonConfig, write_json_file
from ..utils.json import write_json_file
from ..validate import dns_url
from .base import PluginBase
from .const import FILE_HASSIO_DNS
from .validate import SCHEMA_DNS_CONFIG
@ -40,14 +41,13 @@ class HostEntry:
names: List[str] = attr.ib()
class CoreDNS(JsonConfig, CoreSysAttributes):
class PluginDns(PluginBase):
"""Home Assistant core object for handle it."""
slug: str = "dns"
def __init__(self, coresys: CoreSys):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG)
self.slug = "dns"
self.coresys: CoreSys = coresys
self.instance: DockerDNS = DockerDNS(coresys)
self.resolv_template: Optional[jinja2.Template] = None
@ -89,29 +89,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
self._data[ATTR_SERVERS] = value
@property
def version(self) -> Optional[str]:
"""Return current version of DNS."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Return current version of DNS."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of DNS."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-dns"
@image.setter
def image(self, value: str) -> None:
"""Return current image of DNS."""
self._data[ATTR_IMAGE] = value
@property
def latest_version(self) -> Optional[str]:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return latest version of CoreDNS."""
return self.sys_updater.version_dns
@ -120,14 +98,6 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
"""Return True if a task is in progress."""
return self.instance.in_progress
@property
def need_update(self) -> bool:
"""Return True if an update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return False
async def load(self) -> None:
"""Load DNS setup."""
# Initialize CoreDNS Template
@ -147,7 +117,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
if not self.version:
self.version = await self.instance.get_latest_version()
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.info(
"No CoreDNS plugin Docker image %s found.", self.instance.image
@ -194,7 +164,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
# Init Hosts
self.write_hosts()
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update CoreDNS plugin."""
version = version or self.latest_version
old_image = self.image

View File

@ -7,55 +7,31 @@ from contextlib import suppress
import logging
from typing import Awaitable, Optional
from packaging.version import parse as pkg_parse
from awesomeversion import AwesomeVersion
from ..const import ATTR_IMAGE, ATTR_VERSION
from ..coresys import CoreSys, CoreSysAttributes
from ..coresys import CoreSys
from ..docker.multicast import DockerMulticast
from ..docker.stats import DockerStats
from ..exceptions import DockerError, MulticastError, MulticastUpdateError
from ..utils.json import JsonConfig
from .base import PluginBase
from .const import FILE_HASSIO_MULTICAST
from .validate import SCHEMA_MULTICAST_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Multicast(JsonConfig, CoreSysAttributes):
class PluginMulticast(PluginBase):
"""Home Assistant core object for handle it."""
slug: str = "multicast"
def __init__(self, coresys: CoreSys):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_MULTICAST, SCHEMA_MULTICAST_CONFIG)
self.slug = "multicast"
self.coresys: CoreSys = coresys
self.instance: DockerMulticast = DockerMulticast(coresys)
@property
def version(self) -> Optional[str]:
"""Return current version of Multicast."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Return current version of Multicast."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of Multicast."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-multicast"
@image.setter
def image(self, value: str) -> None:
"""Return current image of Multicast."""
self._data[ATTR_IMAGE] = value
@property
def latest_version(self) -> Optional[str]:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return latest version of Multicast."""
return self.sys_updater.version_multicast
@ -64,14 +40,6 @@ class Multicast(JsonConfig, CoreSysAttributes):
"""Return True if a task is in progress."""
return self.instance.in_progress
@property
def need_update(self) -> bool:
"""Return True if an update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return False
async def load(self) -> None:
"""Load multicast setup."""
# Check Multicast state
@ -80,7 +48,7 @@ class Multicast(JsonConfig, CoreSysAttributes):
if not self.version:
self.version = await self.instance.get_latest_version()
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.info(
"No Multicast plugin Docker image %s found.", self.instance.image
@ -121,7 +89,7 @@ class Multicast(JsonConfig, CoreSysAttributes):
self.image = self.sys_updater.image_multicast
self.save_data()
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update Multicast plugin."""
version = version or self.latest_version
old_image = self.image

View File

@ -9,66 +9,35 @@ import secrets
from typing import Awaitable, Optional
import aiohttp
from packaging.version import parse as pkg_parse
from awesomeversion import AwesomeVersion
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_VERSION
from ..coresys import CoreSys, CoreSysAttributes
from ..const import ATTR_ACCESS_TOKEN
from ..coresys import CoreSys
from ..docker.observer import DockerObserver
from ..docker.stats import DockerStats
from ..exceptions import DockerError, ObserverError, ObserverUpdateError
from ..utils.json import JsonConfig
from .base import PluginBase
from .const import FILE_HASSIO_OBSERVER
from .validate import SCHEMA_OBSERVER_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Observer(CoreSysAttributes, JsonConfig):
class PluginObserver(PluginBase):
"""Supervisor observer instance."""
slug: str = "observer"
def __init__(self, coresys: CoreSys):
"""Initialize observer handler."""
super().__init__(FILE_HASSIO_OBSERVER, SCHEMA_OBSERVER_CONFIG)
self.slug = "observer"
self.coresys: CoreSys = coresys
self.instance: DockerObserver = DockerObserver(coresys)
@property
def version(self) -> Optional[str]:
"""Return version of observer."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set current version of observer."""
self._data[ATTR_VERSION] = value
@property
def image(self) -> str:
"""Return current image of observer."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return f"homeassistant/{self.sys_arch.supervisor}-hassio-observer"
@image.setter
def image(self, value: str) -> None:
"""Return current image of observer."""
self._data[ATTR_IMAGE] = value
@property
def latest_version(self) -> str:
def latest_version(self) -> Optional[AwesomeVersion]:
"""Return version of latest observer."""
return self.sys_updater.version_observer
@property
def need_update(self) -> bool:
"""Return true if a observer update is available."""
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return False
@property
def supervisor_token(self) -> str:
"""Return an access token for the Observer API."""
@ -87,7 +56,7 @@ class Observer(CoreSysAttributes, JsonConfig):
if not self.version:
self.version = await self.instance.get_latest_version()
await self.instance.attach(tag=self.version)
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.info(
"No observer plugin Docker image %s found.", self.instance.image
@ -128,7 +97,7 @@ class Observer(CoreSysAttributes, JsonConfig):
self.image = self.sys_updater.image_observer
self.save_data()
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update local HA observer."""
version = version or self.latest_version
old_image = self.image

View File

@ -9,7 +9,7 @@ from typing import Awaitable, Optional
import aiohttp
from aiohttp.client_exceptions import ClientError
from packaging.version import parse as pkg_parse
from awesomeversion import AwesomeVersion, AwesomeVersionException
from supervisor.jobs.decorator import Job, JobCondition
@ -41,7 +41,7 @@ class Supervisor(CoreSysAttributes):
async def load(self) -> None:
"""Prepare Home Assistant object."""
try:
await self.instance.attach(tag="latest")
await self.instance.attach(version=self.version)
except DockerError:
_LOGGER.critical("Can't setup Supervisor Docker container!")
@ -65,17 +65,17 @@ class Supervisor(CoreSysAttributes):
return False
try:
return pkg_parse(self.version) < pkg_parse(self.latest_version)
except (TypeError, ValueError):
return self.version < self.latest_version
except (AwesomeVersionException, TypeError):
return False
@property
def version(self) -> str:
def version(self) -> AwesomeVersion:
"""Return version of running Home Assistant."""
return SUPERVISOR_VERSION
return AwesomeVersion(SUPERVISOR_VERSION)
@property
def latest_version(self) -> str:
def latest_version(self) -> AwesomeVersion:
"""Return last available version of Home Assistant."""
return self.sys_updater.version_supervisor
@ -117,7 +117,7 @@ class Supervisor(CoreSysAttributes):
_LOGGER.error("Can't update AppArmor profile!")
raise SupervisorError() from err
async def update(self, version: Optional[str] = None) -> None:
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update Home Assistant version."""
version = version or self.latest_version

View File

@ -7,6 +7,7 @@ import logging
from typing import Optional
import aiohttp
from awesomeversion import AwesomeVersion
from .const import (
ATTR_AUDIO,
@ -53,42 +54,42 @@ class Updater(JsonConfig, CoreSysAttributes):
await self.fetch_data()
@property
def version_homeassistant(self) -> Optional[str]:
def version_homeassistant(self) -> Optional[AwesomeVersion]:
"""Return latest version of Home Assistant."""
return self._data.get(ATTR_HOMEASSISTANT)
@property
def version_supervisor(self) -> Optional[str]:
def version_supervisor(self) -> Optional[AwesomeVersion]:
"""Return latest version of Supervisor."""
return self._data.get(ATTR_SUPERVISOR)
@property
def version_hassos(self) -> Optional[str]:
def version_hassos(self) -> Optional[AwesomeVersion]:
"""Return latest version of HassOS."""
return self._data.get(ATTR_HASSOS)
@property
def version_cli(self) -> Optional[str]:
def version_cli(self) -> Optional[AwesomeVersion]:
"""Return latest version of CLI."""
return self._data.get(ATTR_CLI)
@property
def version_dns(self) -> Optional[str]:
def version_dns(self) -> Optional[AwesomeVersion]:
"""Return latest version of DNS."""
return self._data.get(ATTR_DNS)
@property
def version_audio(self) -> Optional[str]:
def version_audio(self) -> Optional[AwesomeVersion]:
"""Return latest version of Audio."""
return self._data.get(ATTR_AUDIO)
@property
def version_observer(self) -> Optional[str]:
def version_observer(self) -> Optional[AwesomeVersion]:
"""Return latest version of Observer."""
return self._data.get(ATTR_OBSERVER)
@property
def version_multicast(self) -> Optional[str]:
def version_multicast(self) -> Optional[AwesomeVersion]:
"""Return latest version of Multicast."""
return self._data.get(ATTR_MULTICAST)
@ -197,22 +198,26 @@ class Updater(JsonConfig, CoreSysAttributes):
try:
# Update supervisor version
self._data[ATTR_SUPERVISOR] = data["supervisor"]
self._data[ATTR_SUPERVISOR] = AwesomeVersion(data["supervisor"])
# Update Home Assistant core version
self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine]
self._data[ATTR_HOMEASSISTANT] = AwesomeVersion(
data["homeassistant"][machine]
)
# Update HassOS version
if self.sys_hassos.board:
self._data[ATTR_HASSOS] = data["hassos"][self.sys_hassos.board]
self._data[ATTR_HASSOS] = AwesomeVersion(
data["hassos"][self.sys_hassos.board]
)
self._data[ATTR_OTA] = data["ota"]
# Update Home Assistant plugins
self._data[ATTR_CLI] = data["cli"]
self._data[ATTR_DNS] = data["dns"]
self._data[ATTR_AUDIO] = data["audio"]
self._data[ATTR_OBSERVER] = data["observer"]
self._data[ATTR_MULTICAST] = data["multicast"]
self._data[ATTR_CLI] = AwesomeVersion(data["cli"])
self._data[ATTR_DNS] = AwesomeVersion(data["dns"])
self._data[ATTR_AUDIO] = AwesomeVersion(data["audio"])
self._data[ATTR_OBSERVER] = AwesomeVersion(data["observer"])
self._data[ATTR_MULTICAST] = AwesomeVersion(data["multicast"])
# Update images for that versions
self._data[ATTR_IMAGE][ATTR_HOMEASSISTANT] = data["image"]["core"]

View File

@ -1,10 +1,12 @@
"""Tools file for Supervisor."""
from datetime import datetime
import json
import logging
from pathlib import Path
from typing import Any, Dict
from atomicwrites import atomic_write
from awesomeversion import AwesomeVersion
import voluptuous as vol
from voluptuous.humanize import humanize_error
@ -15,11 +17,31 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
_DEFAULT: Dict[str, Any] = {}
class JSONEncoder(json.JSONEncoder):
"""JSONEncoder that supports Supervisor objects."""
def default(self, o: Any) -> Any:
"""Convert Supervisor special objects.
Hand other objects to the original method.
"""
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, set):
return list(o)
if hasattr(o, "as_dict"):
return o.as_dict()
if isinstance(o, AwesomeVersion):
return o.string
return json.JSONEncoder.default(self, o)
def write_json_file(jsonfile: Path, data: Any) -> None:
"""Write a JSON file."""
try:
with atomic_write(jsonfile, overwrite=True) as fp:
fp.write(json.dumps(data, indent=2))
fp.write(json.dumps(data, indent=2, cls=JSONEncoder))
jsonfile.chmod(0o600)
except (OSError, ValueError, TypeError) as err:
_LOGGER.error("Can't write %s: %s", jsonfile, err)

View File

@ -4,7 +4,7 @@ import re
from typing import Optional, Union
import uuid
from packaging import version as pkg_version
from awesomeversion import AwesomeVersion
import voluptuous as vol
from .const import (
@ -55,23 +55,17 @@ RE_REGISTRY = re.compile(r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$")
# pylint: disable=invalid-name
network_port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
wait_boot = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
docker_image = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
docker_image = vol.Match(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
uuid_match = vol.Match(r"^[0-9a-f]{32}$")
sha256 = vol.Match(r"^[0-9a-f]{64}$")
token = vol.Match(r"^[0-9a-f]{32,256}$")
def version_tag(value: Union[str, None, int, float]) -> Optional[str]:
def version_tag(value: Union[str, None, int, float]) -> Optional[AwesomeVersion]:
"""Validate main version handling."""
if value is None:
return None
try:
value = str(value)
pkg_version.parse(value)
except (pkg_version.InvalidVersion, TypeError):
raise vol.Invalid(f"Invalid version format {value}") from None
return value
return AwesomeVersion(value)
def dns_url(url: str) -> str:
@ -142,14 +136,14 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
vol.Optional(ATTR_CHANNEL, default=UpdateChannel.STABLE): vol.Coerce(
UpdateChannel
),
vol.Optional(ATTR_HOMEASSISTANT): vol.All(version_tag, str),
vol.Optional(ATTR_SUPERVISOR): vol.All(version_tag, str),
vol.Optional(ATTR_HASSOS): vol.All(version_tag, str),
vol.Optional(ATTR_CLI): vol.All(version_tag, str),
vol.Optional(ATTR_DNS): vol.All(version_tag, str),
vol.Optional(ATTR_AUDIO): vol.All(version_tag, str),
vol.Optional(ATTR_OBSERVER): vol.All(version_tag, str),
vol.Optional(ATTR_MULTICAST): vol.All(version_tag, str),
vol.Optional(ATTR_HOMEASSISTANT): version_tag,
vol.Optional(ATTR_SUPERVISOR): version_tag,
vol.Optional(ATTR_HASSOS): version_tag,
vol.Optional(ATTR_CLI): version_tag,
vol.Optional(ATTR_DNS): version_tag,
vol.Optional(ATTR_AUDIO): version_tag,
vol.Optional(ATTR_OBSERVER): version_tag,
vol.Optional(ATTR_MULTICAST): version_tag,
vol.Optional(ATTR_IMAGE, default=dict): vol.Schema(
{
vol.Optional(ATTR_HOMEASSISTANT): docker_image,
@ -173,7 +167,9 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema(
{
vol.Optional(ATTR_TIMEZONE, default="UTC"): validate_timezone,
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str),
vol.Optional(ATTR_VERSION, default=SUPERVISOR_VERSION): version_tag,
vol.Optional(
ATTR_VERSION, default=AwesomeVersion(SUPERVISOR_VERSION)
): version_tag,
vol.Optional(
ATTR_ADDONS_CUSTOM_LIST,
default=["https://github.com/hassio-addons/repository"],

View File

@ -2,6 +2,7 @@
import os
from unittest.mock import patch
from awesomeversion import AwesomeVersion
import pytest
from supervisor.const import SUPERVISOR_VERSION, CoreState
@ -73,7 +74,9 @@ def test_defaults(coresys):
assert ["installation_type", "supervised"] in filtered["tags"]
assert filtered["contexts"]["host"]["arch"] == "amd64"
assert filtered["contexts"]["host"]["machine"] == "qemux86-64"
assert filtered["contexts"]["versions"]["supervisor"] == SUPERVISOR_VERSION
assert filtered["contexts"]["versions"]["supervisor"] == AwesomeVersion(
SUPERVISOR_VERSION
)
assert filtered["user"]["id"] == coresys.machine_id