otherworld/otherworld

264 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
from subprocess import call, check_call, check_output
from time import sleep
from itertools import chain
import os
import sys
import json
CONTAINER_NAME = "otherworld"
DEFAULT_IMAGE_NAME = "debian:stable"
def create_x11_mapping ():
home = os.environ["HOME"]
xdg_dir = os.environ["XDG_RUNTIME_DIR"]
display = os.environ["DISPLAY"]
inner_user = home
Xauthority = os.environ["XAUTHORITY"]
return ["-e", f"DISPLAY={display}",
"-v", "/tmp/.X11-unix/:/tmp/.X11-unix",
"-v", "/dev/snd:/dev/snd",
"-v", f"{xdg_dir}/pulse:/run/pulse:ro",
"-v", f"{home}/.gtkrc-2.0:{inner_user}/.gtkrc-2.0",
"-v", f"{home}/.config/gtk-3.0/:{inner_user}/.config/gtk-3.0",
"-v", f"{home}/.guix-profile/share/fonts/truetype:/user/share/fonts/truetype-guix",
"-v", f"{home}/.guix-profile/share/fonts/opentype:/usr/share/fonts/opentype-guix",
"-v", f"{Xauthority}:{inner_user}/.Xauthority",
"-w", inner_user,
"--shm-size", "2g",
"--privileged"]
DOCKER_CREATE_OPTIONS = ["-t"] + create_x11_mapping()
USER_VOLUMES = ["~/otherworld"]
BACKGROUND_COMMAND = None
DEFAULT_COMMAND = ["bash"]
GENERATED_IMAGE_NAME_TEMPLATE = "image_{container_name}"
CREATE_USER_BOOTSTRAP_SCRIPT="""
chown {user} /home/{user}
chown {user} /home/{user}/.config
cd /home/{user}
cp -Rv /etc/skel .
chown -R {user} skel
mv skel/* skel/.* .
rm -rf skel
"""
def get_generated_image_name (container_name):
return GENERATED_IMAGE_NAME_TEMPLATE.format(container_name=container_name)
def docker_inspect (container):
return json.loads(check_output(["docker", "inspect", container]))
def docker_create (image, container, options, command):
docker_command = ["docker", "create", "--name", container] + options + [image]
if command:
docker_command = docker_command + command
call(docker_command)
def docker_get_user (container, user):
return check_output(["docker", "exec", container, "groups", user])
def docker_create_user (container, user, uid=None):
command = ["docker", "exec", container, "adduser", user, "--gecos", ",,,", "--disabled-password"]
if uid is not None:
command = command + ["--uid", str(uid)]
call(command)
call(["docker", "exec", container, "sh", "-c", CREATE_USER_BOOTSTRAP_SCRIPT.format(user=user)])
def docker_start (container):
call(["docker", "start", container])
def docker_exec (container, command, user, env={}, work_dir=None):
call(["docker", "exec", "--user", user, "-ti"] + \
(["--workdir", work_dir] if work_dir else []) + \
list(chain.from_iterable([["--env", f"{kv[0]}={kv[1]}"] for kv in env.items()])) + \
[container] + command)
def docker_commit (container, image_name):
call(["docker", "commit", container, image_name])
def docker_rm (container):
call(["docker", "rm", "-f", container])
def docker_rmi (container):
call(["docker", "rmi", "-f", container])
def docker_build (build_path, options, image_name):
check_call(["docker", "build"] + options + ["-t", image_name, build_path])
return image_name
def docker_pull (image_name):
check_call(["docker", "pull", image_name])
def resolve_build_path (build_path):
if build_path is not None and build_path.startswith("~/"):
build_path = f"{os.environ['HOME']}/{build_path[2:]}"
return build_path
def expand_user_volumes (volumes):
outer_home = os.environ["HOME"]
inner_home = outer_home
mappings = []
for volume in volumes:
source = volume
target = volume
if ":" in source:
(source, target) = source.split(":")
if source.startswith("~/"):
source = f"{outer_home}/{source[2:]}"
if source.startswith(outer_home) and not os.path.exists(source):
os.makedirs(source)
if target.startswith("~/"):
target = f"{inner_home}/{target[2:]}"
mappings = mappings + ["-v", f"{source}:{target}"]
return mappings
user = os.environ["USER"]
uid = int(check_output(["id", "-u"]).strip())
image_name = os.environ.get("OW_IMAGE", None)
container_name = os.environ.get("OW_CONTAINER", f"{CONTAINER_NAME}_{user}")
build_path = None
command = sys.argv[1:]
command_user = user
user_volumes = USER_VOLUMES
pull = False
actions = []
command_env = {}
command_workdir = None
quiet = False
while len(command) > 0:
arg = command[0]
if arg == "--quiet":
command = command[1:]
quiet = True
elif arg == "--rm":
actions.append("rm")
command = command[1:]
elif arg == "--rmi":
actions.append("rmi")
command = command[1:]
elif arg == "--commit":
actions.append("commit")
command = command[1:]
elif arg == "--sudo":
command_user = "root"
command = command[1:]
elif arg.startswith("--container="):
container_name = arg[len("--container="):]
command = command[1:]
elif arg.startswith("--image="):
image_name = arg[len("--image="):]
command = command[1:]
elif arg.startswith("--build="):
build_path = arg[len("--build="):]
command = command[1:]
elif arg.startswith("--volume="):
user_volumes.append(arg[len("--volume="):])
command = command[1:]
elif arg.startswith("--env="):
env_pair = arg[len("--env="):]
(key, value) = env_pair.split("=", 1)
command_env[key] = value
command = command[1:]
elif arg == "--cd":
command_workdir = os.getcwd()
command = command[1:]
elif arg.startswith("--cd="):
command_workdir = arg[len("--cd="):]
command = command[1:]
elif arg == "--pull":
command = command[1:]
pull = True
else:
break
if command_workdir:
user_volumes.append(command_workdir)
command_workdir = resolve_build_path(command_workdir)
if actions:
if "commit" in actions:
target_image_name = get_generated_image_name(container_name)
if not quiet:
print(f">> Committing container: {container_name} to image: {target_image_name}")
docker_commit(container_name, target_image_name)
if "rm" in actions:
if not quiet:
print(f">> Removing container: {container_name}")
docker_rm(container_name)
if "rmi" in actions:
target_image_name = get_generated_image_name(container_name)
if not quiet:
print(f">> Removing image: {target_image_name}")
docker_rmi(target_image_name)
if not command:
sys.exit(0)
if not command:
command = DEFAULT_COMMAND
container = None
try:
container = docker_inspect(container_name)[0]
except Exception:
build_path = resolve_build_path(build_path)
if build_path is not None:
image_name = docker_build(build_path, ["--pull"] if pull else [], get_generated_image_name(container_name))
else:
if image_name is None:
image_name = get_generated_image_name(container_name)
try:
docker_inspect(image_name)
if not quiet:
print(f">> Using default otherworld image {image_name}")
except Exception:
if not quiet:
print(f">> Default otherworld image {image_name} not found, falling back to {DEFAULT_IMAGE_NAME}")
image_name = DEFAULT_IMAGE_NAME
if pull:
docker_pull(image_name)
if not quiet:
print(f">> Creating container: {container_name} (using image: {image_name})")
docker_create(
image_name,
container_name,
DOCKER_CREATE_OPTIONS + expand_user_volumes(user_volumes),
BACKGROUND_COMMAND
)
container = docker_inspect(container_name)[0]
if not container["State"]["Running"]:
if not quiet:
print(f">> Starting container: {container_name}")
docker_start(container_name)
container = docker_inspect(container_name)[0]
try:
docker_get_user(container_name, user)
except Exception:
if not quiet:
print(f">> Creating container user: {user}")
docker_create_user(container_name, user, uid)
if not quiet:
print(f">> Welcome to {container_name} (IP {container['NetworkSettings']['IPAddress']})")
docker_exec(container_name, command, command_user, command_env, command_workdir)