Merge pull request #517 from home-assistant/dev

Release 108
This commit is contained in:
Pascal Vizeli 2018-06-21 11:33:09 +02:00 committed by GitHub
commit 7d52b3ba01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 642 additions and 107 deletions

32
API.md
View File

@ -227,7 +227,7 @@ return:
```json ```json
{ {
"hostname": "hostname|null", "hostname": "hostname|null",
"features": ["shutdown", "reboot", "update", "hostname"], "features": ["shutdown", "reboot", "update", "hostname", "services"],
"operating_system": "Hass.io-OS XY|Ubuntu 16.4|null", "operating_system": "Hass.io-OS XY|Ubuntu 16.4|null",
"kernel": "4.15.7|null", "kernel": "4.15.7|null",
"chassis": "specific|null", "chassis": "specific|null",
@ -259,6 +259,27 @@ Optional:
- POST `/host/reload` - POST `/host/reload`
#### Services
- GET `/host/services`
```json
{
"services": [
{
"name": "xy.service",
"description": "XY ...",
"state": "active|"
}
]
}
```
- POST `/host/service/{unit}/stop`
- POST `/host/service/{unit}/start`
- POST `/host/service/{unit}/reload`
### Hardware ### Hardware
- GET `/hardware/info` - GET `/hardware/info`
@ -430,7 +451,6 @@ Get all available addons.
"host_ipc": "bool", "host_ipc": "bool",
"host_dbus": "bool", "host_dbus": "bool",
"privileged": ["NET_ADMIN", "SYS_ADMIN"], "privileged": ["NET_ADMIN", "SYS_ADMIN"],
"seccomp": "disable|default|profile",
"apparmor": "disable|default|profile", "apparmor": "disable|default|profile",
"devices": ["/dev/xy"], "devices": ["/dev/xy"],
"auto_uart": "bool", "auto_uart": "bool",
@ -569,14 +589,6 @@ return:
} }
``` ```
- GET `/services/xy`
```json
{
"available": "bool",
"xy": {}
}
```
#### MQTT #### MQTT
This service performs an auto discovery to Home-Assistant. This service performs an auto discovery to Home-Assistant.

View File

@ -17,7 +17,7 @@ RUN apk add --no-cache \
python3-dev \ python3-dev \
g++ \ g++ \
&& pip3 install --no-cache-dir \ && pip3 install --no-cache-dir \
uvloop==0.9.1 \ uvloop==0.10.1 \
cchardet==2.1.1 \ cchardet==2.1.1 \
pycryptodome==3.4.11 \ pycryptodome==3.4.11 \
&& apk del .build-dependencies && apk del .build-dependencies

View File

@ -25,11 +25,12 @@ from ..const import (
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_SECCOMP, ATTR_APPARMOR, SECURITY_PROFILE, SECURITY_DISABLE, ATTR_APPARMOR, SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
from ..utils.json import write_json_file, read_json_file from ..utils.json import write_json_file, read_json_file
from ..utils.apparmor import adjust_profile
from ..exceptions import HostAppArmorError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -319,21 +320,12 @@ class Addon(CoreSysAttributes):
"""Return list of privilege.""" """Return list of privilege."""
return self._mesh.get(ATTR_PRIVILEGED) return self._mesh.get(ATTR_PRIVILEGED)
@property
def seccomp(self):
"""Return True if seccomp is enabled."""
if not self._mesh.get(ATTR_SECCOMP):
return SECURITY_DISABLE
elif self.path_seccomp.exists():
return SECURITY_PROFILE
return SECURITY_DEFAULT
@property @property
def apparmor(self): def apparmor(self):
"""Return True if seccomp is enabled.""" """Return True if apparmor is enabled."""
if not self._mesh.get(ATTR_APPARMOR): if not self._mesh.get(ATTR_APPARMOR):
return SECURITY_DISABLE return SECURITY_DISABLE
elif self.path_apparmor.exists(): elif self.sys_host.apparmor.exists(self.slug):
return SECURITY_PROFILE return SECURITY_PROFILE
return SECURITY_DEFAULT return SECURITY_DEFAULT
@ -493,15 +485,10 @@ class Addon(CoreSysAttributes):
"""Return path to addon changelog.""" """Return path to addon changelog."""
return Path(self.path_location, 'CHANGELOG.md') return Path(self.path_location, 'CHANGELOG.md')
@property
def path_seccomp(self):
"""Return path to custom seccomp profile."""
return Path(self.path_location, 'seccomp.json')
@property @property
def path_apparmor(self): def path_apparmor(self):
"""Return path to custom AppArmor profile.""" """Return path to custom AppArmor profile."""
return Path(self.path_location, 'apparmor') return Path(self.path_location, 'apparmor.txt')
@property @property
def path_asound(self): def path_asound(self):
@ -549,6 +536,27 @@ class Addon(CoreSysAttributes):
return True return True
async def _install_apparmor(self):
"""Install or Update AppArmor profile for Add-on."""
exists_local = self.sys_host.apparmor.exists(self.slug)
exists_addon = self.path_apparmor.exists()
# Nothing to do
if not exists_local and not exists_addon:
return
# Need removed
if exists_local and not exists_addon:
await self.sys_host.apparmor.remove_profile(self.slug)
return
# Need install/update
with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_folder:
profile_file = Path(tmp_folder, 'apparmor.txt')
adjust_profile(self.slug, self.path_apparmor, profile_file)
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
@property @property
def schema(self): def schema(self):
"""Create a schema for addon options.""" """Create a schema for addon options."""
@ -604,6 +612,9 @@ class Addon(CoreSysAttributes):
"Create Home-Assistant addon data folder %s", self.path_data) "Create Home-Assistant addon data folder %s", self.path_data)
self.path_data.mkdir() self.path_data.mkdir()
# Setup/Fix AppArmor profile
await self._install_apparmor()
if not await self.instance.install(self.last_version): if not await self.instance.install(self.last_version):
return False return False
@ -626,6 +637,11 @@ class Addon(CoreSysAttributes):
with suppress(OSError): with suppress(OSError):
self.path_asound.unlink() self.path_asound.unlink()
# Cleanup apparmor profile
if self.sys_host.apparmor.exists(self.slug):
with suppress(HostAppArmorError):
await self.sys_host.apparmor.remove_profile(self.slug)
self._set_uninstall() self._set_uninstall()
return True return True
@ -672,6 +688,9 @@ class Addon(CoreSysAttributes):
return False return False
self._set_update(self.last_version) self._set_update(self.last_version)
# Setup/Fix AppArmor profile
await self._install_apparmor()
# restore state # restore state
if last_state == STATE_STARTED: if last_state == STATE_STARTED:
await self.start() await self.start()
@ -738,7 +757,7 @@ class Addon(CoreSysAttributes):
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
# store local image # store local image
if self.need_build and not await \ if self.need_build and not await \
self.instance.export_image(Path(temp, "image.tar")): self.instance.export_image(Path(temp, 'image.tar')):
return False return False
data = { data = {
@ -750,11 +769,20 @@ class Addon(CoreSysAttributes):
# store local configs/state # store local configs/state
try: try:
write_json_file(Path(temp, "addon.json"), data) write_json_file(Path(temp, 'addon.json'), data)
except (OSError, json.JSONDecodeError) as err: except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't save meta for %s: %s", self._id, err) _LOGGER.error("Can't save meta for %s: %s", self._id, err)
return False return False
# Store AppArmor Profile
if self.sys_host.apparmor.exists(self.slug):
profile = Path(temp, 'apparmor.txt')
try:
self.sys_host.apparmor.backup_profile(self.slug, profile)
except HostAppArmorError:
_LOGGER.error("Can't backup AppArmor profile")
return False
# write into tarfile # write into tarfile
def _write_tarfile(): def _write_tarfile():
"""Write tar inside loop.""" """Write tar inside loop."""
@ -789,7 +817,7 @@ class Addon(CoreSysAttributes):
# read snapshot data # read snapshot data
try: try:
data = read_json_file(Path(temp, "addon.json")) data = read_json_file(Path(temp, 'addon.json'))
except (OSError, json.JSONDecodeError) as err: except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't read addon.json: %s", err) _LOGGER.error("Can't read addon.json: %s", err)
@ -810,7 +838,7 @@ class Addon(CoreSysAttributes):
if not await self.instance.exists(): if not await self.instance.exists():
_LOGGER.info("Restore image for addon %s", self._id) _LOGGER.info("Restore image for addon %s", self._id)
image_file = Path(temp, "image.tar") image_file = Path(temp, 'image.tar')
if image_file.is_file(): if image_file.is_file():
await self.instance.import_image(image_file, version) await self.instance.import_image(image_file, version)
else: else:
@ -833,6 +861,16 @@ class Addon(CoreSysAttributes):
_LOGGER.error("Can't restore origin data: %s", err) _LOGGER.error("Can't restore origin data: %s", err)
return False return False
# Restore AppArmor
profile_file = Path(temp, 'apparmor.txt')
if profile_file.exists():
try:
await self.sys_host.apparmor.load_profile(
self.slug, profile_file)
except HostAppArmorError:
_LOGGER.error("Can't restore AppArmor profile")
return False
# run addon # run addon
if data[ATTR_STATE] == STATE_STARTED: if data[ATTR_STATE] == STATE_STARTED:
return await self.start() return await self.start()

View File

@ -18,7 +18,7 @@ from ..const import (
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, 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, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_SECCOMP, ATTR_APPARMOR) ATTR_APPARMOR)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -108,7 +108,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)], vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_SECCOMP, default=True): vol.Boolean(),
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(), vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),

View File

@ -30,8 +30,8 @@ class RestAPI(CoreSysAttributes):
middlewares=[self.security.token_validation], loop=coresys.loop) middlewares=[self.security.token_validation], loop=coresys.loop)
# service stuff # service stuff
self._handler = None self._runner = web.AppRunner(self.webapp)
self.server = None self._site = None
async def load(self): async def load(self):
"""Register REST API Calls.""" """Register REST API Calls."""
@ -57,6 +57,13 @@ class RestAPI(CoreSysAttributes):
web.post('/host/shutdown', api_host.shutdown), web.post('/host/shutdown', api_host.shutdown),
web.post('/host/update', api_host.update), web.post('/host/update', api_host.update),
web.post('/host/reload', api_host.reload), web.post('/host/reload', api_host.reload),
web.get('/host/services', api_host.services),
web.post('/host/services/{service}/stop', api_host.service_stop),
web.post('/host/services/{service}/start', api_host.service_start),
web.post(
'/host/services/{service}/restart', api_host.service_restart),
web.post(
'/host/services/{service}/reload', api_host.service_reload),
]) ])
def _register_hardware(self): def _register_hardware(self):
@ -213,22 +220,16 @@ class RestAPI(CoreSysAttributes):
async def start(self): async def start(self):
"""Run rest api webserver.""" """Run rest api webserver."""
self._handler = self.webapp.make_handler() await self._runner.setup()
self._site = web.TCPSite(self._runner, "0.0.0.0", 80)
try: try:
self.server = await self.sys_loop.create_server( await self._site.start()
self._handler, "0.0.0.0", "80")
except OSError as err: except OSError as err:
_LOGGER.fatal( _LOGGER.fatal(
"Failed to create HTTP server at 0.0.0.0:80 -> %s", err) "Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
async def stop(self): async def stop(self):
"""Stop rest api webserver.""" """Stop rest api webserver."""
if self.server: await self._site.stop()
self.server.close() await self._runner.cleanup()
await self.server.wait_closed()
await self.webapp.shutdown()
if self._handler:
await self._handler.shutdown(60)
await self.webapp.cleanup()

View File

@ -17,7 +17,7 @@ from ..const import (
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR, ATTR_DISCOVERY, ATTR_APPARMOR,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..validate import DOCKER_PORTS, ALSA_DEVICE
@ -126,7 +126,6 @@ class APIAddons(CoreSysAttributes):
ATTR_HOST_IPC: addon.host_ipc, ATTR_HOST_IPC: addon.host_ipc,
ATTR_HOST_DBUS: addon.host_dbus, ATTR_HOST_DBUS: addon.host_dbus,
ATTR_PRIVILEGED: addon.privileged, ATTR_PRIVILEGED: addon.privileged,
ATTR_SECCOMP: addon.seccomp,
ATTR_APPARMOR: addon.apparmor, ATTR_APPARMOR: addon.apparmor,
ATTR_DEVICES: self._pretty_devices(addon), ATTR_DEVICES: self._pretty_devices(addon),
ATTR_ICON: addon.with_icon, ATTR_ICON: addon.with_icon,

View File

@ -7,11 +7,14 @@ import voluptuous as vol
from .utils import api_process, api_validate from .utils import api_process, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_HOSTNAME, ATTR_FEATURES, ATTR_KERNEL, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_HOSTNAME, ATTR_FEATURES, ATTR_KERNEL,
ATTR_TYPE, ATTR_OPERATING_SYSTEM, ATTR_CHASSIS, ATTR_DEPLOYMENT) ATTR_TYPE, ATTR_OPERATING_SYSTEM, ATTR_CHASSIS, ATTR_DEPLOYMENT,
ATTR_STATE, ATTR_NAME, ATTR_DESCRIPTON, ATTR_SERVICES)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE = 'service'
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_VERSION): vol.Coerce(str),
}) })
@ -70,3 +73,42 @@ class APIHost(CoreSysAttributes):
pass pass
# body = await api_validate(SCHEMA_VERSION, request) # body = await api_validate(SCHEMA_VERSION, request)
# version = body.get(ATTR_VERSION, self.sys_host.last_version) # version = body.get(ATTR_VERSION, self.sys_host.last_version)
@api_process
async def services(self, request):
"""Return list of available services."""
services = []
for unit in self.sys_host.services:
services.append({
ATTR_NAME: unit.name,
ATTR_DESCRIPTON: unit.description,
ATTR_STATE: unit.state,
})
return {
ATTR_SERVICES: services
}
@api_process
def service_start(self, request):
"""Start a service."""
unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.start(unit))
@api_process
def service_stop(self, request):
"""Stop a service."""
unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.stop(unit))
@api_process
def service_reload(self, request):
"""Reload a service."""
unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.reload(unit))
@api_process
def service_restart(self, request):
"""Restart a service."""
unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.restart(unit))

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1 +1 @@
!function(e){function n(n){for(var t,o,a=n[0],i=n[1],u=0,f=[];u<a.length;u++)o=a[u],r[o]&&f.push(r[o][0]),r[o]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(c&&c(n);f.length;)f.shift()()}var t={},r={6:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var a=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=a);var i=document.getElementsByTagName("head")[0],u=document.createElement("script");u.charset="utf-8",u.timeout=120,o.nc&&u.setAttribute("nonce",o.nc),u.src=function(e){return o.p+"chunk."+{0:"311036a0f4514f345e53",1:"a8e86d80be46b3b6e16d",2:"e4eb9811aad7204f14c4",3:"4ac8b99259327a51f355",4:"87cffadba6f33daa568c",5:"c93f37c558ff32991708"}[e]+".js"}(e);var c=setTimeout(function(){f({type:"timeout",target:u})},12e4);function f(n){u.onerror=u.onload=null,clearTimeout(c);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src,i=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");i.type=o,i.request=a,t[1](i)}r[e]=void 0}}u.onerror=u.onload=f,i.appendChild(u)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},o.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],i=a.push.bind(a);a.push=n,a=a.slice();for(var u=0;u<a.length;u++)n(a[u]);var c=i;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]); !function(e){function n(n){for(var t,o,a=n[0],i=n[1],u=0,f=[];u<a.length;u++)o=a[u],r[o]&&f.push(r[o][0]),r[o]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(c&&c(n);f.length;)f.shift()()}var t={},r={6:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var a=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=a);var i=document.getElementsByTagName("head")[0],u=document.createElement("script");u.charset="utf-8",u.timeout=120,o.nc&&u.setAttribute("nonce",o.nc),u.src=function(e){return o.p+"chunk."+{0:"311036a0f4514f345e53",1:"a8e86d80be46b3b6e16d",2:"05994812bec7a524b566",3:"ff92199b0d422767d108",4:"87cffadba6f33daa568c",5:"c93f37c558ff32991708"}[e]+".js"}(e);var c=setTimeout(function(){f({type:"timeout",target:u})},12e4);function f(n){u.onerror=u.onload=null,clearTimeout(c);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src,i=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");i.type=o,i.request=a,t[1](i)}r[e]=void 0}}u.onerror=u.onload=f,i.appendChild(u)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},o.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],i=a.push.bind(a);a.push=n,a=a.slice();for(var u=0;u<a.length;u++)n(a[u]);var c=i;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]);

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -44,13 +44,18 @@ class SecurityMiddleware(CoreSysAttributes):
if hassio_token == self.sys_homeassistant.uuid: if hassio_token == self.sys_homeassistant.uuid:
_LOGGER.debug("%s access from Home-Assistant", request.path) _LOGGER.debug("%s access from Home-Assistant", request.path)
request[REQUEST_FROM] = 'homeassistant' request[REQUEST_FROM] = 'homeassistant'
return await handler(request)
# Host
if hassio_token == self.sys_machine_id:
_LOGGER.debug("%s access from Host", request.path)
request[REQUEST_FROM] = 'host'
# Add-on # Add-on
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
if addon: if addon:
_LOGGER.info("%s access from %s", request.path, addon.slug) _LOGGER.info("%s access from %s", request.path, addon.slug)
request[REQUEST_FROM] = addon.slug request[REQUEST_FROM] = addon.slug
return await handler(request)
raise HTTPUnauthorized() if not request.get(REQUEST_FROM):
raise HTTPUnauthorized()
return await handler(request)

View File

@ -24,6 +24,11 @@ from .dbus import DBusManager
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ENV_SHARE = 'SUPERVISOR_SHARE'
ENV_NAME = 'SUPERVISOR_NAME'
ENV_REPO = 'HOMEASSISTANT_REPOSITORY'
ENV_MACHINE = 'MACHINE_ID'
def initialize_coresys(loop): def initialize_coresys(loop):
"""Initialize HassIO coresys/objects.""" """Initialize HassIO coresys/objects."""
@ -46,6 +51,9 @@ def initialize_coresys(loop):
# bootstrap config # bootstrap config
initialize_system_data(coresys) initialize_system_data(coresys)
# Set Machine/Host ID
coresys.machine_id = os.environ.get(ENV_MACHINE)
return coresys return coresys
@ -95,6 +103,11 @@ def initialize_system_data(coresys):
_LOGGER.info("Create hassio share folder %s", config.path_share) _LOGGER.info("Create hassio share folder %s", config.path_share)
config.path_share.mkdir() config.path_share.mkdir()
# apparmor folder
if not config.path_apparmor.is_dir():
_LOGGER.info("Create hassio apparmor folder %s", config.path_apparmor)
config.path_apparmor.mkdir()
return config return config
@ -139,8 +152,7 @@ def initialize_logging():
def check_environment(): def check_environment():
"""Check if all environment are exists.""" """Check if all environment are exists."""
# check environment variables # check environment variables
for key in ('SUPERVISOR_SHARE', 'SUPERVISOR_NAME', for key in (ENV_SHARE, ENV_NAME, ENV_REPO):
'HOMEASSISTANT_REPOSITORY'):
try: try:
os.environ[key] os.environ[key]
except KeyError: except KeyError:

View File

@ -25,6 +25,7 @@ ADDONS_DATA = PurePath("addons/data")
BACKUP_DATA = PurePath("backup") BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share") SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp") TMP_DATA = PurePath("tmp")
APPARMOR_DATA = PurePath("apparmor")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
@ -156,6 +157,11 @@ class CoreConfig(JsonConfig):
"""Return root share data folder.""" """Return root share data folder."""
return Path(HASSIO_DATA, SHARE_DATA) return Path(HASSIO_DATA, SHARE_DATA)
@property
def path_apparmor(self):
"""Return root apparmor profile folder."""
return Path(HASSIO_DATA, APPARMOR_DATA)
@property @property
def path_extern_share(self): def path_extern_share(self):
"""Return root share data folder extern for docker.""" """Return root share data folder extern for docker."""

View File

@ -2,11 +2,13 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '107' HASSIO_VERSION = '108'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \ URL_HASSIO_VERSION = \
"https://s3.amazonaws.com/hassio-version/{channel}.json" "https://s3.amazonaws.com/hassio-version/{channel}.json"
URL_HASSIO_APPARMOR = \
"https://s3.amazonaws.com/hassio-version/apparmor.txt"
HASSIO_DATA = Path("/data") HASSIO_DATA = Path("/data")
@ -162,7 +164,6 @@ ATTR_PROTECTED = 'protected'
ATTR_CRYPTO = 'crypto' ATTR_CRYPTO = 'crypto'
ATTR_BRANCH = 'branch' ATTR_BRANCH = 'branch'
ATTR_KERNEL = 'kernel' ATTR_KERNEL = 'kernel'
ATTR_SECCOMP = 'seccomp'
ATTR_APPARMOR = 'apparmor' ATTR_APPARMOR = 'apparmor'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
@ -216,3 +217,4 @@ FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot' FEATURES_REBOOT = 'reboot'
FEATURES_UPDATE = 'update' FEATURES_UPDATE = 'update'
FEATURES_HOSTNAME = 'hostname' FEATURES_HOSTNAME = 'hostname'
FEATURES_SERVICES = 'services'

View File

@ -17,6 +17,7 @@ class CoreSys:
"""Initialize coresys.""" """Initialize coresys."""
# Static attributes # Static attributes
self.exit_code = 0 self.exit_code = 0
self.machine_id = None
# External objects # External objects
self._loop = loop self._loop = loop
@ -266,4 +267,4 @@ class CoreSysAttributes:
"""Mapping to coresys.""" """Mapping to coresys."""
if name.startswith("sys_") and hasattr(self.coresys, name[4:]): if name.startswith("sys_") and hasattr(self.coresys, name[4:]):
return getattr(self.coresys, name[4:]) return getattr(self.coresys, name[4:])
raise AttributeError() raise AttributeError(f"Can't resolve {name} on {self}")

View File

@ -37,3 +37,43 @@ class Systemd(DBusInterface):
Return a coroutine. Return a coroutine.
""" """
return self.dbus.Manager.PowerOff() return self.dbus.Manager.PowerOff()
@dbus_connected
def start_unit(self, unit, mode):
"""Start a systemd service unit.
Return a coroutine.
"""
return self.dbus.Manager.StartUnit(unit, mode)
@dbus_connected
def stop_unit(self, unit, mode):
"""Stop a systemd service unit.
Return a coroutine.
"""
return self.dbus.Manager.StopUnit(unit, mode)
@dbus_connected
def reload_unit(self, unit, mode):
"""Reload a systemd service unit.
Return a coroutine.
"""
return self.dbus.Manager.ReloadOrRestartUnit(unit, mode)
@dbus_connected
def restart_unit(self, unit, mode):
"""Restart a systemd service unit.
Return a coroutine.
"""
return self.dbus.Manager.RestartUnit(unit, mode)
@dbus_connected
def list_units(self):
"""Return a list of available systemd services.
Return a coroutine.
"""
return self.dbus.Manager.ListUnits()

View File

@ -124,18 +124,17 @@ class DockerAddon(DockerInterface):
security = [] security = []
# AppArmor # AppArmor
if self.addon.apparmor == SECURITY_DISABLE: apparmor = self.sys_host.apparmor.available
if not apparmor or self.addon.apparmor == SECURITY_DISABLE:
security.append("apparmor:unconfined") security.append("apparmor:unconfined")
elif self.addon.apparmor == SECURITY_PROFILE: elif self.addon.apparmor == SECURITY_PROFILE:
security.append(f"apparmor={self.addon.slug}") security.append(f"apparmor={self.addon.slug}")
# Seccomp # Disable Seccomp / We don't support it official and it
if self.addon.seccomp == SECURITY_DISABLE: # make troubles on some kind of host systems.
security.append("seccomp=unconfined") security.append("seccomp=unconfined")
elif self.addon.seccomp == SECURITY_PROFILE:
security.append(f"seccomp={self.addon.path_seccomp}")
return security or None return security
@property @property
def tmpfs(self): def tmpfs(self):

View File

@ -6,11 +6,6 @@ class HassioError(Exception):
pass pass
class HassioInternalError(HassioError):
"""Internal Hass.io error they can't handle."""
pass
class HassioNotSupportedError(HassioError): class HassioNotSupportedError(HassioError):
"""Function is not supported.""" """Function is not supported."""
pass pass
@ -28,6 +23,15 @@ class HostNotSupportedError(HassioNotSupportedError):
pass pass
class HostServiceError(HostError):
"""Host service functions fails."""
pass
class HostAppArmorError(HostError):
"""Host apparmor functions fails."""
# utils/gdbus # utils/gdbus
class DBusError(HassioError): class DBusError(HassioError):
@ -47,3 +51,20 @@ class DBusFatalError(DBusError):
class DBusParseError(DBusError): class DBusParseError(DBusError):
"""DBus parse error.""" """DBus parse error."""
pass pass
# util/apparmor
class AppArmorError(HostAppArmorError):
"""General AppArmor error."""
pass
class AppArmorFileError(AppArmorError):
"""AppArmor profile file error."""
pass
class AppArmorInvalidError(AppArmorError):
"""AppArmor profile validate error."""
pass

View File

@ -1,10 +1,18 @@
"""Host function like audio/dbus/systemd.""" """Host function like audio/dbus/systemd."""
from contextlib import suppress
import logging
from .alsa import AlsaAudio from .alsa import AlsaAudio
from .apparmor import AppArmorControl
from .control import SystemControl from .control import SystemControl
from .info import InfoCenter from .info import InfoCenter
from ..const import FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME from .services import ServiceManager
from ..const import (
FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME, FEATURES_SERVICES)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import HassioError
_LOGGER = logging.getLogger(__name__)
class HostManager(CoreSysAttributes): class HostManager(CoreSysAttributes):
@ -14,14 +22,21 @@ class HostManager(CoreSysAttributes):
"""Initialize Host manager.""" """Initialize Host manager."""
self.coresys = coresys self.coresys = coresys
self._alsa = AlsaAudio(coresys) self._alsa = AlsaAudio(coresys)
self._apparmor = AppArmorControl(coresys)
self._control = SystemControl(coresys) self._control = SystemControl(coresys)
self._info = InfoCenter(coresys) self._info = InfoCenter(coresys)
self._services = ServiceManager(coresys)
@property @property
def alsa(self): def alsa(self):
"""Return host ALSA handler.""" """Return host ALSA handler."""
return self._alsa return self._alsa
@property
def apparmor(self):
"""Return host apparmor handler."""
return self._apparmor
@property @property
def control(self): def control(self):
"""Return host control handler.""" """Return host control handler."""
@ -32,6 +47,11 @@ class HostManager(CoreSysAttributes):
"""Return host info handler.""" """Return host info handler."""
return self._info return self._info
@property
def services(self):
"""Return host services handler."""
return self._services
@property @property
def supperted_features(self): def supperted_features(self):
"""Return a list of supported host features.""" """Return a list of supported host features."""
@ -41,6 +61,7 @@ class HostManager(CoreSysAttributes):
features.extend([ features.extend([
FEATURES_REBOOT, FEATURES_REBOOT,
FEATURES_SHUTDOWN, FEATURES_SHUTDOWN,
FEATURES_SERVICES,
]) ])
if self.sys_dbus.hostname.is_connected: if self.sys_dbus.hostname.is_connected:
@ -48,11 +69,21 @@ class HostManager(CoreSysAttributes):
return features return features
async def load(self): async def reload(self):
"""Load host functions.""" """Reload host functions."""
if self.sys_dbus.hostname.is_connected: if self.sys_dbus.hostname.is_connected:
await self.info.update() await self.info.update()
def reload(self): if self.sys_dbus.systemd.is_connected:
"""Reload host information.""" await self.services.update()
return self.load()
async def load(self):
"""Load host information."""
with suppress(HassioError):
await self.reload()
# Load profile data
try:
await self.apparmor.load()
except HassioError as err:
_LOGGER.waring("Load host AppArmor on start fails: %s", err)

View File

@ -81,7 +81,7 @@ class AlsaAudio(CoreSysAttributes):
@staticmethod @staticmethod
def _audio_database(): def _audio_database():
"""Read local json audio data into dict.""" """Read local json audio data into dict."""
json_file = Path(__file__).parent.joinpath('audiodb.json') json_file = Path(__file__).parent.joinpath("data/audiodb.json")
try: try:
# pylint: disable=no-member # pylint: disable=no-member
@ -121,7 +121,7 @@ class AlsaAudio(CoreSysAttributes):
alsa_output = alsa_output or self.default.output alsa_output = alsa_output or self.default.output
# Read Template # Read Template
asound_file = Path(__file__).parent.joinpath('asound.tmpl') asound_file = Path(__file__).parent.joinpath("data/asound.tmpl")
try: try:
# pylint: disable=no-member # pylint: disable=no-member
with asound_file.open('r') as asound: with asound_file.open('r') as asound:

121
hassio/host/apparmor.py Normal file
View File

@ -0,0 +1,121 @@
"""AppArmor control for host."""
import logging
import shutil
from pathlib import Path
from ..coresys import CoreSysAttributes
from ..exceptions import DBusError, HostAppArmorError
from ..utils.apparmor import validate_profile
_LOGGER = logging.getLogger(__name__)
SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'}
class AppArmorControl(CoreSysAttributes):
"""Handle host apparmor controls."""
def __init__(self, coresys):
"""Initialize host power handling."""
self.coresys = coresys
self._profiles = set()
self._service = None
@property
def available(self):
"""Return True if AppArmor is availabe on host."""
return self._service is not None
def exists(self, profile):
"""Return True if a profile exists."""
return profile in self._profiles
async def _reload_service(self):
"""Reload internal service."""
try:
await self.sys_host.services.reload(self._service)
except DBusError as err:
_LOGGER.error("Can't reload %s: %s", self._service, err)
def _get_profile(self, profile_name):
"""Get a profile from AppArmor store."""
if profile_name not in self._profiles:
_LOGGER.error("Can't find %s for removing", profile_name)
raise HostAppArmorError()
return Path(self.sys_config.path_apparmor, profile_name)
async def load(self):
"""Load available profiles."""
for content in self.sys_config.path_apparmor.iterdir():
if not content.is_file():
continue
self._profiles.add(content.name)
# Is connected with systemd?
_LOGGER.info("Load AppArmor Profiles: %s", self._profiles)
for service in SYSTEMD_SERVICES:
if not self.sys_host.services.exists(service):
continue
self._service = service
# Load profiles
if self.available:
await self._reload_service()
else:
_LOGGER.info("AppArmor is not enabled on Host")
async def load_profile(self, profile_name, profile_file):
"""Load/Update a new/exists profile into AppArmor."""
if not validate_profile(profile_name, profile_file):
_LOGGER.error("profile is not valid with name %s", profile_name)
raise HostAppArmorError()
# Copy to AppArmor folder
dest_profile = Path(self.sys_config.path_apparmor, profile_name)
try:
shutil.copy(profile_file, dest_profile)
except OSError as err:
_LOGGER.error("Can't copy %s: %s", profile_file, err)
raise HostAppArmorError() from None
# Load profiles
_LOGGER.info("Add or Update AppArmor profile: %s", profile_name)
self._profiles.add(profile_name)
if self.available:
await self._reload_service()
async def remove_profile(self, profile_name):
"""Remove a AppArmor profile."""
profile_file = self._get_profile(profile_name)
# Only remove file
if not self.available:
try:
profile_file.unlink()
except OSError as err:
_LOGGER.error("Can't remove profile: %s", err)
raise HostAppArmorError()
return
# Marks als remove and start host process
remove_profile = Path(
self.sys_config.path_apparmor, 'remove', profile_name)
try:
profile_file.rename(remove_profile)
except OSError as err:
_LOGGER.error("Can't mark profile as remove: %s", err)
raise HostAppArmorError()
_LOGGER.info("Remove AppArmor profile: %s", profile_name)
self._profiles.remove(profile_name)
await self._reload_service()
def backup_profile(self, profile_name, backup_file):
"""Backup A profile into a new file."""
profile_file = self._get_profile(profile_name)
try:
shutil.copy(profile_file, backup_file)
except OSError as err:
_LOGGER.error("Can't backup profile %s: %s", profile_name, err)
raise HostAppArmorError()

View File

@ -6,6 +6,9 @@ from ..exceptions import HostNotSupportedError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MANAGER = 'manager'
HOSTNAME = 'hostname'
class SystemControl(CoreSysAttributes): class SystemControl(CoreSysAttributes):
"""Handle host power controls.""" """Handle host power controls."""
@ -14,15 +17,19 @@ class SystemControl(CoreSysAttributes):
"""Initialize host power handling.""" """Initialize host power handling."""
self.coresys = coresys self.coresys = coresys
def _check_systemd(self): def _check_dbus(self, flag):
"""Check if systemd is connect or raise error.""" """Check if systemd is connect or raise error."""
if not self.sys_dbus.systemd.is_connected: if flag == MANAGER and self.sys_dbus.systemd.is_connected:
_LOGGER.error("No systemd dbus connection available") return
raise HostNotSupportedError() if flag == HOSTNAME and self.sys_dbus.hostname.is_connected:
return
_LOGGER.error("No %s dbus connection available", flag)
raise HostNotSupportedError()
async def reboot(self): async def reboot(self):
"""Reboot host system.""" """Reboot host system."""
self._check_systemd() self._check_dbus(MANAGER)
_LOGGER.info("Initialize host reboot over systemd") _LOGGER.info("Initialize host reboot over systemd")
try: try:
@ -32,7 +39,7 @@ class SystemControl(CoreSysAttributes):
async def shutdown(self): async def shutdown(self):
"""Shutdown host system.""" """Shutdown host system."""
self._check_systemd() self._check_dbus(MANAGER)
_LOGGER.info("Initialize host power off over systemd") _LOGGER.info("Initialize host power off over systemd")
try: try:
@ -42,9 +49,7 @@ class SystemControl(CoreSysAttributes):
async def set_hostname(self, hostname): async def set_hostname(self, hostname):
"""Set local a new Hostname.""" """Set local a new Hostname."""
if not self.sys_dbus.systemd.is_connected: self._check_dbus(HOSTNAME)
_LOGGER.error("No hostname dbus connection available")
raise HostNotSupportedError()
_LOGGER.info("Set Hostname %s", hostname) _LOGGER.info("Set Hostname %s", hostname)
await self.sys_dbus.hostname.set_static_hostname(hostname) await self.sys_dbus.hostname.set_static_hostname(hostname)

View File

@ -1,4 +1,4 @@
"""Power control for host.""" """Info control for host."""
import logging import logging
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -47,7 +47,7 @@ class InfoCenter(CoreSysAttributes):
async def update(self): async def update(self):
"""Update properties over dbus.""" """Update properties over dbus."""
if not self.sys_dbus.systemd.is_connected: if not self.sys_dbus.hostname.is_connected:
_LOGGER.error("No hostname dbus connection available") _LOGGER.error("No hostname dbus connection available")
raise HostNotSupportedError() raise HostNotSupportedError()

99
hassio/host/services.py Normal file
View File

@ -0,0 +1,99 @@
"""Service control for host."""
import logging
import attr
from ..coresys import CoreSysAttributes
from ..exceptions import HassioError, HostNotSupportedError, HostServiceError
_LOGGER = logging.getLogger(__name__)
MOD_REPLACE = 'replace'
class ServiceManager(CoreSysAttributes):
"""Handle local service information controls."""
def __init__(self, coresys):
"""Initialize system center handling."""
self.coresys = coresys
self._services = set()
def __iter__(self):
"""Iterator trought services."""
return iter(self._services)
def _check_dbus(self, unit=None):
"""Check available dbus connection."""
if not self.sys_dbus.systemd.is_connected:
_LOGGER.error("No systemd dbus connection available")
raise HostNotSupportedError()
if unit and not self.exists(unit):
_LOGGER.error("Unit '%s' not found", unit)
raise HostServiceError()
def start(self, unit):
"""Start a service on host."""
self._check_dbus(unit)
_LOGGER.info("Start local service %s", unit)
return self.sys_dbus.systemd.start_unit(unit, MOD_REPLACE)
def stop(self, unit):
"""Stop a service on host."""
self._check_dbus(unit)
_LOGGER.info("Stop local service %s", unit)
return self.sys_dbus.systemd.stop_unit(unit, MOD_REPLACE)
def reload(self, unit):
"""Reload a service on host."""
self._check_dbus(unit)
_LOGGER.info("Reload local service %s", unit)
return self.sys_dbus.systemd.reload_unit(unit, MOD_REPLACE)
def restart(self, unit):
"""Restart a service on host."""
self._check_dbus(unit)
_LOGGER.info("Restart local service %s", unit)
return self.sys_dbus.systemd.restart_unit(unit, MOD_REPLACE)
def exists(self, unit):
"""Check if a unit exists and return True."""
for service in self._services:
if unit == service.name:
return True
return False
async def update(self):
"""Update properties over dbus."""
self._check_dbus()
_LOGGER.info("Update service information")
self._services.clear()
try:
systemd_units = await self.sys_dbus.systemd.list_units()
for service_data in systemd_units[0]:
if not service_data[0].endswith(".service") or \
service_data[2] != 'loaded':
continue
self._services.add(ServiceInfo.read_from(service_data))
except (HassioError, IndexError):
_LOGGER.warning("Can't update host service information!")
@attr.s(frozen=True)
class ServiceInfo:
"""Represent a single Service."""
name = attr.ib(type=str)
description = attr.ib(type=str)
state = attr.ib(type=str)
@staticmethod
def read_from(unit):
"""Parse data from dbus into this object."""
return ServiceInfo(unit[0], unit[1], unit[3])

View File

@ -39,7 +39,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT, default=600): vol.Optional(ATTR_WAIT_BOOT, default=600):
vol.All(vol.Coerce(int), vol.Range(min=60)), vol.All(vol.Coerce(int), vol.Range(min=60)),

View File

@ -1,8 +1,14 @@
"""HomeAssistant control object.""" """HomeAssistant control object."""
import logging import logging
from pathlib import Path
from tempfile import TemporaryDirectory
import aiohttp
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .docker.supervisor import DockerSupervisor from .docker.supervisor import DockerSupervisor
from .const import URL_HASSIO_APPARMOR
from .exceptions import HostAppArmorError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,6 +52,31 @@ class Supervisor(CoreSysAttributes):
"""Return arch of hass.io containter.""" """Return arch of hass.io containter."""
return self.instance.arch return self.instance.arch
async def update_apparmor(self):
"""Fetch last version and update profile."""
url = URL_HASSIO_APPARMOR
try:
_LOGGER.info("Fetch AppArmor profile %s", url)
async with self.sys_websession.get(url, timeout=10) as request:
data = await request.text()
except aiohttp.ClientError as err:
_LOGGER.warning("Can't fetch AppArmor profile: %s", err)
return
with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_dir:
profile_file = Path(tmp_dir, 'apparmor.txt')
try:
profile_file.write_text(data)
except OSError as err:
_LOGGER.error("Can't write temporary profile: %s", err)
return
try:
await self.sys_host.apparmor.load_profile(
"hassio-supervisor", profile_file)
except HostAppArmorError:
_LOGGER.error("Can't update AppArmor profile!")
async def update(self, version=None): async def update(self, version=None):
"""Update HomeAssistant version.""" """Update HomeAssistant version."""
version = version or self.last_version version = version or self.last_version
@ -56,6 +87,7 @@ class Supervisor(CoreSysAttributes):
_LOGGER.info("Update supervisor to version %s", version) _LOGGER.info("Update supervisor to version %s", version)
if await self.instance.install(version): if await self.instance.install(version):
await self.update_apparmor()
self.sys_loop.call_later(1, self.sys_loop.stop) self.sys_loop.call_later(1, self.sys_loop.stop)
return True return True

View File

@ -41,7 +41,7 @@ class Tasks(CoreSysAttributes):
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(self.sys_scheduler.register_task(
self.sys_snapshots.reload, self.RUN_RELOAD_SNAPSHOTS)) self.sys_snapshots.reload, self.RUN_RELOAD_SNAPSHOTS))
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(self.sys_scheduler.register_task(
self.sys_host.load, self.RUN_RELOAD_HOST)) self.sys_host.reload, self.RUN_RELOAD_HOST))
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(self.sys_scheduler.register_task(
self._watchdog_homeassistant_docker, self._watchdog_homeassistant_docker,

66
hassio/utils/apparmor.py Normal file
View File

@ -0,0 +1,66 @@
"""Some functions around apparmor profiles."""
import logging
import re
from ..exceptions import AppArmorFileError, AppArmorInvalidError
_LOGGER = logging.getLogger(__name__)
RE_PROFILE = re.compile(r"^profile ([^ ]+).*$")
def get_profile_name(profile_file):
"""Read the profile name from file."""
profiles = set()
try:
with profile_file.open('r') as profile_data:
for line in profile_data:
match = RE_PROFILE.match(line)
if not match:
continue
profiles.add(match.group(1))
except OSError as err:
_LOGGER.error("Can't read apparmor profile: %s", err)
raise AppArmorFileError()
if len(profiles) != 1:
_LOGGER.error("To many profiles inside file: %s", profiles)
raise AppArmorInvalidError()
return profiles.pop()
def validate_profile(profile_name, profile_file):
"""Check if profile from file is valid with profile name."""
if profile_name == get_profile_name(profile_file):
return True
return False
def adjust_profile(profile_name, profile_file, profile_new):
"""Fix the profile name."""
org_profile = get_profile_name(profile_file)
profile_data = []
# Process old data
try:
with profile_file.open('r') as profile:
for line in profile:
match = RE_PROFILE.match(line)
if not match:
profile_data.append(line)
else:
profile_data.append(
line.replace(org_profile, profile_name))
except OSError as err:
_LOGGER.error("Can't adjust origin profile: %s", err)
raise AppArmorFileError()
# Write into new file
try:
with profile_new.open('w') as profile:
profile.writelines(profile_data)
except OSError as err:
_LOGGER.error("Can't write new profile: %s", err)
raise AppArmorFileError()

View File

@ -14,10 +14,11 @@ _LOGGER = logging.getLogger(__name__)
RE_GVARIANT_TYPE = re.compile( RE_GVARIANT_TYPE = re.compile(
r"(?:boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|" r"(?:boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|"
r"string|objectpath|signature) ") r"string|objectpath|signature) ")
RE_GVARIANT_TULPE = re.compile(r"^\((.*),\)$")
RE_GVARIANT_VARIANT = re.compile( RE_GVARIANT_VARIANT = re.compile(
r"(?<=(?: |{|\[))<((?:'|\").*?(?:'|\")|\d+(?:\.\d+)?)>(?=(?:|]|}|,))") r"(?<=(?: |{|\[))<((?:'|\").*?(?:'|\")|\d+(?:\.\d+)?)>(?=(?:|]|}|,))")
RE_GVARIANT_STRING = re.compile(r"(?<=(?: |{|\[))'(.*?)'(?=(?:|]|}|,))") RE_GVARIANT_STRING = re.compile(r"(?<=(?: |{|\[|\())'(.*?)'(?=(?:|]|}|,|\)))")
RE_GVARIANT_TUPLE_O = re.compile(r"\"[^\"]*?\"|(\()")
RE_GVARIANT_TUPLE_C = re.compile(r"\"[^\"]*?\"|(,?\))")
# Commands for dbus # Commands for dbus
INTROSPECT = ("gdbus introspect --system --dest {bus} " INTROSPECT = ("gdbus introspect --system --dest {bus} "
@ -76,13 +77,16 @@ class DBus:
def _gvariant(raw): def _gvariant(raw):
"""Parse GVariant input to python.""" """Parse GVariant input to python."""
raw = RE_GVARIANT_TYPE.sub("", raw) raw = RE_GVARIANT_TYPE.sub("", raw)
raw = RE_GVARIANT_TULPE.sub(r"[\1]", raw)
raw = RE_GVARIANT_VARIANT.sub(r"\1", raw) raw = RE_GVARIANT_VARIANT.sub(r"\1", raw)
raw = RE_GVARIANT_STRING.sub(r'"\1"', raw) raw = RE_GVARIANT_STRING.sub(r'"\1"', raw)
raw = RE_GVARIANT_TUPLE_O.sub(
lambda x: x.group(0) if not x.group(1) else"[", raw)
raw = RE_GVARIANT_TUPLE_C.sub(
lambda x: x.group(0) if not x.group(1) else"]", raw)
# No data # No data
if raw.startswith("()"): if raw.startswith("[]"):
return {} return []
try: try:
return json.loads(raw) return json.loads(raw)

View File

@ -17,7 +17,7 @@ RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
ALSA_DEVICE = vol.Any(None, vol.Match(r"\d+,\d+")) ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
@ -87,7 +87,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT, default=600): vol.Optional(ATTR_WAIT_BOOT, default=600):

@ -1 +1 @@
Subproject commit b89de4b494e0c2ad3d0c8b9280fafae40aa66327 Subproject commit 626b05454031523c1208515afe5d3012c458f32d

View File

@ -42,7 +42,7 @@ setup(
install_requires=[ install_requires=[
'attr==0.3.1', 'attr==0.3.1',
'async_timeout==3.0.0', 'async_timeout==3.0.0',
'aiohttp==3.2.1', 'aiohttp==3.3.2',
'docker==3.3.0', 'docker==3.3.0',
'colorlog==3.1.2', 'colorlog==3.1.2',
'voluptuous==0.11.1', 'voluptuous==0.11.1',

View File

@ -1,4 +1,4 @@
{ {
"hassio": "105", "hassio": "108",
"homeassistant": "0.70.0" "homeassistant": "0.70.0"
} }