Приходилось ли вам экспериментировать с кодом или системными утилитами в Linux так, чтобы не трястись за базовую систему и не снести всё с потрохами в случае ошибки кода который должен запустится с root-привилегиями?


А как на счет того, что допустим, необходимо протестировать или запустить целый кластер разнообразных микросервисов на одной машине? Сотню или даже тысячу?


С виртуальными машинами управляемые гипервизором такие задачи решить может и получится, но какой ценой? Например, контейнер в LXD на базе дистрибутива Alpine Linux минимально потребляет всего 7.60MB ОЗУ, и где корневой раздел после запуска занимает 9.5MB! Как тебе такое, Илон Маск? Рекомендую ознакомиться с базовыми возможностями LXD — системы контейнеров в Linux


После того, как в целом стало ясно, что такое контейнеры LXD, пойдем дальше и подумаем, а что, если бы была такая платформа-комбайн, где можно было бы безопасно запускать код для хоста, генерировать графики, динамически (интерактивно) связывать UI-виджеты с твоим кодом, дополнять код текстом с блекджеком... форматированием? Что-то типа интерактивного блога? Вауу… Хочу! Хочу! :)


Заглядывай под кат где мы запустим в контейнере JupyterLab — следующей генерации пользовательского интерфейса вместо устаревшего Jupyter Notebook, а также установим такие модули Python как NumPy, Pandas, Matplotlib, IPyWidgets которые позволят вытворять всё перечисленное выше и сохранять это всё в специальном файле — IPython-ноутбуке.


image


Навигация



План взлёта на орбиту ^


image


Накидаем краткий план действий, чтобы нам было проще реализовать схему выше:


  • Установим и запустим контейнер на базе дистрибутива Alpine Linux. Мы будем использовать этот дистрибутив так как он направлен на минималистичность и установим в него только самый необходимый софт, ничего лишнего.
  • Добавим дополнительный виртуальный диск в контейнере которому зададим имя — hostfs и смонтируем к корневой ФС. Этот диск даст возможность использовать файлы на хосте из заданного каталога внутри контейнера. Тем самым данные будут у нас независимы от контейнера. В случае удаления контейнера, данные останутся на хосте. Также, эта схема полезна для разделения одних данных между многими контейнерами не используя штатные сетевые механизмы дистрибутива контейнера.
  • Установим Bash, sudo, необходимые библиотеки, добавим и настроим системного пользователя
  • Установим Python, модули и скомпилируем для них бинарные зависимости
  • Установим и запустим JupyterLab, настроим внешний вид, установим расширения для него.

В этой статье мы с вами начнём с запуска контейнера, не будем рассматривать установку и настройку LXD, всё это вы можете найти в другой статье — Базовые возможности LXD — системы контейнеров в Linux.


Установка и настройка базовой системы ^


Создаём контейнер командой в которой указываем образ — alpine3, идентификатор для контейнера — jupyterlab и при необходимости профили конфигурации:


lxc init alpine3 jupyterlab --profile=default --profile=hddroot

Здесь я использую профиль конфигурации hddroot который указывает создать контейнер с root-разделом в Storage Pool расположенным на физическом HDD диске:


lxc profile show hddroot

config: {}
description: ""
devices:
  root:
    path: /
    pool: hddpool
    type: disk
name: hddroot
used_by: []

lxc storage show hddpool

config:
  size: 10GB
  source: /dev/loop1
  volatile.initial_source: /dev/loop1
description: ""
name: hddpool
driver: btrfs
used_by:
- /1.0/images/ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
- /1.0/profiles/hddroot
status: Created
locations:
- none

Это даёт мне возможность экспериментировать с контейнерами на HDD диске экономя ресурсы SSD диска который также имеется в моей системе :) для которого у меня создан отдельный профиль конфигурации ssdroot.


После создания контейнера он находится в состоянии STOPPED, поэтому нам надо запустить в нём init-систему:


lxc start jupyterlab

Выведем список контейнеров в LXD используя ключ -c который указывает какие columns вывести на экран:


lxc list -c ns4b
+------------+---------+-------------------+--------------+
|    NAME    |  STATE  |       IPV4        | STORAGE POOL |
+------------+---------+-------------------+--------------+
| jupyterlab | RUNNING | 10.0.5.198 (eth0) | hddpool      |
+------------+---------+-------------------+--------------+

При создании контейнера IP адрес выбрался случайным образом, так как мы использовали профиль конфигурации default который был ранее сконфигурирован в статье Базовые возможности LXD — системы контейнеров в Linux.


Мы поменяем этот IP адрес на более запоминающийся, создав сетевой интерфейс на уровне контейнера, а не на уровне профиля конфигурации как это сейчас в текущей конфигурации. Это не обязательно делать, вы можете пропустить это.


Создаём сетевой интерфейс eth0 который линкуем с коммутатором (сетевым мостом) lxdbr0 в котором мы включили NAT по прошлой статье и контейнеру сейчас будет доступ в Интернет, а также интерфейсу назначаем статический IP адрес — 10.0.5.5:


lxc config device add jupyterlab eth0 nic name=eth0 nictype=bridged parent=lxdbr0 ipv4.address=10.0.5.5

После добавления устройства, контейнер необходимо перезагрузить:


lxc restart jupyterlab

Проверяем статус контейнера:


lxc list -c ns4b
+------------+---------+------------------+--------------+
|    NAME    |  STATE  |       IPV4       | STORAGE POOL |
+------------+---------+------------------+--------------+
| jupyterlab | RUNNING | 10.0.5.5 (eth0)  | hddpool      |
+------------+---------+------------------+--------------+

Установка базового софта и настройка системы ^


Для администрирования нашего контейнера необходимо установить следующий софт:


Package Description
bash The GNU Bourne Again shell
bash-completion Programmable completion for the bash shell
sudo Give certain users the ability to run some commands as root
shadow Password and account management tool suite with support for shadow files and PAM
tzdata Sources for time zone and daylight saving time data
nano Pico editor clone with enhancements

Дополнительно, вы можете установить поддержку в системе man-pages установив следующие пакеты — man man-pages mdocml-apropos less


lxc exec jupyterlab -- apk add bash bash-completion sudo shadow tzdata nano

Разберём команды и ключи который мы использовали:


  • lxc — Вызов клиента LXD
  • exec — Метод клиента LXD, который запускает команду в контейнере
  • jupyterlab — Идентификатор контейнера
  • -- — Специальный ключ, который указывает не интерпретировать дальше ключи как ключи для lxc и передать всю оставшуюся строку как есть в контейнер
  • apk — Пакетный менеджер дистрибутива Alpine Linux
  • add — Метод пакетного менеджера который инсталлирует указанные после команды пакеты

Далее, установим в системе тайм-зону Europe/Moscow:


lxc exec jupyterlab -- cp /usr/share/zoneinfo/Europe/Moscow /etc/localtime

После установки тайм-зоны, пакет tzdata в системе больше не нужен, он будет занимать место, поэтому, удалим его:


lxc exec jupyterlab -- apk del tzdata

Проверяем тайм-зону:


lxc exec jupyterlab -- date

Wed Apr 15 10:49:56 MSK 2020

Чтобы не тратить много времени на настройку Bash для новых пользователей в контейнере, следующими действиями мы скопируем в него такие файлы как .bash_profile, .bashrc, .dir_colors из директории /etc/skel вашей системы, это приукрасит Bash в контейнере, в интерактивном режиме. У меня, установка этих файлов из дистрибутива Manjaro Linux не вызывает критических проблем в контейнере, но у вас может быть иначе, и вам нужно самостоятельно разобраться с этим в случае ошибки в контейнере.


Копируем skel-файлы в контейнер. Ключ --create-dirs создаст необходимые директории, если они не существуют:


lxc file push /etc/skel/.bash_profile jupyterlab/etc/skel/.bash_profile --create-dirs
lxc file push /etc/skel/.bashrc jupyterlab/etc/skel/.bashrc
lxc file push /etc/skel/.dir_colors jupyterlab/etc/skel/.dir_colors

Для уже существующего root пользователя скопируем в домашнюю директорию только что установленные в контейнере skel-файлы:


lxc exec jupyterlab -- cp /etc/skel/.bash_profile /root/.bash_profile
lxc exec jupyterlab -- cp /etc/skel/.bashrc /root/.bashrc
lxc exec jupyterlab -- cp /etc/skel/.dir_colors /root/.dir_colors

В Alpine Linux для пользователей устанавливается системная оболочка /bin/sh, мы заменим её у root пользователя на Bash:


lxc exec jupyterlab -- usermod --shell=/bin/bash root

Командой ниже мы сгенерируем случайный пароль для пользователя root который вы увидите на экране консоли после её выполнения. Запомните и запишите его куда-нибудь или забудьте, скорее всего он вам не пригодится больше :)


lxc exec jupyterlab -- /bin/bash -c "PASSWD=\$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); echo \"root:\$PASSWD\" | chpasswd && echo \"New Password: \$PASSWD\""

New Password: sFiXEvBswuWA

Добавим нового системного пользователя — jupyter для которого позже настроим JupyterLab


lxc exec jupyterlab -- useradd --create-home --shell=/bin/bash jupyter

Сгенерируем и установим ему пароль:


lxc exec jupyterlab -- /bin/bash -c "PASSWD=\$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); echo \"jupyter:\$PASSWD\" | chpasswd && echo \"New Password: \$PASSWD\""

New Password: ZIcbzWrF8tki

Далее выполним две команды, первая создаст системную группу sudo, а вторая добавит в неё пользователя jupyter:


lxc exec jupyterlab -- groupadd --system sudo
lxc exec jupyterlab -- groupmems --group sudo --add jupyter

Просмотрим, в какие группы входит пользователь jupyter:


lxc exec jupyterlab -- id -Gn jupyter

jupyter sudo

Всё — ок, двигаемся дальше.


Разрешим всем пользователям которые входят в группу sudo использовать команду sudo. Для этого выполните следующий скрипт, где sed снимет комментарий со строчки параметра в конфигурационном файле /etc/sudoers:


lxc exec jupyterlab -- /bin/bash -c "sed --in-place -e '/^#[ \t]*%sudo[ \t]*ALL=(ALL)[ \t]*ALL$/ s/^[# ]*//' /etc/sudoers"

Установка и настройка JupyterLab ^


JupyterLab — это Python приложение, поэтому мы должны прежде установить этот интерпретатор. Также, JupyterLab мы будем устанавливать с помощью питоновского пакетного менеджера pip, а не системного, потому что в системном репозитории он может быть устаревшим, и поэтому, мы должны вручную разрешить зависимости для него установив следующие пакеты — python3 python3-dev gcc libc-dev zeromq-dev:


lxc exec jupyterlab -- apk add python3 python3-dev gcc libc-dev zeromq-dev

Обновим python-модули и пакетный менеджер pip до актуальной версии:


lxc exec jupyterlab -- python3 -m pip install --upgrade pip setuptools wheel

Устанавливаем JupyterLab через пакетный менеджер pip:


lxc exec jupyterlab -- python3 -m pip install jupyterlab

Так как расширения в JupyterLab являются экспериментальными и официально они не поставляются вместе с пакетом jupyterlab, поэтому, мы должны установить и настроить это вручную.


Установим NodeJS и менеджер пакетов для него — NPM, так как JupyterLab использует их для своих расширений:


lxc exec jupyterlab -- apk add nodejs npm

Чтобы расширения для JupyterLab которые мы установим работали, их нужно устанавливать в пользовательскую директорию так как приложение будет запускаться от пользователя jupyter. Проблема в том, что нет параметра в команде запуска которой можно передать каталог, приложение воспринимает только переменную окружения и поэтому мы её должны определить. Для этого, мы пропишем команду экспорта переменной JUPYTERLAB_DIR в окружении пользователя jupyter, в файл .bashrc, который выполняется каждый раз при входе пользователя в систему:


lxc exec jupyterlab -- su -l jupyter -c "echo -e \"\nexport JUPYTERLAB_DIR=\$HOME/.local/share/jupyter/lab\" >> .bashrc"

Следующей командой установим специальное расширение — менеджер расширений в JupyterLab:


lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=\$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager"

Сейчас уже всё готово для первого запуска JupyterLab, но мы можем еще установить несколько полезных расширений:


  • toc — Table of Contents, генерирует список заголовков в статье/ноутбуке
  • jupyterlab-horizon-theme — Тема оформления UI
  • jupyterlab_neon_theme — Тема оформления UI
  • jupyterlab-ubu-theme — Ещё одна тема оформления от автора этой статьи :) Но в этом случае, будет показана установка из репозитория GitHub

Итак, выполните последовательно следующие команды, чтобы установить эти расширения:


lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=\$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build @jupyterlab/toc @mohirio/jupyterlab-horizon-theme @yeebc/jupyterlab_neon_theme"

lxc exec jupyterlab -- su -l jupyter -c "wget -c https://github.com/microcoder/jupyterlab-ubu-theme/archive/master.zip"

lxc exec jupyterlab -- su -l jupyter -c "unzip -q master.zip && rm master.zip"

lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=\$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build jupyterlab-ubu-theme-master"

lxc exec jupyterlab -- su -l jupyter -c "rm -r jupyterlab-ubu-theme-master"

После установки расширений мы должны их скомпилировать, так как ранее, при установке указывали ключ --no-build для экономии времени. Сейчас мы значительно ускоримся если выполним компиляцию сразу для всех расширений:


lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=\$HOME/.local/share/jupyter/lab; jupyter lab build"

Почистим кеши установок:


lxc exec jupyterlab -- su -l jupyter -c "jupyter lab clean && jlpm cache clean && npm cache clean --force"

Далее выполните следующие две команды для первого запуска JupyterLab. Можно было бы его запустить одной командой, но в этом случае, команда запуска JupyterLab будет запоминаться bash'ем в контейнере, а не на хосте, где и так команд хватает для записи их в историю :)


Логинимся в контейнере как пользователь jupyter:


lxc exec jupyterlab -- su -l jupyter

Далее запустите JupyterLab с ключами и параметрами как указано:


[jupyter@jupyterlab ~]$ jupyter lab --ip=0.0.0.0 --no-browser

Перейдите в web-браузере по адресу http://10.0.5.5:8888 и на открывшейся странице введите token доступа который вы увидите в консоли. Скопируйте его и вставьте на странице, затем нажмите Login. После входа, перейдите слева в меню расширений, как показано на рисунке ниже, где вам предложат при активации менеджера расширений принять на себя риски по безопасности устанавливая расширения от третьих лиц за которые команда JupyterLab development ответственности не несёт:


image


Однако, мы как раз для этого изолируем целиком JupyterLab и помещаем его в контейнер, чтобы сторонние расширения требующие и использующие NodeJS не смогли как минимум похитить данные на диске кроме тех, которые мы откроем внутри контейнера. Добраться к вашим приватным документам на хосте в /home процессам из контейнера вряд ли получится, а если и получится, то на это нужно иметь привилегии на файлы в хостовой системе, так как мы запускаем контейнер в непривилегированном режиме. Исходя из этой информации вы можете оценить риск включения расширений в JupyterLab.


Созданные IPython-ноутбуки (страницы в JupyterLab) сейчас будут создаваться в домашней директории пользователя — /home/jupyter, но в наших планах разделить данные (расшарить) между хостом и контейнером, поэтому, вернитесь в консоль и остановите JupyterLab выполнив hotkey — CTRL+C и ответив y на запрос. Затем разорвите интерактивную сессию пользователя jupyter выполнив хоткей CTRL+D.


Разделяем данные с хостом ^


Чтобы разделить данные с хостом, нужно создать в контейнере такое устройство, которое это позволяет делать и для этого выполните следующую команду где мы указываем следующие ключи:


  • lxc config device add — Команда добавляет конфигурацию устройства
  • jupyter — Идентификатор контейнера в который добавляется конфигурация
  • hostfs — Идентификатор устройства. Вы можете задать любое имя.
  • disk — Указывается тип устройства
  • path — Указывается путь в контейнере к которому LXD смонтирует это устройство
  • source — Указывается источник, путь к каталогу на хосте который вы желаете разделить с контейнером. Укажите путь согласно вашим предпочтениям

lxc config device add jupyterlab hostfs disk path=/mnt/hostfs source=/home/dv/projects/ipython-notebooks

Для каталога /home/dv/projects/ipython-notebooks должно быть установлено разрешение контейнерному пользователю который сейчас имеет UID равный SubUID + UID, смотрите главу Безопасность. Привилегии контейнеров в статье Базовые возможности LXD — системы контейнеров в Linux.


Устанавливаем разрешение на хосте, где владельцем будет контейнерный пользователь jupyter, а переменная $USER укажет вашего хостового пользователя в качестве группы:


sudo chown 1001000:$USER /home/dv/projects/ipython-notebooks

Hello, World! ^


Если у вас еще открыта консольная сессия в контейнере с JupyterLab, то перезапустите её с новым ключом --notebook-dir задав значение /mnt/hostfs в качестве пути до корня ноутбуков в контейнере для устройства которое мы создали в предыдущем шаге:


jupyter lab --ip=0.0.0.0 --no-browser --notebook-dir=/mnt/hostfs

Затем перейдите на страницу http://10.0.5.5:8888 и создайте первый ваш ноутбук нажав кнопку на странице как указано на картинке ниже:


image


Затем в поле на странице введите код на языке Python который выведет классический Hello World!. По окончании ввода нажмите CTRL+ENTER или кнопку "play" на панели инструментов сверху чтобы JupyterLab выполнил это:


image


На этом почти всё готово к использованию, но будет неинтересно, если мы не установим дополнительные Python-модули (полноценные приложения) которые позволяют значительно расширить стандартные возможности Python в JupyterLab, поэтому, двигаемся дальше :)


P.S. Интересно то, что старая реализация Jupyter под кодовым именем Jupyter Notebook никуда не делась и она существует параллельно с JupyterLab. Для перехода к старой версии перейдите по ссылке добавив в адресе суффикс/tree, а переход к новой версии осуществляется с суффиксом /lab, но его не обязательно указывать:



Расширяем возможности Python ^


В этом разделе мы установим такие мощные модули языка Python как NumPy, Pandas, Matplotlib, IPyWidgets результаты работы которых интегрируются в ноутбуки JupyterLab.


Прежде чем установить перечисленные модули Python через пакетный менеджер pip мы должны вначале разрешить системные зависимости в Alpine Linux:


  • g++ — Нужен для компиляции модулей, так как некоторые из них реализованы на языке C++ и подключаются к Python в рантайме как бинарные модули
  • freetype-dev — зависимость для Python модуля Matplotlib

Устанавливаем зависимости:


lxc exec jupyterlab -- apk add g++ freetype-dev

Есть одна проблема, в текущем состоянии дистрибутива Alpine Linux скомпилировать новую версию NumPy не получится, вылетит ошибка компиляции которую мне не удалось разрешить:


ERROR: Could not build wheels for numpy which use PEP 517 and cannot be installed directly

Поэтому, этот модуль мы установим как системный пакет который распространяет уже скомпилированную версию, но немного старее, чем доступна сейчас на сайте:


lxc exec jupyterlab -- apk add py3-numpy py3-numpy-dev

Далее устанавливаем Python-модули через пакетный менеджер pip. Наберитесь терпения, так как некоторые модули будут компилироваться и это займет несколько минут. На моей машине компиляция заняла ~15 минут:


lxc exec jupyterlab -- python3 -m pip install pandas matplotlib ipywidgets

Чистим кеши установок:


lxc exec jupyterlab -- rm -rf /home/*/.cache/pip/*
lxc exec jupyterlab -- rm -rf /root/.cache/pip/*

Тестируем модули в JupyterLab ^


Если у вас запущен JupyterLab, перезапустите его, чтобы новые установленные модули активировались. Для этого в консольной сессии нажмите CTRL+C там где он у вас запущен и введите y на запрос остановки, а затем запустите заново JupyterLab нажав стрелочку на клавиатуре "вверх", чтобы не вводить команду заново и потом Enter чтобы запустить:


jupyter lab --ip=0.0.0.0 --no-browser --notebook-dir=/mnt/hostfs

Перейдите на страницу http://10.0.5.5:8888/lab или обновите в браузере страницу, а затем введите следующий код в новой ячейке ноутбука:


%matplotlib inline

from ipywidgets import interactive
import matplotlib.pyplot as plt
import numpy as np

def f(m, b):
    plt.figure(2)
    x = np.linspace(-10, 10, num=1000)
    plt.plot(x, m * x + b)
    plt.ylim(-5, 5)
    plt.show()

interactive_plot = interactive(f, m=(-2.0, 2.0), b=(-3, 3, 0.5))
output = interactive_plot.children[-1]
output.layout.height = '350px'
interactive_plot

У вас должен получиться результат как на картинке ниже, где IPyWidgets генерирует UI-элемент на странице который интерактивно взаимодействует с исходным кодом, а также Matplotlib выводит результат кода в виде картинки как график функции:


image


Многие примеры IPyWidgets вы можете найти в туториалах здесь


Что ещё? ^


Вы молодцы, если остались и дошли до самого конца статьи. Я специально не стал выкладывать готовый скрипт в конце статьи который бы установил JupyterLab в "один клик", чтобы поощрить тружеников :) Но вы можете это сделать самостоятельно, так как уже знаете как, собрав команды в единый Bash скрипт :)


Также, вы можете:


  • Задать сетевое имя для контейнера вместо IP адреса прописав его в простом /etc/hosts и в браузере набирать адрес http://jupyter.local:8888
  • Поиграться с ограничением ресурсов для контейнера, для этого прочтите главу в базовых возможностях LXD или получите больше информации на сайте разработчика LXD.
  • Поменять тему оформления:

image


И много чего ещё вы можете! На этом всё. Желаю вам успехов!




UPDATE: 15.04.2020 18:30 — Исправил ошибки в главе "Hello, World!"
UPDATE: 16.04.2020 10:00 — Скорректировал и добавил текст в описании активации менеджера расширений JupyterLab
UPDATE: 16.04.2020 10:40 — Исправил найденные ошибки в тексте и немного изменил к лучшему главу "Установка базового софта и настройка системы"