Skip to content

Commit

Permalink
Merge branch 'main' into feature/199-docker-socket-check
Browse files Browse the repository at this point in the history
  • Loading branch information
ahsimb authored Mar 15, 2024
2 parents cc63625 + c1c8077 commit f838f41
Show file tree
Hide file tree
Showing 20 changed files with 623 additions and 91 deletions.
2 changes: 1 addition & 1 deletion doc/changes/changes_1.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ n/a

## Bug Fixes

n/a
* #241: Fixed non-root-user access

## Documentation

Expand Down
2 changes: 1 addition & 1 deletion doc/user_guide/system-requirements.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AI-Lab System Requirements

The editions of the AI-Lab have common requirements to be available on your system:
* CPU Architecture: x86
* CPU Architecture: x86_64 (64bit)
* CPU cores: minimum 1, recommended 2 cores
* Main memory (RAM): minimum 2 GiB, recommended 8 GiB
* Free disk space: minimum 2 GiB
Expand Down
10 changes: 8 additions & 2 deletions exasol/ds/sandbox/lib/dss_docker/create_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,28 @@ def jupyter():
command = get_fact(facts, "jupyter", "command")
if command is None:
return []
docker_group = get_fact(facts, "docker_group")
port = get_fact(facts, "jupyter", "port")
user_name = get_fact(facts, "jupyter", "user")
group = get_fact(facts, "jupyter", "group")
user_home = get_fact(facts, "jupyter", "home")
password = get_fact(facts, "jupyter", "password")
logfile = get_fact(facts, "jupyter", "logfile")
return [
"--home", user_home,
"--jupyter-server", command,
"--port", port,
"--user", user_name,
"--group", group,
"--docker-group", docker_group,
"--password", password,
"--jupyter-logfile", logfile,
]

entrypoint = get_fact(facts, "entrypoint")
if entrypoint is None:
return ["sleep", "infinity"]
entry_cmd = ["python3", entrypoint]
entry_cmd = ["sudo", "python3", entrypoint]
folder = get_fact(facts, "notebook_folder")
if not folder:
return entry_cmd + jupyter()
Expand Down Expand Up @@ -162,10 +168,10 @@ def _commit_container(
notebook_folder_initial = get_fact(facts, "notebook_folder", "initial")
conf = {
"Entrypoint": entrypoint(facts),
"User": get_fact(facts, "jupyter", "user"),
"Cmd": [],
"Volumes": {notebook_folder_final: {}, },
"ExposedPorts": {f"{port}/tcp": {}},
"User": get_fact(facts, "docker_user"),
"Env": [
f"VIRTUAL_ENV={virtualenv}",
f"NOTEBOOK_FOLDER_FINAL={notebook_folder_final}",
Expand Down
7 changes: 4 additions & 3 deletions exasol/ds/sandbox/runtime/ansible/ai_lab_docker_playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
gather_facts: true
vars:
ansible_python_interpreter: python3
user_name: "jupyter"
user_group: "jupyter"
user_home: "/home/jupyter"
user_name: jupyter
user_group: jupyter
docker_group: docker
user_home: /home/jupyter
initial_notebook_folder: "{{ user_home }}/notebook-defaults"
need_sudo: yes
docker_integration_test: true
Expand Down
1 change: 1 addition & 0 deletions exasol/ds/sandbox/runtime/ansible/ec2_playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
ansible_python_interpreter: /usr/bin/python3
user_name: jupyter
user_home: /home/jupyter
docker_group: docker
user_group: jupyter
initial_notebook_folder: "{{ user_home }}/notebooks"
need_sudo: yes
Expand Down
6 changes: 5 additions & 1 deletion exasol/ds/sandbox/runtime/ansible/general_setup_tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
ansible.builtin.set_fact:
dss_facts:
entrypoint: "{{user_home}}/entrypoint.py"
docker_user: "{{ ansible_user }}"
docker_group: "{{ docker_group }}"
jupyter:
virtualenv: "{{ jupyter_virtualenv }}"
command: "{{ jupyter_virtualenv }}/bin/jupyter-lab"
port: "49494"
user: "{{ user_name }}"
group: "{{ user_group }}"
home: "{{ user_home }}"
password: "{{ lookup('ansible.builtin.env', 'JUPYTER_LAB_PASSWORD', default='ailab') }}"
logfile: "{{ user_home }}/jupyter-server.log"
notebook_folder:
Expand Down Expand Up @@ -40,7 +44,7 @@
owner: "{{ user_name }}"
group: "{{ user_group }}"
recurse: true
become: "{{ need_sudo }}"
become: "{{need_sudo}}"
- name: Clear pip Cache
ansible.builtin.file:
path: /root/.cache/pip
Expand Down
4 changes: 2 additions & 2 deletions exasol/ds/sandbox/runtime/ansible/roles/docker/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

- name: Adding docker users (for use without sudo)
user:
name: "{{user_name}}"
name: "{{ user_name }}"
append: yes
groups: docker
groups: "{{ docker_group }}"
become: "{{need_sudo}}"
168 changes: 162 additions & 6 deletions exasol/ds/sandbox/runtime/ansible/roles/entrypoint/files/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import argparse
import logging
import grp
import os
import pwd
import re
import resource
import shutil
import subprocess
import stat
import sys
import time

from inspect import cleandoc
from pathlib import Path
from typing import List, TextIO


_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)
logging.basicConfig(
format="%(asctime)s %(levelname)-7s %(filename)s: %(message)s",
datefmt="%Y-%m-%d %X",
)


def arg_parser():
Expand All @@ -39,6 +48,18 @@ def arg_parser():
"--user", type=str,
help="user name for running jupyter server",
)
parser.add_argument(
"--group", type=str,
help="user group for running jupyter server",
)
parser.add_argument(
"--docker-group", type=str,
help="user group for accessing Docker socket",
)
parser.add_argument(
"--home", type=str,
help="home directory of user running jupyter server",
)
parser.add_argument(
"--password", type=str,
help="initial default password for Jupyter server",
Expand All @@ -55,6 +76,7 @@ def arg_parser():


def start_jupyter_server(
home_directory: str,
binary_path: str,
port: int,
notebook_dir: str,
Expand All @@ -70,10 +92,9 @@ def start_jupyter_server(
def exit_on_error(rc):
if rc is not None and rc != 0:
log_messages = logfile.read_text()
print(
_logger.error(
f"Jupyter Server terminated with error code {rc},"
f" Logfile {logfile} contains:\n{log_messages}",
flush=True,
)
sys.exit(rc)

Expand All @@ -83,8 +104,11 @@ def exit_on_error(rc):
"--no-browser",
"--allow-root",
]

env = os.environ.copy()
env["HOME"] = home_directory
with open(logfile, "w") as f:
p = subprocess.Popen(command_line, stdout=f, stderr=f)
p = subprocess.Popen(command_line, stdout=f, stderr=f, env=env)

url = "http://<host>:<port>"
localhost_url = url.replace("<host>", "localhost").replace("<port>", str(port))
Expand All @@ -110,7 +134,7 @@ def exit_on_error(rc):
time.sleep(poll_sleep)
line = f.readline()
if re.search(regexp, line):
print(success_message, flush=True)
_logger.info(success_message)
break
exit_on_error(p.poll())
exit_on_error(p.wait())
Expand Down Expand Up @@ -151,29 +175,161 @@ def ensure_file(src: Path, dst: Path):

def disable_core_dumps():
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
_logger.info("Disabled coredumps")


def sleep_inifinity():
def sleep_infinity():
while True:
time.sleep(1)


class Group:
def __init__(self, name: str, id: int = None):
self.name = name
self._id = id

@property
def id(self):
if self._id is None:
self._id = grp.getgrnam(self.name).gr_gid
return self._id

def __eq__(self, other) -> bool:
if not isinstance(other, Group):
return False
return other.name == self.name

def __repr__(self):
return f"Group(name='{self.name}', id={self._id})"


class FileInspector:
def __init__(self, path: Path):
self._path = path
self._stat = path.stat() if path.exists() else None

@property
def group_id(self) -> int:
if self._stat is None:
raise FileNotFoundError(self._path)
return self._stat.st_gid

def is_group_accessible(self) -> bool:
if self._stat is None:
_logger.debug(f"File not found {self._path}")
return False
permissions = stat.filemode(self._stat.st_mode)
if permissions[4:6] == "rw":
return True
raise PermissionError(
"No rw permissions for group in"
f" {permissions} {self._path}."
)


class GroupAccess:
"""
If there is already a group with group-ID `gid`, then add the user to
this group, otherwise change the group ID to `gid` for the specified group
name. The other group is expected to exist already and user to be added
to it.
"""
def __init__(self, user: str, group: Group):
self._user = user
self._group = group

def _find_group_name(self, id: int) -> str:
try:
return grp.getgrgid(id).gr_name
except KeyError:
return None

def _run(self, command: str) -> int:
_logger.debug(f"Executing {command}")
return subprocess.run(command.split()).returncode

def enable(self) -> Group:
gid = self._group.id
existing = self._find_group_name(gid)
if existing:
self._run(f"usermod --append --groups {existing} {self._user}")
return Group(existing, gid)
else:
self._run(f"groupmod -g {gid} {self._group.name}")
return self._group


class User:
def __init__(self, user_name: str, group: Group, docker_group: Group):
self.name = user_name
self._id = None
self.group = group
self.docker_group = docker_group

@property
def is_specified(self) -> bool:
return bool(
self.name
and self.group.name
and self.docker_group.name
)

@property
def id(self):
if self._id is None:
self._id = pwd.getpwnam(self.name).pw_uid
return self._id

def enable_group_access(self, path: Path):
file = FileInspector(path)
if file.is_group_accessible():
group = GroupAccess(
self.name,
Group(self.docker_group.name, file.group_id),
).enable()
os.setgroups([group.id])
self.docker_group = group
_logger.info(f"Enabled access to {path}")
return self

def switch_to(self):
gid = self.group.id
uid = self.id
os.setresgid(gid, gid, gid)
os.setresuid(uid, uid, uid)
_logger.debug(
f"uid = {os.getresuid()}"
f" gid = {os.getresgid()}"
f" extra groups = {os.getgroups()}"
)
_logger.info(f"Switched uid/gid to {self.name}/{self.group.name}")
return self


def main():
args = arg_parser().parse_args()
user = User(args.user, Group(args.group), Group(args.docker_group))
if user.is_specified:
user.enable_group_access(Path("/var/run/docker.sock")).switch_to()
if args.notebook_defaults and args.notebooks:
copy_rec(
args.notebook_defaults,
args.notebooks,
args.warning_as_error,
)
_logger.info(
"Copied notebooks from"
f" {args.notebook_defaults} to {args.notebooks}")
disable_core_dumps()
if (args.jupyter_server
and args.notebooks
and args.jupyter_logfile
and args.user
and args.home
and args.password
):
start_jupyter_server(
args.home,
args.jupyter_server,
args.port,
args.notebooks,
Expand All @@ -182,7 +338,7 @@ def main():
args.password,
)
else:
sleep_inifinity()
sleep_infinity()


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit f838f41

Please sign in to comment.