пятница, 4 мая 2012 г.

Обуздание неистового DHCP в Debian 6


Случаются ситуации, когда интрефейс, конфигурируемый по DHCP, не единственный в системе - и настройки маршрутизации и DNS, получаемые автоматически, не должны перезаписывать статических настроек с других интерфейсов. Ниже приводятся два способа, как можно побороться с этой проблемой в Debian 6:

Первый способ. Требует установки дополнительных пакетов.
Для того, чтобы маршрут по умолчанию не перезаписывал значение, сконфигурированное на другом интерфейсе, ставим пакет ifmetric - и в файле /etc/network/interfaces указываем для наших интерфейсов числовые значения metric. Чем значение ниже, тем выше приоритет маршрута через данный интерфейс.
Для того, чтобы DNS и доменное имя, полученные по DHCP, не перезаписывали насмерть файл /etc/resolv.conf, ставим пакет resolvconf - и в файле /etc/network/interfaces в разделе статического интерфейса добавляем строки:
  dns-nameservers A.A.A.A B.B.B.B
 dns-search mydom1.ru mydom2.ru

Второй способ. Правим файл /etc/dhcp/dhclient.conf
В нем есть длинная строчка, начинающаяся со слова request.
Удалив из нее слова: routers, domain-name, domain-name-servers, domain-search, host-name и rfc3442-classless-static-routes, мы как раз запретим запрос этих параметров по DHCP. Способ безусловно проще, но менее гибкий - в случае проблем со статическим интерфейсом мы остаемся вообще без DNS и без маршрута по умолчанию.

А если не лень чуть-чуть попрограммировать, можно изменять поведение DHCP-интерфейса с помощью скриптов, указываемых в соответствующем разделе /etc/network/interfaces в параметрах up и down. Например, нижеприводимый скрипт применяет маршрут, отдаваемый DHCP сервером как маршрут по умолчанию, только к определенному списку сетей:

#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin
mynet="10.0.0.0/8 172.16.1.0/24"
case "$MODE" in
start)
gw=`ip route show dev $IFACE | 
  awk '{if ($1 == "default") {print $3; exit(0)}}'`
if [ -n "$gw" ]
then
ip route delete default dev $IFACE
for n in $mynet
do
ip route add $n via $gw dev $IFACE
done
fi
;;
stop)
for n in $mynet
do
ip route delete $n dev $IFACE
done
;;
esac

суббота, 28 апреля 2012 г.

Хост VirtualBox на сервере CentOS без GUI



Т.к. репозитории CentOS не содержат пакетов VirtualBox, то RPM для RHEL5 берется с https://www.virtualbox.org/wiki/Linux_Downloads - каких-то невероятных зависимостей он не требует, только для установки модулей ядра необходимо иметь kernel-devel.
Ниже описывается работа с виртуальными машинами с помощью CLI утилиты VBoxManage. Подробная справка по ее опциям (на английском) имеется здесь: https://www.virtualbox.org/manual/ch08.html
Смотрим допустимые типы машин:
$ VBoxManage list ostypes
Создаем и регистрируем машину для solaris:
$ VBoxManage createvm --name sol11 --ostype OpenSolaris_64 --register --basefolder $HOME/vbox-sol11/
(последняя опция необязательна, но мне кажется, лучше всегда явно задавать, где будут храниться наши машины).
Какие ресурсы выделены машине по умолчанию, можно увидеть командой:
$ VBoxManage list -l vms
Наверняка нам захочется что-нибудь поменять. Придаем машине 2 процессора, 1 Гб памяти, системные часы, идущие по Гринвичу, и вторую сетевую карту типа bridge (первая типа nat создается автоматически):
$ VBoxManage modifyvm sol11 --memory 1024 --cpus 2 --rtcuseutc on --nic2 bridged --bridgeadapter2 eth1
(последняя опция указывает сетевой интерфейс хост-машины, к которому прицепится виртуальный интерфейс).
Если машин много, лучше смотреть настройки конкретной машины командой:
$ VBoxManage showvminfo sol11
Дисковый контроллер (в нашем случае - SCSI) добавляется командой:
$ VBoxManage storagectl sol11 --name scsi0 --add scsi
Создаем жесткий диск на 10 Гб (тип образа по умолчанию - vdi):
$ VBoxManage createhd --filename $HOME/vbox-sol11/sd0 --size 10240
и прицепляем его на контроллер:
$ VBoxManage storageattach sol11 --storagectl scsi0 --medium $HOME/vbox-sol11/sd0.vdi --port 1 --type hdd
Потом прицепляем образ загрузочного CD-ROM:
$ VBoxManage storagectl sol11 --name ide0 --add ide
(IDE контроллер пришлось создать, потому что диск типа dvddrive почему-то отказался присоединяться к SCSI)
$ VBoxManage storageattach sol11 --storagectl ide0 --medium $HOME/tmp/oi-dev-151a-text-x86.iso --device 0 --port 0 --type dvddrive
На время инсталляции придадим машине RDP-консоль:
$ VBoxManage modifyvm sol11 --vrde on --vrdeport 3389 --vrdeaddress 10.10.10.10
(две последних опции необязательны)
Для того, чтобы работал VRDP возможно придется установить Extension Pack - скачивается он там же, откуда мы брали rpm, и ставится командой:
$ VBoxManage extpack install
Теперь машину можно запускать:
$ VBoxManage startvm sol11 --type headless
Остановка машины:
$ VBoxManage controlvm sol11 poweroff
(еще возможные действия: acpipowerbutton, acpisleepbutton, pause, reset, resume, savestate)
Отцепить ненужный более cdrom можно командой:
$ VBoxManage storageattach sol11 --storagectl ide0 --port 0 --device 0 --medium none
(вместо none можно сделать emptydrive - такая операция допустима на запущенной машине).
Если понадобится проброс ssh c NAT-интерфейса на порт хост-машины (в данном случае 127.0.0.1:2222), то он делается так:

$ VBoxManage modifyvm sol11 --natpf1 sshd,tcp,127.0.0.1,2222,,22

пятница, 9 сентября 2011 г.

Радиус-обманка для тестирования ISG


При тестировании одной биллинговой системы (какой - не скажу - не хочу рекламировать откровенно недоделанный продукт) долго не могли определить - кто виноват в запуске дублирующихся сессий ISG - биллинг или Cisco. Вот и возникла идея создать некую "дурилку" - радиус-сервер, отдающий фиксированные ответы, выглядящие с точки зрения ISG как правильные. Создавалось это чудовище за полчаса из свежеустановленного freeradius-1.1.3 (платформа - CentOS 5.6).
Для начала добавляем нашу циску и локалхост (для упрощения тестирования) в /etc/raddb/clients.conf:



client 10.1.60.4 {
secret = mysecr
shortname = tstnas
nastype = cisco
}
client 127.0.0.1 {
        secret = mysecr
        shortname = localhost
        nastype = other
}

А сами ответы помещаем в /etc/raddb/users:

# сперва - пользователи
usr1145 Password == "d038bec6", Service-Type == Framed-User
        Session-Timeout = 86400,
        Framed-Protocol = PPP,
        Framed-IP-Address = 10.1.70.101,
        Framed-IP-Netmask = 255.255.255.255,
        Acct-Interim-Interval = 300,
        Cisco-AVPair = "accounting-list=BILL",
        Filter-Id = "Consum.out",
        Cisco-Account-Info = "ALOCAL",
        Cisco-Account-Info += "NLOCAL",
        Cisco-Account-Info += "AWORLDIN",
        Cisco-Account-Info += "NWORLDIN",
        Cisco-Account-Info += "AWORLDOUT",
        Cisco-Account-Info += "NWORLDOUT"


usr1146 Password == "f577acec", Service-Type == Framed-User
        Session-Timeout = 86400,
        Framed-Protocol = PPP,
        Framed-IP-Address = 10.1.70.102,
        Framed-IP-Netmask = 255.255.255.255,
        Acct-Interim-Interval = 300,
        Cisco-AVPair = "accounting-list=BILL",
        Filter-Id = "Consum.out",
        Cisco-Account-Info = "ALOCAL",
        Cisco-Account-Info += "NLOCAL",
        Cisco-Account-Info += "AWORLDIN",
        Cisco-Account-Info += "NWORLDIN",
        Cisco-Account-Info += "AWORLDOUT",
        Cisco-Account-Info += "NWORLDOUT"  


# потом - реавторизация препейд-сервиса
usr1145 Password == "cisco", Service-Type == Framed-User, Cisco-Service-Info == "NWORLDIN"
        Cisco-Control-Info = "QV1234567895",
        Idle-Timeout = 90


usr1146 Password == "cisco", Service-Type == Framed-User, Cisco-Service-Info == "NWORLDIN"
        Cisco-Control-Info = "QV1234567896",
        Idle-Timeout = 90


# и наконец - сами сервисы
WORLDIN Password == "cisco", Service-Type == Dialout-Framed-User
        Cisco-AVPair = "ip:traffic-class=output access-group name world-out priority 40",
        Cisco-AVPair += "ip:traffic-class=in default drop",
        Cisco-AVPair += "ip:traffic-class=out default drop",
        Cisco-Service-Info = "IALL-INET-IN",
        Cisco-Service-Info += "MC",
        Cisco-Service-Info += "TP",
        Cisco-AVPair += "prepaid-config=BILL",
        Acct-Interim-Interval = 300


WORLDOUT Password == "cisco", Service-Type == Dialout-Framed-User
        Cisco-AVPair = "ip:traffic-class=input access-group name world-in priority 30",
        Cisco-AVPair += "accounting-list=BILL",
        Cisco-AVPair += "ip:traffic-class=in default drop",
        Cisco-AVPair += "ip:traffic-class=out default drop",
        Cisco-Service-Info = "IALL-INET-OUT",
        Cisco-Service-Info += "MC",
        Cisco-Service-Info += "TP",
        Acct-Interim-Interval = 1800


LOCAL   Password == "cisco", Service-Type == Dialout-Framed-User
        Cisco-AVPair = "ip:traffic-class=input access-group name local-in priority 20",
        Cisco-AVPair += "ip:traffic-class=output access-group name local-out priority 20",
        Cisco-AVPair += "accounting-list=BILL",
        Cisco-AVPair += "ip:traffic-class=in default drop",
        Cisco-AVPair += "ip:traffic-class=out default drop",
        Cisco-Service-Info = "ILOCAL-NET",
        Cisco-Service-Info += "MC",
        Cisco-Service-Info += "TP",
        Acct-Interim-Interval = 1800

И теперь можно, запустив на нашем импровизированном радиус-сервере:
cos56# tcpdump -i eth0 -n -vvvv -s 1204 port 1812 or port 1813
или на циске:
c7200# terminal monitor
c7200# debug radius auth
c7200# debug radius acc
(а еще лучше - для полноты картины - и то, и другое) наблюдать за поднимающимися сессиями. Думаю, тот, кто дочитал до этого места, не нуждается в рассказе о том, как запускать pppd (8).

вторник, 26 апреля 2011 г.

Нелегкая борьба с multipath


Имеется - блейд-сервер IBM HS22, единственным диском для которого является том на хранилище EMC CLARIION. Задача: настроить multipath-доступ к этому тому (по умолчанию, имея два оптических свича и два порта на каждом, мы видим этот том в виде 4 дисков /dev/sd[a-d]).
В CentOS 5.5 вроде имеется паспортное средство установки системы с multipath - надо при инсталяции ввести командную строку ядра linux mpath - но этот путь, мягко выражаясь, не совсем работает - т.е. система прекрасно устанавливается  на устройство /dev/mapper/mpath0, но грузиться с него отказывается - не может найти корневой раздел. 
В debian 6.0 дела чуть лучше - действуем так:
  1.  ставим систему, как обычно (я ставил на /dev/sda, но, вероятно, можно с тем же успехом ставить на любой другой диск).
  2. добавляем пакеты для multipath и пересобираем initrd:
    # apt-get install multipath-tools-boot multipath-tools firmware-qlogic
    # update-initramfs -u
  3. перегружаемся - и ищем имя multipath-устройств:
    $ ls /dev/mapper
    3600601600a02a004b0085add64e011
    3600601600a02a004b0085add64e011-part1
    3600601600a02a004b0085add64e011-part2
    control
    Имена, заканчивающиеся на part[12] и есть наши разделы. Прописываем их в /etc/fstab и в  /boot/grub/grub.cfg (там, где root=UUID=...) - вероятно, для второго действия есть более элегантный способ, но я плохо разбирась в grub2.

    После этого перегружаемся - и все должно работать. Состояние дискового массива можно посмотреть командой (и попроверять работоспособность, по очереди отключая питание на оптических свичах):

    # multipath -ll
     
 В итоге - мораль - если нужен беспроблемный мультипас из коробки, лучше ставить Vmware ESXI - тоже своего рода линукс...

    среда, 9 февраля 2011 г.

    Как не переходить на зимнее время на серверах


    Наконец-то случилось то, на что все разумные люди уже много лет робко надеялись - клоунада с переводом стрелок отменена! Осталось понять, как мы сообщим об этом радостном событии серверам. Итак, задача: 30 октября 2011 навсегда остаться в московском летнем времени. Сервера у меня под CentOS и FreeBSD, потому все последующие упражнения проверялись именно для этих систем.
    До сегодняшнего дня я считал, что летнее московское время соответствует времени GMT+4. Ничего подобного! Простые опыты показали, что все как раз наоборот - GMT-4. Убедиться в этом легко с помощью команды zdump:
    $ zdump Europe/Moscow
    Europe/Moscow  Wed Feb  9 09:48:59 2011 MSK
    $ zdump Etc/GMT-3
    Etc/GMT-3  Wed Feb  9 09:49:12 2011 GMT-3
    $ zdump Etc/GMT+3
    Etc/GMT+3  Wed Feb  9 03:49:15 2011 GMT+3
    (проверяется -3, а не -4 потому, что у нас с вами пока зимнее время).
    Дальше все достаточно просто (одинаково и для Linux и для FreeBSD):
    # ln -sf /usr/share/zoneinfo/Etc/GMT-4 /etc/localtime
    (если директории /usr/share/zoneinfo и /etc находятся на разных устройствах, то безопаснее будет использовать не ln, а cp).
    В CentOS можно еще поправить параметр ZONE в файле /etc/sysconfig/clock, но - судя по коменту в том же файле, это значение на реальную работу системы не влияет.
    Потом на каком-нибудь сервачке, который не жалко, можно заранее поставить время без пяти три 30 октября - и убедиться, что с переходом через 3 часа никакого зимнего времени не наступит.

    пятница, 28 января 2011 г.

    Изменение поведения транзакций в SQLite 3

    Транзакция в SQLite может быть явно начата SQL-командой BEGIN TRANSACTION или неявно начинаться вместе с подключением к базе данных (второе поведение бывает, если подключение произведено не в режиме auto_commit. CLI клиент sqlite3 всегда работает в режиме auto_commit, библиотеки sqlite3 в различных языках программирования могут вести себя по-разному - напр. в питоновском модуле sqlite3 (он же pysqlite2.dbapi2 в python 2.4) auto_commit по умолчанию выключен и может быть включен необязательным аргументом isolation_level метода connect(), установленным в None.
    Тут осмелюсь заметить, что - если многопользовательский доступ имеет для вас значение - я бы предложил так и делать - ибо при запуске транзакций явно SQL-командами  BEGIN TRANSACTION они ведут себя так, как описано ниже, а при использовании же других значений isolation_level, кроме None - т.е. при неявном старте транзакций - их поведение показалось мне весьма своеобразным. Судя по всему, неявная транзакция запускается первой командой изменения данных, а при таком поведении разница между DEFERRED и IMMEDIATE (подробнее см. ниже) делается не наблюдаемой. Остается два вида транзакций - EXCLUSIVE и все остальные.
    Если же мы подключились в режиме auto_commit и команда  BEGIN TRANSACTION явно не запускалась, то каждая SQL-команда изменения схемы данных или самих данных будет запускаться отдельной транзакцией без возможности отката (в чем собственно и состоит auto_commit). Такое поведение проще для понимания, но для большого количества запросов не вполне рационально (напр. у меня 30 млн. команд INSERT в таблицу из 8 числовых полей выполнялись 13 минут без auto_commit (в одной транзакции), а с auto_commit дождаться окончания процесса так и не удалось - методом экспликации можно предположить, что процесс длился бы чуть больше месяца).
    По умолчанию транзакция запускается в режиме DEFERRED (т.е. отложенный). Его отличие от возможных параметров IMMEDIATE (т.е. непостредственный) и EXCLUSIVE (это слово уже кажется переводить нет нужды) обсуждается ниже на примерах.
    Дано: база данных /tmp/test.db с таблицей следующей схемы:
    CREATE TABLE t1 (a integer primary key, b text);
    Создавать несколько таблиц для нашего тестирования бесполезно т.к. в SQLite 3 транзакция все равно блокирует всю базу (как это ни печально).
    Итак, поключаемся к базе двумя CLI клиентами (синий и красный) и начинаем тесты.
    Сперва DEFERRED:
    sqlite> begin transaction;


    sqlite> select * from t1;
    sqlite> insert into t1 (b) values ('red insert on deferred');
    sqlite> select * from t1;
    1|red insert on deferred

    Как видно из этого абзаца, хотя транзакция уже запущена, база не заблокирована ни на чтение, ни на запись.

    sqlite> insert into t1 (b) values ('blue insert on deffered');

    sqlite> select * from t1;
    1|red insert on deferred
    sqlite> insert into t1 (b) values ('red insert on deferred');
    Error: database is locked

    а вот после первого INSERT в синем клиенте, красный уже не может писать в базу (такое поведение и называется отложенным). Читать он может, но результатов работы незаконченной "синей" транзакции он не видит - т.к. read_uncommited по умолчанию выключен.

    sqlite> commit;

    sqlite> select * from t1;
    1|red insert on deferred
    2|blue insert on deffered
    sqlite> insert into t1 (b) values ('red insert on deferred');
    sqlite> select * from t1;
    1|red insert on deferred
    2|blue insert on deffered
    3|red insert on deferred

    после "синего" COMMIT, "красный" увидел результаты накаченной транзакции - и вновь может писать в базу. Добавим еще, что в нашем случае "красный" работал в режиме auto_commit, но при DEFERRED транзакциях ничего бы не изменилось, если б он запускал BEGIN TRANSACTION, потому что при отложенном поведении база блокируется не началом транзакции, а первым оператором изменения данных в ней (т.е. фактически BEGIN TRANSACTION не делает ничего). 
    Теперь посмотрим, как поведут себя те же клиенты при IMMEDIATE транзакциях.
    sqlite> begin immediate transaction;

    sqlite> select * from t1;
    sqlite> insert into t1 (b) values ('red insert on immediate');
    Error: database is locked
    sqlite> begin immediate transaction;
    Error: database is locked

    с началом транзакции база заблокирована на запись - вставить запись нельзя, начать транзакцию тоже нельзя.

    sqlite> insert into t1 (b) values ('blue insert on immediate');

    sqlite> select * from t1;

    "красному" ничего не видно - т.к. read_uncommited никто не включал

    sqlite> commit;

    sqlite> select * from t1;
    1|blue insert on immediate
    sqlite> insert into t1 (b) values ('red insert on immediate');
    sqlite> select * from t1;
    1|blue insert on immediate
    2|red insert on immediate

    после наката транзакции стали видны ее результаты - и снялась блокировка на запись.
    Теперь самый суровый вариант транзакции - EXCLUSIVE:
    sqlite> begin exclusive transaction;

    sqlite> select * from t1;
    Error: database is locked
    sqlite> insert into t1 (b) values ('red insert on exclusive');
    Error: database is locked
    sqlite> begin exclusive transaction;
    Error: database is locked

    "красному" нельзя ничего - база у нас теперь эксклюзивная.

    Осталось написать только про read_uncommited. Включается она по идее должна так:
    PRAGMA read_uncommited = 1;
    Но увидеть ее в деле мне не удалось ни в версии 3.3.6, ни в 3.7.3 (платформа - Linux). Документация безстрастно сообщает, что некоторые прагмы могут не поддерживаться в некоторых версиях - и установка неизвестной прагмы ошибки не вызывает. Вероятно, это тот самый вариант.
    Вообще знакомство с транзакциями в SQLite породило у меня ощущение, что ими можно пользоваться лишь для ускорения операций записи в базу данных (см. выше вопиющий пример с 30 млн. записей). Можно ли рассчитывать на них при реальном наличии конкурентного доступа к БД - лично для меня - большой вопрос.

    четверг, 30 декабря 2010 г.

    Вызов mysql_use_result в Python-MySQL

    Точно так же, как и Perl DBI, Python-MySQL по умолчанию использует API функцию mysql_store_result, что на больших объемах выборки (тестировалось с выборкой возвращающей 17 млн. записей) приводит к использованию невероятного количества памяти - и результатов работы скрипта можно не ждать - ибо система будет увлеченно занята копированием страниц из свопа - и в своп. Метод борьбы с этой бедой - тот же, что и в DBI - нужно использовать функцию mysql_use_result (плата за экономию ресурсов проста и жестока - пока result не закрыт, никакой другой запрос на этом подключении выполнить нельзя).
    На практике делается это так. При использовании модуля MySQLdb вместо обычного вызова метода cursor() без аргументов пишем:
    cu = db.cursor(MySQLdb.cursors.SSCursor)
    Скажем несколько слов о поведении данного типа курсоров. Как легко догадаться, привычный вызов fetchall() делает курсор на серверной стороне абсолютно безсмысленным - так что нужно использовать fetchone(). Вот тут-то нас и поджидает небольшая засада - очевидный цикл:
    for i in range(0, cu.rowcount):
    как раз и не работает, ибо для SSCursor rowcount не определен. Для любителей чисто питонского цикла
    for rw in cu:
    сразу скажу, что данная конструкция, как ни странно, работает правильно. Так же можно организовать цикл способом аналогичным тому, который предлагается ниже для модуля _mysql.
    Так вот, переходим к этому модулю. Выигрыш в производительности он дает выдающийся - на моей выборке быстрее раз в 5 (даже быстрее, чем аналогичный скрипт на Perl), так что ради таких выгод вполне можно потерпеть несколько зубодробительный синтаксис (тот, кто знаком с MySQL C API ничего страшного в этом синтаксисе не увидит).
    Работаем оно так:
    db.query("select * from sometable")
    rs = db.use_result()
    Все это еще не беда. Беда начинается дальше. rs.num_rows() по причинам, изложенным выше, не работает, а потому с циклом придется слегка повозиться. Python - это вам не C и не Perl - оператор присваивания внутри условия цикла не поддерживается, и потому нужно организовывать достаточно уродливый цикл (последний раз что-то подобное приходилось делать в языке 1С Бухгалтерии):

    rw = rs.fetch_row()
    while rw:
        # do something
        rw = rs.fetch_row()
    На этом сюрпризы не заканчиваются. rw в этом примере - вовсе не tuple, состоящий из полей запроса, а tuple, состоящий tuples, состоящих из полей (к счастью, по умолчанию, там этот tuple строго один - для возврата нескольких строк за раз fetch_row нужно вызывать с аргументом maxrows - значение 0 означает "все"). Так что присваивание переменным значений полей будет выглядеть так:
    fld1, fld2, fld3 = rw[0]
    Еще нужно помнить, что если MySQLdb приводит типы данных MySQL к аналогичным типам Python, то в _mysql все данные - тупые строки (если я правильно помню, аналогичные функции C API ведут себя аналогично).