mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-28 11:36:32 +00:00
commit
7d52b3ba01
32
API.md
32
API.md
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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(),
|
||||||
|
@ -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()
|
|
||||||
|
@ -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,
|
||||||
|
@ -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))
|
||||||
|
1
hassio/api/panel/chunk.05994812bec7a524b566.js
Normal file
1
hassio/api/panel/chunk.05994812bec7a524b566.js
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/api/panel/chunk.05994812bec7a524b566.js.gz
Normal file
BIN
hassio/api/panel/chunk.05994812bec7a524b566.js.gz
Normal file
Binary file not shown.
Binary file not shown.
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
BIN
hassio/api/panel/chunk.ff92199b0d422767d108.js.gz
Normal file
BIN
hassio/api/panel/chunk.ff92199b0d422767d108.js.gz
Normal file
Binary file not shown.
@ -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.
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
@ -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'
|
||||||
|
@ -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}")
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
121
hassio/host/apparmor.py
Normal 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()
|
@ -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)
|
||||||
|
@ -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
99
hassio/host/services.py
Normal 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])
|
@ -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)),
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
66
hassio/utils/apparmor.py
Normal 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()
|
@ -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)
|
||||||
|
@ -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
|
2
setup.py
2
setup.py
@ -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',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"hassio": "105",
|
"hassio": "108",
|
||||||
"homeassistant": "0.70.0"
|
"homeassistant": "0.70.0"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user