OMFG! — может воскликнуть читатель. Зачем писать что-то на С когда есть Python, и будет во многом прав. Однако, к счастьюсожалению наш зелёный друг не всесилен. Итак…
В рамках текущего проекта (система управления виртуальными машинами, на базе Libvirt), понадобилось программно рулить loop девайсом в Linux. Первая версия когда основанная на вызове командлайн-команды losetup через subprocess.Popen() весьма сносно работала на моей Ubuntu 8.04, однако после деплоя пошли баг-репорты о том что на RHEL и некоторых других системах заявленный функционал не работает. После некоторых разбирательств выяснилось что в них losetup принимает немного другие аргументы, и просто нашу задачу реализовать не получится.
Поковырявшись в исходниках losetup, я увидел что все необходимые мне операции делаются путём отправки IOCTL вызовов в устройство. С питоновским fcntl.ioctl() у меня что-то не заладилось. Было принято решение опуститься на уровень ниже, написать модуль на C.
Как потом выяснилось fcntl.ioctl() вполне достаточен для реализации всего что мне было нужно. Уже не помню что меня в нём испугало в начале. Наверное нужно работать меньше 10 часов в день ;)
С другой стороны, если бы я сразу его использовал — этого топика бы не было.
Итак ещё раз, для тех кто читает по диагонали — в Питоне есть отличный модуль fcntl.ioctl(). Всё что ниже читать просто как пример.
Всё что можно делать на Питоне — делать на Питоне. То что не получается — выносить в low-level на C.
Того что не получается сделать на питоне — набралось немного: собственно монтирование/размонтирование образа, и проверка, занят ли девайс.
В рамках задачи не стояли требования по поддержке шифрования, и прочих наворотов поэтому со стороны C интерфейс получился достаточно простым:
Модуль, по аналогии с командлайновой утилитой будет называться losetup. Запускаем любимый Eclipse + PyDev и создаём проект. В нём создаём losetup.py в котором будет весь питоновский код модуля.
Модуль который реализует low-level взаимодействие с системой назовём _losetup. Наш losetup будет импортировать _losetup и использовать его для реализации высокоуровнёвого API.
Создаём папку src, в которой кладём два файла losetupmodule.c и losetupmodule.h
losetupmodule.c
В losetupmodule.h просто набор определений безжалостно выдранный из util-linux-ng
Собирать модули можно по разному, но самый простой и надёжный — это через setuptools (distutils).
Создаём setup.py
Вся белая магия в строке «ext_modules=[Extension('_losetup', ['src/losetupmodule.c'], include_dirs=['src'])]». Тут описывается расширение с именем _losetup, код которого находится в src/losetupmodule.c, инклуды в src. Этого достаточно чтобы дистутилс мог собрать расширение, установить его, делать из него всяческие пекеджи (в том числе win32 инсталлер, хотя там и не всё так просто).
Проверяем что всё билдится путём вызова «python setup.py build»
Реализуем метод mount()
Вроде бы несложно, однако возможно не совсем понятно что тут происходит. Давайте разберём основные элементы.
Функции объявленные как METH_VARARGS получают аргументы в виде кортежа. PyArg_ParseTuple() проверяет что аргументы соответствуют указанному шаблону (в данном случае «ss» — две строки), и получает данные, либо, в случае если аргумент не соответствуют шаблону устанавливает ошибку, и возвращает false. Детали о том как это работает можно прочитать в Extracting Parameters in Extension Functions
С точки зрения питона это выглядит так:
Идём дальше
PyErr_SetFromErrno создаём исключение с указаным типом, получает код ошибки из глобальной переменной errno, и возвращает NULL — что означает что произошло исключение. Ссылки на документацию: Intermezzo: Errors and Exceptions , Exception Handling
Для питона это выглядит так:
Нашей функции не нужно возвращать никаких особых данных, поэтому мы возвращаем None. Подробнее можно прочитать в Building Arbitrary Values
Остальные функции реализуются аналогично.
Итак модуль написан. Нужно дать человечеству шанс им воспользоваться. Самый простой способ это сделать — опубликовать модуль на Python Package Index.
Регистрируемся на PyPI.
После регистрации пишем в консоли
вводим данные своего аккаунта, и setuptools создёт пакет на PyPI.
делает source destribution (tgz архив с кодом и метаданными), и заливает его на PyPI.
Результат можно увидеть тут http://pypi.python.org/pypi/losetup/
Идём шелом на ненавистный RHEL, пишем easy_install -U losetup, и, пока мы говорим волшебные слова «крибле-крабле-бумц», setuptools скачает наш пакет, сбилдит его и установит в систему.
Добавляем losetup как зависимость в setup.py основного приложения. Теперь при его инсталляции setuptools поставит и наш модуль.
Вот так, неожиданно легко оказалось опуститься с Python на уровень абстракции ниже, и написать модуль для low-level взаимодействия с системой.
Так-же получили хороший пример того что нужно больше думать и меньше делать. Наш Зелёный Друг могуч, и даже такие экзотические задачи можно решать не расставаясь с ним.
Чего и вам желаю.
Описание задачи
В рамках текущего проекта (система управления виртуальными машинами, на базе Libvirt), понадобилось программно рулить loop девайсом в Linux. Первая версия когда основанная на вызове командлайн-команды losetup через subprocess.Popen() весьма сносно работала на моей Ubuntu 8.04, однако после деплоя пошли баг-репорты о том что на RHEL и некоторых других системах заявленный функционал не работает. После некоторых разбирательств выяснилось что в них losetup принимает немного другие аргументы, и просто нашу задачу реализовать не получится.
Поковырявшись в исходниках losetup, я увидел что все необходимые мне операции делаются путём отправки IOCTL вызовов в устройство. С питоновским fcntl.ioctl() у меня что-то не заладилось. Было принято решение опуститься на уровень ниже, написать модуль на C.
Disclaimer
Как потом выяснилось fcntl.ioctl() вполне достаточен для реализации всего что мне было нужно. Уже не помню что меня в нём испугало в начале. Наверное нужно работать меньше 10 часов в день ;)
С другой стороны, если бы я сразу его использовал — этого топика бы не было.
Итак ещё раз, для тех кто читает по диагонали — в Питоне есть отличный модуль fcntl.ioctl(). Всё что ниже читать просто как пример.
Планирование API
Всё что можно делать на Питоне — делать на Питоне. То что не получается — выносить в low-level на C.
Того что не получается сделать на питоне — набралось немного: собственно монтирование/размонтирование образа, и проверка, занят ли девайс.
В рамках задачи не стояли требования по поддержке шифрования, и прочих наворотов поэтому со стороны C интерфейс получился достаточно простым:
- mount(device, imagepath) — монтирует imagepath в device.
- unmount(device, imaepath) — освобождает device.
- is_used(device) — 1 если устройство смонтировано, и 0 если свободно
Делаем скелет
Модуль, по аналогии с командлайновой утилитой будет называться losetup. Запускаем любимый Eclipse + PyDev и создаём проект. В нём создаём losetup.py в котором будет весь питоновский код модуля.
Модуль который реализует low-level взаимодействие с системой назовём _losetup. Наш losetup будет импортировать _losetup и использовать его для реализации высокоуровнёвого API.
Создаём папку src, в которой кладём два файла losetupmodule.c и losetupmodule.h
losetupmodule.c
#include <Python.h&rt;
#include "losetupmodule.h"
// Исключение которое мы будем бросать в случае какой-то ошибки
static PyObject *LosetupError;
// Монтирование образа в девайс
static PyObject *
losetup_mount(PyObject *self, PyObject *args)
{
return Py_BuildValue("");
}
// Размонтирование девайса
static PyObject *
losetup_unmount(PyObject *self, PyObject *args)
{
return Py_BuildValue("");
}
// Проверка, смонтировано ли что-то в девайсе
static PyObject *
losetup_is_used(PyObject *self, PyObject *args)
{
int fd, is_used;
const char *device;
struct loop_info64 li;
if (!PyArg_ParseTuple(args, "s", &device)) {
return NULL;
}
if ((fd = open (device, O_RDONLY)) < 0) {
return PyErr_SetFromErrno(LosetupError);
}
is_used = ioctl(fd, LOOP_GET_STATUS64, &li) == 0;
close(fd);
return Py_BuildValue("i", is_used);
}
// Таблица методов реализуемых расширением
// название, функция, параметры, описание
static PyMethodDef LosetupMethods[] = {
{"mount", losetup_mount, METH_VARARGS, "Mount image to device. Usage _losetup.mount(loop_device, file)."},
{"unmount", losetup_unmount, METH_VARARGS, "Unmount image from device. Usage _losetup.unmount(loop_device)."},
{"is_used", losetup_is_used, METH_VARARGS, "Returns True is loopback device is in use."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
// Инициализация
PyMODINIT_FUNC
init_losetup(void)
{
PyObject *m;
// Инизиализруем модуль _losetup
m = Py_InitModule("_losetup", LosetupMethods);
if (m == NULL)
return;
// Создаём исключение
LosetupError = PyErr_NewException("_losetup.error", NULL, NULL);
Py_INCREF(LosetupError);
PyModule_AddObject(m, "error", LosetupError);
}
В losetupmodule.h просто набор определений безжалостно выдранный из util-linux-ng
Настраиваем сборку
Собирать модули можно по разному, но самый простой и надёжный — это через setuptools (distutils).
Создаём setup.py
from setuptools import setup, Extension
setup(name='losetup',
version='1.0.1',
description='Python API for "loop" Linux module',
author='Sergey Kirillov',
author_email='serg@rainboo.com',
ext_modules=[Extension('_losetup', ['src/losetupmodule.c'], include_dirs=['src'])],
py_modules=['losetup']
)
Вся белая магия в строке «ext_modules=[Extension('_losetup', ['src/losetupmodule.c'], include_dirs=['src'])]». Тут описывается расширение с именем _losetup, код которого находится в src/losetupmodule.c, инклуды в src. Этого достаточно чтобы дистутилс мог собрать расширение, установить его, делать из него всяческие пекеджи (в том числе win32 инсталлер, хотя там и не всё так просто).
Проверяем что всё билдится путём вызова «python setup.py build»
Наращиваем мышцы
Реализуем метод mount()
static PyObject *
losetup_mount(PyObject *self, PyObject *args)
{
int ffd, fd;
int mode = O_RDWR;
struct loop_info64 loopinfo64;
const char *device, *filename;
// Check parameters
if (!PyArg_ParseTuple(args, "ss", &device, &filename)) {
return NULL;
}
// Initialize loopinfo64 struct, and set filename
memset(&loopinfo64, 0, sizeof(loopinfo64));
strncpy((char *)loopinfo64.lo_file_name, filename, LO_NAME_SIZE-1);
loopinfo64.lo_file_name[LO_NAME_SIZE-1] = 0;
// Open image file
if ((ffd = open(filename, O_RDWR)) < 0) {
if (errno == EROFS) // Try to reopen as read-only on EROFS
ffd = open(filename, mode = O_RDONLY);
if (ffd < 0) {
return PyErr_SetFromErrno(LosetupError);
}
loopinfo64.lo_flags |= LO_FLAGS_READ_ONLY;
}
// Open loopback device
if ((fd = open(device, mode)) < 0) {
close(ffd);
return PyErr_SetFromErrno(LosetupError);
}
// Set image
if (ioctl(fd, LOOP_SET_FD, ffd) < 0) {
close(fd);
close(ffd);
return PyErr_SetFromErrno(LosetupError);
}
close (ffd);
// Set metadata
if (ioctl(fd, LOOP_SET_STATUS64, &loopinfo64)) {
ioctl (fd, LOOP_CLR_FD, 0);
close (fd);
return PyErr_SetFromErrno(LosetupError);
}
close(fd);
return Py_BuildValue("");
}
Вроде бы несложно, однако возможно не совсем понятно что тут происходит. Давайте разберём основные элементы.
if (!PyArg_ParseTuple(args, "ss", &device, &filename)) {
return NULL;
}
Функции объявленные как METH_VARARGS получают аргументы в виде кортежа. PyArg_ParseTuple() проверяет что аргументы соответствуют указанному шаблону (в данном случае «ss» — две строки), и получает данные, либо, в случае если аргумент не соответствуют шаблону устанавливает ошибку, и возвращает false. Детали о том как это работает можно прочитать в Extracting Parameters in Extension Functions
С точки зрения питона это выглядит так:
>>> import _losetup >>> _losetup.mount("aaa") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: function takes exactly 2 arguments (1 given) >>> _losetup.mount(1,2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: argument 1 must be string, not int >>>
Идём дальше
return PyErr_SetFromErrno(LosetupError);
PyErr_SetFromErrno создаём исключение с указаным типом, получает код ошибки из глобальной переменной errno, и возвращает NULL — что означает что произошло исключение. Ссылки на документацию: Intermezzo: Errors and Exceptions , Exception Handling
Для питона это выглядит так:
>>> _losetup.mount('/dev/loop0', '/tmp/somefile') Traceback (most recent call last): File "<stdin>", line 1, in <module> _losetup.error: (2, 'No such file or directory') >>>
return Py_BuildValue("");
Нашей функции не нужно возвращать никаких особых данных, поэтому мы возвращаем None. Подробнее можно прочитать в Building Arbitrary Values
Остальные функции реализуются аналогично.
Публикация на PyPI
Итак модуль написан. Нужно дать человечеству шанс им воспользоваться. Самый простой способ это сделать — опубликовать модуль на Python Package Index.
Регистрируемся на PyPI.
После регистрации пишем в консоли
python setup.py register
вводим данные своего аккаунта, и setuptools создёт пакет на PyPI.
python setup.py sdist upload
делает source destribution (tgz архив с кодом и метаданными), и заливает его на PyPI.
Результат можно увидеть тут http://pypi.python.org/pypi/losetup/
Идём шелом на ненавистный RHEL, пишем easy_install -U losetup, и, пока мы говорим волшебные слова «крибле-крабле-бумц», setuptools скачает наш пакет, сбилдит его и установит в систему.
Добавляем losetup как зависимость в setup.py основного приложения. Теперь при его инсталляции setuptools поставит и наш модуль.
Завершение
Вот так, неожиданно легко оказалось опуститься с Python на уровень абстракции ниже, и написать модуль для low-level взаимодействия с системой.
Так-же получили хороший пример того что нужно больше думать и меньше делать. Наш Зелёный Друг могуч, и даже такие экзотические задачи можно решать не расставаясь с ним.
Чего и вам желаю.
Использованая литература
- Extending and Embedding the Python Interpreter Guido van Rossum