Пара 7 / октябрь
Сеть: sockets, маршруты, Netfilter и Wi-Fi Проверить путь HTTP-запроса и различать ошибки DNS, соединения и приложения.
Проверить путь HTTP-запроса и различать ошибки DNS, соединения и приложения. На одной машине может работать много сетевых служб. IP помогает доставить пакет к узлу, порт помогает операционной системе выбрать сокет. 127.0.0.1 — loopback текущей среды; внутри контейнера позже это будет сам контейнер. 0.0.0.0 в bind означает слушать на всех IPv4-интерфейсах, а не адрес, который надо сообщить пользователю. Прослушивание не доказывает доступность снаружи: нужны маршруты и разрешение firewall. В WSL Windows localhost часто перенаправляется в Linux, но режим сети и настройки имеют значение. Не публикуем учебные порты в интернет ради первого опыта.
Контекст Локальный получатель, транзит и локальный отправитель имеют разные пути.
Проверить путь HTTP-запроса и различать ошибки DNS, соединения и приложения.
Входящий IP-пакет попадает в сетевой стек, проходит соответствующие hooks и решение маршрута. Для локального адресата важен INPUT, для пересылки через хост — FORWARD; локально сформированный пакет проходит OUTPUT, перед выходом участвует POSTROUTING. PREROUTING расположен до решения маршрута. Это упрощённая карта: конкретные priorities, families, bridge/ingress и connection tracking добавляют детали. Firewall проверяет правила в нужной точке, а NAT меняет адреса или порты; NAT не является синонимом ограничения доступа. Если процесс слушает только 127.0.0.1, открытие firewall не заставит его слушать внешний адрес. При таймауте отдельно проверяют наличие слушателя, route и filter, а DNS лишь определяет адрес. Правила на удалённом сервере без запасного доступа не меняем. Учебная демонстрация — схема и чтение имеющегося ruleset, а не очистка правил хоста.
Адрес и порт IP выбирает узел, порт — точку приёма на нём.
01 IP узла
→ 02 Порт
→ 03 Сокет процесса
→ 04 Обработка
Аналогия: адрес дома и номер квартиры.
На одной машине может работать много сетевых служб. IP помогает доставить пакет к узлу, порт помогает операционной системе выбрать сокет. 127.0.0.1 — loopback текущей среды; внутри контейнера позже это будет сам контейнер. 0.0.0.0 в bind означает слушать на всех IPv4-интерфейсах, а не адрес, который надо сообщить пользователю. Прослушивание не доказывает доступность снаружи: нужны маршруты и разрешение firewall. В WSL Windows localhost часто перенаправляется в Linux, но режим сети и настройки имеют значение. Не публикуем учебные порты в интернет ради первого опыта.
DNS и TCP Имя сначала нужно разрешить, затем установить соединение.
01 Имя
→ 02 DNS / адрес
→ 03 TCP / соединение
→ 04 HTTP
Аналогия: справочник может знать адрес магазина, даже если магазин закрыт.
DNS преобразует имя в записи, часто A/AAAA для адресов. Кеш и TTL означают, что изменения не обязательно мгновенно видны всем. Успешный DNS не гарантирует, что по адресу работает нужный сервер. TCP устанавливает соединение; timeout отличается от connection refused. ping не проверяет HTTP и может блокироваться даже у исправного сервиса. getent hosts использует системное разрешение имён, полезное для проверки того, что видит приложение. Разделяем вопросы: имя разрешилось? порт доступен? HTTP ответил? Это уменьшает случайные перезапуски.
HTTP — запрос и ответ Статус, заголовки и тело дают разные доказательства.
01 Метод + путь
→ 02 Заголовки
→ 03 Тело
→ 04 Статус + ответ
Аналогия: бланк заказа, служебная отметка и результат заказа.
Метод и путь задают действие; заголовки несут метаданные, тело — данные. Для курса используем GET /healthz и POST /predict. Статусы 2xx говорят об успешной обработке в смысле HTTP, 4xx — проблеме запроса/доступа, 5xx — ошибке серверной обработки. 200 не гарантирует, что предсказание научно правильно. HTTPS добавляет TLS: шифрование и проверку идентичности сервера сертификатом. curl -i показывает ответ с заголовками, curl -v помогает исследовать соединение, но может раскрывать чувствительные заголовки. На демонстрации используем только фиктивные запросы.
Пакет проходит маршрутизацию и hooks Netfilter Локальный получатель, транзит и локальный отправитель имеют разные пути.
Netfilter: входящий пакет идёт к локальному процессу или через маршрутизатор Интерфейс Входящий IP-пакет PREROUTING До решения маршрута Routing decision Локальный адрес или другой хост локально INPUT Локальному socket транзит FORWARD Передача дальше POSTROUTING После маршрута, перед выходом Локально созданные пакеты проходят OUTPUT. nft и iptables — интерфейсы настройки, не отдельные сетевые стеки. Локальный HTTP-сервис нужен в INPUT-пути, router-трафик — в FORWARD. Адрес/порт приложения всё равно проверяются отдельно.
Входящий IP-пакет попадает в сетевой стек, проходит соответствующие hooks и решение маршрута. Для локального адресата важен INPUT, для пересылки через хост — FORWARD; локально сформированный пакет проходит OUTPUT, перед выходом участвует POSTROUTING. PREROUTING расположен до решения маршрута. Это упрощённая карта: конкретные priorities, families, bridge/ingress и connection tracking добавляют детали. Firewall проверяет правила в нужной точке, а NAT меняет адреса или порты; NAT не является синонимом ограничения доступа. Если процесс слушает только 127.0.0.1, открытие firewall не заставит его слушать внешний адрес. При таймауте отдельно проверяют наличие слушателя, route и filter, а DNS лишь определяет адрес. Правила на удалённом сервере без запасного доступа не меняем. Учебная демонстрация — схема и чтение имеющегося ruleset, а не очистка правил хоста.
Локальный HTTP-сервис нужен в INPUT-пути, router-трафик — в FORWARD. Адрес/порт приложения всё равно проверяются отдельно.
iptables и nftables — способы настроить правила Важен реальный backend, а не только имя команды.
nftables и iptables работают с инфраструктурой Netfilter в ядре nft Нативный nftables ruleset iptables-nft Совместимый CLI → nft backend iptables-legacy Legacy xtables backend Netfilter hooks в Linux Фильтрация / NAT / connection tracking Не смешиваем менеджеры правил без понимания backend. DROP отличается от REJECT; NAT не равен firewall. Ubuntu: nft --version; iptables --version; sudo nft list ruleset — только чтение. WSL2 имеет также сетевые ограничения Windows-хоста.
Netfilter — инфраструктура обработки пакетов в ядре. Iptables является историческим интерфейсом управления xtables; nftables даёт другой ruleset и nft CLI. На многих современных системах iptables-nft переводит знакомый синтаксис в nft backend, тогда как iptables-legacy обращается к legacy механизму. Поэтому iptables --version и nft list ruleset помогают выяснить, что действительно используется. UFW и firewalld — ещё один слой управления, который может генерировать правила; ручное изменение параллельно менеджеру может быть перезаписано. В nftables ruleset состоит из tables, chains и rules; base chain подключается к hook с priority и policy. DROP молча отбрасывает пакет, REJECT возвращает отказ по выбранному механизму. Stateful фильтрация использует состояние соединения, но понятие established не делает приложение доверенным. Пример конфигурации хранится в файле и проверяется в изолированной VM/namespace, без nft flush ruleset на ноутбуке.
Ubuntu: nft --version; iptables --version; sudo nft list ruleset — только чтение. WSL2 имеет также сетевые ограничения Windows-хоста.
wlo1 — имя интерфейса, managed — режим Radio-возможности определяются PHY, драйвером и поддержанными сочетаниями.
Имя wlo1 не задаёт режим Wi-Fi: возможности определяют адаптер и драйвер Managed / station Клиент подключается к AP AP Точка доступа для клиентов Monitor Наблюдение радио-кадров iw dev / iw list Проверяем interface type, supported modes, допустимые сочетания и возможности драйвера WSL2 обычно видит виртуальный Ethernet, а не Wi-Fi PHY ноутбука. Monitor не гарантирует чтение шифрованного трафика. Kali не создаёт аппаратную возможность monitor. Используется тот же Linux wireless stack и поддержка конкретного драйвера.
Predictable interface names вроде wlo1 или wlp2s0 обозначают интерфейс по правилам именования, а не режим «взлома» или тип firewall. Managed/station — Wi-Fi клиент, AP — точка доступа, monitor — получение радио-кадров в пределах возможностей адаптера. Iw dev показывает интерфейсы и type, iw list — capabilities PHY. Не каждый адаптер поддерживает monitor, AP или одновременные комбинации; виртуальная машина может не получить прямого Wi-Fi доступа. WSL2 обычно видит виртуальную сеть, поэтому iw может не показывать Wi-Fi PHY ноутбука. Monitor не равен promiscuous mode обычного Ethernet и не гарантирует расшифровку защищённого трафика. В занятии читаем режимы своего устройства и обсуждаем схему. Переключение рабочего Wi-Fi на monitor отключит обычное подключение; такую демонстрацию делают только на отдельном собственном адаптере, а не на сети аудитории.
Kali не создаёт аппаратную возможность monitor. Используется тот же Linux wireless stack и поддержка конкретного драйвера.
Наблюдение механизмов ip -brief address
ip route
ss -lnt
getent hosts example.com
nft --version
iptables --version
# Physical Linux with iw and a supported wireless adapter
iw dev
iw list | head -n 40
# Isolated user + network namespace; no host rules changedПолный разбор команд в конспекте → Соберите путь собственного HTTP-запроса от curl до LISTEN. Firewall-пример применяется только внутри unshare -Urn, то есть собственного user/network namespace. Если user namespaces заблокированы политикой системы, используйте отдельную VM. Не выполняйте nft -f этого файла в обычном namespace хоста. В ruleset отдельная inet table, base chain INPUT, loopback, established/related и TCP 8000; policy drop блокирует остальное в изолированной среде. В WSL при отсутствии iw PHY запишите границу виртуальной среды; это не повод устанавливать Kali.
Команды и наблюдения Ubuntu / Bash, два терминала
mkdir -p ~/devops-course/web
cd ~/devops-course/web
printf "hello from Ubuntu
" > index.html
python3 -m http.server 8080 --bind 127.0.0.1
# In another Ubuntu terminal
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/missing
ss -ltnp | grep :8080
getent hosts example.comДемонстрационный http.server не используем как production-сервер. Останавливаем Ctrl+C в первом терминале. Ожидаемое наблюдение: Для / ответ 200, для /missing — 404; ss показывает слушающий 127.0.0.1:8080.
Разбор результата Для / ответ 200, для /missing — 404; ss показывает слушающий 127.0.0.1:8080.
Демонстрационный http.server не используем как production-сервер. Останавливаем Ctrl+C в первом терминале.
Для ошибки имени используйте getent hosts definitely-missing.invalid; .invalid зарезервирован для несуществующих имён. Для остановленного порта curl --connect-timeout 3 http://127.0.0.1:8080/ даст ошибку соединения. Для работающего сервера curl -i .../missing даст HTTP 404. Порядок: URL и среда → разрешение имени → слушающий процесс/порт → HTTP-ответ → журнал приложения.
На macOS curl и Python-команды одинаковы при наличии Python. Вместо ss — lsof; вместо getent hosts — dscacheutil для системного разрешения имени. DNS, TCP и HTTP объясняются одинаково, но инструменты и сетевые режимы ОС отличаются.
python3 -m http.server 8080 --bind 127.0.0.1
# In another Terminal window
curl -i http://127.0.0.1:8080/
lsof -nP -iTCP:8080 -sTCP:LISTEN
dscacheutil -q host -a name example.comКоманды macOS необходимо проверить на конкретном устройстве. curl и Python-команды одинаковы при наличии Python. Вместо ss — lsof; вместо getent hosts — dscacheutil для системного разрешения имени. DNS, TCP и HTTP объясняются одинаково, но инструменты и сетевые режимы ОС отличаются.
Три диагноза одного симптома Запустите учебный HTTP-сервер и получите нормальный ответ. Запросите несуществующий путь и запишите HTTP-статус. Остановите сервер и повторите запрос: сравните ошибку соединения с 404. Проверьте несуществующее имя учебного домена с getent; составьте порядок диагностики. Работа в парах: один оператор, второй проверяет гипотезу. Смена ролей в середине. Для ошибки имени используйте getent hosts definitely-missing.invalid; .invalid зарезервирован для несуществующих имён. Для остановленного порта curl --connect-timeout 3 http://127.0.0.1:8080/ даст ошибку соединения. Для работающего сервера curl -i .../missing даст HTTP 404. Порядок: URL и среда → разрешение имени → слушающий процесс/порт → HTTP-ответ → журнал приложения.
Что должно получиться Таблица DNS / TCP / HTTP с командой и наблюдаемым результатом.
Студент не лечит 404 изменением DNS; соединение и ответ различаются.
Для ошибки имени используйте getent hosts definitely-missing.invalid; .invalid зарезервирован для несуществующих имён. Для остановленного порта curl --connect-timeout 3 http://127.0.0.1:8080/ даст ошибку соединения. Для работающего сервера curl -i .../missing даст HTTP 404. Порядок: URL и среда → разрешение имени → слушающий процесс/порт → HTTP-ответ → журнал приложения.
Ошибка для диагностики localhost одного компьютера ошибочно используется как адрес другого.
Симптом → Гипотеза → Проверка
Для ошибки имени используйте getent hosts definitely-missing.invalid; .invalid зарезервирован для несуществующих имён. Для остановленного порта curl --connect-timeout 3 http://127.0.0.1:8080/ даст ошибку соединения. Для работающего сервера curl -i .../missing даст HTTP 404. Порядок: URL и среда → разрешение имени → слушающий процесс/порт → HTTP-ответ → журнал приложения.
Основные выводы Нет, он не выполняет HTTP-запрос. Сам контейнер, а не соседний сервис. Нет, он не выполняет HTTP-запрос.
Сам контейнер, а не соседний сервис.
После пары Проверить публичную учебную презентацию через curl -I; объяснить HTTPS и статус без сканирования посторонних портов.
Конспект, команды и разбор → Проверьте критерии сдачи и дайте возможность повторной попытки. Первоисточники: https://ubuntu.com/server/docs/ https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview
localhost зависит от точки зрения Prometheus хочет прочитать метрики app.
http://localhost:8000 http://app:8000 prometheus → ? app
Выберите адрес внутри сети Compose Внутри контейнера localhost означает этот контейнер. В теме Linux предварительно подчеркните, что это мысленный пример будущего Compose.
Пара 8 / октябрь
Пакеты, зависимости, Python и конфигурация Создать venv, запустить StudyPulse и отделить конфигурацию от исходного кода.
Создать venv, запустить StudyPulse и отделить конфигурацию от исходного кода. apt устанавливает системные пакеты Ubuntu из настроенных репозиториев. apt update обновляет сведения о доступных версиях, а не все программы. pip устанавливает Python-пакеты в выбранное окружение. Системный Python нужен Ubuntu; не меняем его через sudo pip. Для проекта создаём .venv и используем python -m pip, чтобы пакетный менеджер относился к тому же интерпретатору. В нашем базовом сервисе достаточно стандартной библиотеки; дополнительные инструменты вводим только ради задачи. Закреплённые версии зависимостей и проверяемый источник важны для воспроизводимости, но не заменяют обновления безопасности.
Контекст Пакеты системы, Python venv и контейнерный образ управляют разными слоями.
Создать venv, запустить StudyPulse и отделить конфигурацию от исходного кода.
Apt получает metadata репозитория, проверяет доступные пакеты и решает dependencies; dpkg устанавливает отдельный deb, но не заменяет всей логики репозитория. Системный Python, libc и утилиты принадлежат пакетной системе, поэтому sudo pip способен нарушить ожидаемые версии. Venv отделяет Python-пакеты проекта, но не создаёт другую Linux libc. Pip freeze фиксирует часть Python-среды, а не версию ядра и системных библиотек. Контейнерный образ фиксирует пользовательскую среду иначе, но разделяет ядро хоста. Неправильный файл .so или пакет из другого дистрибутива может привести к missing symbols и ABI errors. Сначала определяют, какой компонент и runtime действительно запускаются: command -v, python -c import sys, file/readelf, package ownership. В конфигурации используют абсолютные пути, очевидную рабочую папку и явно заданные переменные, особенно при переходе к systemd.
Пакеты ОС и библиотеки проекта apt и pip управляют разными слоями.
01 Ubuntu / apt
→ 02 Python
→ 03 venv / pip
→ 04 Приложение
Аналогия: ремонт здания и инвентарь отдельной лаборатории имеют разных владельцев.
apt устанавливает системные пакеты Ubuntu из настроенных репозиториев. apt update обновляет сведения о доступных версиях, а не все программы. pip устанавливает Python-пакеты в выбранное окружение. Системный Python нужен Ubuntu; не меняем его через sudo pip. Для проекта создаём .venv и используем python -m pip, чтобы пакетный менеджер относился к тому же интерпретатору. В нашем базовом сервисе достаточно стандартной библиотеки; дополнительные инструменты вводим только ради задачи. Закреплённые версии зависимостей и проверяемый источник важны для воспроизводимости, но не заменяют обновления безопасности.
Виртуальное окружение venv изолирует Python-библиотеки, но не всю ОС.
01 Проект A / .venv
→ 02 Проект B / .venv
→ 03 Общее ядро и файлы
Аналогия: разные ящики инструментов в одной мастерской.
python3 -m venv .venv создаёт окружение. source .venv/bin/activate меняет PATH текущего shell; отдельный терминал не активируется автоматически. Прямой вызов .venv/bin/python работает и без активации, поэтому удобен для служб. Проверяем sys.executable и which python. venv не контейнер: оно не изолирует сеть, процессы и права файлов. Папку .venv не коммитим; её восстанавливают из описания зависимостей. Это первый пример принципа «храним рецепт, а не случайную среду». Не объясняем ошибки импортов переустановкой всего Python без проверки пути.
Код, конфигурация, артефакт Меняем поведение без редактирования программы под каждый ноутбук.
01 Код
02 Окружение
03 Артефакт JSON
04 Проверенный запуск
Аналогия: рецепт, настройки духовки и партия ингредиентов.
HOST, PORT, MODEL_VERSION и MODEL_PATH — настройки учебного сервиса. Значения переменных окружения являются строками и требуют проверки. Порт должен быть допустимым числом, версия — ограниченной безопасной меткой. Файл модели в курсе — маленький JSON с коэффициентом; он иллюстрирует версионирование артефакта и не является обученной ML-моделью. Конфигурация выбирает местоположение, приложение проверяет формат. Переменная окружения сама по себе не делает пароль защищённым: её могут видеть привилегированные процессы и диагностические инструменты. В учебных данных не должно быть персональной информации.
Менеджер пакетов связывает версии зависимостей Пакеты системы, Python venv и контейнерный образ управляют разными слоями.
Сборка системы включает репозитории, зависимости и политику обновлений Репозиторий Метаданные и подписи apt / dnf / pacman Решает зависимости Пакеты Бинарники + libs + config Установленная ОС Версии должны сочетаться Linux-дистрибутивы различаются не только обоями libc, package format, defaults, init, release model и сроки поддержки Не подключаем репозитории другого дистрибутива ради одного файла .so. Python venv решает другой слой. Ремонт dependency начинается с определения активного interpreter и источника пакета, а не с установки всего подряд.
Apt получает metadata репозитория, проверяет доступные пакеты и решает dependencies; dpkg устанавливает отдельный deb, но не заменяет всей логики репозитория. Системный Python, libc и утилиты принадлежат пакетной системе, поэтому sudo pip способен нарушить ожидаемые версии. Venv отделяет Python-пакеты проекта, но не создаёт другую Linux libc. Pip freeze фиксирует часть Python-среды, а не версию ядра и системных библиотек. Контейнерный образ фиксирует пользовательскую среду иначе, но разделяет ядро хоста. Неправильный файл .so или пакет из другого дистрибутива может привести к missing symbols и ABI errors. Сначала определяют, какой компонент и runtime действительно запускаются: command -v, python -c import sys, file/readelf, package ownership. В конфигурации используют абсолютные пути, очевидную рабочую папку и явно заданные переменные, особенно при переходе к systemd.
Ремонт dependency начинается с определения активного interpreter и источника пакета, а не с установки всего подряд.
Наблюдение механизмов command -v python3
python3 -c 'import sys; print(sys.executable); print(sys.version)'
dpkg -S /usr/bin/ls
apt-cache policy python3
python3 -m venv .venv
.venv/bin/python -c 'import sys; print(sys.executable)'Полный разбор команд в конспекте → Сравните системный interpreter и .venv; не меняйте системные пакеты через pip. На Mac package manager и ownership-команды отличаются.
Команды и наблюдения Ubuntu / Bash, папка lab из комплекта
sudo apt update
sudo apt install -y python3-venv curl git
cd ~/devops-course/lab
python3 -m venv .venv
.venv/bin/python -c "import sys; print(sys.executable)"
HOST=127.0.0.1 PORT=8000 MODEL_VERSION=v1 .venv/bin/python app.py
# In another terminal
curl -i http://127.0.0.1:8000/healthzСкопируйте предоставленную папку lab в ~/devops-course/lab заранее. apt требует sudo, сам сервис — нет. Ожидаемое наблюдение: Сервис возвращает JSON со status=ok и model_version=v1; путь интерпретатора находится в .venv.
Разбор результата Сервис возвращает JSON со status=ok и model_version=v1; путь интерпретатора находится в .venv.
Скопируйте предоставленную папку lab в ~/devops-course/lab заранее. apt требует sudo, сам сервис — нет.
Используйте PORT=8001 MODEL_VERSION=v2 .venv/bin/python app.py; затем curl http://127.0.0.1:8001/healthz. POST: curl -i -H "Content-Type: application/json" -d '{"hours":4}' http://127.0.0.1:8001/predict. Значение PORT=nope должно приводить к явной ошибке конфигурации. Версия в healthz — метка запуска, а коэффициент вычисления берётся из артефакта.
На macOS Установите поддерживаемый Python из python.org; если Homebrew уже выбран и установлен, допустим brew install python git. apt на macOS отсутствует. venv и конфигурация курса работают одинаково; не используйте sudo pip для системной среды.
python3 --version
python3 -m venv .venv
.venv/bin/python -c "import sys; print(sys.executable)"
HOST=127.0.0.1 PORT=8000 .venv/bin/python app.pyКоманды macOS необходимо проверить на конкретном устройстве. Установите поддерживаемый Python из python.org; если Homebrew уже выбран и установлен, допустим brew install python git. apt на macOS отсутствует. venv и конфигурация курса работают одинаково; не используйте sudo pip для системной среды.
Один код, два запуска Запустите StudyPulse локально и проверьте /healthz. Отправьте POST /predict с hours=4 через пример из lab/README.md. Остановите сервис; измените PORT и MODEL_VERSION только через окружение. Передайте недопустимый PORT и объясните, почему ранний отказ лучше странного поведения. Работа в парах: один оператор, второй проверяет гипотезу. Смена ролей в середине. Используйте PORT=8001 MODEL_VERSION=v2 .venv/bin/python app.py; затем curl http://127.0.0.1:8001/healthz. POST: curl -i -H "Content-Type: application/json" -d '{"hours":4}' http://127.0.0.1:8001/predict. Значение PORT=nope должно приводить к явной ошибке конфигурации. Версия в healthz — метка запуска, а коэффициент вычисления берётся из артефакта.
Что должно получиться Два ответа /healthz и запись проверки неверной конфигурации.
Исходный app.py не редактируется ради порта; неверная конфигурация не запускается.
Используйте PORT=8001 MODEL_VERSION=v2 .venv/bin/python app.py; затем curl http://127.0.0.1:8001/healthz. POST: curl -i -H "Content-Type: application/json" -d '{"hours":4}' http://127.0.0.1:8001/predict. Значение PORT=nope должно приводить к явной ошибке конфигурации. Версия в healthz — метка запуска, а коэффициент вычисления берётся из артефакта.
Ошибка для диагностики pip и python относятся к разным окружениям.
Симптом → Гипотеза → Проверка
Используйте PORT=8001 MODEL_VERSION=v2 .venv/bin/python app.py; затем curl http://127.0.0.1:8001/healthz. POST: curl -i -H "Content-Type: application/json" -d '{"hours":4}' http://127.0.0.1:8001/predict. Значение PORT=nope должно приводить к явной ошибке конфигурации. Версия в healthz — метка запуска, а коэффициент вычисления берётся из артефакта.
Основные выводы Python-окружение и библиотеки, а не ядро, сеть или права ОС. Связывает менеджер пакетов с выбранным интерпретатором. Python-окружение и библиотеки, а не ядро, сеть или права ОС.
Связывает менеджер пакетов с выбранным интерпретатором.
После пары Написать README запуска и перечислить все настройки StudyPulse.
Конспект, команды и разбор → Проверьте критерии сдачи и дайте возможность повторной попытки. Первоисточники: https://ubuntu.com/server/docs/ https://docs.python.org/3/library/venv.html