diff --git a/API.md b/API.md index db9e103ec..2b5362ad0 100644 --- a/API.md +++ b/API.md @@ -350,15 +350,8 @@ Get all available addons. "installed": "none|INSTALL_VERSION", "detached": "bool", "build": "bool", - "privileged": ["NET_ADMIN", "SYS_ADMIN"], - "devices": ["/dev/xy"], "url": "null|url", - "logo": "bool", - "audio": "bool", - "gpio": "bool", - "stdin": "bool", - "hassio_api": "bool", - "homeassistant_api": "bool" + "logo": "bool" } ], "repositories": [ @@ -392,9 +385,13 @@ Get all available addons. "options": "{}", "network": "{}|null", "host_network": "bool", + "host_ipc": "bool", + "host_dbus": "bool", "privileged": ["NET_ADMIN", "SYS_ADMIN"], "devices": ["/dev/xy"], + "auto_uart": "bool", "logo": "bool", + "changelog": "bool", "hassio_api": "bool", "homeassistant_api": "bool", "stdin": "bool", @@ -408,6 +405,8 @@ Get all available addons. - GET `/addons/{addon}/logo` +- GET `/addons/{addon}/changelog` + - POST `/addons/{addon}/options` ```json diff --git a/Dockerfile b/Dockerfile index 075eaf8fd..6cf63f798 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,25 @@ ARG BUILD_FROM FROM $BUILD_FROM -# add env +# Add env ENV LANG C.UTF-8 -# setup base -RUN apk add --no-cache python3 python3-dev \ - libressl libressl-dev \ - libffi libffi-dev \ - musl musl-dev \ - gcc libstdc++ \ - git socat \ - && pip3 install --no-cache-dir --upgrade pip \ - && pip3 install --no-cache-dir --upgrade cryptography jwcrypto \ - && apk del python3-dev libressl-dev libffi-dev musl-dev gcc +# Setup base +RUN apk add --no-cache \ + python3 \ + git \ + socat \ + libstdc++ \ + && apk add --no-cache --virtual .build-dependencies \ + make \ + python3-dev \ + g++ \ + && pip3 install --no-cache-dir \ + uvloop \ + cchardet \ + && apk del .build-dependencies -# install HassIO +# Install HassIO COPY . /usr/src/hassio RUN pip3 install --no-cache-dir /usr/src/hassio \ && rm -rf /usr/src/hassio diff --git a/hassio/__main__.py b/hassio/__main__.py index 5ab7639c5..023f28470 100644 --- a/hassio/__main__.py +++ b/hassio/__main__.py @@ -10,9 +10,19 @@ import hassio.core as core _LOGGER = logging.getLogger(__name__) +def attempt_use_uvloop(): + """Attempt to use uvloop.""" + try: + import uvloop + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + except ImportError: + pass + + # pylint: disable=invalid-name if __name__ == "__main__": bootstrap.initialize_logging() + attempt_use_uvloop() loop = asyncio.get_event_loop() if not bootstrap.check_environment(): diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index d232c8ddb..cb47ce7cc 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -21,7 +21,8 @@ from ..const import ( STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM, ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, - ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY) + ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, + ATTR_HOST_DBUS, ATTR_AUTO_UART) from .util import check_installed from ..dock.addon import DockerAddon from ..tools import write_json_file, read_json_file @@ -243,11 +244,26 @@ class Addon(object): """Return True if addon run on host network.""" return self._mesh[ATTR_HOST_NETWORK] + @property + def host_ipc(self): + """Return True if addon run on host IPC namespace.""" + return self._mesh[ATTR_HOST_IPC] + + @property + def host_dbus(self): + """Return True if addon run on host DBUS.""" + return self._mesh[ATTR_HOST_DBUS] + @property def devices(self): """Return devices of addon.""" return self._mesh.get(ATTR_DEVICES) + @property + def auto_uart(self): + """Return True if we should map all uart device.""" + return self._mesh.get(ATTR_AUTO_UART) + @property def tmpfs(self): """Return tmpfs of addon.""" @@ -343,6 +359,11 @@ class Addon(object): """Return True if a logo exists.""" return self.path_logo.exists() + @property + def with_changelog(self): + """Return True if a changelog exists.""" + return self.path_changelog.exists() + @property def supported_arch(self): """Return list of supported arch.""" @@ -402,6 +423,11 @@ class Addon(object): """Return path to addon logo.""" return Path(self.path_location, 'logo.png') + @property + def path_changelog(self): + """Return path to addon changelog.""" + return Path(self.path_location, 'CHANGELOG.md') + def write_options(self): """Return True if addon options is written to data.""" schema = self.schema diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 4d82b04e9..a0001d289 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -14,9 +14,10 @@ from ..const import ( ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED, ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_UUID, - ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, + ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, - ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY) + ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, + ATTR_HOST_DBUS, ATTR_AUTO_UART) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL _LOGGER = logging.getLogger(__name__) @@ -92,7 +93,10 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_WEBUI): vol.Match(r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"), vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(), + vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(), + vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")], + vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(), vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"), vol.Optional(ATTR_MAP, default=[]): [vol.Match(RE_VOLUME)], diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index fd906336d..269fbfd6f 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -104,6 +104,8 @@ class RestAPI(object): '/addons/{addon}/rebuild', api_addons.rebuild) self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs) self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo) + self.webapp.router.add_get( + '/addons/{addon}/changelog', api_addons.changelog) self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin) def register_security(self): diff --git a/hassio/api/addons.py b/hassio/api/addons.py index dd8aad01c..bdb4512ee 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -14,7 +14,8 @@ from ..const import ( ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, - CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY) + ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, + CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) from ..validate import DOCKER_PORTS _LOGGER = logging.getLogger(__name__) @@ -74,15 +75,8 @@ class APIAddons(object): ATTR_DETACHED: addon.is_detached, ATTR_REPOSITORY: addon.repository, ATTR_BUILD: addon.need_build, - ATTR_PRIVILEGED: addon.privileged, - ATTR_DEVICES: self._pretty_devices(addon), ATTR_URL: addon.url, ATTR_LOGO: addon.with_logo, - ATTR_STDIN: addon.with_stdin, - ATTR_HASSIO_API: addon.access_hassio_api, - ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, - ATTR_AUDIO: addon.with_audio, - ATTR_GPIO: addon.with_gpio, }) data_repositories = [] @@ -126,9 +120,12 @@ class APIAddons(object): ATTR_BUILD: addon.need_build, ATTR_NETWORK: addon.ports, ATTR_HOST_NETWORK: addon.host_network, + ATTR_HOST_IPC: addon.host_ipc, + ATTR_HOST_DBUS: addon.host_dbus, ATTR_PRIVILEGED: addon.privileged, ATTR_DEVICES: self._pretty_devices(addon), ATTR_LOGO: addon.with_logo, + ATTR_CHANGELOG: addon.with_changelog, ATTR_WEBUI: addon.webui, ATTR_STDIN: addon.with_stdin, ATTR_HASSIO_API: addon.access_hassio_api, @@ -238,6 +235,16 @@ class APIAddons(object): with addon.path_logo.open('rb') as png: return png.read() + @api_process_raw(CONTENT_TYPE_TEXT) + async def changelog(self, request): + """Return changelog from addon.""" + addon = self._extract_addon(request, check_installed=False) + if not addon.with_changelog: + raise RuntimeError("No changelog found!") + + with addon.path_changelog.open('r') as changelog: + return changelog.read() + @api_process async def stdin(self, request): """Write to stdin of addon.""" diff --git a/hassio/api/util.py b/hassio/api/util.py index b04451a1c..78c2efd4c 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -17,10 +17,12 @@ _LOGGER = logging.getLogger(__name__) def json_loads(data): """Extract json from string with support for '' and None.""" + if not data: + return {} try: return json.loads(data) except json.JSONDecodeError: - return {} + raise RuntimeError("Invalid json") def api_process(method): diff --git a/hassio/const.py b/hassio/const.py index 56c7a0110..1a1bf30b2 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -2,7 +2,7 @@ from pathlib import Path from ipaddress import ip_network -HASSIO_VERSION = '0.75' +HASSIO_VERSION = '0.76' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/{}/version.json') @@ -50,9 +50,11 @@ RESULT_OK = 'ok' CONTENT_TYPE_BINARY = 'application/octet-stream' CONTENT_TYPE_PNG = 'image/png' CONTENT_TYPE_JSON = 'application/json' +CONTENT_TYPE_TEXT = 'text/plain' HEADER_HA_ACCESS = 'x-ha-access' ATTR_WATCHDOG = 'watchdog' +ATTR_CHANGELOG = 'changelog' ATTR_DATE = 'date' ATTR_ARCH = 'arch' ATTR_HOSTNAME = 'hostname' @@ -64,6 +66,7 @@ ATTR_SOURCE = 'source' ATTR_FEATURES = 'features' ATTR_ADDONS = 'addons' ATTR_VERSION = 'version' +ATTR_AUTO_UART = 'auto_uart' ATTR_LAST_BOOT = 'last_boot' ATTR_LAST_VERSION = 'last_version' ATTR_BETA_CHANNEL = 'beta_channel' @@ -100,6 +103,8 @@ ATTR_BUILD = 'build' ATTR_DEVICES = 'devices' ATTR_ENVIRONMENT = 'environment' ATTR_HOST_NETWORK = 'host_network' +ATTR_HOST_IPC = 'host_ipc' +ATTR_HOST_DBUS = 'host_dbus' ATTR_NETWORK = 'network' ATTR_TMPFS = 'tmpfs' ATTR_PRIVILEGED = 'privileged' diff --git a/hassio/core.py b/hassio/core.py index 6150b0e1b..0043c505b 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -42,8 +42,8 @@ class HassIO(object): self.scheduler = Scheduler(loop) self.api = RestAPI(config, loop) self.hardware = Hardware() - self.docker = DockerAPI() - self.dns = DNSForward() + self.docker = DockerAPI(self.hardware) + self.dns = DNSForward(loop) # init basic docker container self.supervisor = DockerSupervisor( diff --git a/hassio/dns.py b/hassio/dns.py index e02f0182a..359abadc5 100644 --- a/hassio/dns.py +++ b/hassio/dns.py @@ -11,8 +11,9 @@ COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53" class DNSForward(object): """Manage DNS forwarding to internal DNS.""" - def __init__(self): + def __init__(self, loop): """Initialize DNS forwarding.""" + self.loop = loop self.proc = None async def start(self): @@ -23,6 +24,7 @@ class DNSForward(object): stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, + loop=self.loop ) except OSError as err: _LOGGER.error("Can't start DNS forwarding -> %s", err) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 35d773629..580b51559 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -16,11 +16,12 @@ class DockerAPI(object): This class is not AsyncIO safe! """ - def __init__(self): + def __init__(self, hardware): """Initialize docker base wrapper.""" self.docker = docker.DockerClient( base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto') self.network = DockerNetwork(self.docker) + self.hardware = hardware @property def images(self): diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 8b95bb63f..fa9b075e5 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -45,6 +45,12 @@ class DockerAddon(DockerInterface): """Return name of docker container.""" return "addon_{}".format(self.addon.slug) + @property + def ipc(self): + """Return the IPC namespace.""" + if self.addon.host_ipc: + return 'host' + @property def hostname(self): """Return slug/id of addon.""" @@ -74,14 +80,17 @@ class DockerAddon(DockerInterface): """Return needed devices.""" devices = self.addon.devices or [] - # use audio devices + # Use audio devices if self.addon.with_audio and AUDIO_DEVICE not in devices: devices.append(AUDIO_DEVICE) + # Auto mapping UART devices + if self.addon.auto_uart: + for uart_dev in self.docker.hardware.serial_devices: + devices.append("{0}:{0}:rwm".format(uart_dev)) + # Return None if no devices is present - if devices: - return devices - return None + return devices or None @property def ports(self): @@ -95,6 +104,17 @@ class DockerAddon(DockerInterface): if host_port } + @property + def security_opt(self): + """Controlling security opt.""" + privileged = self.addon.privileged or [] + + # Disable AppArmor sinse it make troubles wit SYS_ADMIN + if 'SYS_ADMIN' in privileged: + return [ + "apparmor:unconfined", + ] + @property def tmpfs(self): """Return tmpfs for docker add-on.""" @@ -123,7 +143,7 @@ class DockerAddon(DockerInterface): """Generate volumes for mappings.""" volumes = { str(self.addon.path_extern_data): { - 'bind': '/data', 'mode': 'rw' + 'bind': "/data", 'mode': 'rw' }} addon_mapping = self.addon.map_volumes @@ -132,44 +152,51 @@ class DockerAddon(DockerInterface): if MAP_CONFIG in addon_mapping: volumes.update({ str(self.config.path_extern_config): { - 'bind': '/config', 'mode': addon_mapping[MAP_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] + '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] + '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] + '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] + '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/class/gpio": { + 'bind': "/sys/class/gpio", 'mode': 'rw' }, - '/sys/devices/platform/soc': { - 'bind': '/sys/devices/platform/soc', 'mode': "rw" + "/sys/devices/platform/soc": { + 'bind': "/sys/devices/platform/soc", 'mode': 'rw' }, }) + # host dbus system + if self.addon.host_dbus: + volumes.update({ + "/var/run/dbus": { + 'bind': "/var/run/dbus", 'mode': 'rw' + }}) + return volumes def _run(self): @@ -193,12 +220,14 @@ class DockerAddon(DockerInterface): hostname=self.hostname, detach=True, init=True, + ipc_mode=self.ipc, 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, + security_opt=self.security_opt, environment=self.environment, volumes=self.volumes, tmpfs=self.tmpfs diff --git a/hassio/hardware.py b/hassio/hardware.py index 09be04acc..d8ba8eb82 100644 --- a/hassio/hardware.py +++ b/hassio/hardware.py @@ -20,6 +20,7 @@ PROC_STAT = Path("/proc/stat") RE_BOOT_TIME = re.compile(r"btime (\d+)") GPIO_DEVICES = Path("/sys/class/gpio") +RE_TTY = re.compile(r"tty[A-Z]+") class Hardware(object): @@ -34,7 +35,7 @@ class Hardware(object): """Return all serial and connected devices.""" dev_list = set() for device in self.context.list_devices(subsystem='tty'): - if 'ID_VENDOR' in device: + if 'ID_VENDOR' in device or RE_TTY.search(device.device_node): dev_list.add(device.device_node) return dev_list diff --git a/setup.py b/setup.py index 9625b45af..f723f5a03 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( 'Topic :: Scientific/Engineering :: Atmospheric Science', 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], keywords=['docker', 'home-assistant', 'api'], zip_safe=False,