Skip to content

Commit

Permalink
WIP: Reworked entry load/unload
Browse files Browse the repository at this point in the history
  • Loading branch information
ualex73 committed Jan 26, 2025
1 parent 740e207 commit ea7025c
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 161 deletions.
95 changes: 25 additions & 70 deletions custom_components/monitor_docker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.reload import async_setup_reload_service

Expand Down Expand Up @@ -182,90 +183,44 @@ async def RunDocker(hass: HomeAssistant, entry: ConfigType) -> None:


#################################################################
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""

async def RunDocker(hass: HomeAssistant, entry: ConfigType) -> None:
"""Wrapper around function for a separated thread."""

# Create docker instance, it will have asyncio threads
hass.data[DOMAIN][entry[CONF_NAME]] = {}
hass.data[DOMAIN][entry[CONF_NAME]][CONFIG] = entry

startCount = 0

while True:
doLoop = True

try:
hass.data[DOMAIN][entry[CONF_NAME]][API] = DockerAPI(hass, entry)
await hass.data[DOMAIN][entry[CONF_NAME]][API].init(startCount)
await hass.data[DOMAIN][entry[CONF_NAME]][API].run()
await hass.config_entries.async_forward_entry_setups(
config_entry, PLATFORMS
)
# await hass.data[DOMAIN][entry[CONF_NAME]][API].load()
except Exception as err:
doLoop = False
if entry[CONF_RETRY] == 0:
raise
else:
_LOGGER.error("Failed Docker connect: %s", str(err))
_LOGGER.error("Retry in %d seconds", entry[CONF_RETRY])
await asyncio.sleep(entry[CONF_RETRY])
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}

startCount += 1
api = None

if doLoop:
# Now run forever in this separated thread
# loop.run_forever()
try:
api = DockerAPI(hass, entry.data)
await api.init()
await api.run()

# We only get here if a docker instance disconnected or HASS is stopping
if not hass.data[DOMAIN][entry[CONF_NAME]][API]._dockerStopped:
# If HASS stopped, do not retry
break
hass.data[DOMAIN][entry.data[CONF_NAME]] = {}
hass.data[DOMAIN][entry.data[CONF_NAME]][CONFIG] = entry.data
hass.data[DOMAIN][entry.data[CONF_NAME]][API] = api

# config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as err:
_LOGGER.error("[%s]: Failed to setup, error=%s", entry.data[CONF_NAME],str(err))

if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if api:
await api.destroy()

# For now this is disabled as selection of conditions is made in the UI.
#
# # Default MONITORED_CONDITIONS_LIST also contains allinone, so we need to fix it up here
# if len(config_entry.data[CONF_MONITORED_CONDITIONS]) == 0:
# # Add whole list, including allinone. Make a copy, no reference
# config_entry.data[CONF_MONITORED_CONDITIONS] = MONITORED_CONDITIONS_LIST.copy()
# # remove the allinone
# config_entry.data[CONF_MONITORED_CONDITIONS].remove(CONTAINER_INFO_ALLINONE)
#
# # Check if CONF_MONITORED_CONDITIONS has only ALLINONE, then expand to all
# if (
# len(config_entry.data[CONF_MONITORED_CONDITIONS]) == 1
# and CONTAINER_INFO_ALLINONE in config_entry.data[CONF_MONITORED_CONDITIONS]
# ):
# config_entry.data[CONF_MONITORED_CONDITIONS] = list(MONITORED_CONDITIONS_LIST) + list(
# [CONTAINER_INFO_ALLINONE]
# )

# Will not work with reconfigure since it will have the same name, and unique name is already ensured in config_flow
# if config_entry.data[CONF_NAME] in hass.data[DOMAIN]:
# _LOGGER.error(
# "Instance %s is duplicate, please assign an unique name",
# config_entry.data[CONF_NAME],
# )
# return False

# Each docker hosts runs in its own thread. We need to pass hass too, for the load_platform
asyncio.create_task(RunDocker(hass, config_entry.data))
raise ConfigEntryNotReady(f"Failed to setup {err}") from err

return True


#################################################################
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

_LOGGER.debug("async_unload_entry")

await hass.data[DOMAIN][entry.data[CONF_NAME]][API].destroy()

return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


#################################################################
Expand Down
4 changes: 2 additions & 2 deletions custom_components/monitor_docker/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ async def async_step_user(
# Test connection to Docker
try:
self._docker_api = DockerAPI(self.hass, user_input)
if not await self._docker_api.init():
errors["base"] = "invalid_connection"
await self._docker_api.init()
#errors["base"] = "invalid_connection"
except Exception as e: # pylint: disable=broad-except
_LOGGER.exception("Unhandled exception in user step")
errors["base"] = str(e)
Expand Down
175 changes: 86 additions & 89 deletions custom_components/monitor_docker/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def __init__(self, hass: HomeAssistant, config: ConfigType):
self._subscribers: list[Callable] = []
self._api: aiodocker.Docker = None

self._tcp_connector = None
self._tcp_session = None
self._tcp_ssl_context = None

_LOGGER.debug("[%s]: Helper version: %s", self._instance, VERSION)

self._interval: int = config[CONF_SCAN_INTERVAL]
Expand All @@ -120,113 +124,104 @@ def __init__(self, hass: HomeAssistant, config: ConfigType):
)

#############################################################
async def init(self, startCount: int = 0) -> bool:
async def init(self, startCount: int = 0):
# Set to None when called twice, etc
self._api = None

_LOGGER.debug("[%s]: DockerAPI init()", self._instance)

try:
# Try to fix unix:// to unix:/// (3 are required by aiodocker)
url: str = self._config[CONF_URL]
if (
url is not None
and url.find("unix://") == 0
and url.find("unix:///") == -1
):
url = url.replace("unix://", "unix:///")
# Get URL
url: str = self._config[CONF_URL]

# When we reconnect with tcp, we should delay - docker is maybe not fully ready
if startCount > 0 and url is not None and url.find("unix:") != 0:
await asyncio.sleep(5)
# HA sometimes makes the url="", which aiodocker does not like
if url is not None and url == "":
url = None

# Check if it is a tcp connection or not
tcpConnection = False
# Try to fix unix:// to unix:/// (3 are required by aiodocker)
if url is not None and url.find("unix://") == 0 and url.find("unix:///") == -1:
url = url.replace("unix://", "unix:///")

# Remove Docker environment variables
os.environ.pop("DOCKER_TLS_VERIFY", None)
os.environ.pop("DOCKER_CERT_PATH", None)
# When we reconnect with tcp, we should delay - docker is maybe not fully ready
if startCount > 0 and url is not None and url.find("unix:") != 0:
await asyncio.sleep(5)

# Setup Docker parameters
connector = None
session = None
ssl_context = None
# Remove Docker environment variables
os.environ.pop("DOCKER_TLS_VERIFY", None)
os.environ.pop("DOCKER_CERT_PATH", None)

if url is not None:
_LOGGER.debug("[%s]: Docker URL is '%s'", self._instance, url)
else:
_LOGGER.debug(
"[%s]: Docker URL is auto-detect (most likely using 'unix://var/run/docker.socket')",
self._instance,
)
# Setup Docker parameters
self._tcp_connector = None
self._tcp_session = None
self._tcp_ssl_context = None

# If is not empty or an Unix socket, then do check TCP/SSL
if url is not None and url.find("unix:") == -1:
# Check if URL is valid
if not (
url.find("tcp:") == 0
or url.find("http:") == 0
or url.find("https:") == 0
):
raise ValueError(
f"[{self._instance}] Docker URL '{url}' does not start with tcp:, http: or https:"
)
if url is not None:
_LOGGER.debug("[%s]: Docker URL is '%s'", self._instance, url)
else:
_LOGGER.debug(
"[%s]: Docker URL is auto-detect (most likely using 'unix://var/run/docker.socket')",
self._instance,
)

if self._config[CONF_CERTPATH] and url.find("http:") == 0:
# fixup URL and warn
_LOGGER.warning(
"[%s] Docker URL '%s' should be https instead of http when using certificate path",
self._instance,
url,
)
url = url.replace("http:", "https:")
# If is not empty or an Unix socket, then do check TCP/SSL
if url and url.find("unix:") == -1:
# Check if URL is valid
if not (
url.find("tcp:") == 0
or url.find("http:") == 0
or url.find("https:") == 0
):
raise ValueError(
f"[{self._instance}] Docker URL '{url}' does not start with tcp:, http: or https:"
)

if self._config[CONF_CERTPATH] and url.find("tcp:") == 0:
# fixup URL and warn
_LOGGER.warning(
"[%s] Docker URL '%s' should be https instead of tcp when using certificate path",
self._instance,
url,
)
url = url.replace("tcp:", "https:")
if self._config[CONF_CERTPATH] and url.find("http:") == 0:
# fixup URL and warn
_LOGGER.warning(
"[%s] Docker URL '%s' should be https instead of http when using certificate path",
self._instance,
url,
)
url = url.replace("http:", "https:")

if self._config[CONF_CERTPATH]:
_LOGGER.debug(
"[%s]: Docker certification path is '%s' SSL/TLS will be used",
self._instance,
self._config[CONF_CERTPATH],
)
if self._config[CONF_CERTPATH] and url.find("tcp:") == 0:
# fixup URL and warn
_LOGGER.warning(
"[%s] Docker URL '%s' should be https instead of tcp when using certificate path",
self._instance,
url,
)
url = url.replace("tcp:", "https:")

# Create our SSL context object
ssl_context = await self._hass.async_add_executor_job(
self._docker_ssl_context
)
if self._config[CONF_CERTPATH]:
_LOGGER.debug(
"[%s]: Docker certification path is '%s' SSL/TLS will be used",
self._instance,
self._config[CONF_CERTPATH],
)

# Setup new TCP connection, otherwise timeout takes toooo long
connector = TCPConnector(ssl=ssl_context)
session = ClientSession(
connector=connector,
timeout=ClientTimeout(
connect=5,
sock_connect=5,
total=10,
),
# Create our SSL context object
self._tcp_ssl_context = await self._hass.async_add_executor_job(
self._docker_ssl_context
)

# Initiate the aiodocker instance now
self._api = aiodocker.Docker(
url=url, connector=connector, session=session, ssl_context=ssl_context
# Setup new TCP connection, otherwise timeout takes toooo long
self._tcp_connector = TCPConnector(ssl=self._tcp_ssl_context)
self._tcp_session = ClientSession(
connector=self._tcp_connector,
timeout=ClientTimeout(
connect=5,
sock_connect=5,
total=10,
),
)

except Exception as err:
exc_info = True if str(err) == "" else False
_LOGGER.error(
"[%s]: Can not connect to Docker API (%s)",
self._instance,
str(err),
exc_info=exc_info,
)
return False
# Initiate the aiodocker instance now. Could raise an exception
self._api = aiodocker.Docker(
url=url,
connector=self._tcp_connector,
session=self._tcp_session,
ssl_context=self._tcp_ssl_context,
)

versionInfo = await self._api.version()
version: str | None = versionInfo.get("Version", None)
Expand All @@ -245,8 +240,6 @@ async def init(self, startCount: int = 0) -> bool:
# Add container name to the list
self._containers[cname] = None

return True

#############################################################
async def run(self):

Expand Down Expand Up @@ -326,6 +319,10 @@ async def destroy(self) -> None:
await container.destroy()
# TBD clear container from list?

# Close session if initialized
if self._tcp_session:
self._tcp_session.detach()

# Clear api value
# self._api = None

Expand Down

0 comments on commit ea7025c

Please sign in to comment.