CUBE SUGAR STORAGE

技術系のことかきます。
Recent Tweets @
Posts tagged "RabbitMQ"

OpenStack のネットワーク部分を担当する Neutron は、以前からプラグインを差し替えることで仮想ネットワークを構築する処理の実装を変更することができた。 そうした中でも、最近は ML2 (Modular Layer 2) という共通のプラグインが使われるようになってきている。 これには恐らく幾つかの理由があって、まず既存のプラグイン (ML2 以前のそれは"モノリシック・プラグイン"と呼んで区別される) 開発では多くのコードを書く必要があった。 その結果、出来上がったプラグイン群には似たようなコードがそれぞれ冗長に存在していた点が挙げられる。 そして、各プラグインは各ベンダーがそれぞれ個別に開発しているため、今のコードが本当に動くのかも不透明な状況だったようだ。 それに対し、ML2 ではプラグイン部分を共通化し、開発に必要なのは Neutron の各処理が走る際に呼ばれるコールバック関数 (メソッド) のみで良くなっている。 コールバック関数は処理毎にまとめられてドライバという形で提供されるので、開発者はそれを実装するだけで良い

以上が ML2 の背景で、今回は試しにドライバを実際に書いてみた。 ただし、実際には何もしないスケルトンなもの。 Neutron の API が呼ばれた際には、仮想ネットワークの構築の代わりにロギングだけを行う。 コードは以下の GitHub リポジトリに置いた。
https://github.com/momijiame/openstack-neutron-ml2-dummy-driver

ちなみに、ML2 プラグインが外部のライブラリから Driver をロードするには Stevedore というライブラリが使われている。 Stevedore 自体は Python のプラグイン機構を抽象化するためのものなので、実際に使われる実装は Setuptools の pkg_resources になる。 pkg_resources をそのまま使って実現するプラグイン機構については、このブログでも以前に一度書いた。
http://momijiame.tumblr.com/post/82484166907/python-setuptools-pkg-resources

今 (2014.2 “Juno”) のところML2 のドライバは Type, Mechanism, Extension の三種類が用意されているようだ。 まず、TypeDriver には仮想ネットワークセグメントを確保する方法について記述する。 MechanismDriver には特に決まりはないが、仮想ネットワークやサブネット、ポートなどの情報をデータベースに書き込むタイミングで行う何らかの処理について記述する。最後の ExtensionDriver については実装例が見当たらなかったためイマイチよく分からず今回は書いていない。

次は、今回書いたダミーのドライバを動かす手順について。 今回は、プラットフォームに CentOS7 を、ディストリビューションに RDO を使った。 (ディストリビューションについてはソースコードでも良かったけど、この組み合わせが面白そうだったので)

$ cat /etc/redhat-release 
CentOS Linux release 7.0.1406 (Core) 
$ uname -r
3.10.0-123.9.2.el7.x86_64

まず、メッセージ・キューの RabbitMQ とデータベースの MariaDB をインストールする。
$ sudo yum -y install epel-release
$ sudo yum -y install rabbitmq-server mariadb-server mariadb-devel

インストールしたらサービスを開始させる。
$ sudo systemctl start rabbitmq-server
$ sudo systemctl enable rabbitmq-server
$ sudo systemctl start mariadb
$ sudo systemctl enable mariadb

RDO の Juno リリースのリポジトリを登録する。
$ sudo yum -y install http://rdo.fedorapeople.org/openstack/openstack-juno/rdo-release-juno.rpm

OpenStack の Neutron Server と ML2 Plugin をインストールする。
$ sudo yum -y install openstack-neutron-ml2

Python のパッケージマネージャ PIP をインストールする。
$ sudo yum -y install python-pip

今回書いた ML2 のダミードライバのソースコードをチェックアウトする。
$ git clone https://github.com/momijiame/openstack-neutron-ml2-dummy-driver.git
$ cd openstack-neutron-ml2-dummy-driver/

RDO のパッケージは RPM で管理されているので、揃えたほうが何かと都合が良さそう。 RPM 形式でビルドしておく。
$ sudo yum -y install rpm-build
$ python setup.py bdist_rpm

上手くいけば dist 以下に RPM ができる。
$ ls dist/ | grep rpm$
dummyml2-0.0.1.dev4.g27d3b8e-1.noarch.rpm
dummyml2-0.0.1.dev4.g27d3b8e-1.src.rpm

できた RPM をインストールする。
$ sudo yum -y install dist/dummyml2-*.noarch.rpm

ここからは設定に入る。 念のため、RPM で入るオリジナルの設定ファイルをバックアップしておいた方が良いだろう。
$ sudo cp -a /etc/neutron/neutron.conf{,.back}
$ sudo cp -a /etc/neutron/plugins/ml2/ml2_conf.ini{,.back}

Neutron Server 用の設定を用意する。
$ cat << EOF | sudo tee /etc/neutron/neutron.conf > /dev/null
[DEFAULT]
core_plugin = ml2
service_plugins = router
[matchmaker_redis]
[matchmaker_ring]
[quotas]
[agent]
[keystone_authtoken]
[database]
connection = mysql://root@localhost/neutron_ml2?charset=utf8
[service_providers]
EOF

ML2 プラグイン用の設定を用意する。
$ cat << EOF | sudo tee /etc/neutron/plugins/ml2/ml2_conf.ini > /dev/null
[ml2]
tenant_network_types = dummytype
type_drivers = dummytype
mechanism_drivers = dummymech
[ml2_type_flat]
[ml2_type_vlan]
[ml2_type_gre]
[ml2_type_vxlan]
[securitygroup]
EOF

MariaDB に Neutron 用のデータベースを用意する。
$ mysql -u root -e "create database neutron_ml2"

サービスが使う設定ファイルのパスが決まっているのでシンボリックリンクを張る。
$ sudo ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini

データベースを初期化する。
$ sudo neutron-db-manage --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini upgrade head

まずは手動で Neutron Server を起動してみる。 特にエラーなく以下のような表示になれば成功。
$ sudo neutron-server --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini
...(snip)...
2014-10-29 20:29:39.459 11984 INFO neutron.service [-] Neutron service started, listening on 0.0.0.0:9696
2014-10-29 20:29:39.460 11984 INFO oslo.messaging._drivers.impl_rabbit [-] Connecting to AMQP server on localhost:5672
2014-10-29 20:29:39.469 11984 INFO neutron.wsgi [-] (11984) wsgi starting up on http://0.0.0.0:9696/
2014-10-29 20:29:39.472 11984 INFO oslo.messaging._drivers.impl_rabbit [-] Connected to AMQP server on localhost:5672

手動で動いたら、次はサービスとして動作させる。 Neutron のサービスがあらかじめ幾つか用意されている。
$ systemctl list-unit-files --type=service | grep neutron
neutron-dhcp-agent.service                  disabled
neutron-l3-agent.service                    disabled
neutron-lbaas-agent.service                 disabled
neutron-metadata-agent.service              disabled
neutron-netns-cleanup.service               disabled
neutron-server.service                      disabled

Neutron Server のサービスを有効にする。
$ sudo systemctl start neutron-server
$ sudo systemctl enable neutron-server

もし何らかのエラーが出るような場合は status を確認してトラブルシュートする。 Systemd は便利だね。
$ systemctl status neutron-server

ログは以下の場所に出力される。
$ tail -f /var/log/neutron/server.log

次にクライアントから仮想ネットワークを作ってみる (ただし、ドライバの内容的に物理的なものは何もできない) けど、ここでちょっと問題がある。 今回は認証コンポーネントの Keystone を用意していない。 なので、認証をスキップして API を呼び出したいんだけど、現行のバージョン (v2.3.9) にはバグがあってこれができない。
$ neutron --version
2.3.9

リポジトリの master/HEAD に修正が入っていることは確認しているので、次のバージョンが出るまではソースコードからインストールしたものを使った方が良さげ。
$ sudo pip install git+https://github.com/openstack/python-neutronclient.git

neutron コマンドのバージョンが 2.3.9.X 以降になっていれば良い。
$ neutron --version
2.3.9.39

クライアント用の設定を用意して読み込む。
$ cat << EOF > ~/neutronrc
export OS_URL=http://localhost:9696
export OS_TOKEN=admin
export OS_AUTH_STRATEGY=noauth
EOF
$ source ~/neutronrc

以下のように net-create がエラーにならず実行できれば成功。
$ neutron net-create network1 --tenant-id 1
Created a new network:
+---------------------------+--------------------------------------+
| Field                     | Value                                |
+---------------------------+--------------------------------------+
| admin_state_up            | True                                 |
| id                        | e82cf5ca-26ae-4eff-8fd6-d007d96d593c |
| name                      | network1                             |
| provider:network_type     | dummytype                            |
| provider:physical_network |                                      |
| provider:segmentation_id  |                                      |
| router:external           | False                                |
| shared                    | False                                |
| status                    | ACTIVE                               |
| subnets                   |                                      |
| tenant_id                 | 1                                    |
+---------------------------+--------------------------------------+

あとは port-create とかね。
$ neutron port-create network1 --tenant-id 1
Created a new port:
+-----------------------+--------------------------------------+
| Field                 | Value                                |
+-----------------------+--------------------------------------+
| admin_state_up        | True                                 |
| allowed_address_pairs |                                      |
| binding:host_id       |                                      |
| binding:profile       | {}                                   |
| binding:vif_details   | {}                                   |
| binding:vif_type      | unbound                              |
| binding:vnic_type     | normal                               |
| device_id             |                                      |
| device_owner          |                                      |
| fixed_ips             |                                      |
| id                    | 6889a112-177a-4e46-ac97-85ddc37235c9 |
| mac_address           | fa:16:3e:87:bd:e4                    |
| name                  |                                      |
| network_id            | e82cf5ca-26ae-4eff-8fd6-d007d96d593c |
| security_groups       | 3770ac7a-bceb-45ac-8a22-f50da8fb5951 |
| status                | DOWN                                 |
| tenant_id             | 1                                    |
+-----------------------+--------------------------------------+

さて、最初に立ち返ると、今回書いたドライバは実際の処理の代わりにログを出すというものだった。 ドライバが上手く動いているか確認してみる。 “CALLED: ” から始まるログがそれ。
$ grep CALLED /var/log/neutron/server.log
2014-10-29 20:30:44.381 12010 INFO dummyml2.lib.interceptor [-] CALLED: __init__(*args=(<dummyml2.drivers.type_dummy.DummyTypeDriver object at 0x3a18bd0>,), **kwargs={})
2014-10-29 20:30:44.382 12010 INFO dummyml2.lib.interceptor [-] CALLED: get_type(*args=(<dummyml2.drivers.type_dummy.DummyTypeDriver object at 0x3a18bd0>,), **kwargs={})
2014-10-29 20:30:44.384 12010 INFO dummyml2.lib.interceptor [-] CALLED: __init__(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>,), **kwargs={})
2014-10-29 20:30:44.386 12010 INFO dummyml2.lib.interceptor [-] CALLED: initialize(*args=(<dummyml2.drivers.type_dummy.DummyTypeDriver object at 0x3a18bd0>,), **kwargs={})
2014-10-29 20:30:44.386 12010 INFO dummyml2.lib.interceptor [-] CALLED: initialize(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>,), **kwargs={})
2014-10-29 20:34:05.890 12010 INFO dummyml2.lib.interceptor [req-758b33c6-3f6f-4f6e-8001-083d8551220d ] CALLED: allocate_tenant_segment(*args=(<dummyml2.drivers.type_dummy.DummyTypeDriver object at 0x3a18bd0>, <sqlalchemy.orm.session.Session object at 0x4658050>), **kwargs={})
2014-10-29 20:34:05.897 12010 INFO dummyml2.lib.interceptor [req-758b33c6-3f6f-4f6e-8001-083d8551220d ] CALLED: create_network_precommit(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>, <neutron.plugins.ml2.driver_context.NetworkContext object at 0x49cef90>), **kwargs={})
2014-10-29 20:34:05.899 12010 INFO dummyml2.lib.interceptor [req-758b33c6-3f6f-4f6e-8001-083d8551220d ] CALLED: create_network_postcommit(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>, <neutron.plugins.ml2.driver_context.NetworkContext object at 0x49cef90>), **kwargs={})
2014-10-29 20:34:21.056 12010 INFO dummyml2.lib.interceptor [req-d0a28f88-99eb-4126-914c-8bf5fedcb79e ] CALLED: create_port_precommit(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>, <neutron.plugins.ml2.driver_context.PortContext object at 0x49e1210>), **kwargs={})
2014-10-29 20:34:21.058 12010 INFO dummyml2.lib.interceptor [req-d0a28f88-99eb-4126-914c-8bf5fedcb79e ] CALLED: create_port_postcommit(*args=(<dummyml2.drivers.mech_dummy.DummyMechanismDriver object at 0x3a260d0>, <neutron.plugins.ml2.driver_context.PortContext object at 0x49e1210>), **kwargs={})

上手くいってるね! めでたしめでたし。

Neutron は OpenStack の中でネットワーク周りを司るコンポーネント。 通常は OpenStack のその他のコンポーネントと組み合わせて使うそれを、動作の理解を深めるために今回は単独で動かしてみる。 使うプラグインは今風の ML2 にする。

使う環境は以下の通り。

$ cat /etc/redhat-release 
CentOS release 6.5 (Final)
$ uname -r
2.6.32-431.11.2.el6.x86_64

まずは、システムの Python を汚さないように virtualenv をインストールしておく。
$ sudo yum -y install python-devel
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | sudo python
$ sudo easy_install pip
$ sudo pip install virtualenv virtualenvwrapper
$ cat << EOF >> ~/.bashrc
source `which virtualenvwrapper.sh`
export WORKON_HOME=$HOME/.virtualenvs
export PIP_RESPECT_VIRTUALENV=true
EOF
$ source ~/.bashrc

次に OpenStack Neutron のソースコードをチェックアウトする。 Icehouse はまだイマイチ安定しない感じなので Havana のタグを使う。
$ sudo yum -y install git
$ git clone https://github.com/openstack/neutron.git
$ cd neutron
$ git checkout -b havana 2013.2.3

virtualenv で新たに Python 仮想環境を作って必要なパッケージをインストールしておく。 test-requirements.txt はテストに必要なパッケージなので動かすだけであれば必要ない。
$ mkvirtualenv neutron-dev
$ pip install -r requirements.txt -r test-requirements.txt 

Neutron に備わっているテストを実行してみる。
$ ./run_tests.sh -N

テストがひと通りパスするようであれば Neutron をインストールする。
$ python setup.py install

Neutron のリポジトリにある設定ファイルのひな形の入ったディレクトリから /etc 以下にシンボリックリンクを張る。
$ sudo ln -s -f `pwd`/etc /etc/neutron
$ sudo chown `whoami` /etc/neutron/

Neutron の使用する RDB として MySQL をインストールする。
$ sudo yum -y install mysql-server
$ sudo chkconfig mysqld on
$ sudo service mysqld start

Neutron 用のデータベースを作成する。
$ mysql -u root -e "create database neutron_ml2"
$ mysql -u root -e "set password = password('password')"
$ mysql -u root -ppassword -e "create database neutron_ml2"

MySQL 用の DB ドライバをインストールする。 requirements.txt に記載されていないのは RDB に何を使うか分からないためかな。
$ sudo yum -y install mysql-devel
$ pip install mysql-python

Neutron の Server と各 Agent が通信するための RPC 用に RabbitMQ をインストールする。
$ sudo yum -y install http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
$ sudo yum -y install http://dl.iuscommunity.org/pub/ius/stable/CentOS/6/x86_64/ius-release-1.0-11.ius.centos6.noarch.rpm
$ sudo yum -y install rabbitmq-server
$ sudo service rabbitmq-server start
$ sudo chkconfig rabbitmq-server on
$ sudo rabbitmqctl change_password guest password

Open vSwitch と Network Namespace に対応した iproute2 をインストールするために RDO をインストールしておく。
$ sudo yum -y install http://rdo.fedorapeople.org/openstack/openstack-havana/rdo-release-havana.rpm
$ sudo yum -y update

Open vSwitch をインストールする。
$ sudo yum -y install openvswitch
$ sudo service openvswitch start
$ sudo chkconfig openvswitch on

Neutron の設定ファイルを設置する。
$ cp /etc/neutron/neutron.conf{,.orig}
$ cat << EOF > /etc/neutron/neutron.conf
[DEFAULT]
auth_strategy = noauth
allow_overlapping_ips = True
policy_file = /etc/neutron/policy.json
debug = True
verbose = True
service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin
core_plugin = neutron.plugins.ml2.plugin.Ml2Plugin
rabbit_password = password
rabbit_host = localhost
rpc_backend = neutron.openstack.common.rpc.impl_kombu
state_path = /var/tmp/neutron
lock_path = \$state_path/lock
notification_driver = neutron.openstack.common.notifier.rpc_notifier
[quotas]
[agent]
root_helper = sudo
[database]
[service_providers]
EOF
$ cat << EOF > /etc/neutron/ml2.conf
[ml2]
type_drivers = local,flat,vlan,gre,vxlan
mechanism_drivers = openvswitch,linuxbridge
[ml2_type_flat]
[ml2_type_vlan]
[ml2_type_gre]
[ml2_type_vxlan]
[database]
connection = mysql://root:password@localhost/neutron_ml2?charset=utf8
[ovs]
local_ip = 192.168.33.10
[agent]
[securitygroup]
firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
EOF
$ cat << EOF >> /etc/neutron/l3_agent.ini
interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver
EOF

動作に必要なブリッジを作成する。
$ sudo ovs-vsctl add-br br-ex
$ sudo ovs-vsctl add-br br-int

以上で動作の準備ができたので Neutron Server を実行する。
$ workon neutron-dev
$ neutron-server --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/ml2.conf 

別のターミナルで Neutron L3 Agent を実行する。 この Agent は仮想ルータの制御などを行う。
$ workon neutron-dev
$ neutron-l3-agent --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/l3_agent.ini

同様に Neutron DHCP Agent を実行する。 この Agent は DHCP サーバを立てて VM の IP アドレスを制御する。
$ workon neutron-dev
$ neutron-dhcp-agent --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/dhcp_agent.ini 

同様に Neutron Open vSwitch Agent を実行する。 この Agent はポート追加の検出やポートへの VLAN タグの追加などを行う。
$ workon neutron-dev
$ neutron-openvswitch-agent --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/ml2.conf

これで、ひと通り必要なプロセスは立ち上がった。 次は API 経由でネットワークを作っていく。 まずは Neutron のコマンドラインツールの動作に必要な環境変数を準備する。 今回は認証周りに Keystone を使わないため OS_AUTH_STRATEGY に noauth を指定する。
$ workon neutron-dev
$ export OS_URL=http://localhost:9696
$ export OS_TOKEN=admin
$ export OS_AUTH_STRATEGY=noauth

各 Agent が正常に動作しているか確認しておく。
$ neutron agent-list
+--------------------------------------+--------------------+-----------------------+-------+----------------+
| id                                   | agent_type         | host                  | alive | admin_state_up |
+--------------------------------------+--------------------+-----------------------+-------+----------------+
| 17264570-cccf-41b2-86b0-455b9cff8127 | DHCP agent         | localhost.localdomain | :-)   | True           |
| 7e70133a-0703-4b4b-97ab-d8fcfa9bc175 | Open vSwitch agent | localhost.localdomain | :-)   | True           |
| acee854c-d29b-441d-a88f-27ad92d48b9e | L3 agent           | localhost.localdomain | :-)   | True           |
+--------------------------------------+--------------------+-----------------------+-------+----------------+

最初に、インターネットに抜けるための外部ネットワークを作成する。 認証に noauth を使うときは –tenant-id を指定するのがポイント。
$ neutron net-create public --tenant-id 1  --router:external True
Created a new network:
+---------------------------+--------------------------------------+
| Field                     | Value                                |
+---------------------------+--------------------------------------+
| admin_state_up            | True                                 |
| id                        | c22d62d9-3ee8-4bae-9f69-f662357d0795 |
| name                      | public                               |
| provider:network_type     | local                                |
| provider:physical_network |                                      |
| provider:segmentation_id  |                                      |
| router:external           | True                                 |
| shared                    | False                                |
| status                    | ACTIVE                               |
| subnets                   |                                      |
| tenant_id                 | 1                                    |
+---------------------------+--------------------------------------+

外部ネットワークにサブネットを追加する。
$ neutron subnet-create public 10.0.0.0/24 --tenant-id 1
Created a new subnet:
+------------------+--------------------------------------------+
| Field            | Value                                      |
+------------------+--------------------------------------------+
| allocation_pools | {"start": "10.0.0.2", "end": "10.0.0.254"} |
| cidr             | 10.0.0.0/24                                |
| dns_nameservers  |                                            |
| enable_dhcp      | True                                       |
| gateway_ip       | 10.0.0.1                                   |
| host_routes      |                                            |
| id               | 3ce3d8e0-fbde-46de-abc8-cdcecb968611       |
| ip_version       | 4                                          |
| name             |                                            |
| network_id       | c22d62d9-3ee8-4bae-9f69-f662357d0795       |
| tenant_id        | 1                                          |
+------------------+--------------------------------------------+

外部ネットワークとネットワークを接続するルータを追加する。
$ neutron router-create router1 --tenant-id 1
Created a new router:
+-----------------------+--------------------------------------+
| Field                 | Value                                |
+-----------------------+--------------------------------------+
| admin_state_up        | True                                 |
| external_gateway_info |                                      |
| id                    | a09cb0ef-c894-4e0f-9d47-262ffa2b8a3c |
| name                  | router1                              |
| status                | ACTIVE                               |
| tenant_id             | 1                                    |
+-----------------------+--------------------------------------+

ルータに外部ネットワークを設定する。
$ neutron router-gateway-set router1 public
Set gateway for router router1

次に VM が繋がるネットワークを作成する。
$ neutron net-create private --tenant-id 1
Created a new network:
+---------------------------+--------------------------------------+
| Field                     | Value                                |
+---------------------------+--------------------------------------+
| admin_state_up            | True                                 |
| id                        | 8d70c69f-d6d5-4519-b779-a3e3935e167d |
| name                      | private                              |
| provider:network_type     | local                                |
| provider:physical_network |                                      |
| provider:segmentation_id  |                                      |
| shared                    | False                                |
| status                    | ACTIVE                               |
| subnets                   |                                      |
| tenant_id                 | 1                                    |
+---------------------------+--------------------------------------+

先ほどと同様にサブネットを設定する。
$ neutron subnet-create private 192.168.0.0/24 --name subnet1 --tenant-id 1
Created a new subnet:
+------------------+--------------------------------------------------+
| Field            | Value                                            |
+------------------+--------------------------------------------------+
| allocation_pools | {"start": "192.168.0.2", "end": "192.168.0.254"} |
| cidr             | 192.168.0.0/24                                   |
| dns_nameservers  |                                                  |
| enable_dhcp      | True                                             |
| gateway_ip       | 192.168.0.1                                      |
| host_routes      |                                                  |
| id               | 20c41132-a179-4ed0-91b4-b20d032255b3             |
| ip_version       | 4                                                |
| name             | subnet1                                          |
| network_id       | 8d70c69f-d6d5-4519-b779-a3e3935e167d             |
| tenant_id        | 1                                                |
+------------------+--------------------------------------------------+

ルータのインターフェースとして作成したサブネットを追加する。
$ neutron router-interface-add router1 subnet1
Added interface 4015a6f9-de67-48b3-958e-674a5d6c16cb to router router1.

ネットワークにポートを作成する。 この操作は、各コンポーネントを使って OpenStack の環境を構築する場合 Nova Compute が行う。 今回は Neutron を単独で動作させているため Nova Compute が行う内容を手作業でやる。
$ neutron port-create private --device-id=vm1 --binding:host_id=`hostname` --tenant-id 1
Created a new port:
+-----------------------+------------------------------------------------------------------------------------+
| Field                 | Value                                                                              |
+-----------------------+------------------------------------------------------------------------------------+
| admin_state_up        | True                                                                               |
| allowed_address_pairs |                                                                                    |
| binding:capabilities  | {"port_filter": true}                                                              |
| binding:host_id       | localhost.localdomain                                                              |
| binding:vif_type      | ovs                                                                                |
| device_id             | vm1                                                                                |
| device_owner          |                                                                                    |
| fixed_ips             | {"subnet_id": "20c41132-a179-4ed0-91b4-b20d032255b3", "ip_address": "192.168.0.2"} |
| id                    | b002cdcc-4225-411a-a4b1-6f695a2a9ee0                                               |
| mac_address           | fa:16:3e:6e:fe:31                                                                  |
| name                  |                                                                                    |
| network_id            | 8d70c69f-d6d5-4519-b779-a3e3935e167d                                               |
| security_groups       | bd362673-d5df-476e-8ed7-44c481cfbab9                                               |
| status                | DOWN                                                                               |
| tenant_id             | 1                                                                                  |
+-----------------------+------------------------------------------------------------------------------------+

上記で作成したものは論理的なポートなので、実際には物理的なポートを作成して対応付ける必要がある。 本体は TAP Device と仮想マシンで行うところを、今回は Virtual Ethernet Device と Network Namespace でエミュレートする。
$ sudo ip link add tapb002cdcc-42 type veth peer name vnet0
$ sudo ifconfig tapb002cdcc-42 hw ether fa:16:3e:6e:fe:31
$ sudo ifconfig tapb002cdcc-42 up
$ sudo ovs-vsctl add-port br-int tapb002cdcc-42
$ sudo ovs-vsctl set Interface tapb002cdcc-42 external-ids:iface-id=b002cdcc-4225-411a-a4b1-6f695a2a9ee0
$ sudo ovs-vsctl set Interface tapb002cdcc-42 external_ids:attached-mac=fa:16:3e:6e:fe:31
$ sudo ovs-vsctl set Interface tapb002cdcc-42 external-ids:iface-status=active
$ sudo ovs-vsctl set Interface tapb002cdcc-42 external-ids:vm-uuid=vm1

上記の操作を実行すると Neutron Open vSwitch Agent がポートの追加を検出して、論理的なポートの status が ACTIVE になる。
$ neutron port-show b002cdcc-4225-411a-a4b1-6f695a2a9ee0
+-----------------------+------------------------------------------------------------------------------------+
| Field                 | Value                                                                              |
+-----------------------+------------------------------------------------------------------------------------+
| admin_state_up        | True                                                                               |
| allowed_address_pairs |                                                                                    |
| binding:capabilities  | {"port_filter": true}                                                              |
| binding:host_id       | localhost.localdomain                                                              |
| binding:vif_type      | ovs                                                                                |
| device_id             | vm1                                                                                |
| device_owner          |                                                                                    |
| extra_dhcp_opts       |                                                                                    |
| fixed_ips             | {"subnet_id": "20c41132-a179-4ed0-91b4-b20d032255b3", "ip_address": "192.168.0.2"} |
| id                    | b002cdcc-4225-411a-a4b1-6f695a2a9ee0                                               |
| mac_address           | fa:16:3e:6e:fe:31                                                                  |
| name                  |                                                                                    |
| network_id            | 8d70c69f-d6d5-4519-b779-a3e3935e167d                                               |
| security_groups       | bd362673-d5df-476e-8ed7-44c481cfbab9                                               |
| status                | ACTIVE                                                                             |
| tenant_id             | 1                                                                                  |
+-----------------------+------------------------------------------------------------------------------------+

以上でひと通り設定できたので、インターネットと通信するのに NAT とルーティングの設定を行う。
$ sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -j MASQUERADE
$ sudo ifconfig br-ex 10.0.0.1
$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo iptables -I FORWARD -j ACCEPT

先ほど作成した Virtual Ethernet Device の br-int に追加していない方を、新たに Network Namespace を作成して追加する。 追加したらインターフェースに IP アドレスを手動で設定する。 これは本来は DHCP Agent が作成する DHCP サーバ経由で設定される。
$ sudo ip netns add vm1
$ sudo ip link set vnet0 netns vm1
$ sudo ip netns exec vm1 ifconfig vnet0 192.168.0.2
$ sudo ip netns exec vm1 route add default gw 192.168.0.1

VM 代わりの Network Namespace からインターネットに向けて ping を打ってみる。
$ sudo ip netns exec vm1 ping 8.8.8.8 -c 3
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=59 time=55.6 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=59 time=57.0 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=59 time=154 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2160ms
rtt min/avg/max/mdev = 55.673/88.920/154.044/46.053 ms
ばっちり。

最後におまけ。 RabbitMQ に流れるメッセージをダンプする方法。 RabbitMQ の Firehose 機能を有効にしたら rabbitracer をインストールして実行する。
$ sudo rabbitmqctl stop_app
$ sudo rabbitmqctl trace_on
$ sudo rabbitmqctl start_app
$ pip install rabbitracer
$ rabbitracer -u guest -p password
ばらばらーっと RabbitMQ に流れるメッセージが JSON 形式で出力される。

VM は Vagrant で作ったので Vagrantfile も置いておく。
# -*- mode: ruby -*-
# vi: set ft=ruby :

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "centos65"
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--memory", "2048", "--cpus", "2"]
  end
end

いじょう。

RabbitMQ には Firehose 機能というものがあって、有効にするとルーティングされるメッセージが全てそこに転送される。 スイッチのミラーポートみたいなイメージかな。 これを使うと AMQP のデバッグが捗る。

Rabbitracer は、その Firehose 機能で転送されてくる全てのメッセージを JSON 形式でダンプする。 以前同様のスクリプトを書いたので、それをリファクタした上で GitHub で公開してみた。
https://github.com/momijiame/rabbitracer

使い方は GitHub の README に書いた。 パッケージは PyPI にも登録したので PIP でインストールできる。

$ pip install rabbitracer

そんなかんじで。

近頃 RabbitMQ (AMQP) と戯れているんだけど、どのエクスチェンジに何が流れているのかは、チマチマ確認してると埒があかない。 そこで、RabbitMQ の持っている Firehose 機能を使って、サーバに流れるメッセージを全て JSON の形でダンプする Python スクリプトを書いてみた。 Firehose 機能というのは、有効にすると RabbitMQ の入力を一元的に扱うエクスチェンジが定義されて、そこにバインディングキーを指定したキューを繋ぐと全メッセージが流れこんでくる、というもの。 何故 JSON 形式なのか、その理由についてはこちらを見てほしい。

まずはじめに、RabbitMQ で Firehose 機能を有効にするには、サーバが起動した状態で以下を実行する。 環境によってはルート権限が必要かもしれないので、そこは適宜 sudo などで読み替える。

$ rabbitmqctl stop_app
$ rabbitmqctl trace_on
$ rabbitmqctl start_app

次に、スクリプトが依存するライブラリをインストールする。 ココらへんはあとで setup.py を書いて自動化する予定。 ちゃんと書いたら Github に上げようかな。
$ pip install kombu furl

さて肝心のスクリプトは以下の通り。 エクスチェンジ “amq.rabbitmq.trace” にバインディングキー “#” を指定して、全てのメッセージを吸い込むようにする。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import json
import uuid
import types

from furl import furl
from kombu import Connection, Exchange, Queue


def process_message(body, message):

    def _is_hidden(attribute_name):
        return attribute_name.startswith('_')

    def _is_acceptable(attribute_refs):
        attribute_type = type(attribute_refs)
        acceptable_types = [
            types.NoneType, types.ListType, types.DictType,
            types.StringTypes, types.UnicodeType, types.BooleanType,
            types.IntType, types.LongType, types.FloatType,
        ]
        for acceptable_type in acceptable_types:
            if attribute_type == acceptable_type:
                return True
        return False

    def _encode(attribute_name, attribute_refs):
        if attribute_name != 'payload':
            return attribute_refs
        properties = message.headers.get('properties')
        content_type = properties.get('content_type')
        if not content_type:
            return attribute_refs
        if content_type.startswith('application/json'):
            return json.loads(attribute_refs)
        return attribute_refs

    dump_dict = {}
    for attr_name in dir(message):
        if _is_hidden(attr_name):
            continue
        attribute = getattr(message, attr_name)
        if not _is_acceptable(attribute):
            continue
        dump_dict[attr_name] = _encode(attr_name, attribute)
    message.ack()
    print(json.dumps(dump_dict))


def parse_arguments():
    description = 'The RabbitMQ firehose dumper script'

    option_n_help = 'hostname'
    option_u_help = 'username'
    option_w_help = 'password'
    option_v_help = 'virtualhost'

    arg_parser = argparse.ArgumentParser(description=description)
    arg_parser.add_argument('-n', '--hostname', help=option_n_help,
                            required=False, default='localhost')
    arg_parser.add_argument('-u', '--username', help=option_u_help,
                            required=False, default='guest')
    arg_parser.add_argument('-w', '--password', help=option_w_help,
                            required=False, default='guest')
    arg_parser.add_argument('-v', '--virtualhost', help=option_v_help,
                            required=False, default='/')
    global ARGS
    ARGS = arg_parser.parse_args()


def main_loop():

    def _build_url():
        f = furl()
        f.scheme = 'amqp'
        f.username = ARGS.username
        f.password = ARGS.password
        f.hostname = ARGS.hostname
        f.path = ARGS.virtualhost
        return str(f)

    trace_exchange = Exchange('amq.rabbitmq.trace', 'topic')
    trace_queue = Queue('firehose.' + str(uuid.uuid4()),
                        exchange=trace_exchange,
                        routing_key='#',
                        auto_delete=True)
    with Connection(_build_url()) as connection:
        with connection.Consumer(trace_queue, callbacks=[process_message]):
            while True:
                connection.drain_events()


def main():
    parse_arguments()
    main_loop()


if __name__ == '__main__':
    main()

起動したらなんか適当にメッセージを流してみよう。 AMQPcat についてはこちらを参照。
$ echo "Hello, World\!" | amqpcat amqp://guest:guest@localhost -t direct -n sample --publisher

さて上記のスクリプトを実行してるコンソールを見ると…。
$ python rabbitracer.py
{"delivery_info": {"consumer_tag": "1", "redelivered": false, "routing_key": "publish.sample", "delivery_tag": 1, "exchange": "amq.rabbitmq.trace"}, "acknowledged": false, "accept": null, "properties": {"application_headers": {"node": "rabbit@localhost", "exchange_name": "sample", "routing_keys": [""], "properties": {"priority": 0, "delivery_mode": 1, "content_type": "application/octet-stream"}}}, "headers": {"node": "rabbit@localhost", "exchange_name": "sample", "routing_keys": [""], "properties": {"priority": 0, "delivery_mode": 1, "content_type": "application/octet-stream"}}, "content_encoding": null, "content_type": null, "delivery_tag": 1}
やったー、諸々の情報含めてメッセージがダンプされたよー。

RabbitMQ はあらかじめインストールして起動しておく。

AMQPcat は Ruby gems でインストールする。

$ sudo gem install amqpcat --no-rdoc --no-ri
$ gem list | grep amqpcat
amqpcat (0.0.1)

メッセージをプッシュする。 -t オプションで Exchange の種別を、-n オプションで Exchange/Queue の名前を指定できる。
$ echo "Hello, World\!" | amqpcat amqp://guest:guest@localhost -t direct -n sample --publisher

RabbitMQ の状態を確認すると Exchange と Queue ができてる。 Queue には 1 つメッセージがキューイングされていることが分かる。
$ rabbitmqctl list_exchanges | grep sample
sample	direct
$ rabbitmqctl list_queues | grep sample
sample	1

メッセージをポップする。
$ amqpcat amqp://guest:guest@localhost -t direct -n sample --consumer
Hello, World!
めでたしめでたし。

RabbitMQ は AMQP (Advanced Message Queuing Protocol) のサーバ (ブローカ) 実装。

インストールは Homebrew から一発。

$ brew install rabbitmq

起動するときは rabbitmq-server を叩く。
$ rabbitmq-server

サーバの制御は rabbitmqctl 経由で行う。
$ rabbitmqctl --help

よく使う操作。
$ rabbitmqctl stop_app # サーバの一時停止
$ rabbitmqctl reset # サーバのリセット
$ rabbitmqctl start_app # サーバの再開
$ rabbitmqctl list_exchanges # Exchange の一覧表示
$ rabbitmqctl list_queues # Queue の一覧表示
$ rabbitmqctl stop # サーバの停止
めでたしめでたし。