Получаем список имён установленных в Debian пакетов с Python3

newbie

Опубликован:  2020-05-02T08:01:20.892044Z
2200

Питон, и в частности Python3, - великолепный инструмент для автоматизации рутины, и поэтому он достаточно часто используется системными администраторами для решения некоторых мелких, сиюминутных, повторяющихся задач на десктопе или сервере. В этом обзоре рассмотрим одну маленькую, но интересную задачу системного администратора Debian и её решение.

В одном из выпусков соседнего блога предложена команда, которая выводит на экран терминала последовательно имена всех установленных в Debian пакетов. Всё бы ничего, да вот команда получилась слишком длинная, сложная и запутанная. Сейчас я разработаю микроскопический скрипт, который автоматизирует эту задачу и даст возможность выполнить аналогичные действия простым и лаконичным вызовом в консоли.

Для начала сформулируем задачу.

На компьютере установлена операционная система Debian buster, необходимо получить список имён всех установленных в этой системе пакетов, с помощью которого впоследствии можно без труда установить все пакеты из списка на другой машине одной командой. Искомый список должен отвечать следующим требованиям:

  • список должен содержать только имена пакетов без указания их версий;
  • имя каждого пакета должно занимать одну строчку в списке;
  • список нужно вывести на экран терминала или сохранить в файл.

Приступим... У меня есть тестовая машина с Debian buster на борту, на её базе я и буду решать эту весьма интересную для новичков в программировании задачу. Из инструментов мне потребуется терминал и текстовый редактор. Программировать на Python3 удобней всего в PyCharm, его и буду использовать, создаю файл dpkglist.py.

5wfE7yPKOv.png

У задачи есть два варианта решения:

  • dpkg -l;
  • sudo apt list --installed.

Первый вариант мне нравится больше, потому что он возможен для любого пользователя системы, тогда как вариант с apt требует прав суперпользователя и, как следствие, поставит меня перед необходимостью автоматизированного ввода пароля. Кроме этого, выхлоп dpkg сложней, и его интересней парсить, поэтому попробуем решить дилемму без необходимости вводить пароль.

Для начала, давайте посмотрим на выхлоп команды dpkg -l в терминале.

dpkg -l

Начало выхлопа выглядит так:

Y58WXeHbMk.png

Выхлоп сам по себе достаточно длинный, в моей системе установлено более 1500 пакетов, в терминале его можно прокручивать клавишами со стрелками, PgUp, PgDn или Home, End. Давайте посмотрим на конец выхлопа.

P9cjm6h8EH.png

Здесь следует обратить внимание, что список пакетов начинается с пакета adduser и заканчивается пакетом zlib1g-dev:amd64. Имена первого и последнего пакета в списке будет полезно знать и помнить в процессе отладки программы. Обращаю внимание, что имя последнего пакета в списке содержит наименование архитектуры, которое следует через двоеточие сразу после имени пакета, и от которого впоследствии надо бы избавиться.

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

Обращаю внимание, что в самом начале выхлопа dpkg, перед именем первого пакета следует так называемая шапка таблицы:

|||/ Имя                                           Версия                          Архитектура  Описание
+++-=============================================-===============================-============-======================================================================================================

В этой таблице меня интересует только поле Имя, и ограничено это поле двумя знаками -, между которыми следуют знаки =. Таким образом, в каждой новой строчке выхлопа ширина поля с именем пакета ограничена количеством знаков "равно" в шапке, и зная позицию первого и второго знаков - в шапке таблицы, можно легко и просто извлечь имя пакета из каждой новой строчки выхлопа. Эту закономерность я и буду использовать далее.

Запускаю интерактивную сессию Python3, начальную пошаговую отладку возможного решения удобно сделать прямо в ней.

python3

FIiVPXkh33.png

В сессии Python3 я могу исполнить любую консольную программу с помощью модуля стандартной библиотеки subprocess и его инструментов. Исполняемую команду я приведу в заданные кондиции с помощью модуля стандартной библиотеки shlex, импортирую:

>>> import shlex
>>> from subprocess import Popen, PIPE
>>> 

Облачаем dkpg -l в список с помощью shlex.split.

>>> cmd = shlex.split('dpkg -l')
>>> 

Такую команду достаточно просто исполнить, для этого создаю экземпляр класса Popen.

>>> with Popen(cmd, stdout=PIPE, stderr=PIPE) as dpkg:
...     res = dpkg.communicate()
... 
>>> 

Здесь следует обратить внимание, что с помощью PIPE я сохраняю содержимое стандартного потока вывода (stdout) и стандартного потока ошибок (stderr) в полученном объекте res. Успешность выполнения команды можно легко проверить.

>>> dpkg.returncode
0
>>> 

Рассмотрим детальнее полученную в итоге переменную res.

>>> type(res)
<class 'tuple'>
>>> len(res)
2
>>> type(res[0])
<class 'bytes'>
>>> 

Как видно, res - это tuple, в котором хранится два элемента - две байт-строки, первый элемент - это содержимое stdout, а второй - stderr. Поскольку выше мы увидели, что процесс dpkg вернул код 0 - успешное завершение процесса, второй элемент в res меня не особо интересует, потому что он будет пустой байт-строкой. А вот на первый элемент res следует посмотреть, выведу первые 50 символов этой строки.

>>> res[0].decode('utf-8')[:50]
'Желаемый=неизвестно[u]/установить[i]/удалить[r]/вы'
>>> 

Эту строку мне и предстоит последовательно распарсить для решения своей задачи. Первым делом преобразую эту строку в список, разделив её по символу окончания строки.

>>> pack = res[0].decode('utf-8').strip().split('\n')
>>> 

Вывожу первый и последний элемент этого списка.

>>> pack[0]
'Желаемый=неизвестно[u]/установить[i]/удалить[r]/вычистить[p]/зафиксировать[h]'
>>> pack[-1]
'ii  zlib1g-dev:amd64                              1:1.2.11.dfsg-2                 amd64        compression library - development'
>>> 

Теперь мне необходимо найти элемент списка, в котором хранится строка с именем первого пакета, напоминаю его имя - adduser.

>>> i = 0
>>> while not pack[i].startswith('ii'):
...     i += 1
... 
>>> pack[i]
'ii  adduser                                       3.118                           all          add and remove users and groups'
>>> 

Зная индекс этого элемента списка - i, я легко могу получить предыдущий элемент списка, который является объектом моего пристального интереса и внимания, поэтому сохраню его в переменную block.

>>> block = pack[i-1]
>>> block
'+++-=============================================-===============================-============-======================================================================================================'
>>> 

В этой строке меня интересуют первый и второй знаки - и их индексы. Определяю их.

>>> first = block.find('-')
>>> second = block.find('-', first+1)
>>> first, second
(3, 49)
>>> block[first]
'-'
>>> block[second]
'-'
>>> 

Теперь, имея все эти переменные - ключевые для этой задачи, я могу достаточно просто отфильтровать из списка pack только интересующие меня данные.

>>> pack = [item[first:second].strip().split(':')[0] for item in pack[i:]]
>>> 

Что я имею в итоге?

>>> pack[0]
'adduser'
>>> pack[-1]
'zlib1g-dev'
>>> len(pack)
1583
>>> 

А в итоге я получил список из 1583 имён установленных в моей системе пакетов. Вывести этот список на экран или сохранить в файл совершенно не составит никакого труда. Покидаю интерактивную сессию Питона.

>>> exit()

Перемещаюсь в тестовый редактор и пишу код программы.

import shlex

from subprocess import Popen, PIPE


def get_names():
    with Popen(shlex.split('dpkg -l'), stdout=PIPE, stderr=PIPE) as dpkg:
        res = dpkg.communicate()[0]
    if not dpkg.returncode:
        pack = res.decode('utf-8').strip().split('\n')
        i = 0
        while not pack[i].startswith('ii'):
            i += 1
        block = pack[i-1]
        first = block.find('-')
        second = block.find('-', first + 1)
        if first != -1 and second != -1:
            return [item[first:second].strip().split(':')[0] for item
                    in pack[i:]]


if __name__ == '__main__':
    packs = get_names()
    if packs:
        for name in packs:
            print(name)

mhm2ZSCoId.png

Сохраняю изменения в файл и пробую запустить программу в терминале.

python3 dpkglist.py | less

GXIzX3PrNr.png

Посмотрим на конец выхлопа.

hHi9G4Re27.png

При желании этот список совершенно нетрудно сохранить в файл, а имея такой файл можно восстановить полноценную систему на другой машине с минимальными затратами времени и усилий оператора. Задача решена.

Комментарии: