Extend Systemd Support / Apparmor (#506)

* Update systemd.py

* Update control.py

* Update control.py

* Create service.py

* Update info.py

* Rename hassio/host/asound.tmpl to hassio/host/data/asound.tmpl

* Rename hassio/host/audiodb.json to hassio/host/data/audiodb.json

* Update alsa.py

* Update alsa.py

* Update control.py

* Fix

* Enable call

* fix

* fix args

* Fix gdbus

* parse service data

* Change handling

* Fix states

* Fix parser for tuples

* Fix parser v2

* Fix tuple handling

* Fix regex string handling

* Faster tuple finder

* fix empty detector

* wrong order

* Finish

* fix lint

* better filtering

* fix match

* Fix mode string
This commit is contained in:
Pascal Vizeli 2018-06-17 02:07:12 +02:00 committed by GitHub
parent 96f47a4c32
commit 561e80c2be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 257 additions and 29 deletions

31
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`
@ -569,14 +590,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

@ -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):

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))

View File

@ -216,3 +216,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

@ -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

@ -28,6 +28,11 @@ class HostNotSupportedError(HassioNotSupportedError):
pass pass
class HostServiceError(HostError):
"""Host service functions fails."""
pass
# utils/gdbus # utils/gdbus
class DBusError(HassioError): class DBusError(HassioError):

View File

@ -3,7 +3,9 @@
from .alsa import AlsaAudio from .alsa import AlsaAudio
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 .service import ServiceManager
from ..const import (
FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME, FEATURES_SERVICES)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -16,6 +18,7 @@ class HostManager(CoreSysAttributes):
self._alsa = AlsaAudio(coresys) self._alsa = AlsaAudio(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):
@ -32,6 +35,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 +49,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:
@ -53,6 +62,9 @@ class HostManager(CoreSysAttributes):
if self.sys_dbus.hostname.is_connected: if self.sys_dbus.hostname.is_connected:
await self.info.update() await self.info.update()
if self.sys_dbus.systemd.is_connected:
await self.services.update()
def reload(self): def reload(self):
"""Reload host information.""" """Reload host information."""
return self.load() return self.load()

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:

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/service.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

@ -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)