Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Issues with home dir bind mount on Linux. #78

Merged
merged 3 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 7 additions & 34 deletions aiidalab_launch/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from enum import Enum, auto
from pathlib import Path, PosixPath, PurePosixPath, WindowsPath
from pathlib import Path, PurePosixPath
from secrets import token_hex
from typing import Any, AsyncGenerator, Generator
from urllib.parse import quote_plus
Expand All @@ -19,7 +19,7 @@
from docker.models.containers import Container
from packaging.version import parse as parse_version

from .util import _async_wrap_iter, get_docker_env
from .util import _async_wrap_iter, docker_mount_for, get_docker_env
from .version import __version__

MAIN_PROFILE_NAME = "default"
Expand Down Expand Up @@ -69,35 +69,6 @@ def _get_aiidalab_default_apps(container: Container) -> list:
return []


def _find_docker_home_mount(container: Container, system_user: str) -> Path | None:
# Find the specified home bind mount path for the existing container.
try:
home_mount = [
mount
for mount in container.attrs["Mounts"]
if mount["Destination"] == f"/home/{system_user}"
][0]
except IndexError:
return None
if home_mount["Type"] == "bind":
docker_root = PurePosixPath("/host_mnt")
docker_path = PurePosixPath(home_mount["Source"])
try:
# Try Windows
drive = docker_path.relative_to(docker_root).parts[0]
return WindowsPath(
f"{drive}:",
docker_path.root,
docker_path.relative_to(docker_root, drive),
)
except NotImplementedError:
return PosixPath(docker_root.root, docker_path.relative_to(docker_root))
elif home_mount["Type"] == "volume":
return home_mount["Name"]
else:
raise RuntimeError("Unexpected mount type.")


@dataclass
class Profile:
name: str = MAIN_PROFILE_NAME
Expand Down Expand Up @@ -147,13 +118,15 @@ def from_container(cls, container: Container) -> Profile:
raise RuntimeError(
f"Container {container.id} does not appear to be an AiiDAlab container."
)
system_user = _get_system_user(container)

system_user = _get_system_user(container)
return Profile(
name=profile_name,
port=_get_host_port(container),
default_apps=_get_aiidalab_default_apps(container),
home_mount=str(_find_docker_home_mount(container, system_user)),
home_mount=str(
docker_mount_for(container, PurePosixPath("/", "home", system_user))
),
image=container.image.tags[0],
system_user=system_user,
)
Expand Down Expand Up @@ -317,7 +290,7 @@ def _ensure_home_mount_exists(self) -> None:
LOGGER.info(
f"Ensure home mount point ({self.profile.home_mount}) exists."
)
home_mount_path.mkdir(exist_ok=True)
home_mount_path.mkdir(exist_ok=True, parents=True)

def create(self) -> Container:
assert self._container is None
Expand Down
39 changes: 32 additions & 7 deletions aiidalab_launch/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import re
import webbrowser
from contextlib import contextmanager
from pathlib import Path, PurePosixPath
from pathlib import Path, PosixPath, PurePosixPath, WindowsPath
from textwrap import wrap
from threading import Event, Thread, Timer
from typing import Any, AsyncGenerator, Generator, Iterable, Optional
from typing import Any, AsyncGenerator, Generator, Iterable, Optional, Union

import click
import click_spinner
Expand Down Expand Up @@ -159,11 +159,36 @@ def confirm_with_value(value: str, text: str, abort: bool = False) -> bool:
return False


def docker_bind_mount_path(path: Path) -> PurePosixPath:
"Construct the expected docker bind mount path (platform independent)."
return PurePosixPath(
"/host_mnt/", path.drive.strip(":"), path.relative_to(path.drive, path.root)
)
def docker_mount_for(
container: docker.models.containers.Container, destination: PurePosixPath
) -> Union[Path, str]:
"""Identify the Docker mount bind path or volume for a given destination."""
try:
mount = [
mount
for mount in container.attrs["Mounts"]
if mount["Destination"] == str(destination)
][0]
except IndexError:
raise ValueError(f"No mount point for {destination}.")
if mount["Type"] == "bind":
docker_root = PurePosixPath("/host_mnt")
docker_path = PurePosixPath(mount["Source"])
try: # Windows
drive = docker_path.relative_to(docker_root).parts[0]
return WindowsPath(
f"{drive}:",
docker_path.root,
docker_path.relative_to(docker_root, drive),
)
except ValueError: # Linux
return PosixPath(docker_path)
except NotImplementedError: # OS-X
return PosixPath(docker_root.root, docker_path.relative_to(docker_root))
elif mount["Type"] == "volume":
return mount["Name"]
else:
raise RuntimeError("Unexpected mount type.")


def get_docker_env(container: docker.models.containers.Container, env_name: str) -> str:
Expand Down
16 changes: 15 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import asyncio
from dataclasses import replace
from pathlib import Path
from time import sleep

import pytest
Expand Down Expand Up @@ -62,7 +63,6 @@ async def test_instance_init(instance):
assert await instance.status() is instance.AiidaLabInstanceStatus.DOWN


@pytest.mark.trylast
async def test_instance_create_remove(instance):
assert await instance.status() is instance.AiidaLabInstanceStatus.DOWN
instance.create()
Expand All @@ -71,6 +71,20 @@ async def test_instance_create_remove(instance):
# function.


async def test_instance_profile_detection(instance):
assert await instance.status() is instance.AiidaLabInstanceStatus.DOWN
instance.create()
assert await instance.status() is instance.AiidaLabInstanceStatus.CREATED
assert instance.profile == Profile.from_container(instance.container)


async def test_instance_home_bind_mount(instance):
instance.profile.home_mount = str(Path.home() / "aiidalab")
instance.create()
assert await instance.status() is instance.AiidaLabInstanceStatus.CREATED
assert instance.profile == Profile.from_container(instance.container)


@pytest.mark.slow
@pytest.mark.trylast
async def test_instance_start_stop(instance):
Expand Down