Разработать утилиту на языке Python, которая запускает команду в изолированном контейнере с использованием namespaces и overlayfs.
- Конфигурация через файл
config.json - Создание PID, Mount, UTS namespaces
- Установка hostname из конфига
- Создание директории
/var/lib/mycontainer/{id} - Использование Alpine Linux в качестве rootfs
- Настройка overlayfs (lowerdir, upperdir, workdir, merged)
- Запуск команды как PID 1 в foreground режиме
- Монтирование /proc внутри контейнера
Для начала создадим директорию утилиты и хранилище контейнеров
~/mycontainer- директория для исходного кода утилиты/var/lib/mycontainer- баз хранилище данных контейнеров (аналог/var/lib/docker)/var/lib/mycontainer/rootfs- базовая файловая система для всех контейнеров
Это соответствует требованию задания о создании директории /var/lib/{имя-утилиты}/{id}.
cd /var/lib/mycontainer
sudo wget https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz
sudo tar -xzf alpine-minirootfs-3.19.1-x86_64.tar.gz -C rootfs
sudo rm alpine-minirootfs-3.19.1-x86_64.tar.gz - Скачали мин образ Alpine Linux
- Распаковали в директорию
/var/lib/mycontainer/rootfsудалили после распаковки архив ненужный - Это будет использоваться как lowerdir в overlayfs
По заданию утилита должна конфигурироваться через config.json по спецификации OCI
Содержимое config.json:
{
"hostname": "mycontainer"
}Код утилиты mycontainer.py :
#!/usr/bin/env python3
import os, sys, json, subprocess, argparse
from pathlib import Path
def load_config(config_path):
with open(config_path) as f:
return json.load(f)
def setup_container_dirs(container_id):
base_path = Path(f"/var/lib/mycontainer/{container_id}")
base_path.mkdir(parents=True, exist_ok=True)
(base_path / "upper").mkdir(exist_ok=True)
(base_path / "work").mkdir(exist_ok=True)
(base_path / "merged").mkdir(exist_ok=True)
return base_path
def mount_overlayfs(container_id, base_path):
rootfs_base = "/var/lib/mycontainer/rootfs"
merged_path = base_path / "merged"
options = f"lowerdir={rootfs_base},upperdir={base_path}/upper,workdir={base_path}/work"
subprocess.run(["mount", "-t", "overlay", "overlay", "-o", options, str(merged_path)], check=True)
return merged_path
def run_container(container_id, command, hostname):
base_path = setup_container_dirs(container_id)
merged_path = mount_overlayfs(container_id, base_path)
print(f"[+] Контейнер {container_id} запущен")
print(f"[+] Команда: {' '.join(command)}")
init_script = f"""#!/bin/sh
mount -t proc proc /proc
hostname {hostname}
exec {" ".join(command)}
"""
script_path = merged_path / "init.sh"
with open(script_path, 'w') as f:
f.write(init_script)
os.chmod(script_path, 0o755)
container_cmd = ["unshare", "--fork", "--pid", "--mount", "--uts", "chroot", str(merged_path), "/init.sh"]
process = subprocess.Popen(container_cmd)
process.wait()
subprocess.run(["umount", str(merged_path)], check=False)
return process.returncode
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--config', '-c', required=True)
parser.add_argument('--id', required=True)
parser.add_argument('cmd', nargs=argparse.REMAINDER)
args = parser.parse_args()
if os.geteuid() != 0:
print("[-] Требуются права root")
sys.exit(1)
config = load_config(args.config)
hostname = config.get('hostname', 'container')
exit_code = run_container(args.id, args.cmd, hostname)
sys.exit(exit_code)
if __name__ == "__main__":
main()Что реализовано в коде:
-
setup_container_dirs()- создает директорию/var/lib/mycontainer/{id}с поддиректориями upper, work, merged. Это соотв требованию о создании директории для каж контейнера -
mount_overlayfs()- монтирует overlayfs:- lowerdir: базовый Alpine rootfs
- upperdir:
/var/lib/mycontainer/{id}/upper - workdir:
/var/lib/mycontainer/{id}/work - merged:
/var/lib/mycontainer/{id}/merged
-
run_container()- основная логика:- Создает скрипт инициализации, который монтирует proc и устанавливает hostname
- Использует
unshareс флагами--pid,--mount,--utsдля создания namespaces - Выполняет
chrootв merged директорию - Запускает команду через
exec, делая её PID 1 - Ожидает завершения (foreground режим)
-
Монтирование /proc - реализовано через
mount -t proc proc /procвнутри скрипта инициализации
Делаем файл исполняемымм
chmod +x mycontainer.pyЗапустим контейнер и проверим, что директория создалась с нужными папками
✅ Директория /var/lib/mycontainer/test1 создана, внутри присутствуют папки upper, work, mergedс
Проверим, что внутри контейнера hostname устанавливается из config.json и изолирован от хоста.
✅ Внутри контейнера hostname = "mycontainer" (из config.json), на хосте другое значение. UTS namespace работает, hostname изолирован
Проверим, что процессы внутри контейнера изолированы. Сравним количество процессов на хосте и внутри контейнера
Вывод: Внутри контейнера виден только 1 процесс, в то время как на хосте их 353 -->(следовательно) PID namespace изолирует процессы
Проверим, что изменения, сделанные в контейнере, сохраняются в upperdir, а базовый rootfs остается неизменным.
- cat /data.txt в контейнере показывает содержимое из merged (объединение lower с upper)
- cat upper/data.txt на хосте показывает, что файл физически лежит в upperdir, слое куда записываются изменения
- ls rootfs/ | grep data.txt пуст — тк базовый образ Alpine не был изменен (lowerdir только для чтения)
В итотге: Файл создан внутри контейнера. Файл сохранен в /var/lib/mycontainer/test4/upper/data.txt. Базовый Alpine rootfs остался неизменным. Overlayfs работает корректно
Проверим, что внутри контейнера смонтирована файловая система /proc для работы утилит (типа ps)
- Команда mount показывает: /proc смонтирован как файловая система типа proc
- Команда ls /proc/ выводит список файлов и директорий, подтверждая доступность
Проверим, что утилита ожидает завершения команды.
time sudo python3 mycontainer.py --config config.json --id fg_test /bin/sleep 2
Видночто утилита "зависнет" на 2 секунды, а затем завершится --> foreground режим — утилита ждет, пока команда внутри контейнера не выполнится
Проверим, что запускаемая команда имеет PID 1 внутри контейнера.
Выполненная команда:
sudo python3 mycontainer.py --config config.json --id test7 /bin/sh -c "ps aux | head -5"
Процесс init.sh имеет PID 1 внутри контейнера — это скрипт инициализации, который монтирует /proc и устанавливает hostname, внутри него выполняется наша команда (ps aux | head -5), а все процессы внутри контейнера имеют свои PID, начинающиеся с 1
В ходе выполнения лабораторной работы разработана утилита mycontainer.py, читающая конфигурацию из config.json, создающая изолированное окружение с помощью PID, Mount и UTS namespaces, настраивающая overlayfs (lowerdir — Alpine Linux, upperdir — хранилище изменений для каждого контейнера), запускающая команду как PID 1 в foreground режиме и монтирующая /proc для работы утилит типа ps