Skip to content
Snippets Groups Projects
Commit 69b47ca7 authored by Martin Mareš's avatar Martin Mareš
Browse files

Parts of the client

parent 55afd775
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/python3
import argparse
from dataclasses import dataclass
import os
from pathlib import Path
from pwd import getpwnam
import socket
import subprocess
import sys
from typing import NoReturn, List, Optional
from shipcat.config import GlobalConfig, ContainerConfig, ConfigError
def die(msg: str) -> NoReturn:
print(msg, file=sys.stderr)
sys.exit(1)
def progress(msg: str) -> None:
print(msg, file=sys.stderr, end="", flush=True)
def load_container_config(name: str) -> ContainerConfig:
try:
cc = ContainerConfig.load(config, name)
except ConfigError as e:
die(str(e))
return cc
def is_root() -> bool:
return os.geteuid() == 0
def assert_root() -> None:
if not is_root():
die('This operation must be performed by root')
@dataclass(frozen=True, order=True)
class SubIdRange:
first: int
count: int
name: str
class SubIds:
ranges: List[SubIdRange]
def __init__(self, file: str) -> None:
ranges = []
with open(file) as f:
for line in f:
name, first_str, count_str = line.rstrip().split(':')
ranges.append(SubIdRange(int(first_str), int(count_str), name))
ranges.sort()
for i in range(1, len(ranges)):
r, pr = ranges[i], ranges[i-1]
if r.first > pr.first + pr.count:
die(f'Inconsistent {name}: entries {r} and {pr} overlap')
self.ranges = ranges
def find_range(self, uid: int, user: str, size: int) -> Optional[SubIdRange]:
uid_str = str(uid)
for r in self.ranges:
if (r.name == user or r.name == uid_str) and r.count >= size:
return r
return None
def alloc_range(self, user: str, size: int) -> SubIdRange:
# Call at most once (does not update the list of ranges)
i = 100000
for r in self.ranges:
if r.first >= i + size:
return SubIdRange(i, size, user)
else:
i = r.first + r.count
return SubIdRange(i, size, user)
def do_init(args: argparse.Namespace) -> None:
name = args.name
cc = load_container_config(name)
assert_root()
progress(f'User {cc.user_name}: ')
try:
pwd = getpwnam(cc.user_name)
progress('already exists\n')
except KeyError:
progress('creating\n')
subprocess.run(
['adduser', '--system', '--group', '--gecos', f'Container {name}', '--disabled-password', cc.user_name],
check=True,
)
pwd = getpwnam(cc.user_name)
uid = pwd.pw_uid
progress('Subuid range: ')
subuids = SubIds('/etc/subuid')
sur = subuids.find_range(uid, cc.user_name, 65536)
if sur is not None:
progress('already exists\n')
else:
progress('allocating\n')
sur = subuids.alloc_range(cc.user_name, 65536)
subprocess.run(
['usermod', '--add-subuids', f'{sur.first}-{sur.first + sur.count - 1}', cc.user_name],
check=True,
)
progress('Subgid range: ')
subgids = SubIds('/etc/subgid')
sgr = subgids.find_range(uid, cc.user_name, 65536)
if sgr is not None:
progress('already exists\n')
else:
progress('allocating\n')
sgr = subgids.alloc_range(cc.user_name, 65536)
subprocess.run(
['usermod', '--add-subgids', f'{sgr.first}-{sgr.first + sgr.count - 1}', cc.user_name],
check=True,
)
progress(f'Using user {cc.user_name}, uid {uid}, subuids {sur.first}+{sur.count}, subgids {sgr.first}+{sgr.count}\n')
root_path = Path(cc.root_dir)
progress(f'Container directory {root_path}: ')
if root_path.is_dir():
progress('already exists\n')
else:
root_path.mkdir(mode=0o750, exist_ok=True)
os.chown(root_path, 0, sgr.first)
progress('created\n')
data_path = root_path / 'data'
progress(f'Data directory {data_path}: ')
if data_path.is_dir():
progress('already exists\n')
else:
data_path.mkdir(mode=0o755, exist_ok=True)
os.chown(data_path, sur.first, sgr.first)
progress('created\n')
progress('IP address: ')
try:
ip = socket.gethostbyname(name)
except OSError as e:
die(str(e))
progress(f'{ip}\n')
def do_create(args: argparse.Namespace) -> None:
name = args.name
cc = load_container_config(name)
assert_root()
data_dir = Path(cc.root_dir) / 'data'
ip = socket.gethostbyname(name)
subprocess.run(
['podman', 'pull', cc.image],
check=True,
)
# FIXME: If the container was running, stop it
subprocess.run(
['podman', 'rm', '-if', name],
check=True,
)
subprocess.run(
[
'podman', 'create',
'--name', name,
'--conmon-pidfile', cc.pid_file,
'--log-driver', 'journald',
'--hostname', name,
'--volume', f'{data_dir}:/data',
'--network', 'static',
'--ip', ip,
'--subuidname', cc.user_name,
'--subgidname', cc.user_name,
cc.image,
],
check=True,
)
def do_start(args: argparse.Namespace) -> None:
pass
parser = argparse.ArgumentParser(
description='FIXME',
)
subparsers = parser.add_subparsers(help='action to perform', dest='action', required=True, metavar='ACTION')
init_parser = subparsers.add_parser('init', help='initialize a new container', description='Initialize a new container. Should be called by root.')
init_parser.add_argument('name', help='Name of the container')
create_parser = subparsers.add_parser('create', help='create a container from an image', description='Create a container from an image.')
create_parser.add_argument('name', help='Name of the container')
start_parser = subparsers.add_parser('start', help='start a container', description='Start a container')
start_parser.add_argument('name', help='Name of the container')
args = parser.parse_args()
try:
config = GlobalConfig.load('shipcat.toml')
except ConfigError as e:
print(e, file=sys.stderr)
sys.exit(1)
if args.action == 'init':
do_init(args)
elif args.action == 'create':
do_create(args)
elif args.action == 'start':
do_start(args)
......@@ -16,7 +16,7 @@ class ConfigError(RuntimeError):
class GlobalConfig:
container_dir: Path
container_config_dir: Path
verbosity: int
@classmethod
......@@ -39,7 +39,7 @@ class GlobalConfig:
def parse(self, walker: Walker) -> None:
with walker.enter_object() as w:
self.container_dir = Path(w['container_dir'].as_str())
self.container_config_dir = Path(w['container_config_dir'].as_str())
self.verbosity = w['verbosity'].as_int(0)
......@@ -50,12 +50,16 @@ class ContainerConfig:
allowed_users: Set[int]
allowed_groups: Set[int]
# Automatically generated
pid_file: str
user_name: str
@classmethod
def load(cls, global_config: GlobalConfig, name: str) -> 'ContainerConfig':
if not re.fullmatch(r'[0-9A-Za-z_-]+', name):
raise ConfigError(f'Invalid container name {name}')
path = global_config.container_dir / (name + '.toml')
path = global_config.container_config_dir / (name + '.toml')
try:
with open(path, 'rb') as f:
root = tomllib.load(f)
......@@ -95,3 +99,6 @@ class ContainerConfig:
except KeyError:
wu.raise_error(f'Unknown group {name}')
self.allowed_groups.add(grp.gr_gid)
self.pid_file = f'/run/{self.container}.pid'
self.user_name = self.container
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment