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

View File

@ -9,7 +9,6 @@ from .discovery import APIDiscovery
from .homeassistant import APIHomeAssistant
from .hardware import APIHardware
from .host import APIHost
from .network import APINetwork
from .proxy import APIProxy
from .supervisor import APISupervisor
from .snapshots import APISnapshots
@ -44,7 +43,6 @@ class RestAPI(CoreSysAttributes):
self._register_panel()
self._register_addons()
self._register_snapshots()
self._register_network()
self._register_discovery()
self._register_services()
@ -61,16 +59,6 @@ class RestAPI(CoreSysAttributes):
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):
"""Register hardware function."""
api_hardware = APIHardware()

View File

@ -7,7 +7,7 @@ import voluptuous as vol
from .utils import api_process, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
ATTR_OS)
ATTR_OPERATING_SYSTEM, ATTR_KERNEL, ATTR_CHASSIS)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
@ -16,6 +16,10 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_HOSTNAME): vol.Coerce(str),
})
class APIHost(CoreSysAttributes):
"""Handle rest api for host functions."""
@ -24,14 +28,25 @@ class APIHost(CoreSysAttributes):
async def info(self, request):
"""Return host information."""
return {
ATTR_TYPE: ,
ATTR_VERSION: ,
ATTR_LAST_VERSION: ,
ATTR_TYPE: None,
ATTR_CHASSIS: self.sys_host.local.chassis,
ATTR_VERSION: None,
ATTR_LAST_VERSION: None,
ATTR_FEATURES: self.sys_host.features,
ATTR_HOSTNAME: ,
ATTR_OS: ,
ATTR_HOSTNAME: self.sys_host.local.hostname,
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
def reboot(self, request):
"""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_TIMEZONE = 'timezone'
ATTR_ARGS = 'args'
ATTR_OS = 'os'
ATTR_OPERATING_SYSTEM = 'operating_system'
ATTR_CHASSIS = 'chassis'
ATTR_TYPE = 'type'
ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features'
@ -159,6 +160,7 @@ ATTR_DISCOVERY = 'discovery'
ATTR_PROTECTED = 'protected'
ATTR_CRYPTO = 'crypto'
ATTR_BRANCH = 'branch'
ATTR_KERNEL = 'kernel'
ATTR_SECCOMP = 'seccomp'
ATTR_APPARMOR = 'apparmor'
@ -213,5 +215,3 @@ FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot'
FEATURES_UPDATE = 'update'
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)
except DBusError:
_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 .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
@ -14,6 +15,7 @@ class HostManager(CoreSysAttributes):
self.coresys = coresys
self._alsa = AlsaAudio(coresys)
self._power = PowerControl(coresys)
self._local = LocalCenter(coresys)
@property
def alsa(self):
@ -25,6 +27,11 @@ class HostManager(CoreSysAttributes):
"""Return host power handler."""
return self._power
@property
def local(self):
"""Return host local handler."""
return self._local
@property
def supperted_features(self):
"""Return a list of supported host features."""
@ -36,12 +43,16 @@ class HostManager(CoreSysAttributes):
FEATURES_SHUTDOWN,
])
if self.sys_dbus.hostname.is_connected:
features.append(FEATURES_HOSTNAME)
return features
async def load(self):
"""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."""
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."""
self.coresys = coresys
def _check_systemd(self):
def _check_dbus(self):
"""Check if systemd is connect or raise error."""
if not self.sys_dbus.systemd.is_connected:
_LOGGER.error("No systemd dbus connection available")
@ -22,7 +22,7 @@ class PowerControl(CoreSysAttributes):
async def reboot(self):
"""Reboot host system."""
self._check_systemd()
self._check_dbus()
_LOGGER.info("Initialize host reboot over systemd")
try:
@ -32,7 +32,7 @@ class PowerControl(CoreSysAttributes):
async def shutdown(self):
"""Shutdown host system."""
self._check_systemd()
self._check_dbus()
_LOGGER.info("Initialize host power off over systemd")
try:

View File

@ -10,15 +10,22 @@ from ..exceptions import DBusFatalError, DBusParseError
_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_VARIANT = re.compile(
r"(?<=(?: |{|\[))<((?:'|\").*?(?:'|\")|\d+(?:\.\d+)?)>(?=(?:|]|}|,))")
RE_GVARIANT_STRING = re.compile(r"(?<=(?: |{|\[))'(.*?)'(?=(?:|]|}|,))")
# Commands for dbus
INTROSPECT = ("gdbus introspect --system --dest {bus} "
"--object-path {obj} --xml")
CALL = ("gdbus call --system --dest {bus} --object-path {inf} "
"--method {inf}.{method} {args}")
"--object-path {object} --xml")
CALL = ("gdbus call --system --dest {bus} --object-path {object} "
"--method {method} {args}")
DBUS_METHOD_GETALL = 'org.freedesktop.DBus.Properties.GetAll'
class DBus:
@ -28,7 +35,7 @@ class DBus:
"""Initialize dbus object."""
self.bus_name = bus_name
self.object_path = object_path
self.data = {}
self.methods = set()
@staticmethod
async def connect(bus_name, object_path):
@ -36,14 +43,14 @@ class DBus:
self = DBus(bus_name, object_path)
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
async def _init_proxy(self):
"""Read object data."""
"""Read interface data."""
command = shlex.split(INTROSPECT.format(
bus=self.bus_name,
obj=self.object_path
object=self.object_path
))
# Ask data
@ -59,14 +66,15 @@ class DBus:
# Read available methods
for interface in xml.findall("/node/interface"):
methods = set()
interface_name = interface.get('name')
for method in interface.findall("/method"):
methods.add(method.get('name'))
self.data[interface.get('name')] = methods
method_name = method.get('name')
self.methods.add(f"{interface_name}.{method_name}")
@staticmethod
def _gvariant(raw):
"""Parse GVariant input to python."""
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_STRING.sub(r"\"\1\"", raw)
@ -74,25 +82,32 @@ class DBus:
try:
return json.loads(raw)
except json.JSONDecodeError as err:
_LOGGER.error("Can't parse '%s': %s", raw, err)
_LOGGER.error("Can't parse '%s': %s", raw, err)
raise DBusParseError() from None
async def call_dbus(self, interface, method, *args):
async def call_dbus(self, method, *args):
"""Call a dbus method."""
command = shlex.split(CALL.format(
bus=self.bus_name,
inf=interface,
object=self.object_path,
method=method,
args=" ".join(map(str, args))
))
# 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)
# Parse and return 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):
"""Send command over dbus."""
# Run command
@ -117,13 +132,9 @@ class DBus:
# End
return data.decode()
def __getattr__(self, interface):
def __getattr__(self, name):
"""Mapping to dbus method."""
interface = f"{self.object_path}.{interface}"
if interface not in self.data:
raise AttributeError()
return DBusCallWrapper(self, interface)
return getattr(DBusCallWrapper(self, self.object_path), name)
class DBusCallWrapper:
@ -134,16 +145,23 @@ class DBusCallWrapper:
self.dbus = dbus
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):
"""Mapping to dbus method."""
if name not in self.dbus.data[self.interface]:
raise AttributeError()
interface = f"{self.interface}.{name}"
if interface not in self.dbus.methods:
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args):
"""Wrap method.
Return a coroutine
"""
return self.dbus.call_dbus(self.interface, self.name, *args)
return self.dbus.call_dbus(self.interface, *args)
return _method_wrapper