From 0bb81136bb53ec78761d9e9f168a79e1b4615026 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 24 Apr 2018 23:38:40 +0200 Subject: [PATCH] Add hostname function --- API.md | 38 ++++++++------------ hassio/api/__init__.py | 12 ------- hassio/api/host.py | 27 ++++++++++---- hassio/api/network.py | 38 -------------------- hassio/const.py | 6 ++-- hassio/dbus/hostname.py | 16 +++++++++ hassio/dbus/networkmanager.py | 1 - hassio/dbus/rauc.py | 1 - hassio/host/__init__.py | 19 +++++++--- hassio/host/local.py | 67 +++++++++++++++++++++++++++++++++++ hassio/host/power.py | 6 ++-- hassio/utils/gdbus.py | 64 +++++++++++++++++++++------------ 12 files changed, 181 insertions(+), 114 deletions(-) delete mode 100644 hassio/api/network.py delete mode 100644 hassio/dbus/networkmanager.py delete mode 100644 hassio/dbus/rauc.py create mode 100644 hassio/host/local.py diff --git a/API.md b/API.md index bd3f72360..9b78563d9 100644 --- a/API.md +++ b/API.md @@ -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` diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 2fbf7143b..20195d21c 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -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() diff --git a/hassio/api/host.py b/hassio/api/host.py index 7285412d9..35035fd4d 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -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.""" diff --git a/hassio/api/network.py b/hassio/api/network.py deleted file mode 100644 index c5c647066..000000000 --- a/hassio/api/network.py +++ /dev/null @@ -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 diff --git a/hassio/const.py b/hassio/const.py index 7cdf21bb4..ad5b26cb4 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -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' diff --git a/hassio/dbus/hostname.py b/hassio/dbus/hostname.py index 548c07abe..793abf7ed 100644 --- a/hassio/dbus/hostname.py +++ b/hassio/dbus/hostname.py @@ -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) diff --git a/hassio/dbus/networkmanager.py b/hassio/dbus/networkmanager.py deleted file mode 100644 index a677e893d..000000000 --- a/hassio/dbus/networkmanager.py +++ /dev/null @@ -1 +0,0 @@ -"""Interface to NetworkManager over dbus.""" diff --git a/hassio/dbus/rauc.py b/hassio/dbus/rauc.py deleted file mode 100644 index 62098b393..000000000 --- a/hassio/dbus/rauc.py +++ /dev/null @@ -1 +0,0 @@ -"""Interface to Rauc OTA over dbus.""" diff --git a/hassio/host/__init__.py b/hassio/host/__init__.py index 70e2b9d43..659b4cdf5 100644 --- a/hassio/host/__init__.py +++ b/hassio/host/__init__.py @@ -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() diff --git a/hassio/host/local.py b/hassio/host/local.py new file mode 100644 index 000000000..ab41be753 --- /dev/null +++ b/hassio/host/local.py @@ -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() diff --git a/hassio/host/power.py b/hassio/host/power.py index 4e9cb3405..f620e15b2 100644 --- a/hassio/host/power.py +++ b/hassio/host/power.py @@ -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: diff --git a/hassio/utils/gdbus.py b/hassio/utils/gdbus.py index 8398ca93e..69599debf 100644 --- a/hassio/utils/gdbus.py +++ b/hassio/utils/gdbus.py @@ -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