mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-06-26 11:56:30 +00:00

* Add support for legacy mode * Update const.py * add legacy mode * Update addon.py * Update addon.py * Update addon.py * Update addon.py
331 lines
9.6 KiB
Python
331 lines
9.6 KiB
Python
"""Init file for HassIO addon docker object."""
|
|
import logging
|
|
import os
|
|
|
|
import docker
|
|
import requests
|
|
|
|
from .interface import DockerInterface
|
|
from .util import docker_process
|
|
from ..addons.build import AddonBuild
|
|
from ..const import (
|
|
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
|
|
|
|
|
class DockerAddon(DockerInterface):
|
|
"""Docker hassio wrapper for HomeAssistant."""
|
|
|
|
def __init__(self, config, loop, api, addon):
|
|
"""Initialize docker homeassistant wrapper."""
|
|
super().__init__(
|
|
config, loop, api, image=addon.image, timeout=addon.timeout)
|
|
self.addon = addon
|
|
|
|
def process_metadata(self, metadata, force=False):
|
|
"""Use addon data instead meta data with legacy."""
|
|
if not self.addon.legacy:
|
|
return super().process_metadata(metadata, force=force)
|
|
|
|
# set meta data
|
|
if not self.version or force:
|
|
if force: # called on install/update/build
|
|
self.version = self.addon.last_version
|
|
else:
|
|
self.version = self.addon.version_installed
|
|
|
|
if not self.arch:
|
|
self.arch = self.config.arch
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return name of docker container."""
|
|
return "addon_{}".format(self.addon.slug)
|
|
|
|
@property
|
|
def hostname(self):
|
|
"""Return slug/id of addon."""
|
|
return self.addon.slug.replace('_', '-')
|
|
|
|
@property
|
|
def environment(self):
|
|
"""Return environment for docker add-on."""
|
|
addon_env = self.addon.environment or {}
|
|
if self.addon.with_audio:
|
|
addon_env.update({
|
|
'ALSA_OUTPUT': self.addon.audio_output,
|
|
'ALSA_INPUT': self.addon.audio_input,
|
|
})
|
|
|
|
return {
|
|
**addon_env,
|
|
'TZ': self.config.timezone,
|
|
}
|
|
|
|
@property
|
|
def devices(self):
|
|
"""Return needed devices."""
|
|
devices = self.addon.devices or []
|
|
|
|
# use audio devices
|
|
if self.addon.with_audio and AUDIO_DEVICE not in devices:
|
|
devices.append(AUDIO_DEVICE)
|
|
|
|
# Return None if no devices is present
|
|
if devices:
|
|
return devices
|
|
return None
|
|
|
|
@property
|
|
def ports(self):
|
|
"""Filter None from addon ports."""
|
|
if not self.addon.ports:
|
|
return None
|
|
|
|
return {
|
|
container_port: host_port
|
|
for container_port, host_port in self.addon.ports.items()
|
|
if host_port
|
|
}
|
|
|
|
@property
|
|
def tmpfs(self):
|
|
"""Return tmpfs for docker add-on."""
|
|
options = self.addon.tmpfs
|
|
if options:
|
|
return {"/tmpfs": "{}".format(options)}
|
|
return None
|
|
|
|
@property
|
|
def network_mapping(self):
|
|
"""Return hosts mapping."""
|
|
return {
|
|
'homeassistant': self.docker.network.gateway,
|
|
'hassio': self.docker.network.supervisor,
|
|
}
|
|
|
|
@property
|
|
def network_mode(self):
|
|
"""Return network mode for addon."""
|
|
if self.addon.host_network:
|
|
return 'host'
|
|
return None
|
|
|
|
@property
|
|
def volumes(self):
|
|
"""Generate volumes for mappings."""
|
|
volumes = {
|
|
str(self.addon.path_extern_data): {
|
|
'bind': '/data', 'mode': 'rw'
|
|
}}
|
|
|
|
addon_mapping = self.addon.map_volumes
|
|
|
|
# setup config mappings
|
|
if MAP_CONFIG in addon_mapping:
|
|
volumes.update({
|
|
str(self.config.path_extern_config): {
|
|
'bind': '/config', 'mode': addon_mapping[MAP_CONFIG]
|
|
}})
|
|
|
|
if MAP_SSL in addon_mapping:
|
|
volumes.update({
|
|
str(self.config.path_extern_ssl): {
|
|
'bind': '/ssl', 'mode': addon_mapping[MAP_SSL]
|
|
}})
|
|
|
|
if MAP_ADDONS in addon_mapping:
|
|
volumes.update({
|
|
str(self.config.path_extern_addons_local): {
|
|
'bind': '/addons', 'mode': addon_mapping[MAP_ADDONS]
|
|
}})
|
|
|
|
if MAP_BACKUP in addon_mapping:
|
|
volumes.update({
|
|
str(self.config.path_extern_backup): {
|
|
'bind': '/backup', 'mode': addon_mapping[MAP_BACKUP]
|
|
}})
|
|
|
|
if MAP_SHARE in addon_mapping:
|
|
volumes.update({
|
|
str(self.config.path_extern_share): {
|
|
'bind': '/share', 'mode': addon_mapping[MAP_SHARE]
|
|
}})
|
|
|
|
# init other hardware mappings
|
|
if self.addon.with_gpio:
|
|
volumes.update({
|
|
'/sys/class/gpio': {
|
|
'bind': '/sys/class/gpio', 'mode': "rw"
|
|
},
|
|
'/sys/devices/platform/soc': {
|
|
'bind': '/sys/devices/platform/soc', 'mode': "rw"
|
|
},
|
|
})
|
|
|
|
return volumes
|
|
|
|
def _run(self):
|
|
"""Run docker image.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
if self._is_running():
|
|
return True
|
|
|
|
# cleanup
|
|
self._stop()
|
|
|
|
# write config
|
|
if not self.addon.write_options():
|
|
return False
|
|
|
|
ret = self.docker.run(
|
|
self.image,
|
|
name=self.name,
|
|
hostname=self.hostname,
|
|
detach=True,
|
|
stdin_open=self.addon.with_stdin,
|
|
network_mode=self.network_mode,
|
|
ports=self.ports,
|
|
extra_hosts=self.network_mapping,
|
|
devices=self.devices,
|
|
cap_add=self.addon.privileged,
|
|
environment=self.environment,
|
|
volumes=self.volumes,
|
|
tmpfs=self.tmpfs
|
|
)
|
|
|
|
if ret:
|
|
_LOGGER.info("Start docker addon %s with version %s",
|
|
self.image, self.version)
|
|
|
|
return ret
|
|
|
|
def _install(self, tag):
|
|
"""Pull docker image or build it.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
if self.addon.need_build:
|
|
return self._build(tag)
|
|
|
|
return super()._install(tag)
|
|
|
|
def _build(self, tag):
|
|
"""Build a docker container.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
build_env = AddonBuild(self.config, self.addon)
|
|
|
|
_LOGGER.info("Start build %s:%s", self.image, tag)
|
|
try:
|
|
image = self.docker.images.build(**build_env.get_docker_args(tag))
|
|
|
|
image.tag(self.image, tag='latest')
|
|
self.process_metadata(image.attrs, force=True)
|
|
|
|
except (docker.errors.DockerException) as err:
|
|
_LOGGER.error("Can't build %s:%s -> %s", self.image, tag, err)
|
|
return False
|
|
|
|
_LOGGER.info("Build %s:%s done", self.image, tag)
|
|
return True
|
|
|
|
@docker_process
|
|
def export_image(self, path):
|
|
"""Export current images into a tar file."""
|
|
return self.loop.run_in_executor(None, self._export_image, path)
|
|
|
|
def _export_image(self, tar_file):
|
|
"""Export current images into a tar file.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
try:
|
|
image = self.docker.api.get_image(self.image)
|
|
except docker.errors.DockerException as err:
|
|
_LOGGER.error("Can't fetch image %s -> %s", self.image, err)
|
|
return False
|
|
|
|
try:
|
|
with tar_file.open("wb") as write_tar:
|
|
for chunk in image.stream():
|
|
write_tar.write(chunk)
|
|
except (OSError, requests.exceptions.ReadTimeout) as err:
|
|
_LOGGER.error("Can't write tar file %s -> %s", tar_file, err)
|
|
return False
|
|
|
|
_LOGGER.info("Export image %s to %s", self.image, tar_file)
|
|
return True
|
|
|
|
@docker_process
|
|
def import_image(self, path, tag):
|
|
"""Import a tar file as image."""
|
|
return self.loop.run_in_executor(None, self._import_image, path, tag)
|
|
|
|
def _import_image(self, tar_file, tag):
|
|
"""Import a tar file as image.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
try:
|
|
with tar_file.open("rb") as read_tar:
|
|
self.docker.api.load_image(read_tar)
|
|
|
|
image = self.docker.images.get(self.image)
|
|
image.tag(self.image, tag=tag)
|
|
except (docker.errors.DockerException, OSError) as err:
|
|
_LOGGER.error("Can't import image %s -> %s", self.image, err)
|
|
return False
|
|
|
|
_LOGGER.info("Import image %s and tag %s", tar_file, tag)
|
|
self.process_metadata(image.attrs, force=True)
|
|
self._cleanup()
|
|
return True
|
|
|
|
def _restart(self):
|
|
"""Restart docker container.
|
|
|
|
Addons prepare some thing on start and that is normaly not repeatable.
|
|
Need run inside executor.
|
|
"""
|
|
self._stop()
|
|
return self._run()
|
|
|
|
@docker_process
|
|
def write_stdin(self, data):
|
|
"""Write to add-on stdin."""
|
|
return self.loop.run_in_executor(None, self._write_stdin, data)
|
|
|
|
def _write_stdin(self, data):
|
|
"""Write to add-on stdin.
|
|
|
|
Need run inside executor.
|
|
"""
|
|
if not self._is_running():
|
|
return False
|
|
|
|
try:
|
|
# load needed docker objects
|
|
container = self.docker.containers.get(self.name)
|
|
socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
|
|
except docker.errors.DockerException as err:
|
|
_LOGGER.error("Can't attach to %s stdin -> %s", self.name, err)
|
|
return False
|
|
|
|
try:
|
|
# write to stdin
|
|
data += b"\n"
|
|
os.write(socket.fileno(), data)
|
|
socket.close()
|
|
except OSError as err:
|
|
_LOGGER.error("Can't write to %s stdin -> %s", self.name, err)
|
|
return False
|
|
|
|
return True
|