298 lines
8.7 KiB
Python
298 lines
8.7 KiB
Python
import subprocess
|
|
import time
|
|
from typing import List
|
|
|
|
import pytest
|
|
from pytest_docker_tools import container, fetch, network, volume, wrappers
|
|
|
|
import docker
|
|
|
|
from ray._common.network_utils import build_address
|
|
|
|
# If you need to debug tests using fixtures in this file,
|
|
# comment in the volume
|
|
# mounts in the head node and worker node containers below and use
|
|
# the repro-ci.py script to spin up an instance. The test
|
|
# setup is a little intricate, as it uses docker-in-docker.
|
|
# You need to ssh into the host machine, find the
|
|
# docker-in-docker container with
|
|
#
|
|
# docker ps
|
|
#
|
|
# Log into the container with
|
|
#
|
|
# docker exec -it <dind-daemon container id> sh
|
|
#
|
|
# And run
|
|
#
|
|
# mkdir -p /tmp/ray
|
|
# chmod 777 /tmp/ray
|
|
#
|
|
# Now you can re-run the test and the logs will show
|
|
# up in /tmp/ray in the docker-in-docker container.
|
|
# Good luck!
|
|
|
|
|
|
class Container(wrappers.Container):
|
|
def ready(self):
|
|
self._container.reload()
|
|
if self.status == "exited":
|
|
from pytest_docker_tools.exceptions import ContainerFailed
|
|
|
|
raise ContainerFailed(
|
|
self,
|
|
f"Container {self.name} has already exited before "
|
|
"we noticed it was ready",
|
|
)
|
|
|
|
if self.status != "running":
|
|
return False
|
|
|
|
networks = self._container.attrs["NetworkSettings"]["Networks"]
|
|
for (_, n) in networks.items():
|
|
if not n["IPAddress"]:
|
|
return False
|
|
|
|
if "Ray runtime started" in super().logs():
|
|
return True
|
|
return False
|
|
|
|
def client(self):
|
|
from http.client import HTTPConnection
|
|
|
|
port = self.ports["8000/tcp"][0]
|
|
return HTTPConnection(f"localhost:{port}")
|
|
|
|
def print_logs(self):
|
|
for (name, content) in self.get_files("/tmp"):
|
|
print(f"===== log start: {name} ====")
|
|
print(content.decode())
|
|
|
|
|
|
# This allows us to assign static ips to docker containers
|
|
ipam_config = docker.types.IPAMConfig(
|
|
pool_configs=[
|
|
docker.types.IPAMPool(subnet="192.168.52.0/24", gateway="192.168.52.254")
|
|
]
|
|
)
|
|
gcs_network = network(driver="bridge", ipam=ipam_config)
|
|
|
|
redis_image = fetch(repository="redis:latest")
|
|
|
|
redis = container(
|
|
image="{redis_image.id}",
|
|
network="{gcs_network.name}",
|
|
command=("redis-server --save 60 1 --loglevel warning"),
|
|
)
|
|
|
|
head_node_vol = volume()
|
|
worker_node_vol = volume()
|
|
head_node_container_name = "gcs" + str(int(time.time()))
|
|
|
|
|
|
def gen_head_node(envs):
|
|
return container(
|
|
image="rayproject/ray:ha_integration",
|
|
name=head_node_container_name,
|
|
network="{gcs_network.name}",
|
|
command=[
|
|
"ray",
|
|
"start",
|
|
"--head",
|
|
"--block",
|
|
"--num-cpus",
|
|
"0",
|
|
# Fix the port of raylet to make sure raylet restarts at the same
|
|
# ip:port is treated as a different raylet.
|
|
"--node-manager-port",
|
|
"9379",
|
|
"--dashboard-host",
|
|
"0.0.0.0",
|
|
],
|
|
volumes={"{head_node_vol.name}": {"bind": "/tmp", "mode": "rw"}},
|
|
environment=envs,
|
|
wrapper_class=Container,
|
|
ports={
|
|
"8000/tcp": None,
|
|
},
|
|
# volumes={
|
|
# "/tmp/ray/": {"bind": "/tmp/ray/", "mode": "rw"}
|
|
# },
|
|
)
|
|
|
|
|
|
def gen_worker_node(envs, num_cpus):
|
|
return container(
|
|
image="rayproject/ray:ha_integration",
|
|
network="{gcs_network.name}",
|
|
command=[
|
|
"ray",
|
|
"start",
|
|
"--address",
|
|
build_address(head_node_container_name, 6379),
|
|
"--block",
|
|
# Fix the port of raylet to make sure raylet restarts at the same
|
|
# ip:port is treated as a different raylet.
|
|
"--node-manager-port",
|
|
"9379",
|
|
"--num-cpus",
|
|
f"{num_cpus}",
|
|
],
|
|
volumes={"{worker_node_vol.name}": {"bind": "/tmp", "mode": "rw"}},
|
|
environment=envs,
|
|
wrapper_class=Container,
|
|
ports={
|
|
"8000/tcp": None,
|
|
},
|
|
# volumes={
|
|
# "/tmp/ray/": {"bind": "/tmp/ray/", "mode": "rw"}
|
|
# },
|
|
)
|
|
|
|
|
|
head_node = gen_head_node(
|
|
{
|
|
"RAY_REDIS_ADDRESS": "{redis.ips.primary}:6379",
|
|
"RAY_raylet_client_num_connect_attempts": "10",
|
|
"RAY_raylet_client_connect_timeout_milliseconds": "100",
|
|
}
|
|
)
|
|
|
|
worker_node = gen_worker_node(
|
|
envs={
|
|
"RAY_REDIS_ADDRESS": "{redis.ips.primary}:6379",
|
|
"RAY_raylet_client_num_connect_attempts": "10",
|
|
"RAY_raylet_client_connect_timeout_milliseconds": "100",
|
|
},
|
|
num_cpus=8,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def docker_cluster(head_node, worker_node):
|
|
yield (head_node, worker_node)
|
|
|
|
|
|
def run_in_container(cmds: List[List[str]], container_id: str):
|
|
"""Run a list of commands in the specified container.
|
|
|
|
Checks that each docker command executed without error.
|
|
Returns the output from each command as a list.
|
|
"""
|
|
|
|
outputs = []
|
|
for cmd in cmds:
|
|
docker_cmd = ["docker", "exec", container_id] + cmd
|
|
print(f"Executing command: {docker_cmd}", time.time())
|
|
try:
|
|
resp = subprocess.check_output(docker_cmd, stderr=subprocess.STDOUT)
|
|
output = resp.decode("utf-8").strip()
|
|
print(f"Output: {output}")
|
|
outputs.append(output)
|
|
except subprocess.CalledProcessError as e:
|
|
error_output = e.output.decode("utf-8") if e.output else "No output"
|
|
print(f"Command failed with return code {e.returncode}")
|
|
print(f"Full error output:\n{error_output}")
|
|
raise
|
|
|
|
return outputs
|
|
|
|
|
|
IMAGE_NAME = "rayproject/ray:runtime_env_container"
|
|
# After `docker save` / `podman load`, Podman typically tags the image as below (not the
|
|
# Docker daemon name). Use that ref for `podman create` so resolution stays local.
|
|
PODMAN_BASE_IMAGE = "localhost/runtime_env_container:latest"
|
|
NESTED_IMAGE_NAME = "localhost/runtime_env_container_nested:latest"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def podman_docker_cluster():
|
|
start_container_command = [
|
|
"docker",
|
|
"run",
|
|
"-d",
|
|
"--privileged",
|
|
"-v",
|
|
"/var/run/docker.sock:/var/run/docker.sock",
|
|
"-v",
|
|
"/var/lib/containers:/var/lib/containers",
|
|
# For testing environment variables
|
|
"--env",
|
|
"RAY_TEST_ABC=1",
|
|
"--env",
|
|
"TEST_ABC=1",
|
|
IMAGE_NAME,
|
|
"tail",
|
|
"-f",
|
|
"/dev/null",
|
|
]
|
|
try:
|
|
container_id = subprocess.check_output(
|
|
start_container_command, stderr=subprocess.STDOUT
|
|
).decode("utf-8")
|
|
except subprocess.CalledProcessError as e:
|
|
error_output = e.output.decode("utf-8") if e.output else "No output"
|
|
print(f"Command failed with return code {e.returncode}")
|
|
print(f"Full error output:\n{error_output}")
|
|
raise
|
|
|
|
container_id = container_id.strip()
|
|
|
|
# Get group id that owns the docker socket file. Add user `ray` to
|
|
# group to get necessary permissions for pulling an image from
|
|
# docker's local storage into podman
|
|
docker_group_id = run_in_container(
|
|
[["stat", "-c", "%g", "/var/run/docker.sock"]], container_id
|
|
)[0]
|
|
run_in_container(
|
|
[
|
|
["id"],
|
|
["sudo", "groupadd", "-g", docker_group_id, "docker"],
|
|
["sudo", "usermod", "-aG", "docker", "ray"],
|
|
[
|
|
"bash",
|
|
"-c",
|
|
f"docker save {IMAGE_NAME} | podman load",
|
|
],
|
|
],
|
|
container_id,
|
|
)
|
|
|
|
# Add custom file to new image tagged `runtime_env_container_nested`,
|
|
# which can be read by Ray actors / Serve deployments to verify the
|
|
# container runtime env plugin. Also add serve application that will
|
|
# be imported by the telemetry test.
|
|
serve_app = """
|
|
from ray import serve
|
|
@serve.deployment
|
|
class Model:
|
|
def __call__(self):
|
|
with open("file.txt") as f:
|
|
return f.read().strip()
|
|
app = Model.bind()
|
|
"""
|
|
|
|
run_in_container(
|
|
[
|
|
["bash", "-c", "echo helloworldalice >> /tmp/file.txt"],
|
|
["bash", "-c", f"echo '{serve_app}' >> /tmp/serve_application.py"],
|
|
["podman", "create", "--name", "tmp_container", PODMAN_BASE_IMAGE],
|
|
["podman", "cp", "/tmp/file.txt", "tmp_container:/home/ray/file.txt"],
|
|
[
|
|
"podman",
|
|
"cp",
|
|
"/tmp/serve_application.py",
|
|
"tmp_container:/home/ray/serve_application.py",
|
|
],
|
|
["podman", "commit", "tmp_container", NESTED_IMAGE_NAME],
|
|
],
|
|
container_id,
|
|
)
|
|
|
|
# For debugging
|
|
run_in_container([["podman", "image", "ls"]], container_id)
|
|
|
|
yield container_id
|
|
|
|
subprocess.check_call(["docker", "kill", container_id])
|