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 ファイルを使った国の判定は下記のように処理できます。
注意点として、IPv4 を数値化した値は 32bit の正の整数ですが、Java に unsigned int のような型はありませんので、long 型などで扱う事になります。
また、Inet4Address
の hashCode()
メソッドで (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
メソッドへ渡します。
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 へ設定) を設定した Listener
を FtpServerFactory
へ addListener
します。
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 を設定しようとするとエラーが発生しました。
- Vagrant 1.6.3
- VirtualBox 4.3.12 for Windows hosts
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) では無くなっている - Vagrant の RedHat 系 OS 設定用のプラグインでは今のところ
eth<番号>
以外の NIC 名を処理できない
要するに、VirtualBox 上で CentOS 7 を実行すると NIC 名が下記のようになりますが、Vagrant で RedHat 系 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 モナドを適用してみました。
使用した環境は下記の通りです。
- Java SE 8u20
- FunctionalJava 4.2 beta1
ソースは 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要素の vector (
V2
) へ格納する
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 に対して SSH で ls -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
authPublickey
は connect
の後に実行する必要があります。
また、未知のホスト鍵をチェックしないようにするには、addHostKeyVerifier
で PromiscuousVerifier
を設定します。 (デフォルトではチェックするようになっています)
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
未知のホスト鍵をチェックしないようにするには setConfig
で StrictHostKeyChecking
を no
にします。
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