IPアドレスから地域を特定する2 - GeoLite Legacy Country CSV

前回、GeoLite2 と GeoIP2 Java API 等のライブラリを使って IP アドレスから国と都市を特定しましたが、今回は GeoLite Legacy の Country CSV ファイル (IPv4用) を使って国を特定する処理を実装してみます。

なお、前回は IPv6 でも処理できましたが、今回は IPv4 のみを処理対象としています。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20141008/

はじめに

GeoLite Legacy の Country CSV ファイル GeoIPCountryWhois.csv のフォーマットは下記のようになっています。

Country CSV フォーマット
<開始IP>,<終了IP>,<開始IPの数値>,<終了IPの数値>,<国名コード>,<国名>

内容は下記の通りです。

GeoIPCountryWhois.csv
"1.0.0.0","1.0.0.255","16777216","16777471","AU","Australia"
"1.0.1.0","1.0.3.255","16777472","16778239","CN","China"
"1.0.4.0","1.0.7.255","16778240","16779263","AU","Australia"
"1.0.8.0","1.0.15.255","16779264","16781311","CN","China"
"1.0.16.0","1.0.31.255","16781312","16785407","JP","Japan"
・・・

3・4 列目の数値は IP アドレスを 32bit の正の整数値で表現したものです。

なお、GeoIPCountryWhois.csv ファイルは GeoLite Legacy Downloadable Databases の GeoLite Country の CSV/zip からダウンロードできます。

国の判定

GeoIPCountryWhois.csv ファイルを使った国の判定は下記のように処理できます。

  • (1) 指定の IPv4 アドレスを数値化
  • (2) GeoIPCountryWhois.csv ファイルの "開始IPの数値" と "終了IPの数値" との間に (1) が含まれている行の国名を返す

注意点として、IPv4 を数値化した値は 32bit の正の整数ですが、Java に unsigned int のような型はありませんので、long 型などで扱う事になります。

また、Inet4AddresshashCode() メソッドで (1) の値を取得できるのですが、unsigned な値ではありませんので下記のような方法で変換します。 (例えば、IP アドレス 150.70.96.0 を数値化した 2521194496 は、Java の int 型では -1773772800 となります)

  • (a) Java 8 から追加された Integer.toUnsignedLong(int) メソッドを使用
  • (b) 0xffffffff と AND 演算する (<Inet4Address の hashCode 値> & 0xffffffff
get_country.groovy
if (args.length < 2) {
    println '<geolite country csv file> <ip address>'
    return
}

// (a)
def toNumForIP = { Integer.toUnsignedLong(it.hashCode()) }
// (b) 以下でも可
// def toNumForIP = { it.hashCode() & 0xffffffff }

def ip = toNumForIP( InetAddress.getByName(args[1]) )

new File(args[0]).eachLine() {
    def r = it.replaceAll('"', '').split(',')

    def from = r[2] as long
    def to = r[3] as long

    if (from <= ip && ip <= to) {
        println r.last()
        System.exit(0)
    }
}

println 'Unknown'

実行結果は下記の通りです。

実行結果1
> groovy get_country.groovy GeoIPCountryWhois.csv 1.21.127.254

Japan
実行結果2
> groovy get_country.groovy GeoIPCountryWhois.csv 223.255.254.1

Singapore
実行結果3
> groovy get_country.groovy GeoIPCountryWhois.csv 192.168.1.1

Unknown

IPアドレスから地域を特定する - MaxMind DB Reader, GeoIP2 Java API

MaxMind が提供している無償の IP Geolocation DB である GeoLite と Java 用ライブラリを使って IP アドレスから国や都市を特定してみました。

今回は Java 用ライブラリの下記 2種類を試してみる事にします。

どちらも Maven のセントラルリポジトリから入手でき、MaxMind DB Reader は low-level API、GeoIP2 は high-level API な印象となっています。

また、GeoLite には下記 2種類があり、上記ライブラリで使えるのは GeoLite2 の方です。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20141004/

MaxMind DB Reader

MaxMind DB Reader を使って IP アドレスから地域を取得するコードを Groovy で実装しました。

GeoLite2 の DB ファイルを引数にして com.maxmind.db.Readerインスタンス化し、get メソッドへ IP アドレスから作成した InetAddress を渡すだけです。

Reader の get メソッドで取得する地域情報は JSONデータ (com.fasterxml.jackson.databind.JsonNode) となります。

get_location_dbreader.groovy
@Grab('com.maxmind.db:maxmind-db:1.0.0')
import com.maxmind.db.Reader

if (args.length < 2) {
    println '<maxmind db file> <ip>'
    return
}

def reader = new Reader(new File(args[0]))

println reader.get(InetAddress.getByName(args[1]))

reader.close()

実行には、GeoLite2 Free Downloadable Databases から GeoLite2 City と GeoLite2 Country のどちらかの MaxMind DB をダウンロード・解凍し .mmdb ファイルを取得しておきます。

今回は GeoLite2 City の MaxMind DB GeoLite2-City.mmdb を使って、IP アドレス 1.21.127.254 の地域判定を行ってみました。 (下記の出力結果は加工しています)

実行結果1
> groovy get_location_dbreader.groovy GeoLite2-City.mmdb 1.21.127.254

{
 "city":{"geoname_id":1850147,"names":{・・・,"en":"Tokyo",・・・,"ja":"東京",・・・}},
 "continent":{"code":"AS","geoname_id":6255147,"names":{・・・,"en":"Asia",・・・,"ja":"アジア",・・・}},
 "country":{"geoname_id":1861060,"iso_code":"JP","names":{・・・,"en":"Japan",・・・"ja":"日本",・・・}},
 "location":{"latitude":35.685,"longitude":139.7514,"time_zone":"Asia/Tokyo"},
 "registered_country":{"geoname_id":1861060,"iso_code":"JP","names":{"de":"Japan","en":"Japan",・・・,"ja":"日本",・・・}},
 "subdivisions":[{"geoname_id":1850144,"iso_code":"13","names":{・・・,"ja":"東京都"}}]
}

結果は、東京だと判定されました。

なお、上記では省略してますが、言語毎の名称は en と ja だけではなく de・es・fr・pt-BR・ru・zh-CN なども設定されています。

次に、別の IP アドレス 223.255.254.1 を試してみます。

実行結果2
> groovy get_location_dbreader.groovy GeoLite2-City.mmdb 223.255.254.1

{
 "continent":{"code":"AS","geoname_id":6255147,"names":{・・・,"en":"Asia",・・・}},
 "country":{"geoname_id":1880251,"iso_code":"SG","names":{・・・,"en":"Singapore",・・・,"ja":"シンガポール",・・・}},
 "location":{"latitude":1.3667,"longitude":103.8,"time_zone":"Asia/Singapore"},
 "registered_country":{"geoname_id":1880251,"iso_code":"SG","names":{・・・,"en":"Singapore",・・・,"ja":"シンガポール",・・・}}
}

今度の結果には city 情報を含んでおらず、特定できなかったようです。

ちなみに、地域が全く特定できなかった場合の get メソッドの結果は null となるようです。 (プライベート IP などを使えば確認できます)

GeoIP2 Java API

次に GeoIP2 の方を使った場合の Groovy スクリプトは下記のようになります。

使用する .mmdb によって DatabaseReader の使用可能なメソッドが異なるようなので注意が必要です。

今回のように GeoLite2-City.mmdb ファイルを使う場合、city メソッドが使えますが、country メソッドは使えないようです。 (UnsupportedOperationException が発生しました)

get_location_geoip2.groovy
@Grab('com.maxmind.geoip2:geoip2:2.0.0')
import com.maxmind.geoip2.DatabaseReader

if (args.length < 2) {
    println '<maxmind db file> <ip>'
    return
}

def reader = new DatabaseReader.Builder(new File(args[0])).build()

def res = reader.city(InetAddress.getByName(args[1]))

println res.country
println res.city

reader.close()

実行結果は下記の通りです。

実行結果1
> groovy get_location_geoip2.groovy GeoLite2-City.mmdb 1.21.127.254

Japan
Tokyo
実行結果2
> groovy get_location_geoip2.groovy GeoLite2-City.mmdb 223.255.254.1

Singapore

SQL から参照しているテーブルを抽出 - FoundationDB SQL Parser

FoundationDB SQL Parser を使って SQL から参照しているテーブル (from 句で使われているテーブル) を抽出する方法をご紹介します。

これを応用すれば SQL から CRUD 図を生成するような処理も比較的容易に実装できると思います。

今回使用したソースは http://github.com/fits/try_samples/tree/master/blog/20140920/

(1) SQL のパース

まずは、SQL をパースしてその結果を出力してみます。

パースは SQLParser オブジェクトの parseStatement メソッドSQL 文字列を渡すだけです。

また、parseStatement メソッドの戻り値である CursorNode オブジェクトの treePrint メソッドを呼び出せばパース結果を整形して出力してくれます。

sql_parse.groovy
@Grab('com.foundationdb:fdb-sql-parser:1.4.0')
import com.foundationdb.sql.parser.SQLParser

def parser = new SQLParser()
// SQL のパース
def node = parser.parseStatement(new File(args[0]).text)
// パース結果の出力
node.treePrint()

実行結果は下記の通りです。

実行結果
> groovy sql_parse.groovy sample1.sql

com.foundationdb.sql.parser.CursorNode@46271dd6
name: null
updateMode: UNSPECIFIED
statementType: SELECT
resultSet:  
    com.foundationdb.sql.parser.SelectNode@11bb571c
    isDistinct: false
    resultColumns:
        com.foundationdb.sql.parser.ResultColumnList@7c51f34b
        ・・・
    fromList:
        com.foundationdb.sql.parser.FromList@2a225dd7
        ・・・
        leftResultSet:
            com.foundationdb.sql.parser.FromBaseTable@125290e5
            tableName: orders
            ・・・
            correlation Name: od
            null
        rightResultSet:
            com.foundationdb.sql.parser.FromBaseTable@6fa34d52
            tableName: order_items
            ・・・
            correlation Name: oi
            null
        joinClause:
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@57576994
            operator: =
            ・・・
        [1]:
        com.foundationdb.sql.parser.FromBaseTable@1205bd62
        tableName: users
        ・・・
        correlation Name: us
        us
    whereClause: 
        com.foundationdb.sql.parser.AndNode@7ef27d7f
        operator: and
        methodName: and
        type: null
        leftOperand: 
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@490caf5f
            operator: >
            ・・・
        rightOperand: 
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@31920ade
            operator: =
            ・・・

上記で使用した SQL は下記の通り。

sample1.sql
select
    *
from
    orders od
    join order_items oi on
        od.order_id = oi.order_id
    ,
    users us
where
    od.ordered_date > ? and
    od.user_id = us.user_id

(2) テーブルの抽出

それでは、SQL 内で参照しているテーブルを抽出してみます。

テーブルの抽出には com.foundationdb.sql.parser.Visitor インターフェースを使うのが簡単です。 (いわゆる Visitor パターン)

具体的には、下記処理を実装した Visitor インターフェースの実装オブジェクトを用意し、パース結果 (CursorNode) の accept メソッドへ渡します。

  • (a) visit メソッドの引数が FromBaseTable オブジェクトだった場合にテーブル名を取得して保持する
  • (b) visit メソッドの戻り値は引数をそのまま返す

skipChildren メソッド等の Visitor インターフェースのその他メソッドは、全ノードを処理するように false を返すようにしておきます。

テーブル名 (スキーマ名付き) を文字列で取得するには FromBaseTable オブジェクトの getOrigTableName メソッドで取得した TableName オブジェクトの getFullTableName メソッドを使います。

ここで、テーブル名取得系のメソッドにはいくつかバリエーションがありますが、主なものは下記の通りです。

クラス メソッド 内容 戻り値の型
FromBaseTable getOrigTableName テーブル名 TableName
FromBaseTable getTableName テーブルの別名 (別名が無ければテーブル名) TableName
FromBaseTable getCorrelationName テーブルの別名 (別名が無ければ null) String
TableName getFullTableName スキーマ名付きテーブル名 String
TableName getTableName スキーマ名なしテーブル名 String

また、下記サンプルでは、テーブル名の重複を排除しテーブル名でソートするように TreeSet へテーブル名を登録するようにしています。

listup_tables.groovy
@Grab('com.foundationdb:fdb-sql-parser:1.4.0')
import com.foundationdb.sql.parser.FromBaseTable
import com.foundationdb.sql.parser.SQLParser
import com.foundationdb.sql.parser.Visitor

def tables = [] as TreeSet

// FromBaseTable (参照しているテーブル)を処理するための Visitor 実装
def tableSelector = [
    visit: { node -> 
        if (node instanceof FromBaseTable) {
            // テーブル名(スキーマ名付き)を取得して tables へ追加
            tables << node.origTableName.fullTableName
        }
        node
    },
    skipChildren: { node -> false },
    stopTraversal: { -> false },
    visitChildrenFirst: { node -> false}
] as Visitor

def parser = new SQLParser()

def node = parser.parseStatement(new File(args[0]).text)
// テーブル名の抽出
node.accept(tableSelector)
// テーブル名の出力
tables.each {
    println it
}

「(1) SQL のパース」 で使用した sample1.sql からテーブル名を抽出すると下記のような結果になりました。

実行結果1
> groovy listup_tables.groovy sample1.sql

order_items
orders
users

更に、下記のような SQL からテーブル名を抽出してみます。

sample2.sql
select
    us.user_id,
    us.name,
    (select count(*) from REFUNDS rf where rf.user_id = us.user_id) as refund_count1,
    (select count(*) from SCH1.REFUNDS rf where rf.user_id = us.user_id) as refund_count2
from
    Users us
where
    us.user_id in (
        select
            ou.user_id
        from (
            select
                od.user_id,
                count(*) ct
            from
                orders od
                join order_items oi on
                    od.item_id = oi.item_id
            where
                oi.item_id in (
                    select item_id from special_items
                ) and
                od.ordered_date > ?
            group by
                od.user_id
        ) ou
        where
            ou.ct >= 5
    )

結果は下記の通り、テーブル名は全て小文字になりましたが正常に抽出されています。

getFullTableName でテーブル名を取得しているので、スキーマ名を付けたテーブルと付けなかったテーブルは別途出力されています。

実行結果2
> groovy listup_tables.groovy sample2.sql

order_items
orders
refunds
sch1.refunds
special_items
users

Apache FtpServer で FTPS サーバーを組み込み実行

Apache FtpServer を使って Groovy で FTPS (FTP over SSL/TLS) サーバーの組み込み実行を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140916/

FTP サーバーの組み込み実行

まずは、普通の FTP サーバーを組み込み実行してみます。

簡単にするため、ユーザー定義をプロパティファイルから読み出すようにし、平文のパスワードを設定するようにします。

プロパティファイルは ftpserver.user.<ユーザー名>.<プロパティ>=<値> のフォーマットで定義します。

一応、下記のような設定を定義すれば動作するようです。 (書き込み不可でよければ writepermission の設定は不要)

user.properties (プロパティファイル)
ftpserver.user.u1.userpassword=p1
ftpserver.user.u1.homedirectory=./data
ftpserver.user.u1.writepermission=true

プロパティファイルからユーザー定義を読み込むには PropertiesUserManagerFactory を使います。

PropertiesUserManagerFactory の passwordEncryptor プロパティへ ClearTextPasswordEncryptor を設定する事で平文のパスワードが使えます。

ftp_server.groovy (FTP サーバー組み込み実行スクリプト
@Grab('org.apache.ftpserver:ftpserver-core:1.0.6')
@Grab('org.slf4j:slf4j-api:1.7.7')
@Grab('org.slf4j:slf4j-simple:1.7.7')
import org.apache.ftpserver.FtpServerFactory
import org.apache.ftpserver.usermanager.ClearTextPasswordEncryptor
import org.apache.ftpserver.usermanager.PropertiesUserManagerFactory

def factory = new FtpServerFactory()

// user.properties プロパティファイルを使用
factory.userManager = new PropertiesUserManagerFactory(
    file: new File('user.properties'),
    passwordEncryptor: new ClearTextPasswordEncryptor()
).createUserManager()

// サーバー起動
factory.createServer().start()

実行すると FTP サーバーが起動します。

実行例
> groovy ftp_server.groovy

[main] INFO org.apache.ftpserver.impl.DefaultFtpServer - FTP server started

cURL を使って動作確認してみます。

FTP 接続
$ curl -u u1:p1 ftp://localhost/

dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b

特に問題なく FTP サーバーへ接続できました。

FTPS サーバーの組み込み実行

次に、本題の FTPS (FTP over SSL/TLS) サーバーを組み込み実行してみます。

FTPS 実行にはキーストアが必要となりますので、JDK 付属の keytool コマンドを使って作成しておきます。

keytool -genkey -keypass <パスワード> -storepass <パスワード> -keystore <ファイル名> で最低限のキーストアファイルを作成できます。

「姓名」とか聞かれますが、テスト用に使うだけなら全て Unknown のままで構いません。

キーストアの作成例
> keytool -genkey -keypass sample -storepass -keystore sample.jks

姓名は何ですか。
  [Unknown]:
組織単位名は何ですか。
  [Unknown]:
組織名は何ですか。
  [Unknown]:
都市名または地域名は何ですか。
  [Unknown]:
都道府県名または州名は何ですか。
  [Unknown]:
この単位に該当する2文字の国コードは何ですか。
  [Unknown]:
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknownでよろしいで
すか。
  [いいえ]:  はい

上記でカレントディレクトリへ sample.jks ファイルが作成されます。

FTPS を使えるようにするには SslConfiguration (上記で作成した sample.jks を keystoreFile へ設定) を設定した ListenerFtpServerFactoryaddListener します。

ftps_server.groovy (FTPS サーバー組み込み実行スクリプト
・・・
import org.apache.ftpserver.listener.ListenerFactory
import org.apache.ftpserver.ssl.SslConfigurationFactory
・・・

def factory = new FtpServerFactory()

def ssl = new SslConfigurationFactory(
    keystoreFile: new File('sample.jks'),
    keystorePassword: 'sample'
)

def listenerFactory = new ListenerFactory(
    sslConfiguration: ssl.createSslConfiguration()
)

factory.addListener('default', listenerFactory.createListener())

factory.userManager = new PropertiesUserManagerFactory(
    ・・・
).createUserManager()

factory.createServer().start()

実行すると FTP・FTPS のどちらでも接続できるサーバーが起動します。

実行例
> groovy ftps_server.groovy

[main] INFO org.apache.ftpserver.impl.DefaultFtpServer - FTP server started

まずは FTP で動作確認してみます。

FTP で接続
$ curl -u u1:p1 ftp://localhost/

dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b

特に問題なく FTP で接続できました。

次に --ssl -k オプションを追加指定して FTPS 接続してみます。 (正式な証明書を使っていれば -k オプションは不要)

今回は -v オプションも指定して詳細出力してみました。

FTPS で接続
$ curl --ssl -k -u u1:p1 -v ftp://localhost/

・・・
> AUTH SSL
・・・
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server key exchange (12):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / DHE-DSS-AES128-GCM-SHA256
* Server certificate:
*        subject: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=Unknown
*        start date: 2014-09-15 11:46:38 GMT
*        expire date: 2014-12-14 11:46:38 GMT
*        issuer: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=Unknown
*        SSL certificate verify result: self signed certificate (18), continuing anyway.
・・・
dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b
・・・

SSL 接続を行っている事が確認できました。

Vagrant で VirtualBox 上の CentOS 7 へ固定 IP を設定

はじめに

Vagrant を使って VirtualBox 上で CentOS 7 を起動する際に、固定 IP を設定しようとするとエラーが発生しました。

Vagrantfile
・・・
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.box = "centos7"
    # ゲスト OS 間の通信用に固定 IP アドレスを設定
    config.vm.network "private_network", ip: "192.168.100.11", virtualbox__intnet: "intnet"
end

CentOS 7 の Box を centos7 という名称で Vagrant へ登録済みです。

なお、virtualbox__intnet の設定が無いとホスト OS と通信するための IP アドレス設定 (ホストオンリーアダプター) になるようです。 (この場合、その IP でゲスト OS 間通信はできない模様)

vagrant up 時のエラー内容
> vagrant up

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos7'...
・・・
==> default: Configuring and enabling network interfaces...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

ARPCHECK=no /sbin/ifup eth1 2> /dev/null

Stdout from the command:

ERROR    : [/etc/sysconfig/network-scripts/ifup-eth] Device eth1 does not seem to be present, delaying initialization.


Stderr from the command:

CentOS 7 自体の起動は成功しており (SSH でログイン可能)、固定 IP アドレスの設定に失敗しています。

エラーの原因

原因は下記の通りです。

  • CentOS 7 では NIC の名称がデフォルトで eth<番号> (例 eth0) では無くなっている
  • VagrantRedHat 系 OS 設定用のプラグインでは今のところ eth<番号> 以外の NIC 名を処理できない

要するに、VirtualBox 上で CentOS 7 を実行すると NIC 名が下記のようになりますが、VagrantRedHat 系 OS のネットワーク設定を担うスクリプトplugins/guests/redhat/cap/configure_networks.rb) では、今のところ下記 NIC 名に対応していません。

  • enp0s3 (アダプター1)
  • enp0s8 (アダプター2)

実は、Fedora のネットワーク設定を担うスクリプトplugins/guests/fedora/cap/configure_networks.rb) では biosdevname -d コマンドを使って NIC 名を取得しており、上記 NIC 名に対応できているようです。

回避方法

本件のエラーを回避するには下記のような方法が考えられます。 (プラグインを用いる等、他の方法も色々あると思います)

  • (a) CentOS の設定を変更して古い NIC 名 (eth<番号>) を使うようにする
  • (b) RedHat 用の configure_networks.rb へ Fedora 用の configure_networks.rb の処理内容を適用する
  • (c) プロビジョニングで固定 IP を設定する

個人的に、本件は Vagrant 側で対応すべき問題だと思うので、今回は (b) と (c) の対応方法をご紹介します。

(b) RedHat 用の configure_networks.rb へ Fedora 用の configure_networks.rb の処理内容を適用する

Fedora の configure_networks.rb の実装内容を RedHat の configure_networks.rb へ適用する方法です。

configure_networks.rb の内容を書き換えてしまうので、あまり望ましい方法では無いかもしれませんが、対応手順は下記のようになります。

  • (1) plugins/guests/redhat/cap/configure_networks.rb のバックアップをとる
  • (2) Fedora 用の configure_networks.rb (plugins/guests/fedora/cap/configure_networks.rb) を RedHat 用の configure_networks.rb (plugins/guests/redhat/cap/configure_networks.rb) へ上書きコピー
  • (3) (2) で更新した RedHat 用の configure_networks.rb ファイルを編集し、モジュール名を GuestFedora から GuestRedHat へ変更
plugins/guests/redhat/cap/configure_networks.rb の内容
・・・
module VagrantPlugins
  module GuestRedHat
  ・・・

なお、Windows 用の Vagrant の場合は embedded\gems\gems\vagrant-1.6.3 に plugins ディレクトリがあります。

Fedora 用の configure_networks.rb を適用して問題ないかを十分検証したわけではありませんが、一応 vagrant up でエラーが発生せず固定 IP アドレスを設定できました。

実行例
> vagrant up

・・・
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
・・・

enp0s8 へ IP アドレスが設定されている事を確認。

> vagrant ssh
Last login: Mon Aug 25 06:50:04 2014 from 10.0.2.2
[vagrant@localhost ~]$ ip addr
・・・
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP
qlen 1000
    ・・・
    inet 192.168.100.11/24 brd 192.168.100.255 scope global enp0s8
    ・・・

(c) プロビジョニングで固定 IP を設定する方法

Vagrant のプロビジョニング機能を使い ip addr add コマンド等で IP アドレスを設定するだけなので、(b) に比べると安全な方法だと思います。

まずは、Vagrantfile へ下記のように provision の定義を追加し、:inline に対して固定 IP アドレスを enp0s8 へ設定し有効化するコマンドを記載します。

Vagrantfile
・・・
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.box = "centos7"

    config.vm.network "private_network", ip: "192.168.100.11", virtualbox__intnet: "intnet"
    # プロビジョニングによる固定 IP の設定
    config.vm.provision :shell, :inline => "sudo ip addr add 192.168.100.11/24 dev enp0s8; sudo ip link set enp0s8 up"
end

vagrant up のネットワーク設定でエラーが発生し途中終了するので、その後に vagrant provision を実行すると固定 IP アドレスを設定します。

実行例1
> vagrant up

・・・
==> default: Configuring and enabling network interfaces...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

ARPCHECK=no /sbin/ifup eth1 2> /dev/null

Stdout from the command:

ERROR    : [/etc/sysconfig/network-scripts/ifup-eth] Device eth1 does not seem to be present, delaying initialization.
・・・

vagrant provision でプロビジョニング機能を実行します。

> vagrant provision
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: RTNETLINK answers: File exists

enp0s8 へ IP アドレスが設定されている事を確認。

> vagrant ssh
・・・
[vagrant@localhost ~]$ ip addr
・・・
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    ・・・
    inet 192.168.100.11/24 brd 192.168.100.255 scope global enp0s8
    ・・・

vagrant up でエラーを発生させないようにする

vagrant up の後に vagrant provision を別途実行するのが面倒な場合。

plugins/guests/redhat/cap/configure_networks.rb を下記のように編集し、ifup が失敗してもエラーを発生させないようにすれば vagrant up 時にプロビジョニングも適用できるようになります。 (ただし、マウントエラーのような他のエラーを発生させない事が前提)

plugins/guests/redhat/cap/configure_networks.rb 編集例 - ifup の実行処理へ error_check: false を追加 (61 行目)
machine.communicate.sudo("ARPCHECK=no /sbin/ifup eth#{interface} 2> /dev/null", error_check: false)
実行例2 - configure_networks.rb を編集した場合
> vagrant up --provision

Bringing machine 'default' up with 'virtualbox' provider...
・・・
==> default: Configuring and enabling network interfaces...
・・・
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: RTNETLINK answers: File exists

なお、vagrant up でプロビジョニングが適用されるのは初回起動時のみのようですので、2回目以降は vagrant up --provision でプロビジョニングを強制適用して起動する必要があります。

成功するまで次を試すような処理へ Either モナドを適用 - FunctionalJava

成功するまで次の処理を試していくような処理に対して Either モナドを適用してみました。

使用した環境は下記の通りです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140825/

はじめに

Either モナドは 2つの異なる値 (Left と Right) を扱う場合に使用し、一般的には失敗(エラー)を伴う処理に対して下記のような使い方をします。

Leftの値 Rightの値
失敗時のエラー内容(例外) 成功時の値

ただし今回は、下記のように処理が成功するまで元の値が Left へ保持されるような使い方をしてみました。

Leftの値 Rightの値
元の値 成功時の値

具体的には、下記のような処理を試します。

  • 実行時引数で指定した日付文字列に対して、Date オブジェクトへのパースが成功するまで次のパース処理を試していく

Either モナドを使わなかった場合

まずは Either モナドを使わず、普通に Java で実装してみました。

下記 (1) ~ (5) のパース処理を順に試して、例外が発生せず null では無い値を返した時点でパース処理を終了します。

  • (1) ISO-8601 タイムゾーン無しの日付文字列(例 2014-08-25T13:20:00)をパース
  • (2) ISO-8601 の日付文字列(例 2014-08-25T13:20:00+09:00)をパース
  • (3) ISO-8601 タイムゾーン付きの日付文字列(例 2014-08-25T13:20:00+09:00[Asia/Tokyo])をパース
  • (4) "yyyy-MM-dd HH:mm:ss" をパース
  • (5) "now" の場合に Date オブジェクトを返す

java.time のクラスを使う必要も無かったのですが、ついでに試してみました。

DateParse.java
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.function.Function;

public class DateParse {
    public static void main(String... args) {
        // SimpleDateFormat を使ったパース
        Function<String, Function<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Date res = parseDate(
            args[0],
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), //(1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.apply("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }

    // 日付文字列のパース
    @SafeVarargs
    public static Date parseDate(String date, Function<String, Date>... funcList) {
        for (Function<String, Date> func : funcList) {
            try {
                Date res = func.apply(date);

                if (res != null) {
                    return res;
                }
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
            }
        }
        return null;
    }
}
実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java DateParse "2014-08-25T13:20:00"
------
Mon Aug 25 22:20:00 JST 2014
実行結果2 - (2) で成功
> java DateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Mon Aug 25 13:20:00 JST 2014
実行結果3 - (3) で成功
> java DateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Mon Aug 25 13:20:00 JST 2014
実行結果4 - (4) で成功
> java DateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Mon Aug 25 13:20:00 JST 2014
実行結果5 - (5) で成功
> java DateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Mon Aug 25 11:10:42 JST 2014
実行結果6 - 全失敗
> java DateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
null

Either モナドを使った場合

同様の処理を FunctionalJava の Either を使って実装します。

Left にパース前の値(String)、Right にパース後の値(Date)を格納できるように Either の型を Either<String, Date> とします。

今回は Left の値 (String) に対して順次パース処理を試すようにしたいので、Either.left() で取得した Either.LeftProjection オブジェクトへパース処理を bind しています。

bind の引数には 「普通の値を取って Either を返す」 処理 (下記では F<String, Either<String, Date>>) を与える必要があるので、下記では eitherK というメソッドを定義し、通常のパース処理 (文字列を取って Date を返す F<String, Date>) を変換してから bind するようにしています。

なお、java.util.function.Function は使わず、全面的に FunctionalJava の fj.F を使うようにしました。

EitherDateParse.java
import fj.F;
import fj.data.Either;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class EitherDateParse {
    public static void main(String... args) {
        F<String, F<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Either<String, Date> res = parseDate(
            Either.left(args[0]),
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), // (1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.f("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }


    @SafeVarargs
    public static Either<String, Date> parseDate(Either<String, Date> date, F<String, Date>... funcList) {
        for (F<String, Date> func : funcList) {
            date = date.left().bind( eitherK(func) );
        }
        return date;
    }

    // F<S, T> の処理が例外発生か null であれば Left、そうでなければ Right を返す処理へ変換
    private static <S, T> F<S, Either<S, T>> eitherK(final F<S, T> func) {
        return s -> {
            try {
                T res = func.f(s);
                return (res == null)? Either.left(s): Either.right(res);
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
                return Either.left(s);
            }
        };
    }
}

実行結果は、Left や Right に包まれている部分を除けば Either を使わなかった場合と基本的に同じです。 (全失敗の場合は異なります)

実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00"
------
Right(Mon Aug 25 22:20:00 JST 2014)
実行結果2 - (2) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果3 - (3) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果4 - (4) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果5 - (5) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Right(Mon Aug 25 11:59:01 JST 2014)
実行結果6 - 全失敗
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
Left(2014-08-25)

Either モナドを使った場合2 - 機能追加

次に下記のような機能を追加してみました。

  • (a) パース全失敗の場合は RuntimeException を throw する
  • (b) パース成功の場合は、その次の日の 0時 0分 0秒 の Date オブジェクトと共に 2要素の vectorV2) へ格納する

Either モナドを使えば if 文などでいちいち条件判定しなくても変換処理等を合成できるのが利点だと思います。

今回は、Either の結果出力に System.out.println() を直接使わず fj.Show.println() を使うようにしてみました。

EitherDateParse2.java
・・・
import fj.data.vector.V;
import fj.data.vector.V2;
import fj.Show;

import org.apache.commons.lang3.time.DateUtils;
・・・
public class EitherDateParse2 {
    public static void main(String... args) {
        ・・・
        Either<String, Date> res = parseDate(
            ・・・
        );

        // (a) パースが全失敗の場合は RuntimeException を throw
        res.left().bind( s -> {
            throw new RuntimeException("failed parse");
        });

        // (b) パース成功の場合は、次の日の 0時0分0秒 の結果と共に V2 へ格納して返す
        Either<String, V2<Date>> res2 = res.right().bind( d ->
            Either.right(
                V.v(d, DateUtils.truncate(DateUtils.addDays(d, 1), Calendar.DATE))
            )
        );

        System.out.println("------");

        Show.<String, V2<Date>>eitherShow(Show.anyShow(), Show.v2Show(Show.anyShow())).println(res2);
    }
    ・・・
}
実行結果 - 成功した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(<Mon Aug 25 13:20:00 JST 2014,Tue Aug 26 00:00:00 JST 2014>)
実行結果 - 全失敗した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
Exception in thread "main" java.lang.RuntimeException: failed parse
        at EitherDateParse2.lambda$main$19(EitherDateParse2.java:37)
        ・・・

Java用 SSH クライアントライブラリ - ganymed-ssh2, sshj, JSch, Apache SSHD

主要な Java 用の SSH クライアントライブラリを使って簡単なサンプルを作成してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140814/

はじめに

Vagrant で実行中のゲスト OS に対して SSHls -al を実行し、その結果を出力するだけの単純な処理を Groovy で実装してみる事にします。

今回の実装のポイントとなる点は下記のようなところだと思います。

  • (1) Vagrant秘密鍵ファイル insecure_private_key で認証
  • (2) 未知のホスト鍵をチェックしないようにする (ssh における StrictHostKeyChecking=no と同等の処理)

Vagrant秘密鍵ファイル insecure_private_keyスクリプトファイルと同じディレクトリへコピーしておく事とします。

また、本件のサンプルコードではエラー処理や文字コード等を配慮していない点にご注意ください。

Ganymed SSH-2

元のオリジナルはかなり以前にメンテナンスが停止されていたり、Orion SSH や Trilead SSH のような同系統のライブラリが他にあったりと、どれを使えばよいのか分かり難い印象がありますが、とりあえず https://code.google.com/p/ganymed-ssh-2/ でメンテナンス継続されているものを使えば良さそうです。

ホスト鍵のチェックは connect メソッドで制御できるようです。(チェックしたければ ServerHostKeyVerifier を渡す)

ganymed_sample.groovy
@Grab('ch.ethz.ganymed:ganymed-ssh2:262')
import ch.ethz.ssh2.Connection

def con = new Connection('127.0.0.1', 2222)
con.connect()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
if (con.authenticateWithPublicKey('vagrant', new File('insecure_private_key'), null)) {
    def session = con.openSession()

    session.execCommand('ls -al')

    println session.stdout.text

    session.close()
}

con.close()
実行結果
> groovy ganymed_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

sshj - SSHv2 library for Java

authPublickeyconnect の後に実行する必要があります。

また、未知のホスト鍵をチェックしないようにするには、addHostKeyVerifierPromiscuousVerifier を設定します。 (デフォルトではチェックするようになっています)

sshj_sample.groovy
@Grab('net.schmizz:sshj:0.10.0')
@Grab('org.slf4j:slf4j-nop:1.7.7')
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.common.IOUtils
import net.schmizz.sshj.transport.verification.PromiscuousVerifier

def client = new SSHClient()
// (2) 未知のホスト鍵をチェックしない
client.addHostKeyVerifier(new PromiscuousVerifier())

client.connect('127.0.0.1', 2222)
// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
client.authPublickey('vagrant', client.loadKeys('insecure_private_key'))

def session = client.startSession()

def cmd = session.exec('ls -al')

println IOUtils.readFully(cmd.inputStream)

session.close()
client.disconnect()
実行結果
> groovy sshj_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

JSch - Java Secure Channel

未知のホスト鍵をチェックしないようにするには setConfigStrictHostKeyCheckingno にします。

jsch_sample.groovy
@Grab('com.jcraft:jsch:0.1.51')
import com.jcraft.jsch.JSch

def jsch = new JSch()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
jsch.addIdentity('insecure_private_key')
// (2) 未知のホスト鍵をチェックしない
jsch.setConfig('StrictHostKeyChecking', 'no')

def session = jsch.getSession('vagrant', '127.0.0.1', 2222)

session.connect()

def ch = session.openChannel('exec')
ch.setCommand('ls -al')

ch.connect()

println ch.inputStream.text

ch.disconnect()
session.disconnect()
実行結果
> groovy jsch_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

Apache SSHD

Apache MINA をベースに SSH サーバーとクライアントの両方の API を備えたライブラリです。 他のライブラリと比べると多少面倒なように思います。

close メソッドに false を指定すると graceful にクローズします。

sshd_sample.groovy
@Grab('org.apache.sshd:sshd-core:0.12.0')
@Grab('org.apache.mina:mina-core:3.0.0-M2')
@Grab('org.bouncycastle:bcpkix-jdk15on:1.51')
@Grab('org.slf4j:slf4j-nop:1.7.7')
import org.apache.sshd.ClientChannel
import org.apache.sshd.SshClient
import org.apache.sshd.common.keyprovider.FileKeyPairProvider

def client = SshClient.setUpDefaultClient()
client.start()

def session = client.connect('vagrant', '127.0.0.1', 2222).await().getSession()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
def keyProvider = new FileKeyPairProvider(['insecure_private_key'] as String[])
session.addPublicKeyIdentity(keyProvider.loadKeys().iterator().next())

def auth = session.auth()
// verify の実行が必要
auth.verify()

if (auth.isSuccess()) {
    def ch = session.createExecChannel('ls -al')
    def baos = new ByteArrayOutputStream()

    ch.setOut(baos)

    ch.open()
    // コマンド完了まで待機
    ch.waitFor(ClientChannel.CLOSED, 0)
    ch.close(false)

    println baos.toString()
}

session.close(false)
client.stop()
実行結果
> groovy sshd_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo