Основы Python для Linux: Быстрый старт

Вводный курс по использованию Python в качестве инструмента системного администрирования. Вы научитесь писать базовые скрипты для взаимодействия с операционной системой и автоматизации рутинных задач в среде Linux.

Интерпретатор Python в экосистеме Linux и создание первого исполняемого скрипта

Когда вы обновляете пакеты в Red Hat (команда dnf) или настраиваете брандмауэр через firewalld, вы неявно запускаете Python-скрипты. В современных дистрибутивах Linux Python — это не просто язык программирования, установленный «сбоку», а фундаментальная часть операционной системы. Базовые системные утилиты написаны на нём, потому что он предлагает идеальный баланс: читаемость и скорость разработки, недоступные в Bash, при наличии прямого доступа к системным вызовам ядра (syscalls), как в C.

Чтобы писать надежные инструменты автоматизации, недостаточно знать синтаксис языка. Необходимо понимать, как текстовый файл с кодом взаимодействует с ядром Linux, файловой системой и переменными окружения.

Системный интерпретатор и правило изоляции

Компьютер не понимает исходный код напрямую. Python является интерпретируемым языком, а точнее — компилируемо-интерпретируемым. Когда вы запускаете скрипт, специальная программа (CPython) сначала транслирует ваш читаемый текст в промежуточный байт-код, а затем виртуальная машина Python выполняет этот байт-код, преобразуя его в команды для процессора и вызовы к ядру Linux.

В большинстве дистрибутивов главный бинарный файл интерпретатора находится по пути /usr/bin/python3. Это так называемый системный Python.

Главное правило системного администратора и DevOps-инженера: никогда не модифицируйте системное окружение Python напрямую. Если вы установите сторонние библиотеки глобально (например, выполнив команду sudo pip install requests), вы рискуете перезаписать зависимости, которые требуются системным утилитам (тому же пакетному менеджеру apt или dnf). Это приведет к поломке самой операционной системы — классическая ситуация «dependency hell» (ад зависимостей).

Для изоляции проектов в Linux используются виртуальные окружения (virtual environments), которые создают локальные копии интерпретатора и библиотек для каждого конкретного скрипта или сервиса. Понимание того, какой именно интерпретатор сейчас выполняет код, критически важно для отладки.

Анатомия исполняемого скрипта

В операционной системе Windows тип файла и способ его запуска определяются расширением (например, .exe или .bat). В Linux расширение файла (включая .py) — это просто часть имени, удобная для человека. Ядро Linux не смотрит на расширение, чтобы понять, как запустить программу.

Для превращения обычного текстового файла в самостоятельную утилиту требуются два компонента: права на исполнение и корректный shebang.

Магия первой строки (Shebang)

Когда вы пытаетесь запустить текстовый файл как программу, ядро Linux вызывает системную функцию execve. Эта функция читает первые два байта файла. Если она видит там шестнадцатеричные значения 0x23 и 0x21 (что в кодировке ASCII соответствует символам #!), ядро понимает: перед ним скрипт, а весь остаток первой строки — это путь к программе-интерпретатору, которой нужно передать этот файл на выполнение.

Эта конструкция называется shebang (от слов hash и bang).

Существует два основных способа написания shebang для Python-скриптов.

Жесткое указание пути:

#!/usr/bin/python3

Этот вариант говорит ядру: «Возьми бинарный файл ровно по этому пути и передай ему мой код». Проблема такого подхода в его негибкости. Если интерпретатор установлен в другом месте (например, в /usr/local/bin/python3 на FreeBSD или macOS), скрипт выдаст ошибку. Кроме того, жесткий путь полностью игнорирует виртуальные окружения.

Динамическое разрешение через env:

#!/usr/bin/env python3

Это стандарт индустрии (best practice). Утилита env — это стандартная программа Linux, которая умеет искать исполняемые файлы в директориях, перечисленных в переменной окружения $PATH.

Когда ядро читает #!/usr/bin/env python3, происходит следующее:

  1. Запускается /usr/bin/env.
  2. env смотрит в переменную окружения $PATH текущего пользователя (которая представляет собой список директорий, разделенных двоеточием, например: /home/user/myenv/bin:/usr/local/bin:/usr/bin).
  3. env ищет файл с именем python3 по очереди в каждой директории из списка.
  4. Как только файл найден, именно он используется для выполнения скрипта.

Если вы активировали виртуальное окружение, оно добавляет свой путь в самое начало переменной $PATH. Благодаря env, ваш скрипт автоматически подхватит интерпретатор из виртуального окружения, не требуя изменения исходного кода.

Права доступа (Execute bit)

Даже с правильным shebang файл не запустится, если у него нет соответствующих метаданных в файловой системе. В Linux каждый файл имеет атрибуты прав доступа: чтение (Read, r), запись (Write, w) и исполнение (eXecute, x).

По умолчанию, когда вы создаете новый файл, он получает права на чтение и запись, но не на исполнение. Попытка запустить его приведет к ошибке Permission denied.

Чтобы сделать скрипт исполняемым, необходимо добавить бит исполнения с помощью утилиты chmod (change mode):

chmod +x system_info.py

Флаг +x добавляет право на запуск для владельца файла, его группы и всех остальных пользователей. В терминале исполняемые файлы часто подсвечиваются другим цветом (обычно зеленым), что визуально отличает их от простых текстовых документов.

Создание первого инструмента

Напишем базовый скрипт для сбора информации о системе. В Linux «всё есть файл», и процессы, и конфигурации можно прочитать напрямую с диска, но Python предоставляет удобные абстракции над системными вызовами через встроенные модули.

Создайте файл system_info.py и добавьте в него следующий код:

#!/usr/bin/env python3

import os
import sys

# Сбор базовой информации
python_version = sys.version.split()[0]
current_user = os.getlogin()
process_id = os.getpid()

# Вывод результатов в стандартный поток (stdout)
print("=== Системный отчет ===")
print(f"Версия Python: {python_version}")
print(f"Текущий пользователь: {current_user}")
print(f"PID текущего скрипта: {process_id}")

В этом коде мы импортируем два фундаментальных модуля:

  • os отвечает за взаимодействие с операционной системой (пользователи, пути, файловая система).
  • sys предоставляет доступ к переменным и функциям, взаимодействующим с самим интерпретатором Python.

Запуск скрипта: почему нужен префикс ./

После выдачи прав (chmod +x system_info.py) новички часто пытаются запустить скрипт, просто написав его имя в консоли:

system_info.py
# bash: system_info.py: command not found

Оболочка (bash или zsh) возвращает ошибку, хотя файл существует в текущей директории. Это происходит из-за механизма безопасности Linux. В отличие от Windows, в Linux текущая директория (обозначаемая точкой .) по умолчанию не входит в переменную $PATH.

Если бы текущая директория была в $PATH, злоумышленник мог бы создать в публичной папке вредоносный скрипт с именем ls. Когда администратор попытался бы просмотреть содержимое папки командой ls, система нашла бы вредоносный скрипт первым и выполнила его от имени администратора.

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

./system_info.py

Символ . означает «текущая папка», а / — разделитель путей. Таким образом мы говорим оболочке: «Не ищи эту команду в $PATH, возьми конкретный файл прямо отсюда».

Альтернативный способ запуска — явный вызов интерпретатора с передачей ему файла в качестве аргумента:

python3 system_info.py

В этом случае shebang игнорируется, а права на исполнение (+x) для самого файла не требуются, так как вы запускаете бинарный файл python3 (к которому у вас есть доступ), а ваш скрипт выступает лишь как текстовый документ, который интерпретатор читает. Однако в DevOps-практике скрипты принято оформлять как самостоятельные утилиты с shebang и правами на запуск.

Скрытая угроза: проблема символов переноса строки

Один из самых частых и неочевидных сбоев при работе со скриптами в Linux возникает, если код был написан в среде Windows, а затем перенесен на сервер.

При попытке запуска такого скрипта вы можете увидеть странную ошибку:

bash: ./system_info.py: /usr/bin/env: bad interpreter: No such file or directory

Текст ошибки сбивает с толку: файл на месте, env существует, python3 установлен. Проблема кроется в невидимых символах переноса строки.

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

  • Linux и macOS используют один символ: LF (Line Feed, в коде обозначается как \n).
  • Windows использует два символа: CR (Carriage Return) и LF (\r\n).

Если вы сохранили файл в Windows, его первая строка выглядит для ядра Linux не как #!/usr/bin/env python3, а как #!/usr/bin/env python3\r.

Утилита env добросовестно берет строку python3\r и пытается найти в системе исполняемый файл, в имени которого на конце есть невидимый символ возврата каретки. Разумеется, такого файла нет, о чем система и сообщает.

Для решения этой проблемы в Linux существует утилита dos2unix, которая очищает файл от лишних символов \r:

dos2unix system_info.py

Если утилиты нет под рукой, можно использовать потоковый редактор sed, который присутствует в любой Unix-системе:

sed -i 's/\r$//' system_info.py

Эта команда находит все символы \r перед концом строки ($) и заменяет их на пустоту, сохраняя результат прямо в файл (флаг -i). Понимание таких низкоуровневых нюансов отличает уверенного инженера: скрипт — это не просто логика, это набор байтов, который операционная система должна корректно интерпретировать на каждом этапе от чтения заголовка файла до выделения памяти под процесс.

Переменные и базовые типы данных для хранения системной информации

Переменные и базовые типы данных для хранения системной информации

Когда администратор выполняет команду cat /proc/loadavg или читает конфигурационный файл, операционная система Linux отдает ему сплошной текст. В классических bash-скриптах всё является строкой: числа, пути, IP-адреса и логические флаги обрабатываются как текст, который приходится резать утилитами awk, grep или cut. Переход к Python требует смены парадигмы: здесь данные имеют строгую природу. Текст остается текстом, но PID процесса становится числом, с которым можно производить математические операции, а список смонтированных дисков превращается в структурированный массив, где каждый элемент доступен по индексу.

В Python переменные работают иначе, чем в командной оболочке. В bash переменная — это контейнер, куда складывается значение, и при присваивании категорически запрещены пробелы вокруг знака равенства (PID=1234). В Python переменная — это просто ярлык (ссылка), который привязывается к объекту в оперативной памяти. Пробелы вокруг = не только разрешены, но и требуются стандартом оформления кода PEP 8 для читаемости.

Python использует динамическую типизацию. Это означает, что при создании переменной не нужно указывать, какой тип данных она будет хранить. Интерпретатор сам определяет тип объекта в момент выполнения. Более того, один и тот же ярлык можно перевесить на объект совершенно другого типа:

# Сначала переменная указывает на целое число
process_status = 0

# Позже в скрипте та же переменная может указывать на строку
process_status = "active"

Для написания надежных скриптов автоматизации критически важно понимать базовые типы данных, так как системные вызовы и библиотеки возвращают информацию в строго определенных форматах.

Строки (str): работа с путями и логами

Строковый тип данных (str) используется для хранения текста: путей к файлам, имен пользователей, вывода команд и содержимого логов. Строки в Python можно заключать как в одинарные, так и в двойные кавычки. Разницы между ними нет, что отличает Python от bash, где двойные кавычки допускают интерполяцию переменных, а одинарные — нет.

Для подстановки значений переменных внутрь строк в современном Python используются f-строки (форматированные строки). Перед кавычками ставится символ f, а переменные помещаются в фигурные скобки:

user = "root"
home_dir = "/root"
# Формирование команды для лог-файла
log_message = f"User {user} has home directory at {home_dir}"

При чтении системных файлов скрипт неизбежно сталкивается с невидимыми символами, такими как перенос строки \n. Если прочитать файл конфигурации построчно, каждая строка будет заканчиваться этим символом, что сломает логику сравнения. Для очистки строк используется метод .strip(), который удаляет пробельные символы и переносы на концах строки.

Другой важнейший для системного администрирования метод — .split(). Он разбивает строку на части по указанному разделителю и возвращает список. Это идеальный инструмент для разбора файлов с четкой структурой, таких как /etc/passwd:

passwd_line = "nagios:x:999:999::/var/spool/nagios:/bin/false"
# Разбиваем строку по двоеточию
fields = passwd_line.split(":")
username = fields[0]  # Результат: "nagios"
shell = fields[-1]    # Результат: "/bin/false"

Числа (int и float): подсчет ресурсов

Системные метрики, такие как идентификаторы процессов (PID), идентификаторы пользователей (UID), объем оперативной памяти в байтах или коды возврата команд, хранятся в виде целых чисел — тип int.

Для метрик, требующих дробных значений (например, средняя загрузка процессора load average, температура датчиков или точное время выполнения скрипта), используется тип float (числа с плавающей точкой).

В Python встроены стандартные математические операторы, но особое внимание при работе с системными ресурсами стоит уделить делению. Обычное деление / всегда возвращает float, даже если числа делятся без остатка. Если нужно перевести байты в мегабайты и получить целое число, используется целочисленное деление //:

ram_bytes = 104857600
# Обычное деление дает 100.0 (float)
ram_mb_float = ram_bytes / 1024 / 1024

# Целочисленное деление дает 100 (int)
ram_mb_int = ram_bytes // 1024 // 1024

Частая ошибка начинающих — попытка выполнить математическую операцию над строкой, полученной из файла. Если прочитать значение 1024 из текстового файла, для Python это будет строка "1024". Чтобы использовать её в вычислениях, необходимо явное преобразование типов (кастование) с помощью функции int():

# Имитация чтения из файла
file_content = "8080"
port = int(file_content)
next_port = port + 1

Логический тип (bool): флаги состояний

Логический тип bool принимает только два значения: True (истина) и False (ложь). В Python они пишутся строго с заглавной буквы. Логические переменные выступают в роли флагов, сигнализирующих о состоянии системы: запущен ли сервис, существует ли директория, обладает ли пользователь правами root.

Логические значения часто получаются в результате операций сравнения. Например, проверка того, является ли текущий пользователь суперпользователем, сводится к проверке равенства его UID нулю:

current_uid = 0
is_root = (current_uid == 0) # Результат: True

Списки (list): упорядоченные коллекции

Когда нужно сохранить набор однотипных данных — список IP-адресов из лога фаервола, аргументы командной строки или перечень активных сетевых интерфейсов — используется тип list. Список создается с помощью квадратных скобок, а элементы разделяются запятыми.

Списки в Python упорядочены (каждый элемент имеет свой порядковый номер, начиная с нуля) и изменяемы (можно добавлять, удалять и перезаписывать элементы в процессе работы скрипта).

# Список сетевых интерфейсов
interfaces = ["eth0", "eth1", "wlan0", "lo"]

# Доступ по индексу
main_interface = interfaces[0] # "eth0"

# Отрицательные индексы считают с конца
loopback = interfaces[-1] # "lo"

Мощный механизм работы со списками — срезы (slices). Они позволяют извлечь часть списка. Синтаксис среза выглядит как [start:stop], где элемент start включается в результат, а stop — нет. Если нужно получить последние десять строк из массива логов, используется конструкция logs[-10:].

Для добавления новых элементов в конец существующего списка применяется метод .append(). Это классический паттерн при сборе данных: скрипт инициализирует пустой список, в цикле обходит систему и добавляет найденные аномалии в этот список.

Словари (dict): структурирование конфигураций

Если список — это просто пронумерованный ряд элементов, то словарь (dict) — это структура, где каждое значение привязано к уникальному имени (ключу). Словари создаются с помощью фигурных скобок. Это самый важный тип данных для DevOps-инженера, так как он идеально отражает структуру конфигурационных файлов, JSON-ответов от API и переменных окружения.

Ключами в словаре могут быть только неизменяемые типы данных (чаще всего строки), а значениями — любые объекты, включая другие списки и словари.

# Хранение информации о сервере
server_info = {
    "hostname": "db-prod-01",
    "ip_address": "192.168.1.50",
    "ssh_port": 2222,
    "is_active": True,
    "roles": ["database", "backup"]
}

# Получение значения по ключу
current_ip = server_info["ip_address"]

Если попытаться обратиться к ключу, которого нет в словаре (например, server_info["uptime"]), скрипт аварийно завершит работу с ошибкой KeyError. В системном администрировании конфигурационные файлы часто бывают неполными, и некоторые параметры могут отсутствовать. Чтобы избежать падения скрипта, используется метод .get(). Он запрашивает значение по ключу, но если ключа нет, возвращает специальное значение None (или любое другое значение по умолчанию, если его указать).

# Безопасное получение параметра
timeout = server_info.get("timeout", 30)
# Если ключа "timeout" нет, переменная получит значение 30

Словари позволяют выстраивать сложные иерархии данных. Например, инвентаризация серверов может быть представлена как словарь, где ключами выступают имена хостов, а значениями — словари с их характеристиками. Такая структура позволяет мгновенно извлекать нужные данные по именам, не перебирая массивы информации в поисках совпадений, что делает скрипты быстрыми и логичными.

Управляющие конструкции и логика обработки конфигурационных файлов

Управляющие конструкции и логика обработки конфигурационных файлов

Любая Linux-система управляется текстовыми файлами. Конфигурации сервисов, сетевые настройки, логи — всё это потоки символов. Однако в стандартном файле sshd_config из тысячи строк реально активными могут быть всего двадцать. Остальное — закомментированные дефолтные значения, пустые строки для читаемости и пояснительный текст. Чтобы скрипт мог принимать решения на основе этих данных, ему нужен механизм отделения полезного сигнала от информационного шума.

Безопасный доступ к файлам: контекстный менеджер

Прежде чем анализировать текст, файл необходимо открыть, передав его от операционной системы в интерпретатор Python. Базовый способ сделать это — функция open(). Но работа с файловыми дескрипторами в Linux требует строгой дисциплины: каждый открытый файл забирает системные ресурсы, и если скрипт завершится с ошибкой или просто «забудет» закрыть файл, дескриптор останется висеть в памяти (утечка ресурсов).

Для решения этой проблемы в Python применяется конструкция with.

Контекстный менеджер with создает изолированную область (блок кода), внутри которой файл гарантированно открыт и доступен. Как только выполнение программы выходит за пределы этого блока — неважно, естественным путем или из-за критической ошибки — Python автоматически вызывает метод закрытия файла.

config_path = "/etc/hostname"

with open(config_path, "r", encoding="utf-8") as file:
    hostname = file.read().strip()
    # Здесь файл открыт, мы можем с ним работать

# Здесь файл уже гарантированно закрыт
print(f"Имя хоста: {hostname}")

В функции open() передаются три важных аргумента:

  1. Путь к файлу.
  2. Режим работы: "r" (read) для чтения, "w" (write) для перезаписи, "a" (append) для добавления в конец.
  3. Кодировка encoding="utf-8". В Linux UTF-8 является стандартом де-факто, но явное указание защищает скрипт от непредсказуемого поведения при запуске в нестандартных локалях.

Итерация: стратегии чтения данных

Получив доступ к файлу, скрипт должен извлечь из него текст. Выбор метода чтения критически важен для стабильности серверов.

Метод Как работает Потребление памяти Применение
file.read() Загружает весь файл целиком в одну большую строку. Равно размеру файла. Чтение мелких файлов (до нескольких мегабайт), таких как /etc/hostname или /etc/timezone.
file.readlines() Загружает весь файл, разбивает его на строки и помещает в список. Выше размера файла (накладные расходы на структуру списка). Редко оправдано. Опасно для логов.
for line in file: Читает файл строго по одной строке за раз, сдвигая внутренний указатель. Минимальное (размер самой длинной строки). Золотой стандарт для парсинга логов и объемных конфигураций.

Цикл for в сочетании с файловым объектом реализует концепцию «ленивого» (lazy) чтения. Если скрипт анализирует лог-файл веб-сервера размером 10 ГБ, цикл for будет держать в оперативной памяти ровно одну текущую строку.

with open("/var/log/syslog", "r", encoding="utf-8") as log_file:
    for line in log_file:
        # Обработка одной строки
        # Предыдущая строка удаляется из памяти сборщиком мусора
        pass

В Python границы циклов и логических блоков определяются исключительно отступами (обычно 4 пробела). Здесь нет операторов вроде do...done или фигурных скобок. Сдвиг вправо означает погружение внутрь конструкции, возврат влево — выход из нее.

Ветвление: логика фильтрации

Чтение строки — это только начало. Далее вступает в работу условная конструкция if/elif/else. В контексте автоматизации Linux её главная задача — нормализация данных.

Рассмотрим типичную задачу: чтение пользовательского конфигурационного файла, где строки могут быть пустыми или начинаться с символа # (комментарий).

В Python логические выражения оцениваются на «истинность» (truthiness). Пустая строка, пустой список или число ноль воспринимаются интерпретатором как ложь (False). Строка, содержащая хотя бы один символ (даже пробел) — как истина (True).

Для управления потоком выполнения внутри цикла используются операторы прерывания:

  • continue — немедленно прерывает текущую итерацию и заставляет цикл перейти к следующей строке. Идеально подходит для отсеивания мусора.
  • break — полностью останавливает цикл. Полезно, если скрипт ищет конкретное значение и, найдя его, не имеет смысла читать оставшиеся тысячи строк.
active_settings = []

with open("/etc/resolv.conf", "r", encoding="utf-8") as conf:
    for line in conf:
        # Очищаем строку от невидимых символов переноса (\n) и пробелов по краям
        clean_line = line.strip()

        # Фильтр 1: Пропускаем пустые строки
        if not clean_line:
            continue

        # Фильтр 2: Пропускаем комментарии
        if clean_line.startswith("#"):
            continue

        # Если строка прошла фильтры, добавляем её в список
        active_settings.append(clean_line)

Конструкция not clean_line элегантно проверяет строку на пустоту. Если после применения метода .strip() от строки ничего не осталось (длина стала равна нулю), она оценивается как False. Оператор not инвертирует это значение в True, и срабатывает continue.

Обработка исключений: защита от падений

Скрипт автоматизации работает в агрессивной среде. Файла может не существовать, у пользователя может не быть прав на его чтение, или диск может оказаться поврежден. Если просто использовать open(), любая из этих ситуаций вызовет аварийную остановку (Traceback) и прервет выполнение всего пайплайна.

Для перехвата и обработки таких ситуаций применяется блок try/except.

target_file = "/etc/shadow" # Файл с хэшами паролей, доступен только root

try:
    with open(target_file, "r", encoding="utf-8") as f:
        data = f.read()
except FileNotFoundError:
    print(f"Критическая ошибка: Файл {target_file} не найден в системе.")
except PermissionError:
    print(f"Ошибка доступа: Для чтения {target_file} требуются права sudo.")
except Exception as e:
    print(f"Неизвестная ошибка при чтении: {e}")

Логика try/except работает как система перехвата. Интерпретатор пытается выполнить код в блоке try. Если возникает ошибка, он немедленно прекращает выполнение этого блока и начинает искать подходящий блок except, соответствующий типу возникшей ошибки. Указание конкретных типов ошибок (FileNotFoundError, PermissionError) позволяет скрипту реагировать осмысленно: например, запросить повышение привилегий или создать конфигурацию по умолчанию, вместо того чтобы просто упасть.

Практический сценарий: сбор данных из /etc/os-release

Объединим изученные конструкции для решения реальной задачи. Файл /etc/os-release присутствует в большинстве современных дистрибутивов Linux и содержит информацию о версии ОС в формате КЛЮЧ=ЗНАЧЕНИЕ.

Особенность этого файла в том, что значения часто заключены в двойные кавычки (например, PRETTY_NAME="Ubuntu 22.04.3 LTS"). Наша задача — прочитать файл, отфильтровать лишнее, разделить ключи и значения, очистить значения от кавычек и сохранить всё в удобный словарь.

os_info = {}
file_path = "/etc/os-release"

try:
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            clean_line = line.strip()

            # Пропускаем пустые строки и комментарии
            if not clean_line or clean_line.startswith("#"):
                continue

            # Проверяем, есть ли знак равенства в строке
            if "=" in clean_line:
                # Разделяем строку ровно на две части по первому знаку "="
                parts = clean_line.split("=", 1)
                key = parts[0]
                value = parts[1]

                # Очищаем значение от возможных кавычек
                value = value.strip('"').strip("'")

                # Записываем в словарь
                os_info[key] = value

    # Безопасное извлечение нужных данных
    distro_name = os_info.get("NAME", "Неизвестный дистрибутив")
    distro_version = os_info.get("VERSION", "Версия не указана")

    print(f"Обнаружена система: {distro_name} ({distro_version})")

except FileNotFoundError:
    print("Не удалось определить ОС: файл /etc/os-release отсутствует.")

В этом сценарии цикл for обеспечивает безопасное чтение файла любого размера. Конструкция if берет на себя роль сита, отсеивая мусор. Метод .split("=", 1) гарантирует, что даже если в самом значении встретится знак равенства, строка будет разрезана только по первому из них. Блок try/except делает скрипт отказоустойчивым на случай запуска в нестандартной среде (например, в минималистичном Docker-контейнере без os-release).

Управляющие конструкции превращают линейный набор команд в интеллектуальный инструмент. Они позволяют скрипту динамически адаптироваться к содержимому файлов, безопасно обходить ограничения среды и формировать структурированные структуры данных, готовые к дальнейшей передаче в системы мониторинга или API.

Взаимодействие с окружением через стандартные модули os и sys

Взаимодействие с окружением через стандартные модули os и sys

Скрипт, который идеально работает на ноутбуке разработчика, часто падает при первом же запуске в CI/CD пайплайне или через cron. Причина редко кроется в синтаксических ошибках. Чаще всего скрипт оказывается «слепым»: он не знает, с какими флагами его запустили, где лежат конфигурационные файлы на новом сервере и как сообщить операционной системе, что произошел сбой. В Linux программа не существует в вакууме — она общается с оболочкой (shell) и ядром. В Python роль моста между вашим кодом и операционной системой выполняют встроенные модули sys и os.

Чтение аргументов командной строки через sys.argv

Любая стандартная утилита Linux принимает аргументы. Когда вы пишете tail -n 50 /var/log/syslog, утилита tail понимает, что нужно вывести именно 50 строк из конкретного файла. Чтобы ваши Python-скрипты могли работать так же гибко, используется модуль sys.

После импорта модуля (import sys) вам становится доступен объект sys.argv. С точки зрения Python, это самый обычный список (list), в котором хранятся слова, переданные в терминале при запуске скрипта.

Главная особенность, о которой часто забывают: нулевой индекс sys.argv[0] всегда содержит имя самого запущенного скрипта. Реальные аргументы, переданные пользователем, начинаются с индекса 1.

Рассмотрим скрипт check_service.py, который должен проверять статус конкретного демона:

import sys

# Проверяем, передал ли пользователь имя сервиса
if len(sys.argv) < 2:
    print("Ошибка: укажите имя сервиса.")
    print(f"Использование: {sys.argv[0]} <имя_сервиса>")
    # Здесь скрипт должен завершиться, но об этом ниже
else:
    service_name = sys.argv[1]
    print(f"Начинаем проверку сервиса: {service_name}")

Если запустить его командой python3 check_service.py nginx, список sys.argv будет состоять из двух элементов: ['check_service.py', 'nginx'].

При работе с sys.argv важно помнить два правила. Во-первых, оболочка Linux разбивает аргументы по пробелам. Если вы хотите передать строку с пробелами как один аргумент, в терминале её нужно взять в кавычки: python3 script.py "my long argument". Во-вторых, все элементы в sys.argv — это строки. Если вы передаете число (например, порт 8080), внутри скрипта оно будет строкой '8080'. Для математических операций или использования в качестве порта для сетевого соединения его придется явно преобразовать в целое число с помощью int().

Для сложных CLI-инструментов с десятками флагов (вида --verbose, --port 8080) парсить sys.argv вручную становится неудобно. В таких случаях используют модуль argparse, но под капотом он всё равно опирается на сырой список sys.argv.

Диалог с оболочкой: коды возврата и sys.exit()

В философии UNIX каждая завершенная программа оставляет после себя след — код возврата (exit status). Это целое число от 0 до 255. Ноль означает «всё прошло успешно», любое другое число означает ошибку. Оболочка Linux (например, bash) использует эти коды для управления логикой.

Если вы пишете в терминале apt update && apt upgrade, вторая команда (upgrade) запустится только в том случае, если первая (update) вернет код 0. Если Python-скрипт просто доходит до последней строчки кода и завершается, он автоматически возвращает 0. Но если внутри скрипта мы обнаружили проблему (например, нет нужного файла конфигурации), мы обязаны сообщить Linux об ошибке, иначе bash сочтет выполнение успешным и продолжит пайплайн.

Для явного завершения программы с нужным кодом используется функция sys.exit().

import sys
import os

config_path = "/etc/myapp/config.ini"

if not os.path.exists(config_path):
    print("КРИТИЧЕСКАЯ ОШИБКА: Конфигурационный файл не найден!")
    sys.exit(1)  # Завершаем скрипт и передаем bash код ошибки 1

print("Конфигурация загружена, продолжаем работу...")

У функции sys.exit() есть крайне полезная для DevOps-инженеров особенность. Если передать в неё не число, а строку текста, Python сделает две вещи одновременно:

  1. Выведет эту строку в стандартный поток ошибок (stderr), а не в обычный вывод (stdout).
  2. Завершит программу с кодом 1.

Это позволяет сократить код обработки критических ошибок до одной строки:

import sys

if len(sys.argv) < 2:
    # Текст уйдет в stderr, код возврата будет 1
    sys.exit(f"Использование: {sys.argv[0]} <путь_к_логу>")

Разделение на stdout и stderr критически важно при автоматизации. Если ваш скрипт генерирует полезные данные (например, JSON с метриками), их обычно перенаправляют в другой инструмент: python3 get_metrics.py > metrics.json. Если бы сообщение об ошибке печаталось обычным print(), оно бы попало внутрь файла metrics.json, сломав его структуру. Использование sys.exit("Ошибка") гарантирует, что текст ошибки появится на экране терминала, а файл останется пустым, при этом пайплайн прервется.

Чтение переменных окружения через os.environ

Переменные окружения — это стандартный способ передачи секретов (токенов, паролей) и настроек в контейнерах Docker и системах CI/CD. Хардкодить пути или пароли прямо в коде Python — плохая практика. Скрипт должен адаптироваться к среде, в которой запущен.

Модуль os предоставляет объект os.environ. По поведению он очень похож на обычный Python-словарь, где ключи — это имена переменных окружения, а значения — их содержимое (всегда в виде строк).

import os

# Получение переменной напрямую
db_host = os.environ["DB_HOST"]
print(f"Подключение к базе данных на {db_host}")

Однако, если переменная DB_HOST не была задана в системе, этот код вызовет исключение KeyError и скрипт упадет. В инфраструктурных задачах часто требуется использовать значения по умолчанию, если переменная не задана. Для этого лучше использовать метод os.getenv().

import os

# Если LOG_LEVEL задан в системе — берем его. Если нет — используем "INFO"
log_level = os.getenv("LOG_LEVEL", "INFO")

# Безопасное получение токена без значения по умолчанию (вернет None, если не задан)
api_token = os.getenv("API_TOKEN")

if not api_token:
    sys.exit("Ошибка: переменная окружения API_TOKEN не задана.")

Важный нюанс, на котором часто спотыкаются новички: изменение os.environ внутри Python-скрипта (например, os.environ["NEW_VAR"] = "123") влияет только на сам этот скрипт и на дочерние процессы, которые он может запустить. Оно никак не изменит переменные окружения в родительской оболочке bash, из которой скрипт был вызван. Процесс не может модифицировать окружение своего родителя на уровне ядра Linux.

Навигация по файловой системе с os.path

При написании автоматизации постоянной задачей является работа с путями к файлам. В Linux разделителем директорий является прямой слеш (/). Можно просто склеивать строки: dir + "/" + filename. Но этот подход хрупок — если переменная dir уже содержит слеш на конце, вы получите двойной слеш (/var/log//syslog), что иногда приводит к неочевидному поведению утилит.

Подмодуль os.path содержит инструменты для безопасной работы с путями. Самый часто используемый метод — os.path.join(). Он берет на себя заботу о правильном количестве разделителей.

import os

base_dir = "/var/log"
log_file = "nginx/access.log"

# Правильное склеивание путей
full_path = os.path.join(base_dir, log_file)
# Результат: /var/log/nginx/access.log

Перед тем как пытаться открыть файл на чтение, хорошей практикой является проверка его существования. Метод os.path.exists() возвращает True, если путь существует (неважно, файл это, директория или симлинк). Если нужно убедиться, что по указанному пути находится именно обычный файл, а не директория, используется os.path.isfile().

import os
import sys

target = sys.argv[1] if len(sys.argv) > 1 else "/etc/passwd"

if not os.path.exists(target):
    sys.exit(f"Путь {target} не существует.")

if not os.path.isfile(target):
    sys.exit(f"{target} существует, но это не файл (возможно, директория).")

# Если мы дошли сюда, можно безопасно открывать файл

Также в арсенале os.path есть методы os.path.basename() и os.path.dirname(). Если вам на вход поступил полный путь /etc/nginx/nginx.conf, basename вернет только имя файла (nginx.conf), а dirname — только путь к директории (/etc/nginx). Это избавляет от необходимости вручную резать строки по символу слеша.

Используя sys.argv для получения команд, os.environ для чтения конфигурации среды, os.path для безопасной работы с путями и sys.exit для правильного информирования системы об ошибках, скрипт превращается из изолированного куска кода в полноценного участника экосистемы Linux. Он корректно встраивается в bash-пайплайны, безопасно принимает секреты через переменные окружения и предсказуемо реагирует на отсутствие нужных файлов.

Практическая сборка скрипта для инвентаризации системных ресурсов

Практическая сборка скрипта для инвентаризации системных ресурсов

Представьте парк из полусотни серверов. На одних крутятся базы данных, на других — веб-приложения, третьи простаивают. Внезапно поступает задача: составить актуальную таблицу с указанием моделей процессоров, объема оперативной памяти и свободного места на корневых разделах всех машин. Заходить на каждый сервер по SSH и вручную копировать вывод утилит htop или df -h — прямой путь к ошибкам и потраченному дню. Намного эффективнее написать собственный инструмент, который соберет эти метрики и выдаст их в строгом, машиночитаемом формате.

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

Иллюзия файлов: чтение данных из ядра через procfs

Когда администратор вводит в терминале команду free -m или lscpu, эти утилиты не измеряют напряжение на контактах процессора и не сканируют чипы памяти напрямую. Они обращаются к директории /proc.

Директория /proc содержит виртуальную файловую систему (procfs). Файлов, которые вы там видите, физически не существует на жестком диске. Это иллюзия, интерфейс, через который ядро Linux экспортирует свои внутренние структуры данных в пользовательское пространство. Когда Python открывает файл на чтение из /proc, ядро «на лету» генерирует текстовый ответ, отражающий состояние системы ровно в ту миллисекунду, когда произошел вызов.

Поскольку это обычный текст, мы можем применять к нему стандартные строковые методы. Начнем со сбора информации о центральном процессоре.

Извлечение характеристик процессора

Вся информация о CPU хранится в виртуальном файле /proc/cpuinfo. Если вывести его содержимое, мы увидим блоки текста, разделенные пустыми строками — по одному блоку на каждое логическое ядро.

Нас интересуют два параметра: точное название модели процессора и общее количество ядер. Название модели записано в строках, начинающихся с model name, например: model name : Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz.

Напишем функцию, которая извлечет эти данные.

def get_cpu_info():
    cpu_info = {
        "model": "Unknown",
        "cores": 0
    }

    try:
        with open("/proc/cpuinfo", "r", encoding="utf-8") as f:
            for line in f:
                # Считаем количество ядер по вхождениям слова processor
                if line.startswith("processor"):
                    cpu_info["cores"] += 1

                # Забираем имя модели (достаточно сделать это один раз)
                elif line.startswith("model name") and cpu_info["model"] == "Unknown":
                    # Разделяем строку по двоеточию и забираем правую часть
                    parts = line.split(":", 1)
                    if len(parts) == 2:
                        cpu_info["model"] = parts[1].strip()

    except FileNotFoundError:
        cpu_info["model"] = "Error: /proc/cpuinfo not found"

    return cpu_info

Обратите внимание на использование split(":", 1). Второй аргумент 1 указывает интерпретатору сделать ровно одно разделение. Это критически важно при парсинге системных логов и конфигураций: если в самом названии модели процессора случайно окажется символ двоеточия, строка не разобьется на лишние фрагменты, и данные не исказятся.

Парсинг состояния оперативной памяти

Следующая цель — оперативная память. За нее отвечает виртуальный файл /proc/meminfo. Его структура проще: это плоский список параметров. Нас интересуют строки MemTotal (общий объем физической памяти) и MemAvailable (память, доступная для запуска новых процессов без ухода в своп).

Формат строк в этом файле выглядит так: MemTotal: 16393452 kB. Единица измерения жестко зафиксирована в килобайтах. Для инвентаризации нам удобнее оперировать гигабайтами.

def get_ram_info():
    ram_info = {
        "total_gb": 0.0,
        "available_gb": 0.0
    }

    try:
        with open("/proc/meminfo", "r", encoding="utf-8") as f:
            for line in f:
                if line.startswith("MemTotal:"):
                    # Разбиваем строку по пробелам и берем второй элемент (число)
                    parts = line.split()
                    kb_value = int(parts[1])
                    # Переводим килобайты в гигабайты (1 ГБ = 1024 * 1024 КБ)
                    ram_info["total_gb"] = round(kb_value / (1024 * 1024), 2)

                elif line.startswith("MemAvailable:"):
                    parts = line.split()
                    kb_value = int(parts[1])
                    ram_info["available_gb"] = round(kb_value / (1024 * 1024), 2)

    except FileNotFoundError:
        pass # Оставляем нули в случае ошибки

    return ram_info

Здесь метод .split() вызывается без аргументов. В таком режиме Python автоматически схлопывает любое количество идущих подряд пробелов и табуляций, разбивая строку на чистые слова. Нам не нужно высчитывать точное количество пробелов между двоеточием и числом.

Системные вызовы для файловой системы: os.statvfs

Если информацию о процессоре и памяти легко прочитать из текстовых файлов, то с дисковым пространством ситуация иная. Данные о смонтированных разделах разбросаны по системе, и парсить их вручную ненадежно. Здесь на помощь приходит модуль os и его функция statvfs(), которая выполняет прямой системный вызов к ядру для получения статистики файловой системы.

В операционных системах семейства POSIX (включая Linux) дисковое пространство измеряется не байтами, а блоками. Блок — это минимальная порция данных, которую файловая система может выделить для хранения файла. Чтобы узнать реальный объем диска, нужно умножить количество блоков на размер одного блока.

Функция os.statvfs("/") возвращает специальный объект с атрибутами. Нас интересуют три из них:

  • f_frsize — размер одного блока во фрагментах (в байтах).
  • f_blocks — общее количество блоков в файловой системе.
  • f_bavail — количество свободных блоков, доступных обычному (непривилегированному) пользователю.

Существует также атрибут f_bfree (абсолютно все свободные блоки), но в Linux файловые системы ext4 по умолчанию резервируют около 5% места исключительно для пользователя root. Если скрипт будет запущен от имени обычного пользователя и попытается записать данные, опираясь на f_bfree, он может получить ошибку нехватки места. Поэтому для мониторинга всегда используется f_bavail.

Формула расчета доступного места в байтах выглядит так: Available_Bytes=f_bavail×f_frsizeAvailable\_Bytes = f\_bavail \times f\_frsize

Реализуем это в виде функции, переведя байты в гигабайты.

import os

def get_disk_info(path="/"):
    disk_info = {
        "total_gb": 0.0,
        "free_gb": 0.0,
        "percent_used": 0.0
    }

    try:
        # Выполняем системный вызов
        st = os.statvfs(path)

        # Вычисляем объемы в байтах
        total_bytes = st.f_blocks * st.f_frsize
        free_bytes = st.f_bavail * st.f_frsize
        used_bytes = total_bytes - free_bytes

        # Переводим в гигабайты (1 ГБ = 1024^3 байт)
        gb_divisor = 1024 ** 3
        disk_info["total_gb"] = round(total_bytes / gb_divisor, 2)
        disk_info["free_gb"] = round(free_bytes / gb_divisor, 2)

        # Считаем процент занятого места
        if total_bytes > 0:
            disk_info["percent_used"] = round((used_bytes / total_bytes) * 100, 1)

    except (FileNotFoundError, PermissionError):
        pass

    return disk_info

Эта функция универсальна: передав в неё путь /var/log или /mnt/data, можно точечно проверять состояние конкретных точек монтирования.

Сборка единого инструмента и сериализация

Теперь у нас есть три независимые функции, собирающие метрики. Чтобы превратить их в полноценный инструмент инвентаризации, нужно объединить их вызовы и сформировать итоговую структуру данных.

Для передачи данных между системами (например, отправки в систему мониторинга Zabbix, Prometheus или просто сохранения в лог) стандартом де-факто является формат JSON. В стандартной библиотеке Python есть встроенный модуль json, который умеет превращать словари в отформатированные строки. Этот процесс называется сериализацией.

Соберем финальный скрипт inventory.py:

#!/usr/bin/env python3
import os
import sys
import json

# ... (здесь располагаются функции get_cpu_info, get_ram_info, get_disk_info) ...

def main():
    # Собираем данные в единый словарь
    system_inventory = {
        "hostname": os.uname().nodename,
        "cpu": get_cpu_info(),
        "memory": get_ram_info(),
        "disk_root": get_disk_info("/")
    }

    # Преобразуем словарь в JSON-строку с отступами для читаемости
    json_output = json.dumps(system_inventory, indent=4)

    # Выводим результат в stdout
    print(json_output)

    # Завершаем работу с кодом успешного выполнения
    sys.exit(0)

if __name__ == "__main__":
    main()

В этом коде мы использовали os.uname().nodename — еще один системный вызов, возвращающий сетевое имя узла (hostname). Конструкция if __name__ == "__main__": гарантирует, что функция main() запустится только при прямом исполнении скрипта, а не при его импорте как модуля в другой файл.

Запустив этот скрипт на сервере, мы получим строго структурированный ответ:

{
    "hostname": "web-node-03",
    "cpu": {
        "model": "AMD EPYC 7763 64-Core Processor",
        "cores": 8
    },
    "memory": {
        "total_gb": 31.2,
        "available_gb": 28.5
    },
    "disk_root": {
        "total_gb": 97.9,
        "free_gb": 74.1,
        "percent_used": 24.3
    }
}

Такой скрипт уже не просто выводит текст на экран. Он генерирует объект, который можно перенаправить в файл (./inventory.py > server_03.json), передать по сети через curl в базу данных или использовать как входные данные для инструментов автоматической настройки инфраструктуры. Мы скрыли хаос текстовых файлов ядра под аккуратным, предсказуемым интерфейсом, используя только встроенные возможности языка.