bit

複数のNICを持つ場合のデフォルトゲートウェイ

先日、仮想Linuxマシン(CentOS7)に複数のネットワークインターフェイス(以下、NICa、NICbとする)を持たせて、NICbが接続している側のゲートウェイをマシン全体のデフォルトゲートウェイ(以下、デフォゲ)にしようとしたのに、起動時にどうしてもルーティングテーブルのデフォゲがNICa側のゲートウェイになって困った。

状況としては、以下の通り:

  • /etc/sysconfig/networkのGATEWAY設定はNICb側
  • nmcliコマンドで、NICa, bともにそれぞれのゲートウェイを設定

調べたところ、以下のことが分かった。

  • NICには優先順位がある。("in numerically ascending order"らしい)
  • NICは、最初に/etc/sysconfig/networkのGATEWAY設定、次に個々のifcfgファイル (/etc/sysconfig/network-scripts内)にあるGATEWAY設定を読み込み、最後のGATEWAY設定がそのNICゲートウェイとして設定される(参考)。
  • そして、全体ルーティングテーブルのデフォルト設定は先勝ちで、優先順位の高いNICゲートウェイがデフォゲになる。

今回のケースでは、NICaのゲートウェイ設定が優先的にデフォゲとして設定されてしまい、/etc/sysconfig/networkのGATEWAYは無視されていた。

ではどうすべきか。原理主義的に行くなら、デフォゲは使わず必要なルーティングはすべて静的に設定というのもある。それは大変という場合は、以下がおすすめ。

メリットは以後の操作をすべてnmcliで出来るようになるのと、グローバル設定をなくせること。デメリットは、sysconfig/network のGATEWAY設定しか知らない人を混乱させるかもということ。まあ、それはコミュニケーションを取ればよい。

そんな感じにすると以下のようになる(192.168.0.0側がNICa、192.168.20.0側がNICbで外につながっている)。

# grep GATEWAY /etc/sysconfig/network
# nmcli con mod enp0s3 ipv4.addresses "192.168.0.98/24"
# nmcli con mod enp0s8 ipv4.addresses "192.168.20.98/24 192.168.20.1"
# nmcli con mod enp0s3 ipv4.never-default yes
# nmcli con mod enp0s8 ipv4.never-default no
# systemctl restart network
# nmcli con show enp0s3 | egrep  'ipv4.(addresses|never)'
ipv4.addresses:                         { ip = 192.168.0.98/24, gw = 0.0.0.0 }
ipv4.never-default:                     yes
# nmcli con show enp0s8 | egrep  'ipv4.(addresses|never)'
ipv4.addresses:                         { ip = 192.168.20.98/24, gw = 192.168.20.1 }
ipv4.never-default:                     no
# ip route
default via 192.168.20.1 dev enp0s8  proto static  metric 1024
192.168.0.0/24 dev enp0s3  proto kernel  scope link  src 192.168.0.98
192.168.20.0/24 dev enp0s8  proto kernel  scope link  src 192.168.20.98
# ping -c 2 8.8.8.8
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=48 time=50.6 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=48 time=42.2 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1003ms
rtt min/avg/max/mdev = 42.265/46.482/50.699/4.217 ms

ボンディングの場合も似たようなポリシーでできると思うが、サブアドレスで一つのNICにまったくセグメントの異なるアドレスを設定している場合は、どうすべきかは知らない。

ちなみに、NICごとにゲートウェイが設定できてしまうのは、デフォゲ先にあるルータが通信できなくなった場合のバックアップゲートウェイを設定する場合に必要な機能とのこと(参考:マルチホーム コンピュータのデフォルトゲートウェイ設定)らしいのだけど、では障害発生時にどうやって切り替えるかは知らない。RHEL HAアドオンみたいなHAミドルでがガリガリ書くしかないような気がする。

bashのwhileループ内の変数をループ外で使う

化石みたいな問題。
よく知られていることだが、下記のような各行を足し算するシェルスクリプトを書くと、最後の結果が0になってびっくりする。

$ cat get_nothing.sh
__calculate_total() {
  local total=0 fpath=$1

  cat $fpath | while read line; do
    total=$(( $total + ${line:-0} ))
  done

  echo "Total reaches $total"
}

__calculate_total $@
$ cat numbers.txt
101
-40
4566
32
$ bash ./get_nothing.sh numbers.txt
Total reaches 0

これはwhile等の制御文だったかパイプの先だったか忘れたが、bashが別プロセス起こしちゃうんで、戻ってくるときにせっかく変更した変数の内容が破棄されるかららしい。
解決方法としては、UNIX FAQとかにexecでファイルディスクリプタを一時的に変更して云々な話が書かれていたりするが、現在の超高度に発達したSIerの開発現場において、そんなマイナーなテク使われても誰も読めなくて迷惑するのでやめてほしい。あと、一時ファイルにwhileループごとに値を書き出すのもやめて。コードレビューする人の 疲労感を無駄に積み増すんじゃない。

一番よい解決方法は、bashやめてPerlなりPythonなりまともな言語で書く。

でも、現在の超高度に発達したSIerの開発現場では、言語一つ変更するのに、周囲とのネゴとか試験仕様書の更新とか、誰も幸せにならないエネルギーが必要なので、シェルスクリプトの範囲内で何とかしてみる方法を考えてみた。

たぶんこう書くと16倍(当社比)くらいすっきり感が出ると思う。

$ cat goes_well.sh
__sum_up() {
  local line total=0

  while read line; do
    total=$(( $total + ${line:-0} ))
  done

  echo $total
  return 0
}

main() {
  local total=0 fpath=$1

  total=`cat $fpath | __sum_up`
  echo "Total reaches $total"

  return 0
}

つまり、while部分を別関数に切り出す。切り出しただけなので、行数はそれほど変わらない。
ファイル読み込み中にエラーが出た場合の処理を書きたい場合も容易に拡張できる。

$ cat goes_well2.sh
__sum_up() {
  local line total=0

  while read line; do
    if [[ "$line" =~ ^-?[0-9]+$ ]]; then
      total=$(( $total + ${line} ))
    else
      return 1
    fi
  done

  echo $total
  return 0
}

main() {
  local rc total=0 fpath=$1

  # Todo: check the fpath is valid.

  total=`cat $fpath 2>/dev/null | __sum_up`
  rc=$?
  if [ $rc -ne 0 ]; then 
    return $rc
  fi

  echo "Total reaches $total"

  return 0
}

main $@
$ bash ./goes_well2.sh numbers.txt ;echo "rc=$?"
Total reaches 4659
rc=0
$ cat numbers2.txt
101
-40
NOT NUMBER
4566
32
$ bash ./calc_total.sh numbers2.txt ;echo "rc=$?"
rc=1

欠点は、"total="のパイプより前のコマンドでエラーが出ても$?が0になってしまう点で、そこのところはあまりいい書き方思いつかないので、まじめに直前にファイル存在チェック書いたほうが読みやすくてよいと思う。

まあでも、やはりbashを使わないのが正解。

ちなみにfor 文で

for nb in `cat numbers.txt`; do
  ..(足し算)..
done
echo $total

などとしても動くが、numbers.txt のファイルの中身が文字列としてメモリに全展開されるので、ファイルサイズが小さいことが保証されていない限りお勧めしない。

ANTLR v4 の文法ファイルのサンプル

最近までいろいろ ANTLR v4 をいじっていて、ようやく文法ファイルの書き方がわかってきた。
でも初心者向けの解説記事を書けるほど頭の中がこなれていないので、サンプルだけ示すことにする。

こんなファイルを考える。

"This"
 "is
   a "
 "\"quoted\" text."
""

このファイルから、「This」「is<改行> a 」「"quoted" text.」「(空文字)」という四つの文字列を認識するパーサを書きたいとする。

ファイル解析のプログラムはスクラッチからがりがり書いてもよいけど、文法が複雑になってくるとプログラムが汚くなりがちである。そこで、構文解析プログラムジェネレータである ANTLR が便利ということになる。

ANTLR 自体の細かい説明は面倒なのでしないけど、要は文法ファイルを与えるとその文法に従ったファイルを解釈するための Java プログラムを生成してくれる。
といった流れは簡単なのだが、文法ファイルの書き方はそれほど初心者には優しくない。

文法ファイルは、字句解析部分と構文解析部分の二つの部分から成り立つが、最近はこれを個別のファイルとして書くのが流行り(?)である。

まず字句解析の文法ファイル StringLexer.g4

lexer grammar StringLexer;

QUOTE: '"' -> pushMode(STRING_MODE) ;
WS: ( ' ' | '\t' ) -> skip;
NL : ( '\r' '\n'? | '\n') -> skip ;
OUTER_CHAR: . -> skip ;

mode STRING_MODE;
RQUOTE: '"' -> popMode ;
ESC: '\\' -> skip, pushMode(ESCCHAR_MODE) ;
NOT_ESCCHAR: . ;

mode ESCCHAR_MODE;
ESCCHAR: ( '\\' | '"' ) -> popMode ;

「lexer」の行はおまじない。
最初の「mode」より上のグローバル部分と、二つの「mode」で、グローバル部分を一つのモードと考えると、合計三つのモードから構成される。
それで、グローバルの上のほうにある「QUOTE」の行では、引用符(") を読んだら、STRING_MODE というモードに移行する、という意味であり、STRING_MODE の最初の「RQUOTE」は、引用符を読み込んだら元のモードに戻るという意味である。

STRING_MODE は引用符の中を処理する部分であり、ESCCHAR_MODE はさらにその中のエスケープ文字を処理する部分である。まあ、あとは説明しなくても分かるでしょ。
エスケープ可能な文字はここでは2種類 (\と")に限定しているが、字句解析の段階でエラーにすると現場プログラム的にはエラー処理が面倒になるので、任意文字にしてもよいかもしれない。

この mode はとても便利である。字句解析ルールは何もしないとグローバルに効いてしまうので、その場合、字句解析プログラムは文字列の中だろうがコメントだろうが、お構いなしに字句解析ルールを当てはめてしまう。
実は最近まで mode を知らなかったので、このグローバル対策のためのバッドノウハウの塊みたいな文法ファイルを書いていたが、mode を使うときれいに字句解析文法ファイルを書ける。素晴らしい。
ただし mode は字句解析文法部分にしか使えないので、必然的に字句解析と構文解析とで文法ファイルを分ける必要が出てくる。

次に構文解析の文法ファイル。

StringParser.g4

parser grammar StringParser;
options { tokenVocab=StringLexer; }

strings
  : string*
  ;

string returns [ String text ]
  :
  QUOTE v=string_text RQUOTE {
    $text = $v.text;
  }
  ;

string_text
  :
  ( ESCCHAR
  | NOT_ESCCHAR
  )*
  ;

options のところが、さっきの字句解析ファイルのトークンを使っているよという宣言。
string ルールのところの RQUOTE の後の部分が、アクションというやつである。何もしないと 「"This"」と左右の引用符がくっついたままの文字列しかプログラムから取得できず面倒になるので、string_text のところだけを取り出して $text という変数に返すようにしている。

二つの文法ファイルができたらあとは、ANTLRに食わせて Java コードを生成させて、動かすだけである。

java -classath antlr-4.2.2-complete.jar org.antlr.v4.Tool -o <出力先> -package<パッケージ> StringLexer.g4 StringParser.g4

Java からの呼び出し方は以下の通り:

public void test(String filepath) throws IOException {
  ANTLRFileStream fromFileStream = new ANTLRFileStream(filepath);
  StringLexer lexer = new StringLexer(fromFileStream);
  CommonTokenStream tokens = new CommonTokenStream(lexer);

  StringParser parser = new StringParser(tokens);
  StringsContext stringsContext = parser.strings();
  for (StringParser.StringContext stringContext : stringsContext.string()) {
    System.out.println("[[" + stringContext.text + "]]");
  }
}

出力結果。

[[This]]
[[is
   a ]]
[["quoted" text.]]
[[]]

ジョギング音楽

ジョギングのときに聞いている曲の紹介。2年前に書いた記事から、だいぶ変わったので再編集。

曲名 アーティスト名 BPM リンク
Holy Ground Taylor Swift 156 iTunes / Youtube
I'll Fight Daughtry 156 iTunes / Youtube
Blurry Puddle of Mudd 157 iTunes / Youtube
Everything Has Changed Taylor Swift 160 iTunes / Youtube
Never Say Never The Fray 160 iTunes / Youtube
What About Now Daughtry 162 iTunes / Youtube
Keep It Together Puddle of Mudd 162 iTunes
We Don't Have To Look Back Now Puddle of Mudd 163 Youtube
Back to the Beginning Again Switchfoot 167 iTunes / iTunes
We Are Never Ever Getting Back Together Taylor Swift 171 iTunes / Youtube
Wild Heart Daughtry 177 iTunes / Youtube

Puddle of Mudd は、ボーカルがよく捕まるためか最近はあまり新曲を出していないが、BPMがちょうどよいので2年前から生き残っている。

Permutation: Javaでの順列組合せの列挙

順列を全部生成するコードを書く必要があって世の中のコードを探したのだけど、あまりきれいなコードがなかったので自分で書いた、単なる備忘録的記事。
まあ、このコードがきれいかというと、うーん、でも使いやすさとシンプルさでは、いろいろ検索した中ではきれいだと思うんだけど。

public class Permutation<T extends <Serializable> {

  private int baseIndex;
  private int index;
  private T[] objs;

  private Permutation<T> subPermutation;

  public Permutation(T[] objs) {
    this(0, 0, objs.clone());
  }

  private Permutation(int baseIndex, int index, T[] objs) {
    if (objs == null || objs.length == 0) {
      throw new IllegalArgumentException();
    }

    this.baseIndex = baseIndex;
    this.index = index;
    this.objs = objs;

    if (this.index < this.objs.length - 1) {
      this.subPermutation =
        new Permutation<T>(this.baseIndex + 1, this.index + 1, this.objs);
    }
  }

  public T[] getTarget() {
    return this.objs;
  }

  public boolean next() {
    if (this.subPermutation == null) {
      return false;
    }

    boolean result = this.subPermutation.next();
    if (result == true) {
      return true;
    }

    this.swap(this.baseIndex, this.index);

    ++this.index;
    if (this.objs.length <= this.index) {
      this.index = this.baseIndex;
      return false;
    }

    this.swap(this.index, this.baseIndex);
    return true;
  }

  @Override
  public String toString() {
    // snip.
  }

  private void swap(int index1, int index2) {
    T tmp = this.objs[index1];
    this.objs[index1] = this.objs[index2];
    this.objs[index2] = tmp;
  }
}

呼び出し方は、

Permutation<String> p = new Permutation<>(new String[] { "a", "b", "c" });
do {
  System.out.println(p.toString());
} while (p.next());

で、次のようになる。

[a,b,c]
[a,c,b]
[b,a,c]
[b,c,a]
[c,b,a]
[c,a,b]

メモリも大して食わないし、そこそこ高速だと思う。

NATなKVMとJava JPDAデバッグ

技術ネタとしては基本レベルの話ではあるのだが、知らない人は本当に知らなくてちょっと萎えるので、今後の私の負荷を下げるために、NAT経由でのJPDAデバッグの話を書いておく。
# 文章の分かりにくい部分を修正。(2/15)

開発用 Windows PCと試験用 Linux サーバの2台があるとする。
f:id:iwsttty:20140215171050p:plain
図の右のLinux サーバ内には、本番を模したサーバ群が KVM 上に構築されていて、アドレスなど最大限本番環境に揃えるために、NAT経由でオフィスのネットワークにつながっている。NATはLinuxホストOSのiptables で設定している。

それで、開発PCからアプリサーバにつながらない時にチェックポイントは四つ:

  1. Linuxサーバのポートフォワーディング(NAT)設定
  2. アプリサーバの iptables 穴開け
  3. Tomcat JPDA設定
  4. Eclipseの「リモートJavaデバッグ」設定

一つ目のNAT設定はつながらない場合の原因第1位なんだろうけど、全部書くと疲れるので要点を書いておく。

  • LinuxサーバのホストOSの iptables が起動していること。
  • 上図 Linuxサーバホストの eth0 に来たパケットに対し、二つの NAT が入っているか確認。
    • DNAT: 送信先アドレスを、宛先ポート番号に応じて KVM上各サーバのアドレスとポートに変換
    • SNAT: 送信元アドレスを 192.168.0.1 (KVM 内ネットワークのゲートウェイアドレス) に変換

このSNAT設定がないと、KVM上のサーバたちは飛んできたパケットの返し先が分からなくなる。
設定内容はこんな感じ↓。ちなみに iptables のコマンドラインオプションなんか覚えてないので、/etc/sysconfig/iptables を編集して再起動(servie iptables reload)している。

# cat /etc/sysconfig/iptables
# (前略)
*nat
# (中略)
-A PREROUTING -i eth0 -p tcp -m tcp --dport 22101 -j DNAT --to-destination 192.168.0.101:22
-A PREROUTING -i eth0 -p tcp -m tcp --dport  8111 -j DNAT --to-destination 192.168.0.101:80
-A PREROUTING -i eth0 -p tcp -m tcp --dport  9111 -j DNAT --to-destination 192.168.0.101:9000
-A POSTROUTING -d 192.168.0.0/24 -p tcp -m tcp -j SNAT --to-source 192.168.0.1
# (後略)
COMMIT

# service iptables restart

二つ目のアプリサーバの穴開けは、忘れないでねというだけのこと。JPDA ポートはディフォルトは塞がっているはず。

三つ目の JPDA 設定は、agentlib のところが以下のようになるよう頑張るだけ。

# ps -ef |grep java
root    28341    1   1 18:24 ttyS0   00:00:12 /usr/bin/java
   -Djava.util.logging.config.file=/opt/apache-tomcat/conf/logging.properties
   -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
   -agentlib:jdwp=transport=dt_socket,address=9000,server=y,suspend=n
   -Djava.endorsed.dirs=/opt/apache-tomcat/endorsed
   -classpath /opt/apache-tomcat/bin/bootstrap.jar:/opt/apache-tomcat/bin/tomcat-juli.jar
   -Dcatalina.base=/opt/apache-tomcat
   -Dcatalina.home=/opt/apache-tomcat
   -Djava.io.tmpdir=/opt/apache-tomcat/temp org.apache.catalina.startup.Bootstrap start

ちなみにポート番号9000は単に都合で変えているだけなので、必要がないならば変える必要はない。
変える場合は Tomcat の setenv.sh に以下のように書いて、JPDA を開始する。

JPDA_ADDRESS=9000

起動時は catalina.sh jpda start するだけ。catalina.sh start は不要。javaプロセスは一つ。

# netstat -nap | grep 9000
tcp    0    0 0.0.0.0:9000    0.0.0.0:*    LISTEN    28341/java


最後はクライアント側。Eclipseの「デバッグの構成」という設定で、「リモートJavaアプリケーション」を設定する。見れば分かると思うので省略。

Apache antのjavaタスクのarg要素に空白を含むパスを複数指定するには

antもANTLRも今更感はあるけど、久しぶりにいじると楽しい。

でも、結構はまる。文法ファイル(*.g4) が複数あって、かつパスに空白が含まれる場合(Google Driveとか)に、java タスクの arg 要素で pathref 属性を使おうとすると、ant が空白でパスを切ってしまって java タスクに思い通りに渡らない。そこで、 pathconvert を以下のように使って、文字列化してしまうとうまくいく。

  <path id="src.antlr">
    <fileset dir="${basedir}/src/antlr">
      <include name="**/*.g4" />
    </fileset>
  </path>

  <target name="antlr_compile">
    <echo message="[DEBUG] ${toString:src.antlr}" />

    <pathconvert property="antlr.args" refid="src.antlr" pathsep=" ">
      <regexpmapper from="^(.*)$" to='"\1"' />
    </pathconvert>

    <echo message="[DEBUG] ${antlr.args}" />

    <java classname="org.antlr.v4.Tool">
      <classpath>
        <pathelement path="${basedir}/lib/antlr-4.1-complete.jar" />
      </classpath>
      <arg line='-o "${basedir}/src/java/iwsttty/example_antlr"' />
      <arg line="-package iwsttty.example_antlr" />
      <arg line="${antlr.args}" />
    </java>
  </target>

antlr タスクでは複数の文法ファイル指定はうまくいかなかったです。