Архив рубрики: Python

libvirt & Co. Облако "на коленке". Часть 2 — Сети

Компоненты

Функционирование виртуальных сетей обеспечивается различными технологиями, которые я бегло опишу:

bridges — сетевые мосты — программные аналоги свичей, позволяют соединить вместе несколько сетевых интерфейсов и передавать между ними пакеты, как если бы они были включены в один свич. Бриджи управляются с помощью команды brctl:

Без подсветки синтаксиса

$ brctl  show  # напечатать все бриджи с подключенными интерфейсами
bridge name bridge id STP enabled interfaces
virbr0 8000.000000000000 yes

# brctl add - добавить бридж
# brctl addif - включить eth в бридж
# brctl delif - отключить интерфейс
# brctl delbr - удалить бридж

$ brctl  show  # напечатать все бриджи с подключенными интерфейсами
bridge name bridge id STP enabled interfaces
virbr0 8000.000000000000 yes

# brctl add - добавить бридж
# brctl addif - включить eth в бридж
# brctl delif - отключить интерфейс
# brctl delbr - удалить бридж

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

tun (tap) — виртуальные сетевые интерфейсы. В отличии от аппаратных привязаны к определенному процессу пользовательского режима, а не к сетевой карте. Родительский процесс может писать/читать данные из виртуального интерфейса имитируя работу сети. В остальном они не отличаются от обычных интерфейсов. С помощью tun/tap работают многие VNP программы, например openvpn, которая создает tun/tap, вычитывает из него данные, шифрует и переправляет по обычной сети на другой компьютер, где второй процесс openvpn принимает данные, расшифровывает и записывает в свой tun/tap, имитируя прямое сетевое соединение между удаленными компьютерами. Как и 95% всех сетевых возможностей linux tun/tap можно управлять с помошью утилиты ip. Пример использования tun из python можно найти тут kharkovpromenade. Tun используются для создания сетевых интерфейсов виртуальынх машин.

iptables — система управления сетевым трафиком в linux. Обеспечивает фильтрация и модификацию трафика, управление сетевыми соединениями, etc. Возможности iptables чрезвычайно обширные и описывать даже примерно я их не буду, приведу только команды, позволяющие увидеть все правила на компьютере:

Без подсветки синтаксиса

# iptables -t nat -S
# iptables -t filter -S
# iptables -t raw -S
# iptables -t mangle -S

# iptables -t nat -S
# iptables -t filter -S
# iptables -t raw -S
# iptables -t mangle -S

Все правила легко читаются даже без знания iptables.

Ок, с этим багажом уже можно разбираться с виртуальными сетями. Для большинства случаев нам не придется делать сети самостоятельно — libvirt берет эту работу на себя, предоставляя нам готовый результат. Начнем с устройства простейшей сети, которую со старта создает libvirt — defaults.

Без подсветки синтаксиса

# virsh n
et-list
Name State Autostart
-----------------------------------------
default active yes

# virsh net-list
Name State Autostart
-----------------------------------------
default active yes

Описание этой сети можно получить с помощью следующих команд:

Без подсветки синтаксиса

# virsh net-info default

Name default
UUID c598e36f-31fd-672e-09e3-2cbe061cd606
Active: yes
Persistent: yes
Autostart: yes
Bridge: virbr0

# virsh net-dumpxml default

# virsh net-info default

Name default
UUID c598e36f-31fd-672e-09e3-2cbe061cd606
Active: yes
Persistent: yes
Autostart: yes
Bridge: virbr0

# virsh net-dumpxml default

Без подсветки синтаксиса


default
c598e36f-31fd-672e-09e3-2cbe061cd606
mode='nat'/>
name='virbr0' stp='on' delay='0' />
address='192.168.122.1' netmask='255.255.255.0'>

start='192.168.122.40' end='192.168.122.254' />




default
c598e36f-31fd-672e-09e3-2cbe061cd606







Тот же самый результат можно получить и из python:

Без подсветки синтаксиса

import libvirt
from xml.etree.ElementTree import fromstring

conn = libvirt.open("qemu:///system")
net = conn.networkLookupByName('default')
xml = fromstring(net.XMLDesc(0))
print "default net addr =", xml.find('ip').attrib['address']
print "default net mask =", xml.find('ip').attrib['netmask']
print "default net bridge =", xml.find('bridge').attrib['name']

import libvirt
from xml.etree.ElementTree import fromstring

conn = libvirt.open("qemu:///system")
net = conn.networkLookupByName('default')
xml = fromstring(net.XMLDesc(0))
print "default net addr =", xml.find('ip').attrib['address']
print "default net mask =", xml.find('ip').attrib['netmask']
print "default net bridge =", xml.find('bridge').attrib['name 9;]

Еще один важный компонент сети — dnsmasq:

Без подсветки синтаксиса

$ ps aux | grep dnsmasq | grep -v grep
nobody 4503 0.0 0.0 25836 976 ? S 02:08 0:00
dnsmasq --strict-order --bind-interfaces
--pid-file=/var/run/libvirt/network/default.pid
--conf-file= --except-interface lo
--listen-address 192.168.122.1
--dhcp-range 192.168.122.40,192.168.122.254
--dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases
--dhcp-lease-max=215 --dhcp-no-override

$ ps aux | grep dnsmasq | grep -v grep
nobody 4503 0.0 0.0 25836 976 ? S 02:08 0:00
dnsmasq --strict-order --bind-interfaces
--pid-file=/var/run/libvirt/network/default.pid
--conf-file= --except-interface lo
--listen-address 192.168.122.1
--dhcp-range 192.168.122.40,192.168.122.254
--dhcp-leasefile=/var/lib/libvirt/dnsmasq/default.leases
--dhcp-lease-max=215 --dhcp-no-override

Конфигурационные файлы сетей хранятся в /var/lib/libvirt/network:

Без подсветки синтаксиса

$ ls -l /var/lib/libvirt/network/
total 4
-rw-r--r-- 1 root root 543 2011-12-24 02:08 default.xml

$ ls -l /var/lib/libvirt/network/
total 4
-rw-r--r-- 1 root root 543 2011-12-24 02:08 default.xml

Итак — что получилось в итоге. Вот эта строка конфигурационного файла:

Без подсветки синтаксиса

 type="network">
network="default" />
mode="nat" />
dev="vnet7" />
address="{mac}" />






Подключила eth0 нашей виртуальной машины к бриджу virbr0 сети default. Эта сеть имеет маску 192.168.122.0/24, подключена через NAT к внешнему миру и обслуживается dhcp сервером. Причем сам virbr0 имеет ip 192.168.122.1 и служит гейтом для этой сети. Адреса из диапазона 192.168.122.2-192.168.122.40 я ранее зарезервировал для ручного распределения, отредактировав и перезапустив сеть.

Теперь вернемся к начальному вопросу — как программно узнать ip адрес, выданный нашей виртуалке? Есть три основных способа:

  • Если с виртуальной машиной уже был обмен данными, то можно посмотреть в кеше маршрутизации 'ip route show cache | grep virbr0' или в кеше аппаратных адресов — 'arp -na'. Способ наименее надежный, так как если обмена не было кеши будут пустые.
  • Достать информацию из базы dhcp сервера — leases. Для dnsmasq это по умолчанию файл /var/lib/libvirt/dnsmasq/default.leases:

Без подсветки синтаксиса

$ cat /var/lib/libvirt/dnsmasq/default.leases
1324718340 00:44:01:61:78:01 192.168.122.99 * *

$ cat /var/lib/libvirt/dnsmasq/default.leases
1324718340 00:44:01:61:78:01 192.168.122.99 * *

В при

libvirt & Co. Облако "на коленке". Часть 3 — Дисковые образы

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

Как хранить

  • raw — самый простой формат, прямая копия данных с диска.
  • qcow2 — основной формат qemu. Обладает большим спектром возможностей
  • vdi — формат, используемый VirtualBox'ом
  • vmdkVMware формат
  • cow, qcow — каменный век

Обсуждать raw смысла не имеет — просто диск байт по байту. qcow2 самый распространенный и функциональный формат виртуальных дисков для qemu/xen.

  • содержит только те кластеры, в которые были записаны данные. Т.е. можно создать образ диска размером в 10G, но реальный размер файла будет расти только при фактической записи на диск.

  • поддерживает «наслоение» файлов. Qcow2 файл позволяет хранить только изменения относительно другого файла, представляющего базовый образ (backing store). При таком режиме базовый файл никогда не модифицируется — запись идет в «верхний» qcow2 файл, а чтение происходит из qcow2, если соответствующий кластер был модифицирован и записан в qcow2 или из базового в противном случае. Это позволяет, например, запустить несколько виртуальных машин на основе одного образа. После подготовки базового образа к нему подключается параллельно несколько qcow2 файлов и на их основе запускаются vm, каждая из которых «идет своей дорогой». Кроме экономии места на диске это также улучшает кеширование. Поддерживается перенесение(commit) изменений назад в базовый файл и некоторые другие возможности. Базовый файл может быть в любом формате — raw, qcow2, etc.
  • шифрование
  • компрессия (только для чтения, запись будет производиться в распакованный сектор)
  • можно делать qcow2 файлы с пред выделенным местом для метаданных, это повышает быстродействие в случае если ожидаются интенсивные дисковые операции.

Базовая утилита для работы с этими форматами — qemu-img.

Без подсветки синтаксиса

$ qemu-img create -f raw img.raw 10G 
Formatting 'img.raw', fmt=raw size=10737418240

$ qemu-img create -f qcow2 img.qcow2 10G
Formatting 'img.qcow2', fmt=qcow2 size=10737418240 encryption=off
cluster_size=0

$ qemu-img create -f qcow2 -o preallocation=metadata img_m.qcow2 10G
Formatting 'img.qcow2', fmt=qcow2 size=10737418240 encryption=off
cluster_size=0 preallocation='metadata'

$ ls -lhsS img*
1.8M -rw-r--r-- 1 koder koder 11G 2011-12-24 21:37 img_m.qcow2
0 -rw-r--r-- 1 koder koder 10G 2011-12-24 21:34 img.raw
136K -rw-r--r-- 1 koder koder 193K 2011-12-24 21:37 img.qcow2

$ qemu-img create -f raw img.raw 10G 
Formatting 'img.raw', fmt=raw size=10737418240

$ qemu-img create -f qcow2 img.qcow2 10G
Formatting 'img.qcow2', fmt=qcow2 size=10737418240 encryption=off
cluster_size=0

$ qemu-img create -f qcow2 -o preallocation=metadata img_m.qcow2 10G
Formatting 'img.qcow2', fmt=qcow2 size=10737418240 encryption=off
cluster_size=0 preallocation='metadata'

$ ls -lhsS img*
1.8M -rw-r--r-- 1 koder koder 11G 2011-12-24 21:37 img_m.qcow2
0 -rw-r--r-- 1 koder koder 10G 2011-12-24 21:34 img.raw
136K -rw-r--r-- 1 koder koder 193K 2011-12-24 21:37 img.qcow2

Первая запись в ls это реальный размер файла на диске, а шестая — заявленный размер. В данном случае мы видим sparse файлы в действии. Все основные файловые системы в linux поддерживают выделение реального места на диске под файл при записи реальных данных и такое поведение установлено по умолчанию. В отличии от raw файлов qcow2 файлы со старта содержат управляющие структуры, так что их размен не нулевой. Впрочем при копировании/архивации и др. raw файлов все-же придется обрабатывать полный размер, в отличии от qcow2.

Без подсветки синтаксиса

$ qemu-img info img.raw  # информация о файле 
image: img.raw
file format: raw
virtual size: 10G (10737418240 bytes)
disk size: 0

$ qemu-img info img.qcow2
image: img.qcow2
file format: qcow2
virtual size: 10G (10737418240 bytes)
disk size: 136K
cluster_size: 65536

$ qemu-img info img.raw  # информация о файле 
image: img.raw
file format: raw
virtual size: 10G (10737418240 bytes)
disk size: 0

$ qemu-img info img.qcow2
image: img.qcow2
file format: qcow2
virtual size: 10G (10737418240 bytes)
disk size: 136K
cluster_size: 65536

Сделаем «наслаивающиеся» qcow2 файлы. Размер для нового файла указывать не нужно — так как он должен быть такого-же размера, как и базовый образ:

Без подсветки синтаксиса

$ qemu-img create -b img.qcow2 -f qcow2 -F qcow2 img_bs.qcow2
Formatting 'img_bs.qcow2', fmt=qcow2 size=10737418240
backing_file='img.qcow2' backing_fmt='qcow2' encryption=off cluster_size=0

$ qemu-img info img_bs.qcow2
image: img_bs.qcow2
file format: qcow2
virtual size: 10G (10737418240 bytes)
disk size: 136K
cluster_size: 65536
backing file: img.qcow2 (actual path: img.qcow2)

$ qemu-img create -b img.qcow2 -f qcow2 -F qcow2 img_bs.qcow2
Formatting 'img_bs.qcow2', fmt=qcow2 size=10737418240
backing_file='img.qcow2' backing_fmt='qcow2' encryption=off cluster_size=0

$ qemu-img info img_bs.qcow2
image: img_bs.qcow2
file format: qcow2
virtual size: 10G (10737418240 bytes)
disk size: 136K
cluster_size: 65536
backing file: img.qcow2 (actual path: img.qcow2)

Базовый файл может быть любого формата, но создаваемый с backing store файл может быть только qcow2. qemu-img также Поддерживает все основные операции — изменение размера образа, смена базового файла, проверка целостности и создание/работа со снимками.

Остальные форматы интересны только при использовании соответствующих систем виртуализации и рассматривать их в контексте qemu/kvm/xen смысле не имеет. qemu-img позволяет конвертировать файлы между всеми форматами, описанными сверху. Единственное исключение — разбитые на несколько файлов диски vmWare. Для работы сначала их нужно объединить в один файл с помошью vdiskmanager, который входит в поставку vmWare Workstation for linux. Таким образом можно с kvm/xen использовать широко доступные в интернете виртуальные машины для vmWare.

Подключение этих файлов к виртуальным машинам libvirt-disk, подключение CD делается примерно так-же, как и raw файлов.

Где хранить

Для локального хранения есть два основных варианта — файл или диск/раздел/LVM устройством ( всем линуксоидам, кто еще не освоил LVM — очень советую это сделать, это мощная система управления дисковым пространством; так-же полезное чтиво — devmapper, на нем и работают в linux LVM2, всякие software RAID & Co). Создание образа на разделе/LVM принципиально не отличается от файла на FS, нужно только указывать qemu-img format не raw а host_device:

Без подсветки синтаксиса

# qemu-img create -f host_device /dev/vm_images/img1 10G

# qemu-img create -f host_device /dev/vm_images/img1 10G

/dev/vm_images/img1 — это логический раздел img1 в группе разделов vm_images (LVM). После этого можно передать его в libvirt:

Без подсветки синтаксиса

 type='block' device='disk'>
name='qemu' type='raw' />
dev='/dev/vm_images/img1' />
dev='vda' bus='virtio' />





Обратите внимание на вместо него можно использовать , но virtio дает значительный прирост производительности. Virtio это система частичной паравиртуализации для kvm. Она позволяет установить в гостевой системе набор драйверов, которые будут обслуживать виртуальные устройства, передавая в гипервизор высокоуровневые запросы и избавляя гостевую систему от превращения их (запросов) в общение с портами и DMA, а гипервизор от имитации низкоуровневых протоколов. Особенно хорошо virtio ускоряет сеть и дисковые операции. В своем роде это аналог vmware tools. Все современные ядра linux поддерживают virtio без дополнительных телодвижений, но, например, ядро из debian lenny — нет. Так что образ debian lenny просто не загрузится с virtio устройства (есть virtio драйвера и для windows).

Это были локальные способы, кроме них есть еще вагон и маленькая тележка сетевых хранилищ (причем они могут быть использованы и как набор блочных устройств в режиме «по одному на VM», так и в режиме удаленного диска с файловой системой). Очевидными преимуществами сетевых хранилищ являются централизованное управление дисками для всего облака и облегчение миграции. Для небольших облаков они не так интересны, так что пробежимся по некоторым из ниx вскользь.

AOE — (ATA Over Ethernet) реализация ATA протокола поверх ethernet. Фактически ATA команды напрямую передаются удаленному винчестеру. Из преимуществ — скорость. Дисковый массив и все рабочие станции должны быть в одном сегменте локальной сети (нет маршрутизации). Основной производитель устройств — Coraid. Впрочем набор утилит открыт и можно достаточно просто самостоятельно сделать AOE сервер примеров в сети предостаточно.

iSCSI — SCSI поверх IP. Многие системы удаленного хранения работают именно на этом проколе. Маршрутизируемый (дисковый массив и рабочие станции могут располагаться в сети где угодно). Возможностей больше, чем у AOE, скорость — ниже.

nbd — Network Block Device. Протокол для раздачи блочных устройств по сети, подробнее обсуждается ниже на примере qemu-nbd.

rbd — Rados block device. Часть проекта ceph — распределенного сетевого хранилища и файловой системы (из интересного — есть прямая привязка к qemu/kvm).

drbd — Distributed Replicated Block Device. Название говорит само за себя.

Из действительно интересного — sheepdog. Это фактически первое(AFAIK) «правильное» распределенное сетевое хранилище (только для kvm/qemu). Построено в соответствии с идеями amazon dynamo (статья обязательно к прочтению всем, кто так или иначе сталкивается с распределенными системами хранения данных. Именно на этих идеях построены все основные NoSQL базы данных, ориентированные на сверхбольшие объемы данных, — Cassandra, REAK, Voldemort). К сожалению проект похож на заброшенный.

Больше информации по этим хранилищам можно получить еще и в блоге Daniel P. Berrange.

Я пока сознательно обхожу стороной производительность дисковых операций на разных форматах, поскольку это довольно большая тема для отдельного поста. Для локальных хранилищ с высоты птичьего полета результат примерно такой
— qcow2 почти такой же по скорости, как raw. Двухслойные qcow2 (и qcow2 поверх raw) примерно в два раза медленнее, если не используется SSD диск. А размещение образа на lvm разделе вместо файла увеличивает скорость работы только на больших блоках (файловые системы хорошо оптимизированы под такие нагрузки, по крайней мере XFS).

Как модифицировать

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

Первый и самый основанной способ модифицировать файл с образом — каким-то образом примонтировать его в локальную файловую систему и дальше работать с локальными файлами. Для этого сначала нужно подключить его к виртуальному блочному устройству — loop.

Без подсветки синтаксиса

# losetup -f --show img.raw
/dev/loop0

# losetup -a
/dev/loop0: [0807]:2754650 (/home/koder/vm_images/img.raw)


# mkdir -p /mnt/vm/img_0
# mount /dev/loop0 /mnt/vm/img_0

# losetup -f --show img.raw
/dev/loop0

# losetup -a
/dev/loop0: [0807]:2754650 (/home/koder/vm_images/img.raw)


# mkdir -p /mnt/vm/img_0
# mount /dev/loop0 /mnt/vm/img_0

После этих команд файловая система из img.raw подключена в /mnt/vm/img_0 т.е. ее корень совпадает с /mnt/vm/img_0, /etc — /mnt/vm/img_0/etc и т.д. По окончанию обработки отключаем файл

Без подсветки синтаксиса

# umount /dev/loop0
# losetup -d /dev/loop0

# umount /dev/loop0
# losetup -d /dev/loop0

Все внесенные изменения будут сохранены в файл образа. Если на образе несколько разделов, то все чуть сложнее — либо сначала с помощью fdisk смотрим таблицу разделов, определяем смещение необходимого раздела и передаем его в опции -o в losetup либо используем kpartx:

Без подсветки синтаксиса

# kpartx -a img2.raw
# mount /dev/mapper/loop0p1 /mnt/vm/img_0

# kpartx -a img2.raw
# mount /dev/mapper/loop0p1 /mnt/vm/img_0

kpartx делает дополнительные виртуальные устройства /dev/mapper/loop0pX для всех разделов на образе (на самом деле он использует devmapper для /dev/loop0).

Однако так можно подключить только raw образы, для всех остальных нужно использовать qemu-nbd. Это сервер, которые умеет раздавать блочное устройство по nbd протоколу. (Вообще nbd довольно интересный прокол — позволяет раздать по сети любое локальное блочное устройство, или, что еще интереснее, иммитировать его из пользовательской программы ,а не из драйвера. Вот, например ndb сервера 1, 2 на python.)

Без подсветки синтаксиса

# modprobe nbd max_part=16 # <- очень важно передать max_part, если nbd
# qemu-nbd -b 127.0.0.1 -p 5555 img.qcow2
# nbd-client 127.0.0.1 5555 /dev/nbd0 # <- подключить к /dev/nb0

# modprobe nbd max_part=16 # <- очень важно передать max_part,
 если nbd
# qemu-nbd -b 127.0.0.1 -p 5555 img.qcow2
# nbd-client 127.0.0.1 5555 /dev/nbd0 # <- подключить к /dev/nb0

Если привязывать сервер не к localhost, то можно подключить это устройство на другом компьютере удаленно. Можно объединить оба шага в один:

Без подсветки синтаксиса

# qemu-nbd --connect=/dev/nbd0 img.qcow2

# qemu-nbd --connect=/dev/nbd0 img.qcow2

По итогу /dev/nbd0 будет представлять образ виртуальной машины, а /dev/nbd0pX — отдельные разделы. Дальнейшая работа не отличается от raw файла — монтирует полученные устройства в локальную файловую систему и елозим байты, вся поддержка формата qcow2 будет выполняться qemu-nbd сервером.

Совершенно другой способ модификации дисковых образов предлагает библиотека libguestfs. Это одна интересных библиотек, которые я видел. Она позволяет модифицировать образы виртуальных машин и многое другое. При это не требуются права root, поддерживаются все файловые системы, поддерживаемые в linux, LVM и все-все-все. Внутри она запускает простенькую виртуальную машину, монтирует в нее указанный файл образа и позволяет модифицировать его используя обширный API, который в итоге общается с VM по RPC. В принципе libguestfs позволяет и смонтировать файловую систему локально, использую fuse. Вообще возможности libguestfs очень обширны — p2v и v2v конвертация, модификация реестра windows, определение установленной операционной системы и ПО, автоматическое монтирование всего в соответствии с локальными fstab и др. Еще из интересного в нее входит guestfish — утилита командной строки, позволяющая выполнять все операции из командной строки, в комплекте с virsh они позволяют писать маленькие системы управления VM прямо на bash. Есть API для C, Python, Perl, Ruby, Ocaml и прочего. Меня, ессно, интересует в первую очередь python.

Для приведения образа vm к удобному для запуска виду нам нужно примерно такая функция:

Без подсветки синтаксиса

def prepare_guest_debian(disk_path, hostname, passwords, eth_devs, format=None, apt_proxy_ip=None):

# создаем и запускаем vm
gfs = guestfs.GuestFS()
gfs.add_drive_opts(disk_path, format=format)
gfs.launch()

# находим раздел с /etc. Не очень чистое решение, но для образов, когда все
# на одном разделе работает
for dev, fs_type in gfs.list_filesystems():
if fs_type in 'ext2 ext3 reiserfs3 reiserfs4 xfs jfs btrfs':
# монтирует раздел в корень файловой системы vm
gfs.mount(dev, '/')
# если есть etc - все ок
if gfs.exists('/etc'):
break
gfs.umount(dev)

# обновляем hostname, для правильной работы hostname нужно так-же модифицировать
# /etc/hosts
gfs.write('/etc/hostname', hostname)

# set eth device names for udev
templ = 'SUBSYSTEM=="net", DRIVERS=="?*", ATTR{{address}}=="{hw}", NAME="{name}"'

# записываем настройки сетевых интерфейсов в /etc/network/interfaces

rules_fc = []
interfaces = ["auto loniface lo inet loopback"]
for dev, (hw, ip, sz, gw) in eth_devs.items():
rules_fc.append(templ.format(hw=hw, name=dev))
interfaces.append("auto " + dev)

if ip == 'dhcp':
interfaces.append("iface {0} inet dhcp".format(dev))
else:
interfaces.append("iface {0} inet static".format(dev))
interfaces.append(" address " + ip)
network = int2ip(ip2int(ip) & ip2int(netsz2netmask(sz)))
interfaces.append(" network " + network)
interfaces.append(" netmask " + netsz2netmask(sz))

gfs.write('/etc/udev/rules.d/70-persistent-net.rules', "n".join(rules_fc))
gfs.write('/etc/network/interfaces', "n".join(interfaces))

# обновляем пароли для пользователей

chars = "".join(chr(i) for i in range(ord('a'), ord('z') + 1))
chars += "".join(chr(i) for i in range(ord('A'), ord('Z') + 1))
chars += "".join(chr(i) for i in range(ord('0'), ord('9') + 1))

hashes = {}
for login, passwd in passwords.items():
salt = "".join(random.choice(chars) for _ in range(8))
hashes[login] = crypt.crypt(passwd, "$6$" + salt)

new_shadow = []
need_logins = set(hashes)

for ln in gfs.read_file('/etc/shadow').split('n'):
ln = ln.strip()
if ln != '' and ln[0] != '#':
login = ln.split(':', 1)[0]
if login in hashes:
new_shadow.append("{login}:{hash}:{rest}".format(login=login,
hash=hashes[login],
rest=ln.split(':', 2)[2]))
need_logins.remove(login)
else:
new_shadow.append(ln)

for login in need_logins:
new_shadow.append("{login}:{hash}:{rest}".format(login=login,
hash=hashes[login],
rest="0:0:999 99:7:::"))

gfs.write('/etc/shadow', "n".join(new_shadow))

# тут еще модификация /etc/passwd, /etc/hosts, создание домащних папок, etc
# полная версия на github


def prepare_guest_debian(disk_path, hostname, passwords, eth_devs, format=None, apt_proxy_ip=None):

# создаем и запускаем vm
gfs = guestfs.GuestFS()
gfs.add_drive_opts(disk_path, format=format)
gfs.launch()

# находим раздел с /etc. Не очень чистое решение, но для образов, когда все
# на одном разделе работает
for dev, fs_type in gfs.list_filesystems():
if fs_type in 'ext2 ext3 reiserfs3 reiserfs4 xfs jfs btrfs':
# монтирует раздел в корень файловой системы vm
gfs.mount(dev, '/')
# если есть etc - все ок
if gfs.exists('/etc'):
break
gfs.umount(dev)

# обновляем hostname, для правильной работы hostname нужно так-же модифицировать
# /etc/hosts
gfs.write('/etc/hostname', hostname)

# set eth device names for udev
templ = 'SUBSYSTEM=="net", DRIVERS=="?*", ATTR{{address}}=="{hw}", NAME="{name}"'

# записываем настройки сетевых интерфейсов в /etc/network/interfaces

rules_fc = []
interfaces = ["auto loniface lo inet loopback"]
for dev, (hw, ip, sz, gw) in eth_devs.items():
rules_fc.append(templ.format(hw=hw, name=dev))
interfaces.append("auto " + dev)

if ip == 'dhcp':
interfaces.append("iface {0} inet dhcp".format(dev))
else:
interfaces.append("iface {0} inet static".format(dev))
interfaces.append(" address " + ip)
network = int2ip(ip2int(ip) & ip2int(netsz2netmask(sz)))
interfaces.append(" network " + network)
interfaces.append(" netmask " + netsz2netmask(sz))

gfs.write('/etc/udev/rules.d/70-persistent-net.rules', "n".join(rules_fc))
gfs.write('/etc/network/interfaces', "n".join(interfaces))

# обновляем пароли для пользователей

chars = "".join(chr(i) for i in range(ord('a'), ord('z') + 1))
chars += "".join(chr(i) for i in range(ord('A'), ord('Z') + 1))
chars += "".join(chr(i) for i in range(ord('0'), ord('9') + 1))

hashes = {}
for login, passwd in passwords.items():
salt = "".join(random.choice(chars) for _ in range(8))
hashes[login] = crypt.crypt(passwd, "$6$" + salt)

new_shadow = []
need_logins = set(hashes)

for ln in gfs.read_file('/etc/shadow').split('n'):
ln = ln.strip()
if ln != '' and ln[0] != '#':
login = ln.split(':', 1)[0]
if login in hashes:
new_shadow.append("{login}:{hash}:{rest}".format(login=login,
hash=hashes[login],
rest=ln.split(':', 2)[2]))
need_logins.remove(login)
else:
new_shadow.append(ln)

for login in need_logins:
new_shadow.append("{login}:{hash}:{rest}".format(login=login,
hash=hashes[login],
rest="0:0:99999:7:::"))

gfs.write('/etc/shadow', "n".join(new_shadow))

# тут еще модификация /etc/passwd, /etc/hosts, создание домащних папок, etc
# полная версия на github

Тут использован прямой интерфейс libguestfs. Наверное, для указанных задач проще смонтировать образ через guestmount и модифицировать локальные файлы, иногда делая chroot (это, например, позволит использовать локальный passwd для обновления пароля). OpenStack делает это именно так.

Итоги

После добавки кода, управляющего образами дисков, рефакторинга и перенесения конфигов в yaml файл tiny_cloud практически готов к использованию. Можно добавить интеграцию с fabric, мониторинг, кеш пакетов и многое другое, но и так уже вполне годно к использованию. Разве что python API сделать удобное.

Ссылки:
          ru.wikipedia.org/wiki/RAW_%28%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%29
          administratosphere.wordpress.com/2008/05/23/sparse-files-what-why-and-how
          en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29
          en.wikipedia.org/wiki/Qcow#qcow2
          en.wikipedia.org/wiki/VDI_%28file_format%29#Virtual_Disk_Image
          en.wikipedia.org/wiki/VMDK
          www.vmware.com
          www.virtualbox.org
          www.vmware.com/support/ws45/doc/disks_vdiskmanager_eg_ws.html
          people.gnome.org/~markmc/qcow-image-format.html
          libvirt.org/formatdomain.html#elementsDisks
          linux.die.net/man/1/qemu-img
          nbd.sourceforge.net
      

Сопоставление объектов с образцом (pattern matching)

В функциональных языках есть интересная возможность, фактически являющаяся расширением идеи перегрузки функций — сопоставление с образцом. Для этого поддерживается специальный синтаксис шаблонов структур данных, позволяющий проверить что объект имеет определенный тип и/или поля, а также извлечь из него некоторые данные. Пример из haskell (частично взято тут wiki):

Без подсветки синтаксиса

-- f - функция от одного целого параметра
-- возвращающая целое
f :: Int -> Int
f 1 = 0
-- если ей передать 1, то она вернет 0
f _ -> 1
-- если что-либо другое - 1

-- map от чего угодно и пустого списка возвращает пустой список
map _ [] = []
-- рекурсия - map от функции и списка это конкатенация
-- f от первого параметра и map от f и остатка списка
map f (x:xs) = f x : map f xs

-- разбор структуры
-- Foo это или Bar или Baz
data Foo = Bar | Baz {bazNumber::Int, bazName::String}
h :: Foo -> Int
-- Baz - это тип структуры, bazName - это имя поля
h Baz {bazName=name} = length name
h Bar {} = 0

-- f - функция от одного целого параметра
-- возвращающая целое
f :: Int -> Int
f 1 = 0
-- если ей передать 1, то она вернет 0
f _ -> 1
-- если что-либо другое - 1

-- map от чего угодно и пустого списка возвращает пустой список
map _ [] = []
-- рекурсия - map от функции и списка это конкатенация
-- f от первого параметра и map от f и остатка списка
map f (x:xs) = f x : map f xs

-- разбор структуры
-- Foo это или Bar или Baz
data Foo = Bar | Baz {bazNumber::Int, bazName::String}
h :: Foo -> Int
-- Baz - это тип структуры, bazName - это имя поля
h Baz {bazName=name} = length name
h Bar {} = 0

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

Но тем не менее желание сделать что-то подобное для python возникало после каждого ковыряния в функциональщине и после каждой конструкции вида:

Без подсветки синтаксиса

if isinstance(x, Message):
if x.mtype == DATA_READY and x.data is not None:
#some code
pass
elif x.mtype == PROCESS_FINISHED:
#some code
pass
# ....
# .....

if isinstance(x, Message):
if x.mtype == DATA_READY and x.data is not None:
#some code
pass
elif x.mtype == PROCESS_FINISHED:
#some code
pass
# ....
# .....

А тут что-то захотелось посмотреть внимательно на модуль ast (abstract syntax tree) — давно не использовал его, последний раз еще во времена 2.4 — тогда очень жалел, что он не позволяет компилировать измененный ast (кстати делал интересный проект по портированию большого куска кода с PyQt3 на PyQt4 и ast позволил значительно автоматизировать этот перенос).

ast позволяет получить из python кода его результат после синтаксического разбора, но еще до компиляции в байтокод, исследовать его и/или изменять и компилировать новый вариант. Пример ast:

    a = f.b(1)

=>

Assign(
targets=[Name(id='a', ctx=Store())],
value=Call(
func=Attribute(
value=Name(id='f', ctx=Load()),
attr='b',
ctx=Load()),
args=[Num(n=1)],
keywords=[],
starargs=None,
kwargs=None
)
)

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

На этом все про ast. Что хотелось от сопоставления с образцом:

  • Чистый python синтаксис, что-бы никаких новых зарезервированных слов и IDE что-бы не ругались
  • Как-можно меньше кода при использовании
  • Обеспечить сопоставление с константой, типом, проверку атрибутов и вложенную проверку

После некоторого времени размышлений остановился на таком варианте:

Без подсветки синтаксиса

with match(x) as res:
1 >> 2
int >> x * 3
str >> func_str(x)
SomeType(c=V_c, d=V_c) >> on_val(V_c)
SomeType(c=V_c, d=V_d) >> on_val2(x, V_c)

print "res =", res

with match(x) as res:
1 >> 2
int >> x * 3
str >> func_str(x)
SomeType(c=V_c, d=V_c) >> on_val(V_c)
SomeType(c=V_c, d=V_d) >> on_val2(x, V_c)

print "res =", res

Как это должно было-бы работать:

Без подсветки синтаксиса

if x == 1:
res = 2
elif isinstance(x, int):
res = x * 3
elif isinstance(x, str):
res = func_str(x)
elif isinstance(x, SomeType) and
hasattr(x, 'c') and
hasattr(x, 'd') and
x.c == x.d:
res = on_val(x. c)
elif isinstance(x, SomeType) and
hasattr(x, 'c') and
hasattr(x, 'd'):
res = on_val2(x, x.c)
else:
raise ValueError("{0!r} don't match any pattern!".format(x))

if x == 1:
res = 2
elif isinstance(x, int):
res = x * 3
elif isinstance(x, str):
res = func_str(x)
elif isinstance(x, SomeType) and
hasattr(x, 'c') and
hasattr(x, 'd') and
x.c == x.d:
res = on_val(x.c)
elif isinstance(x, SomeType) and
hasattr(x, 'c') and
hasattr(x, 'd'):
res = on_val2(x, x.c)
else:
raise ValueError("{0!r} don't match any pattern!".format(x))

Совсем так, как хотелось, сразу не вышло. Вышло так:

Без подсветки синтаксиса

import python_match

@python_match.mathing
def come_func():
# some code
with python_match.match(x) as res:
1 >> 2
int >> x * 3
str >> func_str(x)
SomeType(c=V_c, d=V_c) >> on_val(V_c)
SomeType(c=V_c, d=V_d) >> on_val2(x, V_c)

print res.val

import python_match

@python_match.mathing
def come_func():
# some code
with python_match.match(x) as res:
1 >> 2
int >> x * 3
str >> func_str(x)
SomeType(c=V_c, d=V_c) >> on_val(V_c)
SomeType(c=V_c, d=V_d) >> on_val2(x, V_c)

print res.val

Из необязательных ограничений — нужно импортировать модуль python_match без переименования. Обернуть все функции, где используется сопоставление с образцом, декоратором 'python_match.mathing'.

Как это работает:

  • декоратор с помощью модуля inspect получает исходный код функции, разбирает его в ast и прогоняет через класс MatchReplacer
  • MatchReplacer наследует ast.NodeTransformer и перегружает метод visit_With, в котором подменяет ноду with на измененную конструкцию со сравнениями. Строка до >> изменяется на сравнение, а в строка после — подменяются переменные.
  • класс Match делает сопоставление объектов с образцом, если использовалось сравнение атрибутов.

Осталось некоторое количество ограничений, которые однако не принципиальные, так что поскольку задача скорее стояла из разряда — «как бы это сделать» я не стал заниматься дальнейшими оптимизациями/улучшениями.

Полный код тут — python_match.py, test_pm.py.

Ссылки:
          ru.wikipedia.org/wiki/haskell
          ru.wikipedia.org/wiki/Erlang
          en.wikipedia.org/wiki/Pattern_matching
          en.wikibooks.org/wiki/Haskell/Pattern_matching
          docs.python.org/library/ast.html
          github.com/koder-ua/python-lectures/blob/master/python_match.py
          github.com/koder-ua/python-lectures/blob/master/test_pm.py
          en.wikipedia.org/wiki/Abstract_syntax_tree

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov

Как это сделано?

Отсутствие перегрузки функций — это то что мне всегда не нравилось в python. Не то что бы без них невозможно было жить, да и виртуальные методы типа __len__ сглаживают проблему, но все-таки. И пусть есть PEAK.rules, но его синтаксис всегда раздражал. Ну вот как можно без боли смотреть на это:

Hightlited/Raw

from peak.rules import abstract, when

@abstract()
def pprint(ob):
"""A pretty-printing generic function"""

@when(pprint, (list,))
def pprint_list(ob):
print "pretty-printing a list"

@when(pprint, (int,))
def pprint_int(ob):
print "pretty-printing an integer"

#......

from peak.rules import abstract, when

@abstract()
def pprint(ob):
"""A pretty-printing generic function"""

@when(pprint, (list,))
def pprint_list(ob):
print "pretty-printing a list"

@when(pprint, (int,))
def pprint_int(ob):
print "pretty-printing an integer"

#......

Во-первых опять нужно придумывать для каждого типа свои имена функций, во-вторых не по-питоновски много лишних нажатий клавиш, даже в С++ это — лишнее: @when(pprint, ( :).

Но как-то ничего принципиально лучше придумать не удавалось. В python 3+ можно будет в конце концов сделать отличную перегрузку методов через метаклассы, но до его массового использования в продакшене пока далековато. И вот недавно, при написании статьи про метаклассы в python 3 и находясь под влияние пересмотра одного видео с последнего pycon-videos, пришла в голову мысль которая оказалась рабочей ( впрочем я бы три раза подумал перед тем как положить такой код в файл, который будет использовать кто-то другой).

Ну собственно угадайте как работает написанное ниже (какая магия зашита в method_overloader.overloadable):

Hightlited/Raw

from method_overloader import overloadable

@overloadable()
class A(object):
def overloaded_func(self, x):
"int"
return "Integer func called {0}".format(x)

def overloaded_func(self, x):
"str"
return "String func called {0!r}".format(x)

def overloaded_func(self, x):
"float"
return "Float func called {0!r}".format(x)

def overloaded_func(self, x):
"list"
return "List func called {0!r}".format(x)

t = A()

print "t.overloaded_func(1) =", t.overloaded_func(1)
print "t.overloaded_func('asas') =", t.overloaded_func("asas")
print "t.overloaded_func(1.1) =", t.overloaded_func(1.1)
print "t.overloaded_func([1, 2, 3]]) =", t.overloaded_func([1, 2, 3])

from method_overloader import overloadable

@overloadable()
class A(object):
def overloaded_func(self, x):
"int"
return "Integer func called {0}".format(x)

def overloaded_func(self, x):
"str"
return "String func called {0!r}".format(x)

def overloaded_func(self, x):
"float"
return "Float func called {0!r}".format(x)

def overloaded_func(self, x):
"list"
return "List func called {0!r}".format(x)

t = A()

print "t.overloaded_func(1) =", t.overloaded_func(1)
print "t.overloaded_func('asas') =", t.overloaded_func("asas")
print "t.overloaded_func(1.1) =", t.overloaded_func(1.1)
print "t.overloaded_func([1, 2, 3]]) =", t.overloaded_func([1, 2, 3])

Запускаем —

    .........$ python tracer.py 
t.overloaded_func(1) = Integer func called 1
t.overloaded_func('asas') = String func called 'asas'
t.overloaded_func(1.1) = Float func called 1.1
t.overloaded_func([1, 2, 3]]) = List func called [1, 2, 3]

Все это на обычном python без подмены механизма импорта, без ковыряния в ast и т.п. Ответы можно на koder.mail@gmail.com.

P.S. Если что — в python2.X метаклассе невозможно узнать что происходило в теле класса, можно только узнать что вышло в итоге, т.е.:

Hightlited/Raw

class M(object):
s = 1
s = 2

class M(object):
s = 1
s = 2

в метакласс класса прийдет в качестве словаря класса {s : 2} и узнать что еще было s = 1 в метаклассе нельзя.

Ссылки:
          pypi.python.org/pypi/PEAK-Rules
          blip.tv/pycon-us-videos-2009-2010-2011
          docs.python.org/library/ast.html

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov

Интерфейсы в python или "Предъявите документы!"

При написании не тривиальных приложений возникает вопрос: над какими библиотеками делать еще один абстрактный слой, а над какими — нет? Какие абстракции делать?

Стоит ли делать прослойку над, например, SQLAlchemy? Это же и так прослойка над SQL и DBAPI. Имеет ли смысл делать уровни абстракций над такими достаточно хорошими и отточенными в смысле интерфейсов библиотеками? Читать

Оператор with

Теория

Оператор with появился в python 2.5, но, не смотря на это, используется до сих пор недостаточно широко. Являясь упрощенной версией анонимных блоков кода with позволяет:

  • исполнить код до начала блока
  • исполнить код по выходу из блока, независимо от того это выход по исключению с помощью return или другим способом
  • обработать исключение, возникшее в блоке.

Синтаксически with выглядит следующим образом:

Hightlited/Raw

with operation:
code

with operation:
code

operation может быть объектом, выражением или конструкцией вида expression as var. Как и много других конструкций он является синтаксическим сахаром для более громоздкого выражения:

Hightlited/Raw

with operation as var:
code

with operation as var:
code

=>

Hightlited/Raw

_obj = operation

# вход в блок
var = _obj.__enter__()

try:
code
except Exception as exc:
# если произошло исключение - передаем его управляющему объекту
if not _obj.__exit__(*sys.exception_info()):
# если он вернул False(None) возбуждаем его
raise
# если True - подавляем исключение
else:
# если не было исключения - передаем None * 3
_obj.__exit__(None, None, None)

_obj = operation

# вход в блок
var = _obj.__enter__()

try:
code
except Exception as exc:
# если произошло исключение - передаем его управляющему объекту
if not _obj.__exit__(*sys.exception_info()):
# если он вернул False(None) возбуждаем его
raise
# если True - подавляем исключение
else:
# если не было исключения - передаем None * 3
_obj.__exit__(None, None, None)

Более подробно с with можно ознакомиться в соответствующем PEP-343. with управляется объектом, называемым менеджером контекста (МК) — _obj в примере выше. Есть два основных способа написания МК — класс с методами __enter__ и __exit__ и генератор:

Hightlited/Raw

import os
from contextlib import contextmanager

# Это только пример.
# Использование такого кода для генерации временных файлов
# небезопасно. Используйте функции 'os.tmpfile'.

class TempoFileCreator(object):
def __init__(self):
self.fname = None
self.fd = None< br />
def __inter__(self):
# вызывается по входу в блок
self.fname = os.tmpnam()
self.fd = open(self.fname, "w+")
return self.fname, self.fd

def __exit__(self, exc_type, exc_val, traceback):
# вызывается по выходу из блока
# если в блоке выброшено исключение, то
# его тип, значение и трейс будут переданы в параметрах

self.fd.close()
os.unlink(self.fname)
self.fd = None
self.fname = None

# здесь написано return None => исключение не будет подавляться

@contextmanager
def tempo_file():
# полностью равноценно классу TempoFileCreator
fname = os.tmpnam()
fd = open(fname, "w+")
try:
yield fname, fd
#сейчас исполняется блок
finally:
# это наш __exit__
fd.close()
os.unlink(fd)

import os
from contextlib import contextmanager

# Это только пример.
# Использование такого кода для генерации временных файлов
# небезопасно. Используйте функции 'os.tmpfile'.

class TempoFileCreator(object):
def __init__(self):
self.fname = None
self.fd = None

def __inter__(self):
# вызывается по входу в блок
self.fname = os.tmpnam()
self.fd = open(self.fname, "w+")
return self.fname, self.fd

def __exit__(self, exc_type, exc_val, traceback):
# вызывается по выходу из блока
# если в блоке выброшено исключение, то
# его тип, значение и трейс будут переданы в параметрах

self.fd.close()
os.unlink(self.fname)
self.fd = None
self.fname = None

# здесь написано return None => исключение не будет подавляться

@contextmanager
def tempo_file():
# полностью равноценно классу TempoFileCreator
fname = os.tmpnam()
fd = open(fname, "w+")
try:
yield fname, fd
#сейчас исполняется блок
finally:
# это наш __exit__
fd.close()
os.unlink(fd)

Использование:

Hightlited/Raw

with tempo_file() as (fname, fd):
# читаем-пишем в файл
# по выходу из блока он будет удален
pass

with tempo_file() as (fname, fd):
# читаем-пишем в файл
# по выходу из блока он будет удален
pass

Ядро python реализует только первый вариант для контекст менеджера, второй реализуется в contextlib.contextmanager.

В том случае если во внутреннем блоке кода есть оператор yield, т.е. мы работаем в генераторе, __exit__ будет вызван по выходу из генератора или по его удалению. Таким образом если ссылку на генератор сохранить, то __exit__ не будет вызван до тех пор, пока ссылка будет существовать:

Hightlited/Raw

@contextmanager
def cmanager():
yield
print "Exit"

def some_func():
with cmanager():
yield 1

it = some_func()
for val in it:
pass
# Exit напечатается здесь

it = some_func()

del it # или по выходу из текущего блока
# Exit напечатается здесь

@contextmanager
def cmanager():
yield
print "Exit"

def some_func():
with cmanager():
yield 1

it = some_func()
for val in it:
pass
# Exit напечатается здесь

it = some_func()

del it # или по выходу из текущего блока
# Exit напечатается здесь

Подводя итоги — with позволяет сэкономить 2-4 строки кода на каждое использование и повышает читаемость программы, меньше отвлекая нас от логики деталями реализации.

Практика

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

  • Открытие/создание объекта по входу в блок — закрытие/удаление по выходу:

Hightlited/Raw

with open('/tmp/tt.txt') as fd:
pass
# здесь файл закрывается
# переменная fd доступна, но файл уже закрыт
#

with open('/tmp/tt.txt') as fd:
pass
# здесь файл закрывается
# переменная fd доступна, но файл уже закрыт
#

Чаще всего в python программах не закрывают файл вручную, обоснованно полагаясь на подсчет ссылок. Блоки with кроме явного указания области, где файл открыт имеют еще одно небольшое преимущество, связанное с особенностями обработки исключений:

Hightlited/Raw

def i_am_not_always_close_files(fname):
fd = open(fname)

i_am_not_always_close_files("/tmp/x.txt")
# в этой точке файл уже закрыт

def i_am_not_always_close_files(fname):
fd = open(fname)

i_am_not_always_close_files("/tmp/x.txt")
# в этой точке файл уже закрыт

Если внутри фцнкции i_am_not_always_close_files будет возбуждено исключение, то файл не закроется до того момента, пока оно не будет обработано:

Hightlited/Raw

import sys

def i_am_not_always_close_files(fname):
fd = open(fname)
raise RuntimeError('')

try:
i_am_not_always_close_files("/tmp/x.txt")
except RuntimeError:
#тут файл еще открыт
traceback = sys.exc_info()[2]

# спуск на один кадр стека глубже
# 'fd' в его локальных переменных
print traceback.tb_next.tb_frame.f_locals['fd']

#

# в этой точке файл уже закрыт

import sys

def i_am_not_always_close_files(fname):
fd = open(fname)
raise RuntimeError('')

try:
i_am_not_always_close_files("/tmp/x.txt")
except RuntimeError:
#тут файл еще открыт
traceback = sys.exc_info()[2]

# спуск на один кадр стека глубже
# 'fd' в его локальных переменных
print traceback.tb_next.tb_frame.f_locals['fd']

#

# в этой точке файл уже закрыт

Дело в том, что пока жив объект-исключение он хранит путь исключения со всеми кадрами стека. При использовании with файл закрывается по выходу блока кода, обрамляемого в with, так что в обработчике исключения файл был бы уже закрыт. Впрочем это обычно не существенное различие.

Еще пример:

Hightlited/Raw

# создадим виртуальную машину
with create_virtual_machine(root_passwd) as vm_ip:
# выполним на ней тестирования скрипта автоматической установки
test_auto_deploy_script(vm_ip, root_passwd)
# по выходу уничтожим vm_ip

# создадим виртуальную машину
with create_virtual_machine(root_passwd) as vm_ip:
# выполним на ней тестирования скрипта автоматической установки
test_auto_deploy_script(vm_ip, root_passwd)
# по выходу уничтожим vm_ip

  • Захват/освобождение объекта Эту семантику поддерживают все стандартные объекты синхронизации

Hightlited/Raw

import threading
lock = Threading.Lock()

with lock:
# блокровка захваченна
pass
# блокировка отпущенна


import threading
lock = Threading.Lock()

with lock:
# блокровка захваченна
pass
# блокировка отпущенна

  • Временное изменение настроек (примеры из документации python)

Hightlited/Raw

import warnings
from decimal import localcontext

with warnings.catch_warnings():
warnings.simplefilter("ignore")
# в этом участке кода все предепреждения игнорируются

with localcontext() as ctx:
ctx.prec = 42 # расчеты с типом Decimal выполняются с
# заоблачной точностью
s = calculate_something()

import warnings
from decimal import localcontext

with warnings.catch_warnings():
warnings.simplefilter("ignore")
# в этом участке кода все предепреждения игнорируются

with localcontext() as ctx:
ctx.prec = 42 # расчеты с типом Decimal выполняются с
# заоблачной точностью
s = calculate_something()

  • Смена текущей директории (пример использования библиотеки fabric)

Hightlited/Raw

from fabric.context_managers import lcd

os.chdir('/opt')
print os.getcwd() # => /opt

with lcd('/tmp'):
print os.getcwd() # => /tmp

print os.getcwd() # => /opt

from fabric.context_managers import lcd

os.chdir('/opt')
print os.getcwd() # => /opt

with lcd('/tmp'):
print os.getcwd() # => /tmp

print os.getcwd() # => /opt

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

  • Подмена/восстановление объекта (временный monkey patching, пример использования библиотеки mock)

Hightlited/Raw

import mock

my_mock = mock.MagicMock()
with mock.patch('__builtin__.open', my_mock):
# open подменена на mock.MagicMock
with open('foo') as h:
pass

import mock

my_mock = mock.MagicMock()
with mock.patch('__builtin__.open', my_mock):
# open подменена на mock.MagicMock
with open('foo') as h:
pass

  • Транзакции баз данных….

Менеджер транзакций для sqlalchemy

Hightlited/Raw

from config import DB_URI
from db_session import get_session

class DBWrapper(object):

def __init__(self):
self.session = None

def __enter__(self):
self.session = get_session(DB_URI)

def __exit__(self, exc, *args):
# при выходе из 'with':
if exc is None:
# если все прошло успешно коммитим
# транзакцию и закрываем курсор
self.session.commit()

# если было исключение - откатываем
self.session.close()

# тут методы, скрывающие работу с базой

with DBWrapper() as dbw: # открываем транзакцию
dbw.get_some_data()
dbw.update_some_data("...")

from config import DB_URI
from db_session import get_session

class DBWrapper(object):

def __init__(self):
self.session = None

def __enter__(self):
self.session = get_session(DB_URI)

def __exit__(self, exc, *args):
# при выходе из 'with':
if exc is None:
# если все прошло успешно коммитим
# транзакцию и закрываем курсор
self.session.commit()

# если было исключение - откатываем
self.session.close()

# тут методы, скрывающие работу с базой

with DBWrapper() as dbw: # открываем транзакцию
dbw.get_some_data()
dbw.update_some_data("...")

  • ….и не только баз данных

Hightlited/Raw

from threading import local
import subprocess

# обобщенная транзакция - выполняет набор обратных действий
# при возникновении в блоке 'with' не обработанного исключения

class Transaction(object):
def __init__(self, parent):
self.rollback_cmds = []
self.set_parent(parent)

def set_parent(self, parent):
# родительская транзакция
# если откатывается родительская транзакция, то она автоматом
# откатывает и дочерние, даже если они было уже успешно закрыты
# если откатывается дочерняя, то родительская может продолжить
# исполнение, если код выше по стеку обработает исключение

if parent is not None:
self.parent_add = parent.add
else:
self.parent_add = lambda *cmd : None

def __enter__(self):
return self

def __exit__(self, exc, *dt):
if exc is None:
self.commit()
else:
self.rollback()

def add(self, cmd):
self.parent_add(cmd)
self.transaction.append(cmd)

def commit(self):
self.transaction = []

def rollback(self):
for cmd in reversed(self.transaction):
if isinstance(cmd, basestring):
subprocess.check_call(cmd, shell=True)
else:
cmd[0](*cmd[1:])


class AutoInheritedTransaction(object):
# словарь, id потока => [список вложенных транзакций]
# позволяет автоматически находить родительскую транзакцию
# в том случае, если для каждого потока может быть не более
# одной цепи вложенных транзакций

transactions = local()

def __init__(self):
super(AutoInheritedTransaction, self).__init__(self.current())
self.register()

def register(self):
self.transaction.list = getattr(self.transaction, 'list') + [self]

@classmethod
def current(cls):
return getattr(self.transaction, 'list', [None])[-1]

used_loop_devs = []

with AutoInheritedTransaction() as tr:
# создаем loop устройство
loop_name = subprocess.check_output("losetup -f --show /tmp/fs_image")
# вызов для его удаления
tr.add("losetup -d " + loop_name)

# записываем новое устройство в массив
used_loop_devs.append(loop_name)
tr.add(lambda : used_loop_devs.remove(
used_loop_devs.index(
loop_name)))

# монтируем его
subprocess.check_output("mount {0} /mnt/some_dir")
tr.add("umount /mnt/some_dir")

some_code

from threading import local
import subprocess

# обобщенная транзакция - выполняет набор обратных действий
# при возникновении в блоке 'with' не обработанного исключения

class Transaction(object):
def __init__(self, parent):
self.rollback_cmds = []
self.set_parent(parent)

def set_parent(self, parent):
# родительская транзакция
# если откатывается родительская транзакция, то она автоматом
# откатывает и дочерние, даже если они было уже успешно закрыты
# если откатывается дочерняя, то родительская может продолжить
# исполнение, если код выше по стеку обработает исключение

if parent is not None:
self.parent_add = parent.add
else:
self.parent_add = lambda *cmd : None

def __enter__(self):
return self

def __exit__(self, exc, *dt):
if exc is None:
self.commit()
else:
self.rollback()

def add(self, cmd):
self.parent_add(cmd)
self.transaction.append(cmd)

def commit(self):
self.transaction = []

def rollback(self):
for cmd in reversed(self.transaction):
if isinstance(cmd, basestring):
subprocess.check_call(cmd, shell=True)
else:
cmd[0](*cmd[1:])


class AutoInheritedTransaction(object):
# словарь, id потока => [список вложенных транзакций]
# позволяет автоматически находить родительскую транзакцию
# в том случае, если для каждого потока может быть не более
# одной цепи вложенных транзакций

transactions = local()

def __init__(self):
super(AutoInheritedTransaction, self).__init__(self.current())
self.register()

def register(self):
self.transaction.list = getattr(self.transaction, 'list') + [self]

@classmethod
def current(cls):
return getattr(self.transaction, 'list', [None])[-1]

used_loop_devs = []

with AutoInheritedTransaction() as tr:
# создаем loop устройство
loop_name = subprocess.check_output("losetup -f --show /tmp/fs_image")
# вызов для его удаления
tr.add("losetup -d " + loop_name)

# записываем новое устройство в массив
used_loop_devs.append(loop_name)
tr.add(lambda : used_loop_devs.remove(
used_loop_devs.index(
loop_name)))

# монтируем его
subprocess.check_output("mount {0} /mnt/some_dir")
tr.add("umount /mnt/some_dir")

some_code

Эта модель программирования позволяет группировать в одной точке код прямой и обратной операции и избавляет от вложенных try/finally. Также with предоставляет естественный интерфейс для STM. cpython-withatomic — один из вариантов STM для руthon с поддержкой with.

  • Подавление исключений

Hightlited/Raw

def supress(*ex_types):
# стоит добавить логирования подавляемого исключения
try:
yield
except Exception as x:
if not isinstance(x, ex_types):
raise

with supress(OSError):
os.unlink("some_file")

def supress(*ex_types):
# стоит добавить логирования подавляемого исключения
try:
yield
except Exception as x:
if not isinstance(x, ex_types):
raise

with supress(OSError):
os.unlink("some_file")

  • Генерация XML/HTML других структурированных языков.

Hightlited/Raw

from xmlbuilder import XMLBuilder

# новый xml документ

x = XMLBuilder('root')
x.some_tag
x.some_tag_with_data('text', a='12')

# вложенные теги
with x.some_tree(a='1'):
with x.data:
x.mmm
x.node(val='11')

print str(x) # <= string object


from xmlbuilder import XMLBuilder

# новый xml документ

x = XMLBuilder('root')
x.some_tag
x.some_tag_with_data('text', a='12')

# вложенные теги
with x.some_tree(a='1'):
with x.data:
x.mmm
x.node(val='11')

print str(x) # <= string object

Получим в итоге:

Hightlited/Raw



/>
a="12">text
a="1">

/>
val="11" />






text






Код библиотеки находится на xmlbuilder.

  • Трассировка блока в логере (установка sys.settrace)

Hightlited/Raw

import sys
import contextlib

def on_event(fr, evt, data):
print fr, evt, data
return on_event

@contextlib.contextmanager
def trace_me():

prev_trace = sys.gettrace()
sys.settrace(on_event)
try:
yield
finally:
sys.settrace(prev_trace)
print "after finally"


with trace_me():
print "in with"
x = 1
y = 2
print "before gettrace"
sys.gettrace()
print "after gettrace"

import sys
import contextlib

def on_event(fr, evt, data):
print fr, evt, data
return on_event

@contextlib.contextmanager
def trace_me():

prev_trace = sys.gettrace()
sys.settrace(on_event)
try:
yield
finally:
sys.settrace(prev_trace)
print "after finally"


with trace_me():
print "in with"
x = 1
y = 2
print "before gettrace"
sys.gettrace()
print "after gettrace"

Этот
код напечатает:

    in with
before gettrace
after gettrace
call None
line None
line None
line None
call None
line None
after finally

Для лучшего понимания трассировки питона — python-aware-python.

Ссылки:
          www.python.org/dev/peps/pep-0343
          docs.python.org/reference/compound_stmts.html#the-with-statement
          github.com/koder-ua/megarepo/tree/master/xmlbuilder/xmlbuilder
          www.voidspace.org.uk/python/mock/compare.html#mocking-a-context-manager
          en.wikipedia.org/wiki/Monkey_patch
          www.sqlalchemy.org
          fabfile.org
          en.wikipedia.org/wiki/Software_Transaction_Memory
          bitbucket.org/arigo/cpython-withatomic
          blip.tv/pycon-us-videos-2009-2010-2011/pycon-2011-python-aware-python-4896752

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov