ISC DHCP serverでIPアドレスのリース時に外部コマンドを実行する

ISC DHCP serverDHCPサーバを運用するとき,IPアドレスをリースするタイミングで任意のコマンドを実行したいことがある.例えば,クライアントのMACアドレスとリースしたIPアドレスのペアをデータベースに入れておきたい,といった場合.

こういった場合,on commit/on release/on expiryが使える.dhcpd.confに次のような設定を書いておくと,それぞれのタイミングで外部のコマンドが実行される.

on commit {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

on release {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

on expiry {
    execute("/path/to/script", "arg1", "arg2", "arg3");
}

各コマンドはそれぞれ次のタイミングで実行される:

  • on commit
    • サーバがクライアントにIPアドレスをリースしたタイミング
  • on release
    • クライアントがIPアドレスをリリースしたタイミング
  • on expiry
    • サーバがクライアントにリースしたIPアドレスのリース期限が切れたタイミング

実行する外部コマンドにIPアドレスMACアドレスを渡す

クライアントのMACアドレスおよびIPアドレスは,それぞれhardware変数およびleased-address変数に格納されている.ただしこれらの変数の値は整数で表現されているので,通常そのままでは扱い辛い.そこで文字列に変換した上で外部コマンドに渡す.

on commit {
    set clip = binary-to-ascii(10, 8, ".", leased-address);
    set clhw = concat (
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,1,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,2,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,3,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,4,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,5,1))),2), ":",
        suffix (concat ("0", binary-to-ascii (16, 8, "", substring(hardware,6,1))),2)
    );
    execute("/path/to/script", clip, clhw);
}

たまにMACアドレスの取得方法として次のような方法を紹介している記事がある.この方法だと"02:01:23:0a:bc:de"が"2:1:23:a:bc:de"と変換されてしまう.

set clhw = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));

外部コマンド側の実装

on commit時にIPアドレスMACアドレスのペアをRedisに突っ込み,on expiryで対応するエントリを削除するスクリプトだと,下のような感じになる.

#!/usr/bin/env python
"""Manage bindings between an IP address and a MAC address"""

import redis

import sys
import argparse

kvs = redis.Redis(host="127.0.0.1", port=6379, db=1)


def on_commit(ip_address, mac_address):
    """Insert a newly-leased IP address and its corresponding MAC address to the redis server"""
    kvs.set(ip_address, mac_address)

    return


def on_expiry(ip_address):
    """Remove the expired IP address from the redis server"""
    kvs.delete(ip_address)

    return


def main():
    """Parse command-line arguments and call a corresponding function"""
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    subparser_commit = subparsers.add_parser("commit")
    subparser_commit.set_defaults(
        func=lambda command_args: on_commit(command_args.ip_address, command_args.mac_address))
    subparser_commit.add_argument("ip_address")
    subparser_commit.add_argument("mac_address")

    subparser_expiry = subparsers.add_parser("expiry")
    subparser_expiry.set_defaults(
        func=lambda command_args: on_expiry(command_args.ip_address))
    subparser_expiry.add_argument("ip_address")

    args = parser.parse_args()

    try:
        subcommand_func = args.func
    except AttributeError:
        parser.print_usage()
        sys.exit(2)

    subcommand_func(args)

if __name__ == '__main__':
    main()

以前,この仕組みを使ってCaptive Portalのようなものを書いて運用していた.WebページにアクセスしてきたクライアントのIPアドレスからMACアドレスが引けるようになるので,アカウント(人)とMACアドレス(デバイス)の紐付けが簡単に実現できる.

参考

過去に蓄積されたデータの集計にNorikraを使う

最近Norikraを触っていて,ある程度使い方が分かってきたのと,丁度v1.0.0もリリースされたので,忘れないうちにこのタイミングでメモしておく.

本来Norikraはリアルタイムなログ等に対して処理をするものであるけども,今回は過去に蓄積されたログに対して集計処理を行ってみることにする*1

今回のキーポイントは,

  • win:ext_timed_batchを使って,ログの発生時刻を指定する
  • LOOPBACKを使って,あるクエリで発生したeventを別のtargetに直接流し込む

の2点.

やりたいこと

下のような,2台のホストで1分毎に生成されたデータがあるとする.

# host1.csv
timestamp, metric1, metric2, ...
1396278000, 0, 0
1396278060, 1, 2
1396278120, 2, 4
1396278180, 3, 6
1396278240, 4, 8
1396278300, 5, 10
1396278360, 6, 12
1396278420, 7, 14
1396278480, 8, 16
1396278540, 9, 18
1396278600, 10, 20
# host2.csv
timestamp, metric1, metric2, ...
1396278000, 0, 0
1396278060, 2, 4
1396278120, 4, 8
1396278180, 6, 12
1396278240, 8, 16
1396278300, 10, 20
1396278360, 12, 24
1396278420, 14, 28
1396278480, 16, 32
1396278540, 18, 36
1396278600, 20, 40

これらのデータに対し,同一時刻に発生したメトリックどうしを加算したい.

timestamp, metric1_sum, metric2_sum, ...
1396278000, 0, 0
1396278060, 3, 6
1396278120, 6, 12
1396278180, 9, 18
1396278240, 12, 24
1396278300, 15, 30
1396278360, 18, 36
1396278420, 21, 42
1396278480, 24, 48
1396278540, 27, 54
1396278600, 30, 60

さらに,5分間の平均値と最大値を調べたい.

timestamp, metric1_avg, metric1_max
1396278000, 6.0, 12
1396278300, 21.0, 27

実際には,ホスト数もメトリックももっとたくさんある,という想定. こういった用途であれば,データベースに突っ込んで集計するなり,自前で集計スクリプトを書くなりすれば対応できるような気がする.ただ,集計対象のメトリックが頻繁に変更されるような状況で,かつデータベースや集計スクリプトのメンテナンスにあまり重きを置けない場合*2,そのあたりを簡単に扱えるものが欲しくなってくる.そこでNorikra,という流れ.

過去のログを扱う::ext_timed_batchでログの発生時刻を指定する

通常,ログがnorikraに入った時のシステムの実時間=ログの発生時刻として扱われる.それだと過去のデータを処理対象とする場合困るので,発生時刻を外部から与えてやる必要がある.そこで使うのがext_timed_batch

例えばこんな感じのクエリを登録する.

select
  min(timestamp) as timestamp,
  sum(metric1) as metric_sum
from
  host_data.win:ext_timed_batch(timestamp * 1000, 1 min, 1396278000000L)
where hostname in ("host1", "host2")

このクエリを登録した状態で,ターゲットhost_data

[{"timestamp": 1396278000, "hostname": "host1", "metric1": 0}]

というようなデータを送ると,2014-04-01 00:00(JST) (=unix epochで1396278000)に発生したログとして扱ってもらえる.ext_timed_batchの第3引数はtime windowの開始点を与える.設定しない場合,最初のイベントが発生した時刻を基準にbatchが実行されるようになる.外部から与えるtimestampのタイムゾーム周りがややこしい場合,明示的に指定しておいたほうが無難に思える.

注意点として,流し込むデータは時間順にソートされている必要がある.ext_timed_batchに限らず*_batchでは,あるtime windowの境界を跨ぐデータが到着した時点で,前のtime windowに対するeventが発行される.なので,データが時間順にソートされていないと,正しい結果が得られない.

複数のクエリで処理する::LOOPBACK()でeventを別のtargetに送る

あるクエリを実行した後,その結果得られたeventに対して更にクエリを投げたい場合,以前であればeventをfetchした上で対象targetに送り直す必要があった*3.Norikra v1.0.0からLOOPBACK()が導入され,この処理が自動化された.

使い方は簡単で,クエリ登録時のGroupをLOOPBACK(target名)としてやればよい. 例えば,上で紹介したクエリのGroupをLOOPBACK(metric_aggregated)とした上で,下のクエリを登録する.

select
  min(timestamp) as timestamp,
  avg(metric_sum) as metric_avg,
  max(metric_sum) as matric_max
from
  metric_aggregated.win:ext_timed_batch(timestamp * 1000, 10 min, 1396278000000L)

この状態でターゲットhost_dataにeventを送ると,最初のクエリを実行した上でその結果がターゲットmetric_aggregatedに送られ,2番目のクエリが実行される. あとはこのクエリのeventをfetchすれば,当初のお目当てのデータが得られる.

まとめ

今回は,過去に蓄積されたログデータの集計にNorikraを使ってみた.本来想定されている使い方ではない上に,ext_timed_batch使うのはオススメしないと@tagomorisさんが言っていたりするので,気付いていない落とし穴があるのかもしれない.

ただ,データ集計の条件が簡単に書ける&データストア類が必須でないという点は,コードを書いたりデータベースを運用することが日常的ではない場所で使う上で,意外とメリットになりそうな気はする.

サンプルコード

gist466004c500c6576c3644

*1:このスライドの表で言うところのschema-less dataに対するretrospection.

*2:具体的にはopsな現場

*3:Fluentdで自動化できるとはいえ,面倒臭い

Python 3.3 + oursql 0.9.3 導入メモ

いくつか躓いた箇所があったのでメモ.

正しく動作する導入方法

Launchpad上のオフィシャルページからPython 3.x向けのパッケージをダウンロードし,展開する.

$ wget https://launchpad.net/oursql/py3k/py3k-0.9.3/+download/oursql-0.9.3.zip
$ unzip oursql-0.9.3.zip

展開したパッケージ内に含まれるoursqlx/oursql.c (Cythonが生成したコード)を削除する.

$ cd oursql-0.9.3
$ rm oursqlx/oursql.c

ビルトとインストールを実行.正しくimportできることを確認.

$ python setup.py build_ext
$ python setup.py install
$ python3.3
Python 3.3.0 (default, Sep 29 2012, 22:07:38)
[GCC 4.7.2 20120921 (Red Hat 4.7.2-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import oursql
>>>

上手く動作しない導入方法とその原因

pipから導入

pipを使った場合,Python 2.x向けのパッケージがダウンロードされてしまう.そのため,setup.pyでSyntax Errorが発生し,導入できない.

$ pip install oursql
Downloading/unpacking oursql
  Downloading oursql-0.9.3.1.tar.gz (164Kb): 164Kb downloaded
  Running setup.py egg_info for package oursql
    Traceback (most recent call last):
      File "<string>", line 14, in <module>
      File "/.../.virtualenvs/oursql/build/oursql/setup.py", line 53
        print "cython not found, using previously-cython'd .c file."
                                                                   ^
    SyntaxError: invalid syntax
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):

  File "<string>", line 14, in <module>

  File "/.../.virtualenvs/oursql/build/oursql/setup.py", line 53

    print "cython not found, using previously-cython'd .c file."

                                                               ^

SyntaxError: invalid syntax

----------------------------------------
Command python setup.py egg_info failed with error code 1 in /.../.virtualenvs/oursql/build/oursql
Storing complete log in /.../.pip/pip.log

ダウンロードしたパッケージをそのまま導入

Launchpadからダウンロードしたzipファイル内には,Cythonで生成された.cファイルが同梱されている.そのため,python setup.py build_extすると,同梱されている.cファイルを利用してビルドが実行される.しかし,同梱されている.cファイルはバグの存在する過去のバージョンのPythonを用いて生成されているため,モジュールのimportに失敗してしまう.

$ wget https://launchpad.net/oursql/py3k/py3k-0.9.3/+download/oursql-0.9.3.zip
$ unzip oursql-0.9.3.zip
$ cd oursql-0.9.3
$ python setup.py build_ext
$ python setup.py install
$ python3.3
Python 3.3.0 (default, Sep 29 2012, 22:07:38)
[GCC 4.7.2 20120921 (Red Hat 4.7.2-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import oursql
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "oursql.pyx", line 52, in init oursql (oursqlx/oursql.c:23138)
ValueError: level must be >= 0
>>>

参考

Androidのjava.net.URIでIPv6 scope_idが扱えない件

タイトル通り.

URI v6linklocal = new URI("http://[fe80::1%9]/foo");

を呼ぶとURISyntaxExceptionが返ってくる.

scope_idについては,RFC 4007で文書化されている.しかしAndroid Developersに

A Uniform Resource Identifier that identifies an abstract or physical resource, as specified by RFC 2396.

書かれている通り,Androidjava.net.URIRFC 2396に準拠している.そのため,最初に示したようなURIは誤りとして扱われる.

今のところ,直接的な対処法は無さそう.

Fedora17 デスクトップ環境構築メモ

すぐ忘れるのでメモ.随時追加.

環境

OSインストール直後

MIN_UID / MIN_GIDの変更

Fedora 16から,一般ユーザのUID/GIDが1000から始まるようになった.が,NFSの関係でUID/GIDを500にしたいので設定変更. 初回起動時のウィザード(firstboot)では適当なアカウントを作っておくのがポイント.起動後,/etc/login.defsを編集.

(snip)
UID_MIN                   500
(snip)
GID_MIN                   500
(snip)

参照

Trimを有効にする

/etc/fstabを編集.オプション"discard"を追加.

UUID=xxxx /     ext4    defaults,discard 1 1
UUID=yyyy /boot ext4    defaults,discard 1 2

参照

アプリケーションのインストール & 設定

シェル関係

dotfiles類をgit resositoryから持ってくるためのスクリプト"setup-newenv.sh"を持ってきて実行.

$ sudo yum install git zsh screen
$ git clone https://gist.github.com/4411268.git
$ cd 4411268
$ sh setup-newenv.sh
$ chsh -s /usr/bin/zsh

アプリケーションのインストール

ひたすらインストール.

$ sudo yum install gcc automake kernel-devel \
gnome-tweak-tool gnome-shell-extension-dock gnome-shell-extension-alternative-status-menu gnome-shell-extension-remove-accessibility-icon \
dconf-editor gconf-editor vlgothic* \
python3 subversion tig \
haparm powertop \
thunderbird emacs ibus-skk \
virt-manager libvirt openssh-askpass remmina-plugins-rdp \
vlc

アプリケーションの設定

X

/etc/X11/xorg.conf.d/20-trackpoint.confを作成.ThinkPadのTrackpointでスクロールできるように.

Section "InputClass"
        Identifier      "Trackpoint Wheel Emulation"
        MatchProduct    "TPPS/2 IBM TrackPoint|DualPoint Stick|Synaptics Inc. Composite TouchPad / TrackPoint|ThinkPad USB Keyboard with TrackPoint|USB Trackpoint pointing device|Composite TouchPad / TrackPoint"
        MatchDevicePath "/dev/input/event*"
        Option          "EmulateWheel"          "true"
        Option          "EmulateWheelButton"    "2"
        Option          "Emulate3Buttons"       "false"
        Option          "XAxisMapping"          "6 7"
        Option          "YAxisMapping"          "4 5"
EndSection

参照

Gnome

インストールしたshell extensionをgnome-tweak-toolで有効に.

dockを左に & サイズ調整.

$ gsettings set org.gnome.shell.extensions.dock position "left"
$ gsettings set org.gnome.shell.extensions.dock size "42"

xdg-user-dirs-updateを無効に.

$ cp /etc/xdg/user-dirs.conf .config/
$ vi .config/user-dirs.conf
(snip)
enabled=False
(snip)

ハイバネート

swapが無いとhibernateが使えないことをインストール後に知ったので作成.

$ sudo time dd if=/dev/zero of=/swap bs=1M count=8192
$ sudo mkswap /swap
$ sudo swapon /swap
$ sudo filefrag -v /swap
Filesystem type is: ef53
File size of /swap is 8589934592 (2097152 blocks, blocksize 4096)
 ext logical physical expected length flags
   0       0  1308672            2048
   1    2048  1359872  1310720   4096
   2    6144  1568768  1363968   4096
(snip)
$ sudo vi /etc/sysconfig/grub
(snip)
GRUB_CMDLINE_LINUX="... resume=/dev/sda2 resume_offset=1308672"
(snip)
$ sudo grub2-mkconfig -o  /boot/grub2/grub.cfg
$ sudo vi /etc/fstab
(snip)
/swap  swap  swap  defaults  0 0
(snip)

SSHの踏み台有り・無しを,ネットワーク的居場所に応じて自動で切り替える

SSHで何がしかのホストに入るとき,自ホストのネットワーク的な居場所に応じて踏み台使用の有無を自動で切り替えるようなものを書いてみた.

やりたいこと

自分の居場所に関係無く,同一コマンドでSSHできるようにすることが最終的なゴール.

あるネットワークAからは踏み台無しでSSHできるけども,それ以外のネットワークからは踏み台を通さないとSSHできないようなホストがあるとする.

今までは,$HOME/.ssh/configに

Host univ-server1
    Host server1.example.jp
Host univ-server1-outside
    Host server1.example.jp
    ProxyCommand ssh univ-proxy nc %h %p
Host univ-proxy
    Host proxy.example.jp

と書いておいて,ネットワークAからは

$ ssh univ-server1

ネットワークA以外からは

$ ssh univ-server1-outside

と叩くようにしていた.


これを,自分の居場所に関係無く

$ ssh univ-server1

と叩けば入れるような状況にしたい.使い分けが面倒だし.

方法

$HOME/.ssh/configを以下のような感じで書く.ssh-autoproxy.pyはgithubから持ってきて適当にパスの通った場所へ.

Host univ-server*
    ProxyCommand ssh-autoproxy.py univ-proxy %h %p 192.0.2.0/24 198.51.100.0/24
Host univ-server1
    Host server1.example.jp
Host univ-server2
    Host server2.example.jp
Host univ-proxy
    Host proxy.example.jp

上記の例では,192.0.2.0/24と198.51.100.0/24以外からは踏み台が必要なホストを想定している.

これで,踏み台使用の有無に関係なく,

$ ssh univ-server1

を叩けば入れるようになる.

Know issue

  • インターネット接続性が無いと,アレな挙動を見せる
  • v4/v6 dual stackな環境だと,アレな挙動を見せる

Code

githubからどうぞ.ipaddrモジュールはPEP 3144版が必要なので注意.

ACアダプタ接続時,画面オフしないようにする (Fedora16+Gnome3)

 $ gsettings set org.gnome.settings-daemon.plugins.power sleep-display-battery 600 # バッテリー駆動時は600sで画面オフ
 $ gsettings set org.gnome.settings-daemon.plugins.power sleep-display-ac 0 # ACアダプタ接続時は画面オフしない

液晶モニタのスピーカーから音声を出すようにしていると,画面出力がオフになると同時に音声も切れてしまう,という問題がこれで解決.