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:
parent
32fb550969
commit
97c35de49a
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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("_", "-"),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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."""
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue