December 24, 2011

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


Компоненты

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

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

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

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

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

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.

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

Без подсветки синтаксиса
# 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
Без подсветки синтаксиса
<network>
  <name>default</name>
  <uuid>c598e36f-31fd-672e-09e3-2cbe061cd606</uuid>
  <forward mode='nat'/>
  <bridge name='virbr0' stp='on' delay='0' />
  <ip address='192.168.122.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.122.40' end='192.168.122.254' />
    </dhcp>
  </ip>
</network>

Тот же самый результат можно получить и из 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']

Еще один важный компонент сети - 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

Конфигурационные файлы сетей хранятся в /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

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

Без подсветки синтаксиса
<interface type="network">
    <source network="default" /> <!-- эта девушка! -->
    <forward mode="nat" />
    <target dev="vnet7" />
    <mac address="{mac}" />
</interface>

Подключила 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 * *

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

  • Получить адрес с помощью arp сканирования. Фактически в сеть посылается набор запросов вида "у кого есть такой ip addres". Этот запрос используется что-бы определить mac адрес машины с заданным ip, перед формирование ethernet фрейма. Способ наиболее универсальный, поскольку именно так работает tcp/ip стек - независимо от того как система получила этот ip адрес она должна ответить на arp запрос или будет вообще недоступна для ip протокола.
Без подсветки синтаксиса
# arp-scan -I virbr0 -l # использовать маску сети из описания интерфейса
# arp-scan -I virbr0 192.168.122.0/24 # то же самое, только руками

Interface: virbr0, datalink type: EN10MB (Ethernet)
Starting arp-scan 1.8.1 with 256 hosts (http://www.nta-monitor.com/tools/arp-scan/)
192.168.122.99  00:44:01:61:78:01       (Unknown)

1 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.8.1: 256 hosts scanned in 1.374 seconds (186.32 hosts/sec). 1 responded

Собственно 192.168.122.99 это и есть наш ip адрес (у вас он, естественно, может быть другим).

Без подсветки синтаксиса
$ ip addr show virbr0
5: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN 
    link/ether ca:8e:0a:8b:36:14 brd ff:ff:ff:ff:ff:ff
    inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0

Сделаем то же самое на python с помощью scapy. Scapy вообще очень мощная библиотека для сетевых манипуляций, см. например scapy-traceroute.

Без подсветки синтаксиса
# -*- coding:utf8 -*-
# для разбора/преобразования ip адресов

import ipaddr

from scapy.all import srp, Ether, ARP, conf

# scapy в основном ориентированна на интерактивное использование 
# и по умолчанию выводит много информации в sys.stdout
# запретим это
conf.verb = 0

def arp_scan(ip_addr, netmask, iface):

    netsize = ipaddr.IPNetwork("{0}/{1}".format(ip_addr, netmask)).prefixlen 

    ans, unans = srp(
                Ether(dst="ff:ff:ff:ff:ff:ff") / \
                    ARP(pdst="{0}/{1}".format(ip_addr, netsize)),
                     timeout=0.1, iface=iface)

    for request, responce in ans:
        yield responce.payload.fields['hwsrc'], responce.payload.fields['psrc']

for hw_addr, ipaddr in arp_scan('192.168.122.2', '255.255.255.0', 'virbr0'):
    print "{0} => {1}".format(hw_addr, ipaddr)

Теперь осталось объединить все это вместе - нам нужна функция, которая по имени виртуальной машины выдаст все ее ip адреса:

Показать код
Без подсветки синтаксиса
# python test.py qemu:///system debian
WARNING: No route found for IPv6 destination :: (no default route?)
default virbr0 00:44:01:61:78:01 192.168.122.99
# ssh root@192.168.122.99
blah-blah-blah

root@debian-amd64:~#

Отлично. Имея доступ по ssh мы уже может делать с vm что нам нужно. Добавим в tiny_cloud.py автоматический логин в vm по имени. Для этого воспользуемся утилитой expect

Без подсветки синтаксиса
# apt-get install expect
Без подсветки синтаксиса
expect_login = """expect -c'
spawn ssh {0}@{1};
while {{1}} {{
  expect {{
    eof                          {{ break }};
    "The authenticity of host"   {{ send "yes\\n" }};
    "password:"                  {{ send "{2}\\n"; interact; break;}};
  }};
}};
wait'
"""

found = False
for ipaddr in get_all_ips(conn, name):

    # проверяем, что ssh порт открыт на этом ip
    s = socket.socket()
    s.settimeout(1)

    try:
        s.connect((ipaddr, 22))
    except socket.error:
        raise

    # запускаем ssh и ждем пока пользователь завершит сеанс

    os.system(expect_login.format(user, ipaddr, passwd))
    found = True
    break

if not found:
    raise RuntimeError("No one interface of {0} accepts ssh connection".format(name))

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

Возможности libvirt по управлению сетями очень обширны и хорошо описаны в ее документации. В частности libvirt позволяет создавать сети с прямым подключением к lan, частные сети и "спрятанные" за NAT, а так-же умеет проброс портов. Для основных задач этого вполне достаточно.

В качестве примера сделаем сеть из 3х виртуалок, две из которых будут в частной lan сети local_net, а третья одним интерфейсом в default, а вторым в local_net.

Для этого нужно немного подправить образ. Логинимся в vm и удаляем файлы /etc/udev/rules.d/70-persistent-net.rules и /etc/udev/rules.d/010_netinterfaces.rules. В противном случае debian будет переименовывать сетевые интерфейсы при смене аппаратного адреса. Выполняем poweroff, отлогиниваемся и ждем пока vm остановится - python tiny_cloud.py list перестанет ее показывать и сделаем три копии образа vm - deb1.qcow2, deb2.qcow2, deb3.qcow2.

Сделаем новую сеть, для этого определим ее в xml формате для сети

Показать код

Зарегистрируем ее в libvirt и активируем:

Без подсветки синтаксиса
# virsh net-define local_net.xml
# virsh net-list --all
Name                 State      Autostart
-----------------------------------------
default              active     yes       
local_net            inactive   no
# virsh net-start local_net
# virsh net-list
Name                 State      Autostart
-----------------------------------------
default              active     yes       
local_net            active     no

Проверяем, что все запустилось - в системе должно быть два dnsmasq сервера и должен добавить virbr2 мост, но правила для NAT не должны появиться в таблице filter. Обратите внимание - local_net не будет запускаться автоматически при старте компьютера, ее нужно активировать после каждого перезапуска перед стартом соответствующих vm или выполнить

Без подсветки синтаксиса
# virsh net-autostart local_net

Модифицируем шаблоны для vm - делаем две копии vm_templ.xml. В первой копии меняем сеть назначения для интерфейса на local_net, во второй добавляем еще один интерфейс, направленный на local_net. Так-же уменьшим объем оперативной памяти до 256М.

Показать код

Запускаем систему:

Без подсветки синтаксиса
# python tiny_cloud.py start --name deb1 --template vm_2if_templ.xml
# python tiny_cloud.py start --name deb2 --template vm_local_net_templ.xml 
# python tiny_cloud.py start --name deb3 --template vm_local_net_templ.xml 
# python tiny_cloud.py list

5 deb3
4 deb2
3 deb1

Немного модифицируем tiny_cloud.py, что-бы он показывал ip адреса виртуалок.

Без подсветки синтаксиса
# python tiny_cloud.py list
5 deb3 192.168.152.63
4 deb2 192.168.152.62
3 deb1 192.168.122.99,192.168.152.64

Логинимся в deb1, смотрим на интерфейсы, проверяем, что 8.8.8.8(google DNS), deb2 и deb3 пингуются. Логинимся в deb2, проверяем что deb1 и deb3 пингуются, а 8.8.8.8 - нет.

Код tiny_cloud.py со всеми сегодняшними добавками:

Показать код

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

Ссылки:
          www.linuxfoundation.org/collaborate/workgroups/networking/bridge
          backreference.org/2010/03/26/tuntap-interface-tutorial
          en.wikipedia.org/wiki/TUN/TAP
          kharkovpromenade.com.ua/?id=9
          wiki.libvirt.org/page/Networking
          libvirt.org/formatnetwork.html
          en.wikipedia.org/wiki/Expect
          en.wikipedia.org/wiki/Iptables
          www.secdev.org/projects/scapy/doc
          www.secdev.org/projects/scapy/doc/usage.html#tcp-traceroute-2
          en.wikipedia.org/wiki/Network_address_translation

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

No comments: