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
