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 async_timeout==3.0.1
atomicwrites==1.4.0 atomicwrites==1.4.0
attrs==20.3.0 attrs==20.3.0
awesomeversion==20.12.4
brotli==1.0.9 brotli==1.0.9
cchardet==2.1.7 cchardet==2.1.7
colorlog==4.6.2 colorlog==4.6.2

View File

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

View File

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

View File

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

View File

@ -93,6 +93,7 @@ from ..const import (
from ..coresys import CoreSys from ..coresys import CoreSys
from ..discovery.validate import valid_discovery_service from ..discovery.validate import valid_discovery_service
from ..validate import ( from ..validate import (
docker_image,
docker_ports, docker_ports,
docker_ports_description, docker_ports_description,
network_port, network_port,
@ -144,7 +145,6 @@ _SCHEMA_LENGTH_PARTS = (
"p_max", "p_max",
) )
RE_DOCKER_IMAGE = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
RE_DOCKER_IMAGE_BUILD = re.compile( RE_DOCKER_IMAGE_BUILD = re.compile(
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$" r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
) )
@ -186,7 +186,7 @@ def _simple_startup(value) -> str:
SCHEMA_ADDON_CONFIG = vol.Schema( SCHEMA_ADDON_CONFIG = vol.Schema(
{ {
vol.Required(ATTR_NAME): vol.Coerce(str), 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_SLUG): vol.Coerce(str),
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)], 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_ICON, default="mdi:puzzle"): vol.Coerce(str),
vol.Optional(ATTR_PANEL_TITLE): vol.Coerce(str), vol.Optional(ATTR_PANEL_TITLE): vol.Coerce(str),
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(), 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_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(), vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(), vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
@ -267,7 +267,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
), ),
False, 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.Optional(ATTR_TIMEOUT, default=10): vol.All(
vol.Coerce(int), vol.Range(min=10, max=300) vol.Coerce(int), vol.Range(min=10, max=300)
), ),
@ -294,8 +294,8 @@ SCHEMA_BUILD_CONFIG = vol.Schema(
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema( SCHEMA_ADDON_USER = vol.Schema(
{ {
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): version_tag,
vol.Optional(ATTR_IMAGE): vol.Coerce(str), vol.Optional(ATTR_IMAGE): docker_image,
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match, vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
vol.Optional(ATTR_ACCESS_TOKEN): token, vol.Optional(ATTR_ACCESS_TOKEN): token,
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): vol.Coerce( 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 ..exceptions import APIError, APIForbidden, DockerAPIError, HassioError
from ..utils import check_exception_chain, get_message_from_exception_chain from ..utils import check_exception_chain, get_message_from_exception_chain
from ..utils.json import JSONEncoder
from ..utils.log_format import format_message from ..utils.log_format import format_message
@ -112,12 +113,16 @@ def api_return_error(
JSON_MESSAGE: message or "Unknown error, see supervisor", JSON_MESSAGE: message or "Unknown error, see supervisor",
}, },
status=400, status=400,
dumps=lambda x: json.dumps(x, cls=JSONEncoder),
) )
def api_return_ok(data: Optional[Dict[str, Any]] = None) -> web.Response: def api_return_ok(data: Optional[Dict[str, Any]] = None) -> web.Response:
"""Return an API ok answer.""" """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( async def api_validate(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,15 +9,14 @@ from pathlib import Path, PurePath
import shutil import shutil
from typing import Awaitable, Optional from typing import Awaitable, Optional
from awesomeversion import AwesomeVersion
import jinja2 import jinja2
from packaging.version import parse as pkg_parse
from ..const import ATTR_IMAGE, ATTR_VERSION from ..coresys import CoreSys
from ..coresys import CoreSys, CoreSysAttributes
from ..docker.audio import DockerAudio from ..docker.audio import DockerAudio
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import AudioError, AudioUpdateError, DockerError from ..exceptions import AudioError, AudioUpdateError, DockerError
from ..utils.json import JsonConfig from .base import PluginBase
from .const import FILE_HASSIO_AUDIO from .const import FILE_HASSIO_AUDIO
from .validate import SCHEMA_AUDIO_CONFIG 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") 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.""" """Home Assistant core object for handle audio."""
slug: str = "audio"
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize hass object.""" """Initialize hass object."""
super().__init__(FILE_HASSIO_AUDIO, SCHEMA_AUDIO_CONFIG) super().__init__(FILE_HASSIO_AUDIO, SCHEMA_AUDIO_CONFIG)
self.slug = "audio"
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerAudio = DockerAudio(coresys) self.instance: DockerAudio = DockerAudio(coresys)
self.client_template: Optional[jinja2.Template] = None self.client_template: Optional[jinja2.Template] = None
@ -50,29 +48,7 @@ class Audio(JsonConfig, CoreSysAttributes):
return self.sys_config.path_extern_audio.joinpath("asound") return self.sys_config.path_extern_audio.joinpath("asound")
@property @property
def version(self) -> Optional[str]: def latest_version(self) -> Optional[AwesomeVersion]:
"""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]:
"""Return latest version of Audio.""" """Return latest version of Audio."""
return self.sys_updater.version_audio return self.sys_updater.version_audio
@ -81,14 +57,6 @@ class Audio(JsonConfig, CoreSysAttributes):
"""Return True if a task is in progress.""" """Return True if a task is in progress."""
return self.instance.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: async def load(self) -> None:
"""Load Audio setup.""" """Load Audio setup."""
# Initialize Client Template # Initialize Client Template
@ -103,7 +71,7 @@ class Audio(JsonConfig, CoreSysAttributes):
if not self.version: if not self.version:
self.version = await self.instance.get_latest_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: except DockerError:
_LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image) _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 import secrets
from typing import Awaitable, Optional 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 ..const import ATTR_ACCESS_TOKEN
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys
from ..docker.cli import DockerCli from ..docker.cli import DockerCli
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import CliError, CliUpdateError, DockerError from ..exceptions import CliError, CliUpdateError, DockerError
from ..utils.json import JsonConfig from .base import PluginBase
from .const import FILE_HASSIO_CLI from .const import FILE_HASSIO_CLI
from .validate import SCHEMA_CLI_CONFIG from .validate import SCHEMA_CLI_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class HaCli(CoreSysAttributes, JsonConfig): class PluginCli(PluginBase):
"""HA cli interface inside supervisor.""" """HA cli interface inside supervisor."""
slug: str = "cli"
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize cli handler.""" """Initialize cli handler."""
super().__init__(FILE_HASSIO_CLI, SCHEMA_CLI_CONFIG) super().__init__(FILE_HASSIO_CLI, SCHEMA_CLI_CONFIG)
self.slug = "cli"
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerCli = DockerCli(coresys) self.instance: DockerCli = DockerCli(coresys)
@property @property
def version(self) -> Optional[str]: def latest_version(self) -> Optional[AwesomeVersion]:
"""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:
"""Return version of latest cli.""" """Return version of latest cli."""
return self.sys_updater.version_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 @property
def supervisor_token(self) -> str: def supervisor_token(self) -> str:
"""Return an access token for the Supervisor API.""" """Return an access token for the Supervisor API."""
@ -86,7 +55,7 @@ class HaCli(CoreSysAttributes, JsonConfig):
if not self.version: if not self.version:
self.version = await self.instance.get_latest_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: except DockerError:
_LOGGER.info("No cli plugin Docker image %s found.", self.instance.image) _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.image = self.sys_updater.image_cli
self.save_data() 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.""" """Update local HA cli."""
version = version or self.latest_version version = version or self.latest_version
old_image = self.image old_image = self.image

View File

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

View File

@ -7,55 +7,31 @@ from contextlib import suppress
import logging import logging
from typing import Awaitable, Optional 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
from ..coresys import CoreSys, CoreSysAttributes
from ..docker.multicast import DockerMulticast from ..docker.multicast import DockerMulticast
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import DockerError, MulticastError, MulticastUpdateError from ..exceptions import DockerError, MulticastError, MulticastUpdateError
from ..utils.json import JsonConfig from .base import PluginBase
from .const import FILE_HASSIO_MULTICAST from .const import FILE_HASSIO_MULTICAST
from .validate import SCHEMA_MULTICAST_CONFIG from .validate import SCHEMA_MULTICAST_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Multicast(JsonConfig, CoreSysAttributes): class PluginMulticast(PluginBase):
"""Home Assistant core object for handle it.""" """Home Assistant core object for handle it."""
slug: str = "multicast"
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize hass object.""" """Initialize hass object."""
super().__init__(FILE_HASSIO_MULTICAST, SCHEMA_MULTICAST_CONFIG) super().__init__(FILE_HASSIO_MULTICAST, SCHEMA_MULTICAST_CONFIG)
self.slug = "multicast"
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerMulticast = DockerMulticast(coresys) self.instance: DockerMulticast = DockerMulticast(coresys)
@property @property
def version(self) -> Optional[str]: def latest_version(self) -> Optional[AwesomeVersion]:
"""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]:
"""Return latest version of Multicast.""" """Return latest version of Multicast."""
return self.sys_updater.version_multicast return self.sys_updater.version_multicast
@ -64,14 +40,6 @@ class Multicast(JsonConfig, CoreSysAttributes):
"""Return True if a task is in progress.""" """Return True if a task is in progress."""
return self.instance.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: async def load(self) -> None:
"""Load multicast setup.""" """Load multicast setup."""
# Check Multicast state # Check Multicast state
@ -80,7 +48,7 @@ class Multicast(JsonConfig, CoreSysAttributes):
if not self.version: if not self.version:
self.version = await self.instance.get_latest_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: except DockerError:
_LOGGER.info( _LOGGER.info(
"No Multicast plugin Docker image %s found.", self.instance.image "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.image = self.sys_updater.image_multicast
self.save_data() self.save_data()
async def update(self, version: Optional[str] = None) -> None: async def update(self, version: Optional[AwesomeVersion] = None) -> None:
"""Update Multicast plugin.""" """Update Multicast plugin."""
version = version or self.latest_version version = version or self.latest_version
old_image = self.image old_image = self.image

View File

@ -9,66 +9,35 @@ import secrets
from typing import Awaitable, Optional from typing import Awaitable, Optional
import aiohttp 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 ..const import ATTR_ACCESS_TOKEN
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys
from ..docker.observer import DockerObserver from ..docker.observer import DockerObserver
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import DockerError, ObserverError, ObserverUpdateError from ..exceptions import DockerError, ObserverError, ObserverUpdateError
from ..utils.json import JsonConfig from .base import PluginBase
from .const import FILE_HASSIO_OBSERVER from .const import FILE_HASSIO_OBSERVER
from .validate import SCHEMA_OBSERVER_CONFIG from .validate import SCHEMA_OBSERVER_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
class Observer(CoreSysAttributes, JsonConfig): class PluginObserver(PluginBase):
"""Supervisor observer instance.""" """Supervisor observer instance."""
slug: str = "observer"
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize observer handler.""" """Initialize observer handler."""
super().__init__(FILE_HASSIO_OBSERVER, SCHEMA_OBSERVER_CONFIG) super().__init__(FILE_HASSIO_OBSERVER, SCHEMA_OBSERVER_CONFIG)
self.slug = "observer"
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerObserver = DockerObserver(coresys) self.instance: DockerObserver = DockerObserver(coresys)
@property @property
def version(self) -> Optional[str]: def latest_version(self) -> Optional[AwesomeVersion]:
"""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:
"""Return version of latest observer.""" """Return version of latest observer."""
return self.sys_updater.version_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 @property
def supervisor_token(self) -> str: def supervisor_token(self) -> str:
"""Return an access token for the Observer API.""" """Return an access token for the Observer API."""
@ -87,7 +56,7 @@ class Observer(CoreSysAttributes, JsonConfig):
if not self.version: if not self.version:
self.version = await self.instance.get_latest_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: except DockerError:
_LOGGER.info( _LOGGER.info(
"No observer plugin Docker image %s found.", self.instance.image "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.image = self.sys_updater.image_observer
self.save_data() 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.""" """Update local HA observer."""
version = version or self.latest_version version = version or self.latest_version
old_image = self.image old_image = self.image

View File

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

View File

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

View File

@ -1,10 +1,12 @@
"""Tools file for Supervisor.""" """Tools file for Supervisor."""
from datetime import datetime
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from atomicwrites import atomic_write from atomicwrites import atomic_write
from awesomeversion import AwesomeVersion
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -15,11 +17,31 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
_DEFAULT: Dict[str, Any] = {} _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: def write_json_file(jsonfile: Path, data: Any) -> None:
"""Write a JSON file.""" """Write a JSON file."""
try: try:
with atomic_write(jsonfile, overwrite=True) as fp: 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) jsonfile.chmod(0o600)
except (OSError, ValueError, TypeError) as err: except (OSError, ValueError, TypeError) as err:
_LOGGER.error("Can't write %s: %s", jsonfile, err) _LOGGER.error("Can't write %s: %s", jsonfile, err)

View File

@ -4,7 +4,7 @@ import re
from typing import Optional, Union from typing import Optional, Union
import uuid import uuid
from packaging import version as pkg_version from awesomeversion import AwesomeVersion
import voluptuous as vol import voluptuous as vol
from .const import ( 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 # pylint: disable=invalid-name
network_port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) 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)) 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}$") uuid_match = vol.Match(r"^[0-9a-f]{32}$")
sha256 = vol.Match(r"^[0-9a-f]{64}$") sha256 = vol.Match(r"^[0-9a-f]{64}$")
token = vol.Match(r"^[0-9a-f]{32,256}$") 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.""" """Validate main version handling."""
if value is None: if value is None:
return None return None
return AwesomeVersion(value)
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
def dns_url(url: str) -> str: def dns_url(url: str) -> str:
@ -142,14 +136,14 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
vol.Optional(ATTR_CHANNEL, default=UpdateChannel.STABLE): vol.Coerce( vol.Optional(ATTR_CHANNEL, default=UpdateChannel.STABLE): vol.Coerce(
UpdateChannel UpdateChannel
), ),
vol.Optional(ATTR_HOMEASSISTANT): vol.All(version_tag, str), vol.Optional(ATTR_HOMEASSISTANT): version_tag,
vol.Optional(ATTR_SUPERVISOR): vol.All(version_tag, str), vol.Optional(ATTR_SUPERVISOR): version_tag,
vol.Optional(ATTR_HASSOS): vol.All(version_tag, str), vol.Optional(ATTR_HASSOS): version_tag,
vol.Optional(ATTR_CLI): vol.All(version_tag, str), vol.Optional(ATTR_CLI): version_tag,
vol.Optional(ATTR_DNS): vol.All(version_tag, str), vol.Optional(ATTR_DNS): version_tag,
vol.Optional(ATTR_AUDIO): vol.All(version_tag, str), vol.Optional(ATTR_AUDIO): version_tag,
vol.Optional(ATTR_OBSERVER): vol.All(version_tag, str), vol.Optional(ATTR_OBSERVER): version_tag,
vol.Optional(ATTR_MULTICAST): vol.All(version_tag, str), vol.Optional(ATTR_MULTICAST): version_tag,
vol.Optional(ATTR_IMAGE, default=dict): vol.Schema( vol.Optional(ATTR_IMAGE, default=dict): vol.Schema(
{ {
vol.Optional(ATTR_HOMEASSISTANT): docker_image, 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_TIMEZONE, default="UTC"): validate_timezone,
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str), 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( vol.Optional(
ATTR_ADDONS_CUSTOM_LIST, ATTR_ADDONS_CUSTOM_LIST,
default=["https://github.com/hassio-addons/repository"], default=["https://github.com/hassio-addons/repository"],

View File

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