HinemosのノードをWebサービスAPI経由で定義登録する

HinemosのジョブネットをWebサービスAPI経由で実行する」および「HinemosのジョブネットをWebサービスAPI経由で定義登録する」の派生。
Hinemosのインストールを ANSIBLE で自動化し、ジョブ定義登録も自動化できるようになった。ところがノードの登録が自動化できていなかった。

対応の道筋はジョブの場合と全く同じで以下の通りにする。使う要素はこれまで同様なので細かい説明は割愛する。

  1. wsimport でスタブクラスを生成する。
  2. 既存の定義を取得してみてみる。
  3. デフォルトの定義情報をシリアライズで保存する。
  4. シリアライズ+αで登録する。

wsimport でスタブクラスを生成

ジョブ機能の場合はエンドポイントは /HinemosWS/JobEndpoint?wsdl であったが、リポジトリ機能の場合は /HinemosWS/RepositoryEndpoint?wsdl になる。

# wsimport -keep http://garnet-vm09:8080/HinemosWS/RepositoryEndpoint?wsdl
parsing WSDL...



Generating code...


Compiling code...

# ls -l
total 4
drwxr-xr-x 3 root root  4096 May  3 02:17 com

# tree com
com
└── clustercontrol
    └── ws
        └── repository
            ├── AddNode.class
            ├── AddNode.java
            ├── (中略)
            ├── RepositoryEndpoint.class
            ├── RepositoryEndpoint.java
            ├── RepositoryEndpointService.class
            ├── RepositoryEndpointService.java
            ├── (中略)
            └── UsedFacility.java

3 directories, 206 files

既存の定義を取得してみてみる

取得用のサービスが何なのか少々迷う。getNodeListAll サービスがそれっぽいのだが、マネージャ側のソースでは getNodeFacilityIdList と getNode を使うように指示がある。ノードなのにファシリティ?と思ってしまうあたり、そろそろノード・スコープ・ファシリティという言葉の整理をしないとまずい。

Hinemos ver4.1 ユーザマニュアル第4版からノードとスコープの説明を抜粋する。

3.1.2 スコープとノード
Hinemosでは、「スコープ」と「ノード」という2つの単位で管理対象を扱います。

ノード
実際の管理対象のマシンを仮想化したものです。ノード情報として以下の情報を登録することができます。
 ・ハードウェア、ネットワーク、OS情報
 ・サービス(SNMP, WBEM, IPMI, WinRM)
 ・デバイス情報(CPU, メモリ, NIC, ディスク, ファイルシステム, 汎用デバイス)
 ・サーバ仮想化、ネットワーク仮想化、クラウド管理
 ・その他の情報

スコープ
複数のノードをグループ化したものです。Hinemosで提供される機能の処理単位の多くは、スコープ単位となっています。スコープに対して行った処理は、登録されている各ノードに反映されることになります。

また、スコープは複数のスコープをその下層のスコープとして登録することもできます。この場合は、スコープは階層構造を持ち、ツリーを形成することになります。

ジョブの投げ先を指定する項目の大きなくくりは [Scope] である。実際にノードを Registered Nodes 下から選び画面に表示されるのは Facility Name なのだ。今一スコープ感(謎)がない。Registered Nodes 下のノードは同名で要素が1個だけのスコープなんです、という説明されればぎりぎり納得できるが。

画面上ちょっと微妙な気分だったが、スタブクラスの下記の継承関係を確認したところで、プログラム的にはどう扱えばよいかは理解できた。スコープとノードはファシリティという概念でまとめられているので、ファシリティに対して(ジョブを)投げていると考えれば落ち着く(ファシリティはノードかもしれんしスコープかもしれん)。

前置きが長いが、GetNodeList.java としてノードの一覧を取得するプログラムを作成。getNodeFacilityIdList を使うので、親のスコープとしてRegistered Nodes (のFacilityIDである REGISTERED)を指定。

import com.clustercontrol.ws.repository.*;
import javax.xml.ws.BindingProvider;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

public class GetNodeList {
    private static RepositoryEndpoint servicePort;

    private static void initServicePort() {
        // アクセス先/認証情報
        final String ENDPOINT_URL = "http://garnet-vm09:8080/HinemosWS/RepositoryEndpoint";
        final String USERNAME = "hinemos";
        final String PASSWORD = "hinemos";

        RepositoryEndpointService service = new RepositoryEndpointService();
        servicePort = service.getRepositoryEndpointPort();

        // アクセス先設定/認証情報設定
        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_URL);
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);
    }

    public static void main( String[] args ) {
        initServicePort();

        // 実行
        try {
            // getNodeListAll では FacilityID, FacilityName, Desc, IPバージョン, IPv4アドレス, IPv6アドレス が得られるが他は含まれない
            // getNodeFacilityIdList 同様に getNode と併用が必要
            // マネージャ側のソースでは getNodeFacilityIdList + getNode を使うように記載あり(何故?)
            //System.out.println("getNodeListAll starting...");
            //List<NodeInfo> nodeList = servicePort.getNodeListAll();
            //System.out.println("getNodeListAll complted.");

            final String PARENT_FACILITY_ID = "REGISTERED"; // Registered Nodes
            final String OWNER_ROLE_ID = "ALL_USERS";
            final int LEVEL = 0; // 0: 全て, 1: 直下の子要素のみ

            System.out.println("getNodeFacilityIdList starting...");
            List<String> nodeFacilityIdList = servicePort.getNodeFacilityIdList(PARENT_FACILITY_ID, OWNER_ROLE_ID, LEVEL);
            System.out.println("getNodeFacilityIdList completed.");
            
            System.out.println("getNode starting...");
            List<NodeInfo> nodeList = new ArrayList<NodeInfo>();
            for ( String fid : nodeFacilityIdList ) nodeList.add(servicePort.getNode(fid));
            System.out.println("getNode completed.");

            System.out.println("getNodeFacilityIdList/getNode Output:");
            for ( NodeInfo n : nodeList )
                printNodeInfo(n);
        }
        catch ( HinemosUnknown_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidRole_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidUserPass_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( FacilityNotFound_Exception e ) {
            e.printStackTrace(System.out);
        }
    }

    private static void printNodeInfo( NodeInfo node ) {
        System.out.printf("- FacilityID: %s,  FacilityName: %s,  FacilityType: %s,  FacilityDesc: %s\n",
            node.getFacilityId(),
            node.getFacilityName(),  
            node.getFacilityType(),
            node.getDescription());
        System.out.printf("  Created: %s,  Modified: %s\n",
            node.getCreateDatetime(),
            node.getModifyDatetime());
        System.out.printf("  IPversion: %s,  IPv4Addr: %s,  Platform: %s,  NodeName: %s\n",
            node.getIpAddressVersion(),
            node.getIpAddressV4(),
            node.getPlatformFamily(),
            node.getNodeName());
        System.out.printf("  SNMPVersion: %s,  SNMPPort: %s\n",
            node.getSnmpVersion(),
            node.getSnmpPort());
    }
}

実行結果は以下の通り、取得できている。

# java GetNodeList
getNodeFacilityIdList starting...
getNodeFacilityIdList completed.
getNode starting...
getNode completed.
getNodeFacilityIdList/getNode Output:
- FacilityID: garnet-vm09,  FacilityName: garnet-vm09,  FacilityType: 1,  FacilityDesc: 
  Created: 1430412693463,  Modified: 1430412693463
  IPversion: 4,  IPv4Addr: 172.16.1.119,  Platform: LINUX,  NodeName: garnet-vm09
  SNMPVersion: 2c,  SNMPPort: 161
- FacilityID: LINUX,  FacilityName: Linuxノードサンプル,  FacilityType: 1,  FacilityDesc: 
  Created: 1430594599307,  Modified: 1430594599307
  IPversion: 4,  IPv4Addr: 172.16.1.130,  Platform: LINUX,  NodeName: linuxnodename
  SNMPVersion: 2c,  SNMPPort: 161
- FacilityID: WINDOWS,  FacilityName: Windowsノードサンプル,  FacilityType: 1,  FacilityDesc: 
  Created: 1430594661268,  Modified: 1430594661268
  IPversion: 4,  IPv4Addr: 172.16.1.1,  Platform: WINDOWS,  NodeName: windowsnodename
  SNMPVersion: 2c,  SNMPPort: 161

デフォルトの定義情報をシリアライズで保存する

上記の一覧にある LINUXWINDOWSシリアライズする。一応差がプラットフォームのところだけか確認する。

FacilityIDさえわかっていれば getNode サービスを直接使えば良いので、取得+シリアライズの簡単なプログラムになる。
MarshalNodeParts.java として作成。

# cat MarshalNodeParts.java 
import com.clustercontrol.ws.repository.*;
import javax.xml.ws.BindingProvider;
import java.util.Map;

import java.io.*;
import javax.xml.bind.JAXB;

public class MarshalNodeParts {
    private static RepositoryEndpoint servicePort;

    private static void initServicePort() {
        // アクセス先/認証情報
        final String ENDPOINT_URL = "http://garnet-vm09:8080/HinemosWS/RepositoryEndpoint";
        final String USERNAME = "hinemos";
        final String PASSWORD = "hinemos";

        RepositoryEndpointService service = new RepositoryEndpointService();
        servicePort = service.getRepositoryEndpointPort();

        // アクセス先設定/認証情報設定
        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_URL);
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);
    }

    public static void main( String[] args ) {
        initServicePort();

        // 実行
        try {
            final String SAMPLE_LIN_NODE_FACILITY_ID = "LINUX";
            final String SAMPLE_WIN_NODE_FACILITY_ID = "WINDOWS";

            System.out.println("getNode starting...");
            NodeInfo sampleLinNode = servicePort.getNode(SAMPLE_LIN_NODE_FACILITY_ID);
            NodeInfo sampleWinNode = servicePort.getNode(SAMPLE_WIN_NODE_FACILITY_ID);
            System.out.println("getNode completed.");

            System.out.println("marshalling starting...");
            OutputStream os = new FileOutputStream(SAMPLE_LIN_NODE_FACILITY_ID + ".xml");
            JAXB.marshal(sampleLinNode, os);
            os.close();
            os = new FileOutputStream(SAMPLE_WIN_NODE_FACILITY_ID + ".xml");
            JAXB.marshal(sampleWinNode, os);
            os.close();
            System.out.println("marshalling completed...");
        }
        catch ( HinemosUnknown_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidRole_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidUserPass_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( FacilityNotFound_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( IOException e ) {
            e.printStackTrace(System.out);
        }
    }

    private static void printNodeInfo( NodeInfo node ) {
        System.out.printf("- FacilityID: %s,  FacilityName: %s,  FacilityType: %s,  FacilityDesc: %s\n",
            node.getFacilityId(),
            node.getFacilityName(),  
            node.getFacilityType(),
            node.getDescription());
        System.out.printf("  Created: %s,  Modified: %s\n",
            node.getCreateDatetime(),
            node.getModifyDatetime());
        System.out.printf("  IPversion: %s,  IPv4Addr: %s,  Platform: %s,  NodeName: %s\n",
            node.getIpAddressVersion(),
            node.getIpAddressV4(),
            node.getPlatformFamily(),
            node.getNodeName());
        System.out.printf("  SNMPVersion: %s,  SNMPPort: %s\n",
            node.getSnmpVersion(),
            node.getSnmpPort());
    }
}

実行結果は以下の通り、問題なくXMLが作成された。
LinuxWindowsで差があったのは platformFamily フィールドのみだった。

# java MarshalNodeParts
getNode starting...
getNode completed.
marshalling starting...
marshalling completed...

# ls -l *.xml
-rw-r--r-- 1 root root 2918 May  3 06:01 LINUX.xml
-rw-r--r-- 1 root root 2924 May  3 06:01 WINDOWS.xml

シリアライズ+αで登録する

LinuxWindowsで自明な項目(platformFamily)以外に差がないので、LINUX.xml を NODE.xml としてこれを読み込む。

# mv LINUX.xml NODE.xml 
# ls -l *.xml
-rw-r--r-- 1 root root 2918 May  3 06:01 NODE.xml
-rw-r--r-- 1 root root 2924 May  3 06:01 WINDOWS.xml

AddNodeLoremipsum.java としてノード loremipsum を追加するプログラムを作成。

import com.clustercontrol.ws.repository.*;
import javax.xml.ws.BindingProvider;
import java.util.Map;

import java.io.*;
import javax.xml.bind.JAXB;

public class AddNodeLoremipsum {
    private static RepositoryEndpoint servicePort;

    private static void initServicePort() {
        // アクセス先/認証情報
        final String ENDPOINT_URL = "http://garnet-vm09:8080/HinemosWS/RepositoryEndpoint";
        final String USERNAME = "hinemos";
        final String PASSWORD = "hinemos";

        RepositoryEndpointService service = new RepositoryEndpointService();
        servicePort = service.getRepositoryEndpointPort();

        // アクセス先設定/認証情報設定
        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_URL);
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);
    }

    public static void main( String[] args ) {
        initServicePort();

        // 実行
        try {
            final String SAMPLE_NODE_FACILITY_ID = "loremipsum";
            final String SAMPLE_NODE_FACILITY_NAME = "LOREM IPSUM";
            final String SAMPLE_NODE_NODENAME = "loremipsum";
            final String SAMPLE_NODE_IPADDRESSV4 = "172.16.1.250";

            System.out.println("loremipsum node creation starting...");
            NodeInfo loremipsum = createLinuxNode(SAMPLE_NODE_FACILITY_ID, SAMPLE_NODE_FACILITY_NAME, "");
            setNodeAddress(loremipsum, SAMPLE_NODE_NODENAME, 4, SAMPLE_NODE_IPADDRESSV4, "");
            System.out.println("loremipsum node creation completed.");

            System.out.println("addNode starting...");
            servicePort.addNode(loremipsum);
            System.out.println("addNode completed...");
        }
        catch ( HinemosUnknown_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidRole_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidUserPass_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( FacilityDuplicate_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidSetting_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( IOException e ) {
            e.printStackTrace(System.out);
        }
    }

    private static NodeInfo unmarshalNodeParts( String facilityId, String facilityName, String desc, String platform ) throws IOException {
        final String NODE_PARTS_FILE = "NODE.xml";

        InputStream is = new FileInputStream(NODE_PARTS_FILE);
        NodeInfo node = JAXB.unmarshal(is, NodeInfo.class);
        is.close();

        // 固有情報消去/設定
        node.setFacilityId(facilityId);
        node.setFacilityName(facilityName);
        node.setDescription(desc);
        node.setPlatformFamily(platform);
        node.setCreateDatetime(System.currentTimeMillis());
        node.setModifyDatetime(node.getCreateDatetime());
        node.setNodeName("");
        node.setIpAddressVersion(4);
        node.setIpAddressV4("");
        node.setIpAddressV6("");

        return node;
    }

    private static NodeInfo createLinuxNode( String facilityId, String facilityName, String desc ) throws IOException {
        return unmarshalNodeParts(facilityId, facilityName, desc, "LINUX");
    }
    private static NodeInfo createWindowsNode( String facilityId, String facilityName, String desc ) throws IOException {
        return unmarshalNodeParts(facilityId, facilityName, desc, "WINDOWS");
    }

    private static void setNodeAddress( NodeInfo node, String nodeName, int ipAddressVersion, String ipAddressV4, String ipAddressV6 ) {
        node.setNodeName(nodeName);
        node.setIpAddressVersion(ipAddressVersion);
        node.setIpAddressV4(ipAddressV4);
        node.setIpAddressV6(ipAddressV6);
    }
}

実行結果は以下の通り。プログラム実行後にクライアント上の更新を行うとノードが追加されている。


One more thing: getNodePropertyBySNMP によるノード情報取得を元に登録する

とくめいさんより getNodePropertyBySNMP によるノード情報取得を元に登録する方法もある旨コメントを頂きました。
この方法だとデシリアライズを使わずに、より多くの実機情報を登録できるのでいいですね。ありがとうございます!

getNodePropertyBySNMP と GUI の対応

getNodePropertyBySNMP はノード追加のダイアログ上の [Find By SNMP] に対応するサービスだ。対象ノードのSNMPと疎通して情報を埋めてくれる。

やってみよう

追加対象ノードのIPアドレスを引数で受け取り追加するプログラムを AddNodeBasedOnSnmp.java として作成。

  • FacilityID, FacilityName はノード名を使う。
  • OwnerRoleID は ALL_USERS 決めうちとした。
  • IPアドレス以外の SNMP 設定も決めうちとした。
  • getNodePropertyBySNMP サービスは存在しないノードに対して実行しても例外にはならない。ノード名すら取れない結果はおかしいと判定した。
import com.clustercontrol.ws.repository.*;
import javax.xml.ws.BindingProvider;
import java.util.Map;

public class AddNodeBasedOnSnmp {
    private static RepositoryEndpoint servicePort;

    private static void initServicePort() {
        // アクセス先/認証情報
        final String ENDPOINT_URL = "http://garnet-vm09:8080/HinemosWS/RepositoryEndpoint";
        final String USERNAME = "hinemos";
        final String PASSWORD = "hinemos";

        RepositoryEndpointService service = new RepositoryEndpointService();
        servicePort = service.getRepositoryEndpointPort();

        // アクセス先設定/認証情報設定
        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_URL);
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);
    }

    public static void main( String[] args ) {
        initServicePort();

        // 実行
        try {
            final int    SNMP_PORT = 161;
            final String SNMP_COMMUNITY = "public";
            final String SNMP_VERSION = "2c";

            if ( args.length == 0 ) {
                System.out.println("usage: java AddNodeBasedOnSnmp <ipaddress>");
                System.exit(1);
            }
            
            final String ipaddress = args[0];

            System.out.println("getNodePropertyBySNMP starting...");
            NodeInfo node = servicePort.getNodePropertyBySNMP(
                ipaddress,
                SNMP_PORT, SNMP_COMMUNITY, SNMP_VERSION);
            System.out.println("getNodePropertyBySNMP completed.");

            // ノード名も取得できないのはおかしいので排除
            // (存在しないIPアドレスを指定しても getNodePropertyBySNMP 自体は失敗しない)
            if ( node.getNodeName().equals("") ) {
                System.out.println("Could not determine nodename!");
                System.out.println("SNMP service seems to be disabled at the target node.");
                System.exit(1);
            }

            // FacilityID, FacilityName を決める(ノード名と同じ)
            // その他設定しないと addNode 実行時に InvalidSetting_Exception になったものを設定
            node.setFacilityId(node.getNodeName());
            node.setFacilityName(node.getNodeName());
            node.setOwnerRoleId("ALL_USERS");

            System.out.println("addNode starting...");
            servicePort.addNode(node);
            System.out.println("addNode completed.");
        }
        catch ( HinemosUnknown_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidRole_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidUserPass_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( FacilityDuplicate_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidSetting_Exception e ) {
            e.printStackTrace(System.out);
        }
    }
}


実行結果は以下の通り、SNMPが動作している 172.16.1.120 に対してうまく登録処理できた!

# java AddNodeBasedOnSnmp
usage: java AddNodeBasedOnSnmp <ipaddress>

# java AddNodeBasedOnSnmp 172.16.1.150
getNodePropertyBySNMP starting...
getNodePropertyBySNMP completed.
Could not determine nodename!
SNMP service seems to be disabled at the target node.

# java AddNodeBasedOnSnmp 172.16.1.120
getNodePropertyBySNMP starting...
getNodePropertyBySNMP completed.
addNode starting...
addNode completed.

HinemosのジョブネットをWebサービスAPI経由で実行する

HinemosにはWebサービスAPIと呼ばれる機能があり、機能をHTTP経由で使用できる。使用できるというか、ご本家のクライアントとマネージャのやりとりも全てそれ経由らしい。つまり、全ての機能をHTTP経由で利用できるということだ。

何がしたいか?

ジョブネット定義を自動で登録したい(キリッ

状況!

WebサービスAPIの資料や情報がやたら少ないorz

ググって出てくる数少ない参考サイトはレベルが高く「WebサービスAPIへアクセスするためのエンドポイント、またAPIが用意しているメソッドについては、Hinemosマネージャのソースコードに全て記載されていますので、ここでは詳しく触れません。」とのお言葉。そして、ジョブネット定義というヤヤコシイこと(かつ素直にオプションを買えば楽になりそうなこと)ではなく、ジョブネットの実行の例なのだ。とどめにJavaではなくPerlとかPythonの例なのだ。Javaは!?

そして私自身のJavaスキルはというと、「JavaからSOAPのサービス呼び出しってどうやるの?」な状況である。

どうする?

状況の通りいろいろと苦しい。ともかくWebサービスAPI経由でなにかできるところまでたどり着く必要がある。

というわけで、まずは「言語違いでも例が多少あるジョブネット実行をJavaからやってみよう」(その途中でいろいろ見えてくるだろう)となった。

環境は「Hinemosのインストール」でインストールした環境を使う。Hinemos用のJDKは7だが、作成するプログラムはJDK8で開発する。8でなくとも6以降ならどれでもよいはず。

やってみよう! スタブ作成

SOAPのサービスは WSDL という形式でカタログ的なものがサーバ側から提供される。
Hinemosのジョブ機能のカタログは http://<マネージャのホスト名>:8080/HinemosWS/JobEndpoint?wsdl にアクセスすることで確認できる。ジョブネット実行のために呼び出すrunJobサービス(参考サイトの例からこれを呼べば良いことは解った)の定義も含まれている。

上記の WSDL を読んでもピンとこないが、「SOAP Webサービスクライアントを作ろう(JAX-WS, Apache CXF 編)」を頼りに進めよう。
Java的(タイプセーフ)に事を進めるために、WSDL からスタブのクラス群を生成してくれるコマンドがある。それがJDK6から標準で取り込まれている「wsimport」コマンドだ。このコマンドで生成されたクラス群とJavaらしくやり取りすることで、それがSOAP Webサービスへのアクセスに自動的に置き換えられる。これはなかなかわかりやすい。

検証用に /tmp/sandbox ディレクトリを作成。

# mkdir /tmp/sandbox

作成したディレクトリに移動して wsimport を WSDL のURLをオプションに指定して実行。-keep は自動生成した .java ファイルも残しておくために指定。

# cd /tmp/sandbox/
# wsimport -keep http://garnet-vm09:8080/HinemosWS/JobEndpoint?wsdl
parsing WSDL...



Generating code...


Compiling code...

エラー無く完了したので生成結果を確認する。com/clustercontrol/ws/jobmanagement 下に RunJob.class などそれらしいクラスが生成されている。

# ls -l
total 4
drwxr-xr-x 3 root root 4096 May  1 05:41 com

# tree com
com
└── clustercontrol
    └── ws
        ├── calendar
        │&#160;&#160; ├── CalendarDetailInfo.class
        │&#160;&#160; ├── CalendarDetailInfo.java
        │&#160;&#160; ├── (中略)
        │&#160;&#160; ├── Ymd.class
        │&#160;&#160; └── Ymd.java
        ├── jobmanagement
        │&#160;&#160; ├── AddFileCheck.class
        │&#160;&#160; ├── AddFileCheck.java
        │&#160;&#160; ├── (中略)
        │&#160;&#160; ├── JobEndpoint.class
        │&#160;&#160; ├── JobEndpoint.java
        │&#160;&#160; ├── JobEndpointService.class
        │&#160;&#160; ├── JobEndpointService.java
        │&#160;&#160; ├── (中略)
        │&#160;&#160; ├── RunJob.class
        │&#160;&#160; ├── RunJob.java
        │&#160;&#160; ├── (中略)
        │&#160;&#160; └── UserNotFound.java
        └── (後略)

5 directories, 248 files

やってみよう! スタブを使用してWebサービスアクセス

スタブを使用してジョブネットを実行するためのプログラムとして JobKick.java を作成。

/tmp/sandbox/JobKick.java

import com.clustercontrol.ws.jobmanagement.*;
import javax.xml.ws.BindingProvider;
import java.util.Map;

public class JobKick {
    public static void main( String[] args ) {
        // アクセス先/認証情報
        final String ENDPOINT_URL = "http://garnet-vm09:8080/HinemosWS/JobEndpoint";
        final String USERNAME = "hinemos";
        final String PASSWORD = "hinemos";

        // 実行するジョブのジョブユニットIDおよびジョブID
        final String JOBUNIT_ID = "JobUnit01";
        final String JOB_ID = "JobNet01";

        JobEndpointService service = new JobEndpointService();
        JobEndpoint servicePort = service.getJobEndpointPort();

        // アクセス先設定/認証情報設定
        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_URL);
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);

        // runJobサービス実行のための引数準備
        OutputBasicInfo obi = new OutputBasicInfo();
        JobTriggerInfo jti = new JobTriggerInfo();
            jti.setTriggerType(2);           // 手動投入
            jti.setTriggerInfo("hinemos");   // 実行ユーザ

        // 実行(やたら例外多い...)
        try {
            System.out.println("runJob starting...");

            String out = servicePort.runJob(JOBUNIT_ID, JOB_ID, obi, jti);

            System.out.println("runJob complted.");
            System.out.println("runJob Output: " + out);
        }
        catch ( FacilityNotFound_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( HinemosUnknown_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidRole_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( InvalidUserPass_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( JobInfoNotFound_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( JobMasterNotFound_Exception e ) {
            e.printStackTrace(System.out);
        }
        catch ( JobSessionDuplicate_Exception e ) {
            e.printStackTrace(System.out);
        }
    }
}
runJobメソッドの引数

以下の3点を参考にして理解した。

  • 生成された com/clustercontrol/ws/jobmanagement/JobEndpoint.java (引数の数、型ぐらい)
  • Hinemos のソース HinemosManager/src_ws/com/clustercontrol/ws/jobmanagement/JobEndpoint.java ( どう使われるか、何を求めているか )
  • WebサービスAPIでジョブを起動する

ソースコード見るのはちょっと・・・な感じだったが WebサービスAPI のソースが src_ws 下に纏められており、確認しやすかった。ソースみてね、という言葉も理解できてきた(かも)。

HinemosManager/src_ws/com/clustercontrol/ws/jobmanagement/JobEndpoint.java から抜粋。

        /**         * ジョブを実行します。<BR>
         * 
         * JobManagementExecute権限が必要
         * 
         * @param jobunitId 所属ジョブユニットのジョブID
         * @param jobId ジョブID
         * @param info ログ出力情報
         * @param triggerInfo 実行契機情報
         * @throws JobMasterNotFound
         * @throws JobInfoNotFound
         * @throws HinemosUnknown
         * @throws FacilityNotFound
         * @throws InvalidRole
         * @throws InvalidUserPass
         * @see com.clustercontrol.jobmanagement.ejb.session.JobControllerBean#createJobInfo(String, String, NotifyRequestMessage, JobTriggerInfo}
         * @see com.clustercontrol.jobmanagement.session.JobRunManagementBean#runJob(String, String)
         */
        public String runJob(String jobunitId, String jobId, OutputBasicInfo info, JobTriggerInfo triggerInfo)
            throws FacilityNotFound, HinemosUnknown, JobInfoNotFound, JobMasterNotFound, InvalidUserPass, InvalidRole, JobSessionDuplicate
        {

実行するジョブネットはジョブユニットのIDおよびジョブネットのIDを指定することで一意になる。

JobEndpointService, JobEndpoint クラスの使用

runJobメソッド呼び出し部分がコア部分なのだが、service, servicePort の作成など段階を踏まなければならなかった。JobEndpointServiceクラス、JobEndpointクラスを使うことをどうやって導き出したのか覚えが無いのだが、おそらくこんな順番だったと記憶。

  1. RunJob.java の中身をみて、実行に結びつきそうなメソッドが無いと落胆。
  2. com/clustercontrol/ws/jobmanagement 下を grep -i runjob *.java 検索し、runJob メソッドを JobEndpoint インターフェースが持つ事を認識。でもインターフェースだから実行できん!
  3. com/clustercontrol/ws/jobmanagement 下を grep -i JobEndpoint *.java 検索し、JobEndpointService の getJobEndpointPort メソッドが生成してくれると理解。「SOAP Webサービスクライアントを作ろう(JAX-WS, Apache CXF 編)」でも以下の2ステップで getMessage サービスをメソッドとして持つインスタンスを生成しているから、おそらくこれで正解と結論。
public String execute(String text, int num) {
  ZaneliWS ws = new ZaneliWS();
  ZaneliWSSoap soap = ws.getZaneliWSSoap();
  return soap.getMessage(text, num);
}
認証情報の設定

下記のコードが認証情報を設定している部分である。
web service clients with wsimport and jax-ws」の例を参考にしてなんとかうまくいった。

        BindingProvider bp = (BindingProvider)servicePort;
        Map<String, Object> requestContext = bp.getRequestContext();
        requestContext.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
        requestContext.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);

この設定がないと下記の様な例外が発生してしまう。まぁ、認証情報与えずにジョブネット実行できちゃだめだよね。

# java JobKick
runJob starting...
com.clustercontrol.ws.jobmanagement.InvalidUserPass_Exception: Authorization does not exist
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
	at com.sun.xml.internal.ws.fault.SOAPFaultBuilder.createException(SOAPFaultBuilder.java:135)
	at com.sun.xml.internal.ws.client.sei.StubHandler.readResponse(StubHandler.java:238)
	at com.sun.xml.internal.ws.db.DatabindingImpl.deserializeResponse(DatabindingImpl.java:189)
	at com.sun.xml.internal.ws.db.DatabindingImpl.deserializeResponse(DatabindingImpl.java:276)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:104)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:77)
	at com.sun.xml.internal.ws.client.sei.SEIStub.invoke(SEIStub.java:147)
	at com.sun.proxy.$Proxy35.runJob(Unknown Source)
	at JobKick.main(JobKick.java:36)

やってみよう! 実行

無事にジョブユニット JobUnit01 のジョブネット JobNet01 が実行された。

  • runJobメソッドから返る文字列は [Session ID] である。
  • ジョブネットの完了前に runJob メソッドは制御が返ってくる(キックのみが仕事)。

Hinemosでジョブネットを作成・実行する

JP1/AJS とは多少趣が違うところもあったので、ジョブネットの作成・実行の手順をメモしておく。
環境は「Hinemosのインストール」でインストールした環境を使う。

Windows 7環境を英語環境にしているためメニュー表示は英語になっていますが、Hinemosクライアント自体は日本語対応しているはずです。

クライアントからマネージャへの接続

スタートメニューの[Hinemos]-[Client4.1.3]-[HinemosClient4.1.3]からクライアントを起動する。
以下の入力でログインする。

  • UserID: hinemos
  • Password: hinemos (初期パスワード)
  • URL to connect: http://<マネージャのホスト名>:8080/HinemosWS/

(初回)ログインが完了すると寂しい画面がまっているので、メニューの[Perspective]-[Open Perspective]から、使用する機能ごとの画面を開く。

エージェントの登録

エージェントをインストールしただけではそのエージェントにジョブを投げることは「まだ」できない。エージェントをマネージャに登録しなければならない。

登録は「Repository」画面で行うため、[Perspective]-[Open Perspective]から[Repository]を開く。

Repository[Node]領域で右クリック、[Create]でノードの登録を開始する。

以下の必須項目を入力して登録する。

  • Facility ID: このノードを識別するID
  • Facility Name: このノードの名前 (ここは「○○サーバ」とか日本語でも良さそう)
  • Platform: Linux(LINUX)
  • IP Version: 4
  • IPv4: ノードのIPアドレス
  • Node Name: ホスト名

Hinemosでは直接「エージェント」に指示を出すという考えではなく、「Facility」という概念(グループ)に対して指示を出すという考えなのかな。単一のノードに指示を出す場合でも、それは単一の要素しかもたない「Facility」に指示を出すことに等しいとか・・・そういうことかな。多数のノードを同時に扱う上ではこの仕組みは良いね。(理解誤りがあるかもしれません)

[Register]をクリックして登録!

Repository[Node]の一覧に登録されていればOK。

ジョブネット定義登録

エージェントの登録が完了したらいよいよジョブネットの定義だ。

登録は「Job」画面で行うため、[Perspective]-[Open Perspective]から[Job]を開く。

Job[List]領域の[Job]トップ項目を右クリック、[Create JobUnit]でジョブユニット追加を開始する。

「ジョブユニット」は「ジョブネット」を複数包含するフォルダのような概念だと捉えた。ジョブネットのIDやジョブのIDもこのジョブユニット内で一意であることが求められており、名前の空間でもある。JP1/AJSのグループ(フォルダのようなもの)に近いが、Hinemosではこのジョブユニットも実行できるのがちょっと違うかな。

ジョブユニットのIDと名前を入力して追加する。

ジョブユニットが作成されたら、ジョブユニットを右クリック、[Create JobNet]でジョブネット追加を開始する。

ジョブネットのIDと名前を入力して追加する。

ジョブネットが作成されたら、ジョブネットを右クリック、[Create Job]でジョブ追加を開始する。

ジョブのIDと名前を入力して[Command]タブに移動する。

[Scope]がジョブの投入先を指定する項目だ。変数指定もできるようだがとりあえずは固定で。事前に登録しておいてエージェントが[Refer]から開くダイアログの[Registered Nodes]下にあるのでそれを指定する。

実行するコマンドを[Start Command]に指定してジョブ登録!

ジョブネット下にジョブが登録されたことを確認する。

ジョブネット1-ジョブ1の後続として実行されるジョブ「ジョブネット1-ジョブ2」を同様に追加する。
後続にするため、[Wait Rule]タブで先行ジョブのID(Job01)とそのジョブの終了状態を追加する。この条件を設定しないと、ジョブネット実行直後にジョブネット1-ジョブ1とジョブネット1-ジョブ2の両方が起動するジョブネットになってしまう。

[Wait Rule]タブの設定が終わったら[Command]タブに移動する。

先と同様に[Scope]および[Start Command]を指定してジョブ登録!

一応ジョブネットと呼べるものが出来上がった。さぁ投入(実行)・・・と行きたいのだがまだ投入できない。ジョブユニット名の右に表示されている[Edit Mode]は編集中(しかも今は新規登録)を示すものだ。マネージャ側にしっかり定義を登録(保存)する必要がある。

Job[List]領域右上のアイコンから、[Register]とツールチップが表示されるアイコンをクリックし、登録を実施。

Successed to ... 表示を確認する。

ジョブユニット名の右の[Edit Mode]が無くなったことを確認する。これで遂に実行準備が整った。

ジョブネット実行(投入)

実行するジョブネットを右クリック、[Run]で実行!

確認もOK!

Job[History]にジョブネットの実行状況が表示されていることを確認する。Job[Job Detail]タブでJob01はRunningだが、Job02はWaitになっており順序定義もうまく働いている。

表示の更新はデフォルト10分間隔なので、設定で早めるか、Job[History]領域右上のアイコンから、[Update]とツールチップが表示されるアイコンをクリックして手動更新する。

実行完了、オールグリーン!


ジョブの標準出力・標準エラー出力の確認もできる。

JP1/AJSの感覚では、Job[Job Detail]でジョブをダブルクリックしたくなるが、Hinemosは各ジョブを(恐らくは複数ノードになり得る)「Facility」に投げている。そのためさらにノードごとの絞り込みが必要だ。そしてそれはJob[Node Detail]タブ上で行える。Job[Job Detail]でジョブを選択すると、連動してJob[Node Detail]タブも表示が更新される。このJob[Node Detail]の[Message]列が標準出力・標準エラー出力の内容である(区別はされず画面上と同じく混ざる)。

Hinemosのインストール

GW中の時間を使い「Hinemos」のインストールをした。

Why Hinemos ?

無償で使用できるジョブスケジューラとして「JobScheduler」、「RUNDECK」、「Hinemos」をピックアップした。
一番私が慣れ親しんでいるジョブスケジューラは日立の「JP1/AJS」であるので、それに近いものが取っ付きやすい。

日本語処理に難がありそう?な JobScheduler、選択肢中最もホットだろうと思われるもJP1/AJSとはだいぶ遠くなりそうなRUNDECK、というわけで Hinemos を選択。

後ろ向き感があるけど、国内最大手のSIerさんのかつぐ製品を触ってみたいという興味ももちろんある。「Cool Hinemos! Hinemos5.0にとっても期待している運用エンジニア」によれば、Hinemos 5系が2015年の5月(投稿時点の翌日はもう5月)に出るということであり、内容もいろいろ凄い(今回使うのは4.1.3だけど)。

環境

  • マネージャ兼エージェント
    • CentOS 6.4 x86_64
    • ホスト名 garnet-vm09
    • Hinemos Manager 4.1.3 ( hinemos_manager-4.1.3_rhel6_64.tar.gz )
    • Hinemos Agent 4.1.3 ( hinemos_agent-4.1.3_rhel5-7.tar.gz )
  • クライアント
    • Windows 7 (32ビット)
    • Hinemos Client 4.1.3 ( HinemosClientInstaller-4.1.3_win32.msi )

JP1/AJSではマネージャはエージェント機能を内包しているが、Hinemosのマネージャは純粋にマネージャなので、マネージャのサーバ自身にジョブを投げたい場合はマネージャとエージェントを両方インストールする。

インストール

インストールとか結構面倒なのか?と思ったが、インストールガイドに沿ってやれば楽々できた。

ANSIBLE向け

インストールは簡単だったが、ANSIBLEプレイブック作成の練習もかねてANSIBLE対応してみた。
一応ロールになっているが、/tmp とか 4.1.3 固有のファイルとかいろいろとハードコードあり。

hinemos.install/
├── agent.yml
├── hosts
├── manager.yml
└── roles
    ├── hinemos.agent.install
    │&#160;&#160; ├── files
    │&#160;&#160; │&#160;&#160; └── hinemos_agent-4.1.3_rhel5-7.tar.gz
    │&#160;&#160; └── tasks
    │&#160;&#160;     └── main.yml
    └── hinemos.manager.install
        ├── files
        │&#160;&#160; ├── hinemos_manager-4.1.3_rhel6_64.tar.gz
        │&#160;&#160; └── install.sh
        └── tasks
            └── main.yml

manager.yml

- hosts: all
  roles:
    - hinemos.manager.install

agent.yml
マネージャのホスト名だけハードコーディングしてロールに渡しているので注意。

- hosts: all
  vars:
     argbuilder:
        manager: garnet-vm09
  roles:
    - { role: hinemos.agent.install, arg: "{{ argbuilder }}" }

roles/hinemos.manager.install/tasks/main.yml

- name: (require) is SELinux disabled.
  shell: getenforce
  register: result
  changed_when: false
  failed_when: "'Disabled' not in result.stdout"

- name: (require) is expect installed.
  shell: "rpm -qi expect"
  register: result
  changed_when: false
  failed_when: "result.rc != 0"

- name: (do) install required packages.
  yum: name={{ item }} state=present
  with_items:
    - java-1.7.0-openjdk
    - java-1.7.0-openjdk-devel

- name: (check) Hinemos manager is already installed if hinemos user exists.
  shell: "grep -q ^hinemos: /etc/passwd"
  register: isInstalled
  changed_when: false
  failed_when: "isInstalled.rc not in [0,1]"

- debug: msg="isInstalled.rc = {{ isInstalled.rc }}"

- name: (do) transfer installer archive.
  unarchive: src=hinemos_manager-4.1.3_rhel6_64.tar.gz dest=/tmp
  when: "isInstalled.rc != 0"

- name: (do) install.
  script: install.sh /tmp Hinemos_Manager-4.1.3_rhel6_64
  when: "isInstalled.rc != 0"

- name: (verify) hinemos user exits?
  shell: "grep -q ^hinemos: /etc/passwd"
  register: result
  changed_when: false
  failed_when: "result.rc != 0"

- name: (verify) hinemos group exists?
  shell: "grep -q ^hinemos: /etc/group"
  register: result
  changed_when: false
  failed_when: "result.rc != 0"

- name: (verify) hinemos installation directory and sub directories exist?
  shell: "test -d {{ item }}"
  register: result
  changed_when: false
  failed_when: "result.rc != 0"
  with_items:
    - /opt/hinemos
    - /opt/hinemos/bin
    - /opt/hinemos/etc
    - /opt/hinemos/lib
    - /opt/hinemos/postgresql
    - /opt/hinemos/sbin
    - /opt/hinemos/var

roles/hinemos.manager.install/files/install.sh
英語版インストーラを使っている。日本語でもできると思うけど。
PostgreSQL上のDB初期化に使われるSQLに差がでるようで、説明とかが英語になるかも。ちょっと使いには関係ないのでこれで。

#!/bin/bash -e

TEMPDIR=$1
HINEMOSARCHDIR=$2

pushd $TEMPDIR/$HINEMOSARCHDIR

# run installer under timeout/expect...
timeout 300 expect -d -c "
set timeout 10
spawn $TEMPDIR/$HINEMOSARCHDIR/manager_installer_EN.sh

expect \"Starting Hinemos Manager Installation?\"
  sleep 1
  send \"Y\\r\"
expect \"New password:\"
  sleep 1
  send \"hinemos\\r\"
expect \"Retype new password:\"
  sleep 1
  send \"hinemos\\r\"
expect \"Please input JAVA_HOME.\"
  sleep 1
  send \"\\r\"
expect \") is right?\"
  sleep 1
  send \"Y\\r\"
expect \"Do you use WBEM for process monitoring or performance monitoring?\"
  sleep 1
  send \"N\\r\"
expect \"Please enter the IP address of the ftp server which is needed by the collective run feature.\"
  sleep 1
  send \"\\r\"
expect \"127.0.0.1 is right?\"
  sleep 1
  send \"Y\\r\"

set timeout 180
expect \"Installation of Hinemos Manager is completed.\"
" > /tmp/expect.log 2>&1

exit $?

roles/hinemos.agent.install/tasks/main.yml

- name: (do) install required packages.
  yum: name={{ item }} state=present
  with_items:
    - java-1.7.0-openjdk
    - java-1.7.0-openjdk-devel
    - net-snmp
    - krb5-workstation
    - openssh-clients
    - expect

- name: (check) Hinemos agent is already installed if /opt/hinemos_agent directory exists.
  shell: "test -d /opt/hinemos_agent"
  register: isInstalled
  changed_when: false
  failed_when: "isInstalled.rc not in [0,1]"

- debug: msg="isInstalled.rc = {{ isInstalled.rc }}"

- name: (do) transfer installer archive.
  unarchive: src=hinemos_agent-4.1.3_rhel5-7.tar.gz dest=/tmp
  when: "isInstalled.rc != 0"

- name: (do) install.
  shell: /tmp/Hinemos_Agent-4.1.3_rhel5-7/agent_installer_EN.sh -i -m {{ arg.manager }}
  when: "isInstalled.rc != 0"

- name: (verify) hinemos installation directory and sub directories exist?
  shell: "test -d {{ item }}"
  register: result
  changed_when: false
  failed_when: "result.rc != 0"
  with_items:
    - /opt/hinemos_agent
    - /opt/hinemos_agent/bin
    - /opt/hinemos_agent/conf
    - /opt/hinemos_agent/lib
    - /opt/hinemos_agent/sbin
    - /opt/hinemos_agent/var

起動・停止

マネージャ
  • /opt/hinemos/bin/hinemos_start.sh
  • /opt/hinemos/bin/hinemos_status.sh
  • /opt/hinemos/bin/hinemos_stop.sh
エージェント
  • /opt/hinemos_agent/bin/agent_start.sh
  • /opt/hinemos_agent/bin/agent_stop.sh

「結果には原因がある」ということを頭に叩き込む

Windows がまだ 95, 98, ME であったパソコン普及期には、「何もしていないのにOSが起動しなくなりました。」「何もしていないのにネットに繋がらなくなりました。」というような質問・悲鳴が溢れていた。もちろん本人が何もしていなくても時限的に発生してくる問題もあるのだが、あらゆる結果には原因が存在する。特にコンピュータの世界では絶対にそうだと言ってもよい。あくまでも「私には原因として思い当たるものは何もないが、問題が発生した」ということなのだ。

先の質問・悲鳴でも、「何をしたか」「何があったか」を全て説明して貰うと原因は明らかになることが多かった。

  • OSが起動しない人→「そういえば、Windows Updateってのをやれって表示されたんでやりました。」
  • ネットが繋がらない人→「部屋の模様替えをしたのでPCを移動しました(LANケーブルを抜き差ししました)。」「(家庭内PC番長の)兄がBBルーターを買い替えてました。」

「結果には原因がある」というコンピュータの世界の絶対法則を頭に叩き込むべきだ。原因がわからないのは仕方がない。しかし、原因があるはずだがまだ解らない、というニュアンスと、さっぱりわからない、事象が勝手に起こっている、というニュアンスとでは差がある。

「天罰だ」といって殺人事件の調査をしない探偵

「天罰」を信じている探偵と、「この中に犯人がいるはずだ」と信じている探偵では調査の本気度が変わってくるだろう。「結果には原因がある」ということを叩き込まれた人は「何もしていないわけがない」「何もしていないとしても、誰も認識していない設定により何かが(自動処理などで)起こったはずだ」と本気で調査することができる。

原因特定の近道

問題が起こった以上、なにか原因があるのは間違いない。振り返れば、問題を起こすようなものだと認識していなかった行為が原因であることのなんと多いことか。もともとそのように認識していれば、慎重にもなるし、問題が発生しても自己解決できてしまう。騒ぐ様な問題になっている時点で「自分には原因とは思えない行為に原因があるのでは」と考えなければならない。その考えのもとで、主観を加えずに時系列にそって何をしたか、何が起こったかを振り返る。それが原因特定の近道だ。

「何もしていない」という言葉の持つ曖昧さ

「何もしていない」という言葉は曖昧だ。主観が入り込む余地がありすぎる。だからこそ「何もしていない」と言いたい時に単に「何もしていない」とそのまま言うことに躊躇いを感じる。

  • 本当に何もしていない。
    • サーバに接続もしていない。
  • 原因だとその人が思えることは何もしていない。
    • サーバに接続だけはしたが、コマンド実行などはしていない。
    • サーバに接続してコマンドも実行したが、参照系のコマンドしか実行していない。
    • 手順書に沿って書かれている通りにやっただけ。
    • 普段通りのことをやっただけ。
    • ちょっと普段と違うことがあったが気に留めなかった、忘れた。

「型」を意識した応対を原則とする

プログラミングをしたことがある方なら、「int を返す関数として宣言したなら int を返さなければならない」と説明すると理解頂けると思う。問いの「型」を認識したうえで、原則的にはその「型」で返すことを意識するべきである。

  • 「何が(What)」「いつ(When)」「どこで(Where)」「誰が(Who)」と問われたなら「何を」「いつ」「どこで」「誰が」を答えること。
  • 方法(How)を問われたならば方法を答えること。
  • 理由(Why)を問われたならば理由を答えること。
  • Yes/Noや選択肢からの選択(Which)を問われたならばYes/Noや選択結果で答えること。
  • 数値(How much, How many)を問われたならば数値を答えること。

ふと耳に入ってくるやり取りのなかで、「あぁ、その回答では上司の人はいらだってしまうだろうな」と思う答えは大概「型」をはずしている。

例: 問い「○○に■■の件のメールを送った?」

送ったか?という問いには「送った」か「送っていない」での答えを含めるのが大原則でしょう。付加的な情報はその後だ。

  • ×「あ、すぐ送ります!」
  • ○「(はい、)(いついつに)送付済みです!」
  • ○「(すいません、)まだ送っていませんでした。すぐに送ります!」

例: 問い「アプリが通信できないっていうことだけど、まず接続先へのping疎通確認はできてる?」

ping疎通確認をしたか?という問いには「確認した」か「確認していない」をまず答え、確認したのならばその結果を加える。さらに次の確認観点まで踏み込めば「わかってるな」となる。

  • ×「アプリのログに○○というメッセージが出ているので通信に失敗してると思うんです。」
  • ○「ping疎通確認は実施していませんでした。確認してみます!」
  • ○「(はい、)ホスト□□に対してのpingは成功しています。」
  • ◎「(はい、)ホスト□□に対してのpingは成功しています。あと、telnetでのポート指定疎通もできています。疎通はしていますが、アプリのログに○○というメッセージが出ているので、アプリの設定まわりに原因があるのではないかと当たりをつけて確認中です。」

上記の×のような「型」を外したやり取りをしていませんか?私も(確信犯的に)たまにやります(おぃ)が、基本的にはルール違反です。

もちろん、int 型を返す約束のメソッドが「例外」を出す場合もあるように、あまりに問い(入力)がおかしいならば「ちょっと待ってください!」という場合もある。しかし、「例外」がそうであるように、受け手側に準備がないと会話に混乱を生じる場合がある。その場合でも「私は貴方の質問の『型』を理解して、それに答えようとしたが、答えを出すには不都合がある」というニュアンスを伝えると、受け手側が理解しやすいと思う。

例: 問い「○○と△△を動かすにはメモリの量はどのぐらいにすればよいのかな?」

技術者として答えるために必要な情報が足りておらず、情報が欲しい気持ちはわかる。しかし欲しい情報を質問しても「何故それを質問されるのか?」が解ってもらえなければ「問いに対して問いを返して来た」と捉えられてしまうこともある。

  • ×「ちょっと待ってください、それだけの情報では答えられませんよ!」
  • △「○○で管理するサーバ台数は何台ぐらいですか?△△の利用者数は何人程度でしょうか?」
  • ○「○○と△△を動かすのに必要なメモリの量は利用条件に依存して決まります。○○であれば主として管理するサーバ台数、△△であれば主として利用者数に依存しています。それぞれどの程度か解れば概算レベルではお答えできますが、把握されていますか?」

rp_filter が静的ルーティング下の traceroute に及ぼす影響

静的ルーティングを設定した WindowsLinux (RHEL6.4) で、それぞれ tracert と traceroute を実行したとき不思議な差が出た。解明までのプロセスが自分の好みにドンピシャだったので順を追って書く。

環境

環境を大まかに書くと下図のようになる。外部との通信用のデフォルトゲートウェイと、内部のシステムである targethost と通信するための内部ネットワークに接続するゲートウェイ A がある。Windows Server 2012 のサーバと RHEL 6.4 のサーバそれぞれに、targethost 向けの静的ルートを入れ、ゲートウェイにはゲートウェイ A を設定した。

設定が終わったので、「さて、正しいルートを通っているかな♪」と tracert と traceroute を叩く。すると違いが。

Windows Server 2012 での実行結果は希望通りのものだ。何の問題もない。

> tracert -d t.t.t.t
Tracing route to t.t.t.t over a maximum of 30 hops

  1    1 ms   <1 ms   <1 ms  a.a.a.a
  2   27 ms   23 ms   29 ms  x.x.x.x
  3   10 ms   17 ms    9 ms  y.y.y.y
  4   11 ms   11 ms   12 ms  z.z.z.z
  5   18 ms   12 ms   10 ms  t.t.t.t

Trace complete.

RHEL6.4 での実行結果は最初と最後以外が * (応答無しを示すアスタリスク) になってしまう。

# traceroute -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.658 ms   1.111 ms   1.558 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  17.492 ms  18.005 ms  18.002 ms

同じネットワーク設定で、同じ静的ルーティングを設定しているのに、tracert と traceroute の結果には差がある。応答があるため、targethost と通信はできているが、経路が正しいかわからない(実際にはゲートウェイ指定までがサーバの責任ではあるのだが)。

最初の着眼点: tracert と traceroute の違い

まずこの差を見た時に思い出された古い経験があった。tracert と traceroute は機能は同じでも通信に使うプロトコルが違うのだ。

Windows の tracert は ICMP を使う。traceroute は TCP, UDP, ICMP を使えるが、デフォルトでは UDP のパケットを送出する。Windows Server 2012 と RHEL6.4 のサーバで違いが出た原因はこれだろうと推測。traceroute に ICMP を使わせればよいと考えて -I オプションをつけて実行。

状況変わらず。tracert と traceroute のプロトコルの違いが原因ではなかったことになる。

# traceroute -I -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.824 ms   1.276 ms   1.739 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  23.943 ms  25.183 ms  25.400 ms

次の着眼点: タイムアウト

traceroute の man を眺めて、traceroute の原理を思い返して、次に試したのがタイムアウトのオプション指定だった。ルーターからの Time Exceeded の通知が遅いのかと思い -w 60 オプションをつけて実行。

60秒待たされたが状況変わらず。冷静に考えれば Windows Server 2012 側のレスポンス速度はミリ秒オーダーであり、デフォルトの5秒を超えるとは考え難かった。

# traceroute -I -n -w 60 t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   9.642 ms   9.684 ms   9.576 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  20.978 ms  21.206 ms  20.643 ms

急展開の発見: ルーターからの Time Exceeded はサーバーまで返っている!

ルーターからの応答が本当に無いのか、それとも Time Exceeded ではない何かなど、traceroute の想定していない応答になってしまっているのか、それを確認したくて tcpdump を実行してみた。するとなんと、全てのルーターからちゃんと ICMP Time Exceeded が返って来ているではないか。

tcpdump の見やすさのため、以下のオプションを指定して実行した。

  • -q 1: 各TTLごとに1パケットずつ(通常は3パケット)
  • -z 1: 1秒に1パケットずつ(通常は16パケット同時)
# traceroute -I -n -q 1 -z 1 t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   1.963 ms
 2  *
 3  *
 4  *
 5  t.t.t.t  10.490 ms

別のターミナルで実行しておいた tcpdump の結果はこうなる。見事に ICMP time exceeded が確認できる。

# tcpdump -n -i eth1 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes

06:14:41.565256 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 1, length 40
06:14:41.566080 IP a.a.a.a > rhel6.4: ICMP time exceeded in-transit, length 68

06:14:42.564260 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 2, length 40
06:14:42.598026 IP x.x.x.x > rhel6.4: ICMP time exceeded in-transit, length 36

06:14:43.565176 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 3, length 40
06:14:43.573244 IP y.y.y.y > rhel6.4: ICMP time exceeded in-transit, length 36

06:14:44.565501 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 4, length 40
06:14:44.579159 IP z.z.z.z > rhel6.4: ICMP time exceeded in-transit, length 76

06:14:45.566333 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 5, length 40
06:14:45.576810 IP t.t.t.t > rhel6.4: ICMP echo reply, id 15728, seq 5, length 40

サーバまで ICMP Time Exceeded が返っているとなると、それが traceroute には伝わっていないことになる。そんな芸当ができるのはカーネルファイアウォールぐらいしかない。しかし、iptables は利用していないため、ファイアウォール要因ではない・・・

記憶のリンク: rp_filter カーネルパラメータ

そこまで推測が進んだ時にあるカーネルパラメータの存在が(大袈裟に言えば電撃的に)思い出された。「rp_filter」だ。この rp_filter は Oracle RAC を組む為の Oracle Grid Infrastructure のインストレーション・ガイドに登場するカーネルパラメータで、「よくわからんけど経路云々でパケットをドロップするかもしれない危険なものだから(RACの相互接続はプライベードで閉じているネットワークなので)無効化すべき」程度には認識していた。

2.7.9 複数のプライベート・インターコネクトとEnterprise Linux

カーネル2.6.31以上(Oracle Unbreakable Enterprise Kernel 2.6.32を含む)では、戻り経路フィルタのバグが修正されています。この修正の結果、プライベート・インターコネクトに複数のNICを使用するOracle RACシステムでは、現在、rp_filterパラメータに固有の設定が必要です。この要件は、Linuxカーネル2.6.32以上を実行しているすべてのExadataシステムにも適用されます。rp_filterパラメータにこれらの設定を行わないと、インターコネクト・パケットが遮断または破棄される可能性があります。

rp_filterの値で、戻り経路フィルタがフィルタなし(0)、厳密なフィルタ(1)または緩いフィルタ(2)に設定されます。プライベート・インターコネクトの場合は、rp_filterの値を0または2に設定します。プライベート・インターコネクトNICを1に設定すると、プライベート・インターコネクトで接続の問題が発生する可能性があります。プライベート・インターコネクトは、分離されたプライベートのネットワーク上にあるはずなので、このフィルタを無効または解放することは危険だとは考えられていません。

たとえば、eth1およびeth2がプライベート・インターコネクトNICで、eth0がパブリック・ネットワークNICの場合、/etc/sysctl.confで次のエントリを使用して、プライベート・アドレスのrp_filterを2(緩いフィルタ)に設定し、パブリック・アドレスを1(厳密なフィルタ)に設定します。

net.ipv4.conf.eth2.rp_filter = 2
net.ipv4.conf.eth1.rp_filter = 2
net.ipv4.conf.eth0.rp_filter = 1
Enterprise Linux 5.6(Enterprise Linux 5 Update 6)にはinitscripts-8.45.33-1.0.4.el5.i386.rpmを使用した修正が含まれ、これによって、カーネル・パラメータnet.ipv4.conf.default.rp_filterが2(解放モード)に設定されます。そのため、Unbreakable Linux KernelをEnterprise Linux 5.6の最上位に適用した後、すべてのNICのrp_filter値が2に設定されているため、手動での変更が不要になる場合があります。パブリック・ネットワークでより厳密な戻り経路フィルタが必要な場合は、パブリックNICのrp_filterを1に設定します。

犯人はお前だ!: rp_filter カーネルパラメータ

パケットをドロップする可能性があるということで真面目に rp_filter を調べてみる。以下の説明が一番わかりやすい。

Linux Advanced Routing & Traffic Control HOWTO 13.1. 戻り経路フィルタ (Reverse Path Filtering)

デフォルトでは、ルータはすべてをルーティングします。 パケットが「明らかに」自分のネットワークには属していなくてもです。 よくある例は、プライベートの IP 空間がインターネットに漏れてしまう問題です。 195.96.96.0/24 に向かう経路があるインターフェースに対しては、 212.64.94.1 から発したパケットは、本来到着しないはずです。

ほとんどの人はこの機能を無効にしたいと思うはずですから、 カーネルハッカー達はこれを簡単できるようにしてくれました。 /proc 以下にあるファイルを使うと、カーネルに対してこの指示ができます。 この方法は戻り経路フィルタ (Reverse Path Filtering) と呼ばれています。基本的には、あるパケットに対する返信が、 そのパケットの入ってきたインターフェースに向かわない場合、 このパケットはインチキだとみなされて無視されることになります。

tcpdump の結果を見ているから、ルータ X, Y, Z からの ICMP Time Exceeded パケットのソースアドレスがそれぞれ x.x.x.x, y.y.y.y, z.z.z.z であることは明らかである。そして、targethost (t.t.t.t) への静的ルーティングは定義しているが、x.x.x.x, y.y.y.y, z.z.z.z にもそれらが所属するネットワーク向けにも静的ルーティングは定義していない。つまり、仮に x.x.x.x, y.y.y.y, z.z.z.z に返信しようとしても、それらが入って来たインターフェースは使われず、デフォルトゲートウェイに向いているインターフェースを使用する。まさに rp_filter が「インチキ」と判定するパケットだ。

targethost (t.t.t.t) との通信に使うインターフェース(eth1)の rp_filter を無効(0) にして traceroute を実行する。

# cat /proc/sys/net/ipv4/conf/eth1/rp_filter 
1
# echo 0 > /proc/sys/net/ipv4/conf/eth1/rp_filter 
# cat /proc/sys/net/ipv4/conf/eth1/rp_filter 
0

# traceroute -I -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.658 ms   1.111 ms   1.558 ms
 2  x.x.x.x  26.490 ms  27.348 ms  28.381 ms
 3  y.y.y.y  19.065 ms  19.799 ms  20.006 ms
 4  z.z.z.z  21.561 ms  21.791 ms  21.974 ms
 5  t.t.t.t  17.492 ms  18.005 ms  18.002 ms

キタ━━━━(゚∀゚)━━━━!!


これでこれまでの事象の説明がつくことになる。

Windows Server 2012

Windows には rp_filter のような動作はないのだろう。

RHEL 6.4

RHEL 6 ではデフォルトで rp_filter = 1 であり、ルーティング情報上、送信に使わないインターフェースから入って来た「インチキ」パケットを破棄する。

rp_filter = 0 にすることでその動作は無効になる。

まとめ

覚えておくべきことは以下の通りと思う。

  • Windows の tracert と Linux の traceroute はデフォルトで使用するプロトコルが異なる。
    • tracert は ICMP を使う。
    • traceroute は UDP を使う。
    • traceroute で ICMP を使うには -I オプションを使用する。
  • RHEL 6 (7も) では静的ルーティングしているホスト・ネットワークに対する traceroute では途中のホップが無応答(*)表示になる。
    • 原因は rp_filter カーネルパラメータにある。
    • 対応方法は以下の案がある。
      • 一時的に使用するインターフェース(ethN)の rp_filter カーネルパラメータを 0 にする。
      • tcpdump -i ethN icmp」 + 「traceroute -I -n -q 1 -z 1」で生パケットから途中のホップの返信を確認する。
      • 途中経路のネットワークも静的ルーティングに加える(途中経路をサーバが知る必要はないので設計上おかしいが1つの技術観点上の方法として)。

今ある知識を動員してあがきまくっているうちに、単なる情報として記憶されていたもの(今回だと「rp_filter」)が呼び覚まされて急激に理解、利用されていくというプロセスは個人的にとても心地よい。水溶液に解けていた物質がなんらかのショックにより急激に結晶化するような。