Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@ When running, don't forget to mount `/var/run/docker.sock` into the container.
You should also force a container hostname to avoid unpredictable Docker hashes.

For example:

```
# With Docker
docker run --rm -ti \
-v /var/run/docker.sock:/var/run/docker.sock \
--hostname worker01 \
--env DOCKER_HOST=/var/run/docker.sock
--env AYON_API_KEY=verysecureapikey \
--env AYON_SERVER_URL="http://172.18.0.1:5000" \
ynput/ayon-ash
```

```
# With (Rootless) Podman
podman run --rm -ti \
--security-opt label=disable \
-v /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock \
--env CONTAINER_HOST=/run/user/1000/podman/podman.sock
--hostname worker01 \
--env AYON_API_KEY=verysecureapikey \
--env AYON_SERVER_URL="http://172.18.0.1:5000" \
--env AYON_USE_PODMAN="true" \
ynput/ayon-ash
```

Expand All @@ -45,3 +59,17 @@ the server url) will be passed to the spawned services.
### AYON_HOSTNAME

Optional setting to override the hostname.

### AYON_NETWORK

Optional setting to specify the network where the **backend** is running. Otherwise infered by the running containers.

### AYON_NETWORK_MODE

Optional setting to specify the network **mode** which the **backend** is running on. Otherwise infered by the running containers.

### DOCKER_HOST | CONTAINER_HOST

Optional setting to specify a different Docker/Podman socket.


5 changes: 3 additions & 2 deletions ash/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ def __init__(self):
while True:
try:
response = self.get("users/me")
except Exception:
logging.warning("Unable to connect to the server... Retrying")
except Exception as e:
logging.warning(f"Unable to connect to the server: {e}\nRetrying...")

time.sleep(5)
continue
break
Expand Down
59 changes: 48 additions & 11 deletions ash/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import socket
import sys
from typing import Literal
from urllib.parse import urlparse

import docker
import dotenv
from nxtools import critical_error, logging
from nxtools import critical_error, log_traceback, logging
from pydantic import BaseModel, Field, ValidationError

from .containers import PODMAN, get_container_client

logging.user = "ash"
dotenv.load_dotenv()

Expand All @@ -23,21 +25,42 @@ class Config(BaseModel):


def get_local_info():
client = docker.DockerClient(base_url="unix://var/run/docker.sock")
api = docker.APIClient(base_url="unix://var/run/docker.sock")
"""Infer info from ASH's container.

We get the "network" and "network_mode" from the current running
ASH (what runs this code) container.

These two can be provided via `AYON_NETWORK` and `AYON_NETWORK_MODE`.
"""
client, api = get_container_client()

logging.info("Querying existing containers...")
for container in client.containers.list():
insp = api.inspect_container(container.id)
if PODMAN:
insp = container.inspect()
else:
insp = api.inspect_container(container.id)
if insp["Config"]["Hostname"] != socket.gethostname():
logging.debug(
f"Hostname for container {insp['Name']} doesn't match ash's, ignoring."
)
continue
# print(json.dumps(insp, indent=4))
break
else:
logging.error("Weird, no container found for this host")
sys.exit(1)

networks = insp["NetworkSettings"]["Networks"]
try:
network = next(iter(insp["NetworkSettings"]["Networks"].keys()), None)
network_mode = insp["HostConfig"]["NetworkMode"]
except Exception as e:
logging.error(
"ASH is not running in a defined network... make sure it's in"
"the same network as ayon-docker containers."
)
log_traceback(e)

return {"networks": list(networks.keys())}
return {"network": network, "network_mode": network_mode}


def get_config() -> Config:
Expand All @@ -46,6 +69,19 @@ def get_config() -> Config:
key = key.lower()
if not key.startswith("ayon_"):
continue
if key == "ayon_server_url":
# We won't be able to connect if we receive an `AYON_SERVER_URL`
# such as `http://localhost:5000` or `http://ayon-docker_server_1`
# So here we try to resolve it to an actual IP. If we fail, means
# we can't reach the backend at all.
try:
server_hostname = urlparse(val).hostname
assert server_hostname is not None, "Invalid URL"
server_ip = socket.gethostbyname(server_hostname)
val = val.replace(server_hostname, server_ip)
except Exception:
critical_error("Unable to resolve `AYON_SERVER_URL` {original_value}")

data[key.replace("ayon_", "", 1)] = val
try:
config = Config(**data)
Expand All @@ -57,11 +93,12 @@ def get_config() -> Config:

critical_error("Unable to configure API")

local_info = get_local_info()

if config.network is None and config.network_mode is None:
config.network = local_info["networks"][0]
local_info = get_local_info()
config.network = local_info["network"]
config.network_mode = local_info["network_mode"]

logging.debug(f"ASH Config is: {config}")
return config


Expand Down
45 changes: 45 additions & 0 deletions ash/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os

from nxtools import logging

PODMAN = os.getenv("AYON_USE_PODMAN", False)

DOCKER_HOST = os.getenv("DOCKER_HOST", None)

if not DOCKER_HOST:
DOCKER_HOST = os.getenv("CONTAINER_HOST", None)

if not DOCKER_HOST:
if PODMAN:
DOCKER_HOST = "unix:///run/user/1000/podman/podman.sock"
else:
DOCKER_HOST = "unix://var/run/docker.sock"


def get_container_client():
"""Creates a Client connection to the Socket

Depending on teh container runtime we use, it will import and
create the class acordingly.

Note that podman does not require the "APIClient" to inspect `Containers.`

Returns:
tuple(client, api): The Client object, and in case of Docker the APIClient.
"""
client = None
api = None

if PODMAN:
from podman import PodmanClient

client = PodmanClient(base_url=DOCKER_HOST)
logging.info("Using container client: Podman")
else:
from docker import APIClient, DockerClient

client = DockerClient(base_url=DOCKER_HOST)
api = APIClient(base_url=DOCKER_HOST)
logging.info("Using container client: Docker")

return client, api
30 changes: 23 additions & 7 deletions ash/service_logging.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import threading

from nxtools import logging
from nxtools import log_traceback, logging


class ServiceLog:
Expand All @@ -10,16 +10,32 @@ def __init__(self, service_name: str, container):
threading.Thread(target=self._run, daemon=True).start()

def _run(self):
logging.info(f"Starting log stream for {self.service_name}")
for line in self.container.logs(stream=True, tail=1, follow=True):
print(f"{line.decode().strip()}")
logging.info(
f"Starting log stream for {self.service_name}, last 10 lines were..."
)
for line in self.container.logs(stream=True, tail=10, stderr=True):
log_string = line.decode().strip()
log_elements = log_string.split(" ")

log_date = log_elements[0]
log_time = log_elements[1]
log_severity = log_elements[2]

log_message = log_string.split(log_severity)[-1]
print(
f"{log_date} {log_time} {log_severity} {self.service_name} {log_message}"
)

# service exited
# print the status code and free the container

status_code = self.container.wait()["StatusCode"]
logging.warning(f"{self.service_name} exited with code {status_code}")
self.container = None
try:
status_code = self.container.wait()["StatusCode"]
logging.warning(f"{self.service_name} exited with code {status_code}")
except Exception as e:
logging.warning("Lost connection to the container:")
log_traceback(e)
self.container = None


class ServiceLogger:
Expand Down
18 changes: 12 additions & 6 deletions ash/services.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import docker
from nxtools import logging, slugify

from .config import config
from .containers import get_container_client
from .models import ServiceConfigModel
from .service_logging import ServiceLogger


class Services:
client: docker.DockerClient | None = None
client = None
prefix: str = "io.ayon.service"

@classmethod
def connect(cls):
cls.client = docker.DockerClient(base_url="unix://var/run/docker.sock")
client, _ = get_container_client()
cls.client = client

@classmethod
def get_running_services(cls) -> list[str]:
Expand All @@ -22,10 +23,13 @@ def get_running_services(cls) -> list[str]:
if cls.client is None:
return result

logging.debug("Checking for Running services.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove log statements, which are executed in every iteration as they flood the logs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

particularly lines 26, 32, 95, 113


for container in cls.client.containers.list():
labels = container.labels
if service_name := labels.get(f"{cls.prefix}.service_name"):
result.append(service_name)
logging.debug("Found {0} running services: {1}".format(len(result), result))
return result

@classmethod
Expand Down Expand Up @@ -88,7 +92,7 @@ def ensure_running(
#
# Check whether it is running already
#

logging.info("Checking if Service is already running.")
container = None

for container in cls.client.containers.list():
Expand All @@ -104,7 +108,10 @@ def ensure_running(
except AssertionError:
logging.error("SERVICE MISMATCH. This shouldn't happen. Stopping.")
container.stop()

else:
logging.debug(
f"Service {service_name} already running at {container.id}"
)
break
else:
# And start it
Expand Down Expand Up @@ -132,5 +139,4 @@ def ensure_running(

container = cls.spawn(image, hostname, environment, labels)

# Ensure container logger is running
ServiceLogger.add(service_name, container)
5 changes: 5 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ ignore_errors = true
follow_imports = skip
ignore_missing_imports = true

[mypy-podman.*]
ignore_errors = true
follow_imports = skip
ignore_missing_imports = true

[mypy-requests.*]
ignore_errors = true
follow_imports = skip
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pydantic = "^1.10.2"
psutil = "^5.9.2"
python-dotenv = "^0.21.0"
docker = "^6.0.0"
podman = "^4.5.0"

[tool.poetry.dev-dependencies]
pytest = "^7.0"
Expand Down