Add hostname function

This commit is contained in:
Pascal Vizeli 2018-04-24 23:38:40 +02:00
parent 2a81ced817
commit 0bb81136bb
12 changed files with 181 additions and 114 deletions

38
API.md
View File

@ -217,8 +217,11 @@ return:
### Host ### Host
- POST `/host/reload` - POST `/host/reload`
- POST `/host/shutdown` - POST `/host/shutdown`
- POST `/host/reboot` - POST `/host/reboot`
- GET `/host/info` - GET `/host/info`
```json ```json
@ -228,14 +231,21 @@ return:
"last_version": "", "last_version": "",
"features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"], "features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"],
"hostname": "", "hostname": "",
"os": "", "operating_system": "",
"audio": { "kernel": "",
"input": "0,0", "chassis": ""
"output": "0,0"
}
} }
``` ```
- POST `/host/options`
```json
{
"hostname": "",
}
```
- POST `/host/update` - POST `/host/update`
Optional: Optional:
@ -284,24 +294,6 @@ Optional:
} }
``` ```
### Network
- GET `/network/info`
```json
{
"hostname": ""
}
```
- POST `/network/options`
```json
{
"hostname": "",
}
```
### Home Assistant ### Home Assistant
- GET `/homeassistant/info` - GET `/homeassistant/info`

View File

@ -9,7 +9,6 @@ from .discovery import APIDiscovery
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .hardware import APIHardware from .hardware import APIHardware
from .host import APIHost from .host import APIHost
from .network import APINetwork
from .proxy import APIProxy from .proxy import APIProxy
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .snapshots import APISnapshots from .snapshots import APISnapshots
@ -44,7 +43,6 @@ class RestAPI(CoreSysAttributes):
self._register_panel() self._register_panel()
self._register_addons() self._register_addons()
self._register_snapshots() self._register_snapshots()
self._register_network()
self._register_discovery() self._register_discovery()
self._register_services() self._register_services()
@ -61,16 +59,6 @@ class RestAPI(CoreSysAttributes):
web.post('/host/reload', api_host.reload), web.post('/host/reload', api_host.reload),
]) ])
def _register_network(self):
"""Register network function."""
api_net = APINetwork()
api_net.coresys = self.coresys
self.webapp.add_routes([
web.get('/network/info', api_net.info),
web.post('/network/options', api_net.options),
])
def _register_hardware(self): def _register_hardware(self):
"""Register hardware function.""" """Register hardware function."""
api_hardware = APIHardware() api_hardware = APIHardware()

View File

@ -7,7 +7,7 @@ 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_TYPE, ATTR_HOSTNAME, ATTR_FEATURES, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
ATTR_OS) ATTR_OPERATING_SYSTEM, ATTR_KERNEL, ATTR_CHASSIS)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -16,6 +16,10 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_VERSION): vol.Coerce(str),
}) })
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_HOSTNAME): vol.Coerce(str),
})
class APIHost(CoreSysAttributes): class APIHost(CoreSysAttributes):
"""Handle rest api for host functions.""" """Handle rest api for host functions."""
@ -24,14 +28,25 @@ class APIHost(CoreSysAttributes):
async def info(self, request): async def info(self, request):
"""Return host information.""" """Return host information."""
return { return {
ATTR_TYPE: , ATTR_TYPE: None,
ATTR_VERSION: , ATTR_CHASSIS: self.sys_host.local.chassis,
ATTR_LAST_VERSION: , ATTR_VERSION: None,
ATTR_LAST_VERSION: None,
ATTR_FEATURES: self.sys_host.features, ATTR_FEATURES: self.sys_host.features,
ATTR_HOSTNAME: , ATTR_HOSTNAME: self.sys_host.local.hostname,
ATTR_OS: , ATTR_OPERATING_SYSTEM: self.sys_host.local.operating_system,
ATTR_KERNEL: self.sys_host.local.kernel,
} }
@api_process
async def options(self, request):
"""Edit host settings."""
body = await api_validate(SCHEMA_OPTIONS, request)
# hostname
if ATTR_HOSTNAME in body:
await self.sys_host.local.set_hostname(body[ATTR_HOSTNAME])
@api_process @api_process
def reboot(self, request): def reboot(self, request):
"""Reboot host.""" """Reboot host."""

View File

@ -1,38 +0,0 @@
"""Init file for HassIO network rest api."""
import logging
import voluptuous as vol
from .utils import api_process, api_process_hostcontrol, api_validate
from ..const import ATTR_HOSTNAME
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_HOSTNAME): vol.Coerce(str),
})
class APINetwork(CoreSysAttributes):
"""Handle rest api for network functions."""
@api_process
async def info(self, request):
"""Show network settings."""
return {
ATTR_HOSTNAME: self._host_control.hostname,
}
@api_process_hostcontrol
async def options(self, request):
"""Edit network settings."""
body = await api_validate(SCHEMA_OPTIONS, request)
# hostname
if ATTR_HOSTNAME in body:
if self._host_control.hostname != body[ATTR_HOSTNAME]:
await self._host_control.set_hostname(body[ATTR_HOSTNAME])
return True

View File

@ -61,7 +61,8 @@ ATTR_LONG_DESCRIPTION = 'long_description'
ATTR_HOSTNAME = 'hostname' ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone' ATTR_TIMEZONE = 'timezone'
ATTR_ARGS = 'args' ATTR_ARGS = 'args'
ATTR_OS = 'os' ATTR_OPERATING_SYSTEM = 'operating_system'
ATTR_CHASSIS = 'chassis'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
ATTR_SOURCE = 'source' ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features' ATTR_FEATURES = 'features'
@ -159,6 +160,7 @@ ATTR_DISCOVERY = 'discovery'
ATTR_PROTECTED = 'protected' ATTR_PROTECTED = 'protected'
ATTR_CRYPTO = 'crypto' ATTR_CRYPTO = 'crypto'
ATTR_BRANCH = 'branch' ATTR_BRANCH = 'branch'
ATTR_KERNEL = 'kernel'
ATTR_SECCOMP = 'seccomp' ATTR_SECCOMP = 'seccomp'
ATTR_APPARMOR = 'apparmor' ATTR_APPARMOR = 'apparmor'
@ -213,5 +215,3 @@ FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot' FEATURES_REBOOT = 'reboot'
FEATURES_UPDATE = 'update' FEATURES_UPDATE = 'update'
FEATURES_HOSTNAME = 'hostname' FEATURES_HOSTNAME = 'hostname'
FEATURES_NETWORK_INFO = 'network_info'
FEATURES_NETWORK_CONTROL = 'network_control'

View File

@ -21,3 +21,19 @@ class Hostname(DBusInterface):
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to hostname") _LOGGER.warning("Can't connect to hostname")
@dbus_connected
def set_hostname(self, hostname):
"""Change local hostname.
Return a coroutine.
"""
return self.dbus.SetHostname(hostname)
@dbus_connected
def get_properties(self):
"""Return local host informations.
Return a coroutine.
"""
return self.dbus.get_properties(DBUS_NAME)

View File

@ -1 +0,0 @@
"""Interface to NetworkManager over dbus."""

View File

@ -1 +0,0 @@
"""Interface to Rauc OTA over dbus."""

View File

@ -2,7 +2,8 @@
from .alsa import AlsaAudio from .alsa import AlsaAudio
from .power import PowerControl from .power import PowerControl
from ..const import FEATURES_REBOOT, FEATURES_SHUTDOWN from .local import LocalCenter
from ..const import FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -14,6 +15,7 @@ class HostManager(CoreSysAttributes):
self.coresys = coresys self.coresys = coresys
self._alsa = AlsaAudio(coresys) self._alsa = AlsaAudio(coresys)
self._power = PowerControl(coresys) self._power = PowerControl(coresys)
self._local = LocalCenter(coresys)
@property @property
def alsa(self): def alsa(self):
@ -25,6 +27,11 @@ class HostManager(CoreSysAttributes):
"""Return host power handler.""" """Return host power handler."""
return self._power return self._power
@property
def local(self):
"""Return host local handler."""
return self._local
@property @property
def supperted_features(self): def supperted_features(self):
"""Return a list of supported host features.""" """Return a list of supported host features."""
@ -36,12 +43,16 @@ class HostManager(CoreSysAttributes):
FEATURES_SHUTDOWN, FEATURES_SHUTDOWN,
]) ])
if self.sys_dbus.hostname.is_connected:
features.append(FEATURES_HOSTNAME)
return features return features
async def load(self): async def load(self):
"""Load host functions.""" """Load host functions."""
pass if self.sys_dbus.hostname.is_connected:
await self.local.update()
async def reload(self): def reload(self):
"""Reload host information.""" """Reload host information."""
pass return self.load()

67
hassio/host/local.py Normal file
View File

@ -0,0 +1,67 @@
"""Power control for host."""
import logging
from ..coresys import CoreSysAttributes
from ..exceptions import HassioError, HostNotSupportedError
_LOGGER = logging.getLogger(__name__)
UNKNOWN = 'Unknown'
class LocalCenter(CoreSysAttributes):
"""Handle local system information controls."""
def __init__(self, coresys):
"""Initialize system center handling."""
self.coresys = coresys
self._data = {}
@property
def hostname(self):
"""Return local hostname."""
return self._data.get('Hostname', UNKNOWN)
@property
def chassis(self):
"""Return local chassis type."""
return self._data.get('Chassis', UNKNOWN)
@property
def kernel(self):
"""Return local kernel version."""
return self._data.get('KernelRelease', UNKNOWN)
@property
def operating_system(self):
"""Return local operating system."""
return self._data.get('OperatingSystemPrettyName', UNKNOWN)
@property
def cpe(self):
"""Return local CPE."""
return self._data.get('OperatingSystemCPEName', UNKNOWN)
def _check_dbus(self):
"""Check if systemd is connect or raise error."""
if not self.sys_dbus.hostname.is_connected:
_LOGGER.error("No hostname dbus connection available")
raise HostNotSupportedError()
async def update(self):
"""Update properties over dbus."""
self._check_dbus()
_LOGGER.info("Update local host information")
try:
self._data = await self.sys_dbus.hostname.get_properties()
except HassioError:
_LOGGER.warning("Can't update host system information!")
async def set_hostname(self, hostname):
"""Set local a new Hostname."""
self._check_dbus()
_LOGGER.info("Set Hostname %s", hostname)
await self.sys_dbus.hostname.set_hostname(hostname)
await self.update()

View File

@ -14,7 +14,7 @@ class PowerControl(CoreSysAttributes):
"""Initialize host power handling.""" """Initialize host power handling."""
self.coresys = coresys self.coresys = coresys
def _check_systemd(self): def _check_dbus(self):
"""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 not self.sys_dbus.systemd.is_connected:
_LOGGER.error("No systemd dbus connection available") _LOGGER.error("No systemd dbus connection available")
@ -22,7 +22,7 @@ class PowerControl(CoreSysAttributes):
async def reboot(self): async def reboot(self):
"""Reboot host system.""" """Reboot host system."""
self._check_systemd() self._check_dbus()
_LOGGER.info("Initialize host reboot over systemd") _LOGGER.info("Initialize host reboot over systemd")
try: try:
@ -32,7 +32,7 @@ class PowerControl(CoreSysAttributes):
async def shutdown(self): async def shutdown(self):
"""Shutdown host system.""" """Shutdown host system."""
self._check_systemd() self._check_dbus()
_LOGGER.info("Initialize host power off over systemd") _LOGGER.info("Initialize host power off over systemd")
try: try:

View File

@ -10,15 +10,22 @@ from ..exceptions import DBusFatalError, DBusParseError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Use to convert GVariant into json
RE_GVARIANT_TYPE = re.compile(
r"(?:boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|"
r"string|objectpath|signature) ")
RE_GVARIANT_TULPE = re.compile(r"^\((.*),\)$") 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"(?<=(?: |{|\[))'(.*?)'(?=(?:|]|}|,))")
# Commands for dbus
INTROSPECT = ("gdbus introspect --system --dest {bus} " INTROSPECT = ("gdbus introspect --system --dest {bus} "
"--object-path {obj} --xml") "--object-path {object} --xml")
CALL = ("gdbus call --system --dest {bus} --object-path {inf} " CALL = ("gdbus call --system --dest {bus} --object-path {object} "
"--method {inf}.{method} {args}") "--method {method} {args}")
DBUS_METHOD_GETALL = 'org.freedesktop.DBus.Properties.GetAll'
class DBus: class DBus:
@ -28,7 +35,7 @@ class DBus:
"""Initialize dbus object.""" """Initialize dbus object."""
self.bus_name = bus_name self.bus_name = bus_name
self.object_path = object_path self.object_path = object_path
self.data = {} self.methods = set()
@staticmethod @staticmethod
async def connect(bus_name, object_path): async def connect(bus_name, object_path):
@ -36,14 +43,14 @@ class DBus:
self = DBus(bus_name, object_path) self = DBus(bus_name, object_path)
self._init_proxy() # pylint: disable=protected-access self._init_proxy() # pylint: disable=protected-access
_LOGGER.info("Connect to dbus: %s", bus_name) _LOGGER.info("Connect to dbus: %s - %s", bus_name, object_path)
return self return self
async def _init_proxy(self): async def _init_proxy(self):
"""Read object data.""" """Read interface data."""
command = shlex.split(INTROSPECT.format( command = shlex.split(INTROSPECT.format(
bus=self.bus_name, bus=self.bus_name,
obj=self.object_path object=self.object_path
)) ))
# Ask data # Ask data
@ -59,14 +66,15 @@ class DBus:
# Read available methods # Read available methods
for interface in xml.findall("/node/interface"): for interface in xml.findall("/node/interface"):
methods = set() interface_name = interface.get('name')
for method in interface.findall("/method"): for method in interface.findall("/method"):
methods.add(method.get('name')) method_name = method.get('name')
self.data[interface.get('name')] = methods self.methods.add(f"{interface_name}.{method_name}")
@staticmethod @staticmethod
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_TULPE.sub(r"[\1]", 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)
@ -77,22 +85,29 @@ class DBus:
_LOGGER.error("Can't parse '%s': %s", raw, err) _LOGGER.error("Can't parse '%s': %s", raw, err)
raise DBusParseError() from None raise DBusParseError() from None
async def call_dbus(self, interface, method, *args): async def call_dbus(self, method, *args):
"""Call a dbus method.""" """Call a dbus method."""
command = shlex.split(CALL.format( command = shlex.split(CALL.format(
bus=self.bus_name, bus=self.bus_name,
inf=interface, object=self.object_path,
method=method, method=method,
args=" ".join(map(str, args)) args=" ".join(map(str, args))
)) ))
# Run command # Run command
_LOGGER.info("Call %s no %s", method, interface) _LOGGER.info("Call %s on %s", method, self.object_path)
data = await self._send(command) data = await self._send(command)
# Parse and return data # Parse and return data
return self._gvariant(data) return self._gvariant(data)
def get_properties(self, interface):
"""Read all properties from interface.
Return a coroutine.
"""
return self.call_dbus(DBUS_METHOD_GETALL, interface)
async def _send(self, command): async def _send(self, command):
"""Send command over dbus.""" """Send command over dbus."""
# Run command # Run command
@ -117,13 +132,9 @@ class DBus:
# End # End
return data.decode() return data.decode()
def __getattr__(self, interface): def __getattr__(self, name):
"""Mapping to dbus method.""" """Mapping to dbus method."""
interface = f"{self.object_path}.{interface}" return getattr(DBusCallWrapper(self, self.object_path), name)
if interface not in self.data:
raise AttributeError()
return DBusCallWrapper(self, interface)
class DBusCallWrapper: class DBusCallWrapper:
@ -134,16 +145,23 @@ class DBusCallWrapper:
self.dbus = dbus self.dbus = dbus
self.interface = interface self.interface = interface
def __call__(self):
"""Should never be called."""
_LOGGER.error("DBus method %s not exists!", self.interface)
raise DBusFatalError()
def __getattr__(self, name): def __getattr__(self, name):
"""Mapping to dbus method.""" """Mapping to dbus method."""
if name not in self.dbus.data[self.interface]: interface = f"{self.interface}.{name}"
raise AttributeError()
if interface not in self.dbus.methods:
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args): def _method_wrapper(*args):
"""Wrap method. """Wrap method.
Return a coroutine Return a coroutine
""" """
return self.dbus.call_dbus(self.interface, self.name, *args) return self.dbus.call_dbus(self.interface, *args)
return _method_wrapper return _method_wrapper