CactiのDownは何をもって検知しているのか調べてみた


## 経緯
snmpでロードアベレージなどの値が取得できない状態なのに、CactiではDownを検知しない、という状態が何回かありました。
前々から気になっていたのですが、せっかくなので今回調べてみました。

私の環境では、snmpでの疎通確認しか使用していない為、正しくない表現もあると思います。
その場合、ご指摘頂けましたら幸いです。


## 確認した環境
[Cacti公式サイト][1]からv0.8.8bをダウンロードして、ソースコードを調べてみます。

※ なお、以下にCactiのソースコードを記載していきますが、ライセンスは[GNU General Public License][2]となっています。

## 詳細
### 1. monitorプラグインから調べ始める

CactiでホストがDownすると、「Attention・・・」と音声を流してくれたりするプラグインです。
よく夜勤で眠くなると、眠気覚ましに流すあれですｗ

[monitor][3]プラグインのv1.3-1を確認。

`monitor.php`を見ていきましたが、`$host_down`がtrue / false で音を出す、出さないの内容でしたので、コードは割愛。

となると、Cacti本体でやはりDownやUpの判断をしているようなので、本体のコードを確認していきます。

### 2. host情報に何か手がかりはあるか？

Cacti本体だと、それなりの情報になるので、まずはホストに関する情報があるであろう、`host.php`から確認しました。

`down`で検索をかけると、monitorプラグインでも出てきた`$host_down`と、その近くに`$ping = new Net_Ping`という情報が！

```host.php
if (($host["availability_method"] == AVAIL_PING) ||
	($host["availability_method"] == AVAIL_SNMP_AND_PING) ||
	($host["availability_method"] == AVAIL_SNMP_OR_PING)) {
	/* create new ping socket for host pinging */
		$ping = new Net_Ping;

		$ping->host = $host;
		$ping->port = $host["ping_port"];

		/* perform the appropriate ping check of the host */
		if ($ping->ping($host["availability_method"], $host["ping_method"],
			$host["ping_timeout"], $host["ping_retries"])) {
			$host_down = false;
			$color     = "#000000";
	}else{
			$host_down = true;
			$color     = "#ff0000";
	}
```

### 3. Pingに手がかりがあるのか？

コマンドを発行する場所だと思われる`lib`ディレクトリに`ping.php`がある。

`function`の後に`ping_icmp`/`ping_snmp`/`ping_udp`/`ping_tcp`と実行する箇所が！
恐らくこの部分が確認処理の箇所だと思うので、以下該当項目別で実際にソースコードを確認していきます。


* `ping_icmp`では、"min/avg/max"の結果が取れれば疎通可、取れなければTime out、それ以外のホストまで辿り着けなければ"Destination address not specified"と処理しているようです。

```ping.php
	function ping_icmp() {
(中略)
			if (strtolower(PHP_OS) != "winnt") {
				$position = strpos($result, "min/avg/max");

				if ($position > 0) {
					$output  = trim(str_replace(" ms", "", substr($result, $position)));
					$pieces  = explode("=", $output);
					$results = explode("/", $pieces[1]);

					$this->ping_status = $results[1];
					$this->ping_response = "ICMP Ping Success (" . $results[1] . " ms)";

					return true;
				}else{
					$this->status = "down";
					$this->ping_response = "ICMP ping Timed out";

					return false;
				}
			}else{
				$position = strpos($result, "Minimum");

				if ($position > 0) {
					$output  = trim(substr($result, $position));
					$pieces  = explode(",", $output);
					$results = explode("=", $pieces[2]);

					$this->ping_status = trim(str_replace("ms", "", $results[1]));
					$this->ping_response = "ICMP Ping Success (" . $this->ping_status . " ms)";

					return true;
				}else{
					$this->status = "down";
					$this->ping_response = "ICMP ping Timed out";

					return false;
				}
			}
		}else{
			$this->ping_status   = "down";
			$this->ping_response = "Destination address not specified";

			return false;
```

* `ping_snmp`では、`sysuptime`のOIDを用いて確認。  
OIDで取得したsnmpの起動時刻が、`get_time`で出した現在時刻との差分が+-10%以上であればレスポンスあり、それ以外の場合にはDownと判断しているようです。

```ping.php
	function ping_snmp() {
		/* initialize variables */
		$this->snmp_status   = "down";
		$this->snmp_response = "Host did not respond to SNMP";
		$output              = "";

		/* get start time */
		$this->start_time();

		/* by default, we look at sysUptime */
		if ($this->avail_method == AVAIL_SNMP_GET_NEXT) {
			if (version_compare("5", phpversion(), "<")) {
				$oid = ".1.3";
			}else{
				$oid = ".1.3.6.1.2.1.1.3.0";
			}
		}else if ($this->avail_method == AVAIL_SNMP_GET_SYSDESC) {
			$oid = ".1.3.6.1.2.1.1.1.0";
		}else {
			$oid = ".1.3.6.1.2.1.1.3.0";
		}
(中略)
		/* determine total time +- ~10% */
		$this->time = $this->get_time($this->precision);

		/* check result for uptime */
		if (strlen($output)) {
			/* calculte total time */
			$this->snmp_status   = $this->time*1000;
			$this->snmp_response = "Host responded to SNMP";

			return true;
		}else{
			$this->snmp_status   = "down";
			$this->snmp_response = "Host did not respond to SNMP";

			return false;
		}
	} /* ping_snmp */
```

* `ping_udp`では、ソケット接続で通信を確認。  
まずソケット通信ができているか否か。その上で、通信がtime outしないかどうかで判断しているようです。

```ping.php
	function ping_udp() {
(中略)
				/* get start time */
				$this->start_time();

				/* write to the socket */
				socket_write($this->socket, $this->request, $this->request_len);

				/* get the socket response */
				switch(socket_select($r = array($this->socket), $w = NULL, $f = NULL, $to_sec, $to_usec)) {
				case 2:
					/* connection refused */
					$error = "refused";
					break;
				case 1:
					/* get the end time */
					$this->time = $this->get_time($this->precision);

					/* get packet response */
					$code = @socket_recv($this->socket, $this->reply, 256, 0);

					/* get the error, if applicable */
					$err = socket_last_error($this->socket);

					/* set the return message */
					$this->ping_status = $this->time * 1000;
					$this->ping_response = "UDP Ping Success (" . $this->time*1000 . " ms)";

					$this->close_socket();
					return true;
				case 0:
					/* timeout */
					$error = "timeout";
					break;
				}

				$retry_count++;
			}
		} else {
			$this->ping_response = "Destination address not specified";
			$this->ping_status   = "down";

			return false;
		}
	} /* end ping_udp */
```

* `ping_tcp`でもソケット接続で通信を確認。  
まずソケット通信ができているか否か。その上で、通信がtime outしていないかどうかで判断しているようです。

```ping.php
	function ping_tcp() {
(中略)
				/* set start time */
				$this->start_time();

				/* allow immediate return */
				socket_set_nonblock($this->socket);
				@socket_connect($this->socket, $host_ip, $this->port);
				socket_set_block($this->socket);

				switch(socket_select($r = array($this->socket), $w = array($this->socket), $f = array($this->socket), $to_sec, $to_usec)){
				case 2:
					/* connection refused */
					$this->time = $this->get_time($this->precision);

					if (($this->time*1000) <= $this->timeout) {
						$this->ping_response = "TCP Ping connection refused (" . $this->time*1000 . " ms)";
						$this->ping_status   = $this->time*1000;
					}

					$this->close_socket();

					return true; /* "connection refused" says: host is alive (else ping would time out) */
				case 1:
					/* connected, so calculate the total time and return */
					$this->time = $this->get_time($this->precision);

					if (($this->time*1000) <= $this->timeout) {
						$this->ping_response = "TCP Ping Success (" . $this->time*1000 . " ms)";
						$this->ping_status   = $this->time*1000;
					}

					$this->close_socket();

					return true;
				case 0:
					/* timeout */
					$this->ping_response = "TCP ping timed out";
					$this->ping_status   = "down";

					$this->close_socket();

					return false;
				}
			}
		} else {
			$this->ping_response = "Destination address not specified";
			$this->ping_status   = "down";

			return false;
		}
	} /* end ping_tcp */
```

以上、簡単ですが、Cactiの疎通確認方法を見ていきましたが、今回確認できたことで、個人的にとても勉強になりました。

[1]: http://www.cacti.net/
[2]: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
[3]: http://docs.cacti.net/plugin:monitor