Make check_port an async function (#4677)

* Make check_port asyncio

This requires to change the ingress_port property to a async method.

* Avoid using wait_for

* Add missing async

* Really await

* Set dynamic ingress port on add-on installation/update

* Fix pytest issue

* Rename async_check_port back to check_port

* Raise RuntimeError in case port is not set

* Make sure port gets set on add-on restore

* Drop unnecessary async

* Simplify check_port by using asyncio.get_running_loop()
This commit is contained in:
Stefan Agner 2023-12-05 21:49:35 +01:00 committed by GitHub
parent c2d4be3304
commit 883e54f989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 49 additions and 35 deletions

View File

@ -395,7 +395,7 @@ class Addon(AddonModel):
port = self.data[ATTR_INGRESS_PORT] port = self.data[ATTR_INGRESS_PORT]
if port == 0: if port == 0:
return self.sys_ingress.get_dynamic_port(self.slug) raise RuntimeError(f"No port set for add-on {self.slug}")
return port return port
@property @property
@ -539,7 +539,7 @@ class Addon(AddonModel):
# TCP monitoring # TCP monitoring
if s_prefix == "tcp": if s_prefix == "tcp":
return await self.sys_run_in_executor(check_port, self.ip_address, port) return await check_port(self.ip_address, port)
# lookup the correct protocol from config # lookup the correct protocol from config
if t_proto: if t_proto:
@ -602,6 +602,16 @@ class Addon(AddonModel):
_LOGGER.info("Removing add-on data folder %s", self.path_data) _LOGGER.info("Removing add-on data folder %s", self.path_data)
await remove_data(self.path_data) await remove_data(self.path_data)
async def _check_ingress_port(self):
"""Assign a ingress port if dynamic port selection is used."""
if not self.with_ingress:
return
if self.data[ATTR_INGRESS_PORT] == 0:
self.data[ATTR_INGRESS_PORT] = await self.sys_ingress.get_dynamic_port(
self.slug
)
@Job( @Job(
name="addon_install", name="addon_install",
limit=JobExecutionLimit.GROUP_ONCE, limit=JobExecutionLimit.GROUP_ONCE,
@ -630,6 +640,8 @@ class Addon(AddonModel):
self.sys_addons.data.uninstall(self) self.sys_addons.data.uninstall(self)
raise AddonsError() from err raise AddonsError() from err
await self._check_ingress_port()
# Add to addon manager # Add to addon manager
self.sys_addons.local[self.slug] = self self.sys_addons.local[self.slug] = self
@ -716,6 +728,7 @@ class Addon(AddonModel):
try: try:
_LOGGER.info("Add-on '%s' successfully updated", self.slug) _LOGGER.info("Add-on '%s' successfully updated", self.slug)
self.sys_addons.data.update(store) self.sys_addons.data.update(store)
await self._check_ingress_port()
# Cleanup # Cleanup
with suppress(DockerError): with suppress(DockerError):
@ -756,6 +769,7 @@ class Addon(AddonModel):
raise AddonsError() from err raise AddonsError() from err
self.sys_addons.data.update(self.addon_store) self.sys_addons.data.update(self.addon_store)
await self._check_ingress_port()
_LOGGER.info("Add-on '%s' successfully rebuilt", self.slug) _LOGGER.info("Add-on '%s' successfully rebuilt", self.slug)
finally: finally:
@ -1199,6 +1213,7 @@ class Addon(AddonModel):
_LOGGER.info("Restore/Update of image for addon %s", self.slug) _LOGGER.info("Restore/Update of image for addon %s", self.slug)
with suppress(DockerError): with suppress(DockerError):
await self.instance.update(version, restore_image) await self.instance.update(version, restore_image)
self._check_ingress_port()
# Restore data and config # Restore data and config
def _restore_data(): def _restore_data():

View File

@ -140,8 +140,7 @@ class HomeAssistantAPI(CoreSysAttributes):
return None return None
# Check if port is up # Check if port is up
if not await self.sys_run_in_executor( if not await check_port(
check_port,
self.sys_homeassistant.ip_address, self.sys_homeassistant.ip_address,
self.sys_homeassistant.api_port, self.sys_homeassistant.api_port,
): ):

View File

@ -154,7 +154,7 @@ class Ingress(FileConfiguration, CoreSysAttributes):
return True return True
def get_dynamic_port(self, addon_slug: str) -> int: async def get_dynamic_port(self, addon_slug: str) -> int:
"""Get/Create a dynamic port from range.""" """Get/Create a dynamic port from range."""
if addon_slug in self.ports: if addon_slug in self.ports:
return self.ports[addon_slug] return self.ports[addon_slug]
@ -163,7 +163,7 @@ class Ingress(FileConfiguration, CoreSysAttributes):
while ( while (
port is None port is None
or port in self.ports.values() or port in self.ports.values()
or check_port(self.sys_docker.network.gateway, port) or await check_port(self.sys_docker.network.gateway, port)
): ):
port = random.randint(62000, 65500) port = random.randint(62000, 65500)

View File

@ -39,20 +39,19 @@ def process_lock(method):
return wrap_api return wrap_api
def check_port(address: IPv4Address, port: int) -> bool: async def check_port(address: IPv4Address, port: int) -> bool:
"""Check if port is mapped.""" """Check if port is mapped."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5) sock.setblocking(False)
try: try:
result = sock.connect_ex((str(address), port)) async with asyncio.timeout(0.5):
sock.close() await asyncio.get_running_loop().sock_connect(sock, (str(address), port))
except (OSError, TimeoutError):
# Check if the port is available return False
if result == 0: finally:
return True if sock is not None:
except OSError: sock.close()
pass return True
return False
def check_exception_chain(err: Exception, object_type: Any) -> bool: def check_exception_chain(err: Exception, object_type: Any) -> bool:

View File

@ -50,15 +50,15 @@ async def test_save_on_unload(coresys: CoreSys):
assert coresys.ingress.save_data.called assert coresys.ingress.save_data.called
def test_dynamic_ports(coresys: CoreSys): async def test_dynamic_ports(coresys: CoreSys):
"""Test dyanmic port handling.""" """Test dyanmic port handling."""
port_test1 = coresys.ingress.get_dynamic_port("test1") port_test1 = await coresys.ingress.get_dynamic_port("test1")
assert port_test1 assert port_test1
assert coresys.ingress.save_data.called assert coresys.ingress.save_data.called
assert port_test1 == coresys.ingress.get_dynamic_port("test1") assert port_test1 == await coresys.ingress.get_dynamic_port("test1")
port_test2 = coresys.ingress.get_dynamic_port("test2") port_test2 = await coresys.ingress.get_dynamic_port("test2")
assert port_test2 assert port_test2
assert port_test2 != port_test1 assert port_test2 != port_test1

View File

@ -1,14 +1,15 @@
"""Check ports.""" """Check ports."""
from ipaddress import ip_address from ipaddress import ip_address
from supervisor.coresys import CoreSys
from supervisor.utils import check_port from supervisor.utils import check_port
def test_exists_open_port(): async def test_exists_open_port(coresys: CoreSys):
"""Test a exists network port.""" """Test a exists network port."""
assert check_port(ip_address("8.8.8.8"), 53) assert await check_port(ip_address("8.8.8.8"), 53)
def test_not_exists_port(): async def test_not_exists_port(coresys: CoreSys):
"""Test a not exists network service.""" """Test a not exists network service."""
assert not check_port(ip_address("192.0.2.1"), 53) assert not await check_port(ip_address("192.0.2.1"), 53)