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]
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
@property
@ -539,7 +539,7 @@ class Addon(AddonModel):
# TCP monitoring
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
if t_proto:
@ -602,6 +602,16 @@ class Addon(AddonModel):
_LOGGER.info("Removing add-on data folder %s", 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(
name="addon_install",
limit=JobExecutionLimit.GROUP_ONCE,
@ -630,6 +640,8 @@ class Addon(AddonModel):
self.sys_addons.data.uninstall(self)
raise AddonsError() from err
await self._check_ingress_port()
# Add to addon manager
self.sys_addons.local[self.slug] = self
@ -716,6 +728,7 @@ class Addon(AddonModel):
try:
_LOGGER.info("Add-on '%s' successfully updated", self.slug)
self.sys_addons.data.update(store)
await self._check_ingress_port()
# Cleanup
with suppress(DockerError):
@ -756,6 +769,7 @@ class Addon(AddonModel):
raise AddonsError() from err
self.sys_addons.data.update(self.addon_store)
await self._check_ingress_port()
_LOGGER.info("Add-on '%s' successfully rebuilt", self.slug)
finally:
@ -1199,6 +1213,7 @@ class Addon(AddonModel):
_LOGGER.info("Restore/Update of image for addon %s", self.slug)
with suppress(DockerError):
await self.instance.update(version, restore_image)
self._check_ingress_port()
# Restore data and config
def _restore_data():

View File

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

View File

@ -154,7 +154,7 @@ class Ingress(FileConfiguration, CoreSysAttributes):
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."""
if addon_slug in self.ports:
return self.ports[addon_slug]
@ -163,7 +163,7 @@ class Ingress(FileConfiguration, CoreSysAttributes):
while (
port is None
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)

View File

@ -39,20 +39,19 @@ def process_lock(method):
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."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
sock.setblocking(False)
try:
result = sock.connect_ex((str(address), port))
sock.close()
# Check if the port is available
if result == 0:
return True
except OSError:
pass
return False
async with asyncio.timeout(0.5):
await asyncio.get_running_loop().sock_connect(sock, (str(address), port))
except (OSError, TimeoutError):
return False
finally:
if sock is not None:
sock.close()
return True
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
def test_dynamic_ports(coresys: CoreSys):
async def test_dynamic_ports(coresys: CoreSys):
"""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 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 != port_test1

View File

@ -1,14 +1,15 @@
"""Check ports."""
from ipaddress import ip_address
from supervisor.coresys import CoreSys
from supervisor.utils import check_port
def test_exists_open_port():
async def test_exists_open_port(coresys: CoreSys):
"""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."""
assert not check_port(ip_address("192.0.2.1"), 53)
assert not await check_port(ip_address("192.0.2.1"), 53)