LGTM

Looks Good To Me

MRAI の代表的なふるまい

BGP には古くからMRAI (Minimum Route Advertisement Interval) という機能がある.

イマイチ理解していなかったが,動作を確認しておく必要があり いくつかの実装を調査した.

ざっくり言うと

vSRX IOS-XRv Quagga
新しいprefix の追加時 MRAI 待つ MRAI 待つ(*2) MRAI 待たない
最後の経路削除時(*1) MRAI 待つ MRAI 待たない MRAI 待たない
経路切替時 Best Path になってからMRAI 待つ 前回のUpdate からMRAI 待つ MRAI 周期でのみUpdate
  • (*1) prefix ごとの,最後の経路(path)
  • (*2) 5% 程度,待たないpacket があったため要調査

このようにさまざまな実装がある.

以下目次:

MRAI (Minimum Route Advertisement Interval) とは

簡単にいえば「BGP Update Message の送る頻度を,prefix ごとに制限する」機能のこと.多くのルーターではBGP neighbor 単位で設定できる.

インターネットでは,各事業者のネットワーク拡張や障害などによるBGP 経路の変化が日常的に起こっているが,短時間に大量に変化したり,長時間経路がフラップし続けるとヤバい場合がある.たとえば,末端のちいさなルーターで経路計算が追いつかず,通信に影響が出たりする.

BGP は受け取った経路をもとに各ルーターがベストパスを計算し,ピアに伝達する (BGP Update を送る) プロトコルのため,ピアから大量のBGP Update を受け取った場合は力の限り計算し,力の限り 別ピアに送りつける.

MRAI はこの動作を緩和するしくみで,力の限り計算するが,結果を送りつけるときには 一定期間の間をあけて やさしく送ることができる.

多くの実装で「期間」を設定することができるが,「何と何のあいだの期間か」は実装による. だいたい共通するのは,

  • BGP neighbor 単位で設定できる
  • 単位は秒
  • ピア & prefix (BGP 経路の宛先情報) のペアごとにタイマーを持つ

おおまかには,MRAI は経路計算結果をバッファしておいて 一定時間待ってフラッシュするしくみと捉えておけばいいと思う.同じprefix について何度も経路計算したとしてもフラッシュ時には1 Update になるため,結果的にBGP Update の数を抑えることができる.

MRAI にはデメリットもある点に注意.バッファすることでBGP Convergence が遅くなる場合もあり,インターネットを少しずつ不安定にしてしまうかもしれない.これについてはまた別のエントリーで書く.

MRAI の標準化

この機能は10年以上も前から実装があるが,2008 年にInternet Draft が書かれている.最新のI-D もExpire していて 現在でも標準はない.各社独自実装している状況.

最新のI-D によると

The Minimum Route Advertisement Interval (MRAI) timer is specified in RFC4271 [BGP]. This timer acts to rate-limit updates, on a per- destination basis. [BGP] suggests values of 30s and 5s for this interval for eBGP and iBGP respectively. The MRAI must also be applied to withdrawals according to RFC4271, a change from the earlier RFC1771.

Withdrawn にも適用せよ,とのこと.

MRAI の動作

  • Juniper vSRX 15.1X49
  • Cisco IOS-XRv 5.2.2
  • quagga 0.99.23

のふるまいを調べてみた.各々設定は

Juniper vSRX
protocols bgp {
  group foo {
    out-delay 30;
  }
}
  • default: 0s
Cisco IOS-XRv
router bgp 65000
 neighbor 192.168.0.75
  advertisement-interval 30
  • default: 0s (iBGP), 30s (eBGP)
Quagga
router bgp 65000
 neighbor 192.168.0.75 advertisement-interval 30
  • default: 5s (iBGP), 30s (eBGP)

こんな感じ.

経路の追加 / 削除時

まず,ベストパス選択がない場合の動作.RIB にない経路を入れ,消す.

左から重複のない新たな経路20000 prefix をBGP Update として入れ,経路処理して右に送り出すまでの時間を見る.十分時間経過したあとにBGP Update (Withdraw) を入れつつ同様に観測する.

経路の追加: Juniper vSRX (MRAI なし)

横軸に時刻(s),BGP Update Message (パケット) を受信したタイミングを青レーンに描画,BGP Update Message (パケット) を送信したタイミングを黒レーンに描画した.

Best Path Selection がないとはいえ結構な遅延があるが,Update 受信 → 経路処理 → Update 送信 までInterval をおかず処理していることがわかる.

経路の追加: Juniper vSRX (MRAI: 30s)

グラフのスケールが違ってて申し訳ないが,経路処理の遅延に加えて30s のInterval をおいていることがわかる.

経路の削除: Juniper vSRX (MRAI なし)

追加と同様に,MRAI がなければInterval なし.

経路の削除: Juniper vSRX (MRAI: 30s)

追加と同様に,Withdraw 時にも30s のInterval が乗る.

経路の追加: Cisco IOS-XRv (MRAI なし)

次にIOS-XRv の結果.Interval は見られない.

Juniper xSRX に比べてBGP Update 処理遅延が小さいことも興味深い.
(同じような処理プロセスだが爆速なのか,並列化など手続き自体を工夫しているのか調べてない)

経路の追加: Cisco IOS-XRv (MRAI: 30s)

Interval を待たず 何か一部お漏らししているように見えるが,(青い四角の下の黒い線) たぶん30s 待ちたいんだろうなあと汲み取った.Interval なくUpdate を送ってしまっているのは,全体300 packet 中の20 packet.

経路の削除: Cisco IOS-XRv (MRAI なし)

Interval は見られない.

経路の削除: Cisco IOS-XRv (MRAI: 30s)

興味深いのは,Interval が見られない点.

送信したBGP Update (黒い線全体) は60 packet ほど.経路追加時には20 packet ほどInterval を待たないBGP Update があったが,必要になればあとで詳しく調べる予定.

経路の追加: Quagga (MRAI: 30s)

次にQuagga のふるまい.一見Interval をおいているように見えるが,よくみると4s に満たない程度なので,これはBGP Update 処理遅延と考えるのが妥当.

経路の削除: Quagga (MRAI: 30s)

Cisco IOS-XRv のようなふるまいで,Interval がない.

ここまでのまとめ: MRAI の動作 (経路の追加 / 削除時)

長くなったので一旦まとめ.

vSRX IOS-XRv Quagga
新しいprefix の追加時 MRAI 待つ MRAI 待つ(*2) MRAI 待たない
最後の経路削除時(*1) MRAI 待つ MRAI 待たない MRAI 待たない
  • (*1) prefix ごとの,最後の経路(path)
  • (*2) 5% 程度待たないpacket があったため要調査

MRAI の動作 (続き)

経路切替時

次にBest Path Selection が走る場合.

同じAS と2 ピア張り,連続的にBest Path Selection が実行されるよう強弱 *1 をつけて同じprefix のBGP Update を送る.

経路の切替: Juniper vSRX (MRAI: 30s)

下のグラフは,

  • 横軸: 時刻(s)
  • 左からBGP Update を受信した時刻 = 正の値をプロット
  • 右へBGP Update を送信した時刻 = 負の値をプロット
  • 線が長い = 強い経路
  • 線の色が同じ = 同じ経路 *2

のように描画したもの.

MRAI: 30s に対して10s ごとに強弱のBGP Update を入れた場合:

40s ごとに強弱のBGP Update を入れた場合:

Juniper vSRX は

  • 送信するべき経路がBest Path になってから,30s (MRAI) 経過して初めてBGP Update を送信
  • 送信するべき経路のBest Path が<30s の間に変わり続ける場合,BGP Update は送信されない

のようにふるまうことがわかる.

経路の切替: Cisco IOS-XRv (MRAI: 30s)

MRAI: 30s に対して10s ごとに強弱のBGP Update を入れた場合:


(60s すぎのBGP Update 受信(青) と送信(オレンジ) が重複している部分,送信のほうが若干早い)

40s ごとに強弱のBGP Update を入れた場合:

Cisco IOS-XRv は

  • 前回BGP Update を送信してから,30s (MRAI) を経過して初めてBGP Update を送信
  • 30s 待つ間にBest Path が変わっていれば30s ですぐ送信するし,変わっていなければ以降Best Path が変わるまで送信しない
    • 変わった時点ですぐ送信

のようにふるまうことがわかる.

経路の切替: Quagga (MRAI: 30s)

MRAI: 30s に対して10s ごとに強弱のBGP Update を入れた場合:

40s ごとに強弱のBGP Update を入れた場合:

Quagga は

  • 30s (MRAI) ごとのサイクルでのみBGP Update を送信する
  • 30s おきにBest Path が変わっていれば送信するし,変わってなければ送信しない

のようにふるまうことがわかる.

ここまでの3実装のふるまい(経路切替時)について,あるprefix に限定した動作であることに注意.prefix が異なれば送信タイミングは独立している.

2016/07/23 追記

コード読んだところQuagga はprefix ごとのタイマーを持たない.BGP neighbor 単位.prefix が違っても 一定のサイクルでまとめてUpdate 送信する.

まとめ

全体のまとめ.

vSRX IOS-XRv Quagga
新しいprefix の追加時 MRAI 待つ MRAI 待つ(*2) MRAI 待たない
最後の経路削除時(*1) MRAI 待つ MRAI 待たない MRAI 待たない
経路切替時 Best Path になってからMRAI 待つ 前回のUpdate からMRAI 待つ MRAI 周期でのみUpdate
  • (*1) prefix ごとの,最後の経路(path)
  • (*2) 5% 程度,待たないpacket があったため要調査

見てわかるように,実装によりふるまいがけっこう違う.

目的によってMRAI がハマるとき / ハマらないときがある気がするけど,「このベンダーのこのバージョンでは…」といった具体的な仕様はさておき,多種多様な実装があることを知っておけばひとまず十分.MRAI を検討するときに

  • 守りたいものはなにか
  • MRAI で自衛できるものか
  • 使っているルーターのMRAI はどのように動くか.守りたいものを守れるか
  • MRAI を入れる → Convergence Time が伸びて困ることはないか
  • 周辺のAS はどのようにふるまうか

などを考えればよいかな,と思う.あとデフォルトが0s でない装置もあるので知らず知らずのうちに使っているかもしれない.

*1:強弱はMED で制御した

*2:BGP Community でマークした

unicast route の変更により,flowspec validation は走るか

flowspec は forwarding に関するルールをルーター間で伝え合うプロトコルで,「他者から受け取ったルールを適用する前にvalidation しなさい」と仕様で決まっている.

では,validation するのはいつだろう?

  1. flowspec route を受け取ったとき
  2. flowspec route に関連するunicast route に変更があったとき

1 . は当然として,2 . のタイミングでvalidation が走るかどうかで振る舞いが大きく変わりそう.

RFC5575 によれば

A flow specification received from an external autonomous system will need to be validated against unicast routing before being accepted.

と書かれているものの,「いつvalidate するか」については細かく規定されていない.関連I-D にもない.

いつものように実装にまかされてるやつなので,Juniper vSRX, Cisco IOS-XRv で試してみた.

環境

testing network

eBGP ピアを2 ルーター間で張り,

  1. unicast route を1つずつ広告する
  2. destination prefix が同じflowspec route も1つずつ広告する
  3. ピアを張ったまま,1 . をwithdraw して2 . の経路をvalidate し直すかを見る

結果

Juniper vSRX, Cisco IOS-XRv とも unicast route の変更時にもvalidation が走る

妥当な動きというか,直観的でよかった.そうでなければ「validation fail するはずなのに動いてる」「ebgp ピアがflap したらflowspec 動作が変わる」「soft clear したら動作が変わる」「flowspec route を止めて出し直したら動作が変わる」みたいになる.

以下,参考まで.

設定

Juniper vSRX

routing-options {
    static {
        route 10.0.1.0/24 discard;  # deactivate したり activate したりする
    }
    autonomous-system 64600;
    flow {
        route flow1 {
            match destination 10.0.1.0/24;
            then discard;
        }
    }
}
protocols {
    bgp {
        group ebgp {
            family inet {
                unicast;
                flow;
            }
            export redistribute-static;
            peer-as 64601;
            neighbor 192.168.0.74;
        }
    }
}
policy-options {
    policy-statement redistribute-static {
        from protocol static;
        then accept;
    }
}

Cisco IOS-XRv

class-map type traffic match-all flow1
 match destination-address ipv4 10.0.2.0 255.255.255.0  !
 end-class-map
!
policy-map type pbr drop1
 class type traffic flow1
  drop
 !
 class type traffic class-default
 !
 end-policy-map
!
route-policy permit-all
  pass
end-policy
!
router static
 address-family ipv4 unicast
  10.0.2.0/24 Null0   ! no したり入れたりする
 !
!
router bgp 64601
 address-family ipv4 unicast
  redistribute static
 !
 address-family ipv4 flowspec
 !
 neighbor-group ebgp
  remote-as 64600
  address-family ipv4 unicast
   route-policy permit-all in
   route-policy permit-all out
   soft-reconfiguration inbound always
  !
  address-family ipv4 flowspec
   route-policy permit-all in
   route-policy permit-all out
   soft-reconfiguration inbound always
  !
 !
 neighbor 192.168.0.67
  use neighbor-group ebgp
 !
!
flowspec
 address-family ipv4
  service-policy type pbr drop1
 !
!
end

unicast route があるとき

koji@vsrx> show route table inetflow.0 detail

inetflow.0: 2 destinations, 2 routes (2 active, 0 holddown, 0 hidden)
...
10.0.2/24,*/term:2 (1 entry, 1 announced)
        *BGP    Preference: 170/-101
                Next hop type: Fictitious
                Address: 0x8f3df04
                Next-hop reference count: 2
                State: <Active Ext>
                Local AS: 64600 Peer AS: 64601
                Age: 10:38
                Task: BGP_64601.192.168.0.74+179
                Announcement bits (1): 0-Flow
                AS path: 64601 I
                Communities: traffic-rate:64601:0
                Accepted

                # Accept されている

                Validation state: Accept, Originator: 192.168.0.74
                Via: 10.0.2.0/24, Active
                Localpref: 100
                Router ID: 172.16.0.3
RP/0/0/CPU0:ios#sh bgp ipv4 flowspec Dest:10.0.1.0/24
Thu May 22 10:52:24.962 UTC
BGP routing table entry for Dest:10.0.1.0/24/40
Versions:
  Process           bRIB/RIB  SendTblVer
  Speaker                  6           6
Last Modified: May 22 10:46:16.486 for 00:06:08
Paths: (1 available, best #1)
  Not advertised to any peer
  Path #1: Received by speaker 0
  Not advertised to any peer
  64600, (received & used)
    0.0.0.0 from 192.168.0.67 (172.16.0.2)

      ! best になっている

      Origin IGP, localpref 100, valid, external, best, group-best
      Received Path ID 0, Local Path ID 1, version 6
      Extended community: FLOWSPEC Traffic-rate:0,0

unicast route がないとき

koji@vsrx> show route table inetflow.0 detail all
...
10.0.2/24,*/term:N/A (1 entry, 0 announced)
         BGP                 /-101
                Next hop type: Fictitious
                Address: 0x8f3df04
                Next-hop reference count: 2
                State: <Hidden Ext>
                Local AS: 64600 Peer AS: 64601
                Age: 17:41
                Task: BGP_64601.192.168.0.74+179
                AS path: 64601 I
                Communities: traffic-rate:64601:0

                # Reject されている

                Validation state: Reject, Originator: 192.168.0.74
                Via: 0.0.0.0/0, Active
                Localpref: 100
                Router ID: 172.16.0.3
RP/0/0/CPU0:ios#sh ip bgp ipv4 flowspec Dest:10.0.1.0/24
Thu May 22 10:58:36.526 UTC
BGP routing table entry for Dest:10.0.1.0/24/40
Versions:
  Process           bRIB/RIB  SendTblVer
  Speaker                  7           7
Last Modified: May 22 10:54:16.486 for 00:04:20
Paths: (1 available, no best path)
  Not advertised to any peer
  Path #1: Received by speaker 0
  Not advertised to any peer
  64600, (received & used)
    0.0.0.0 from 192.168.0.67 (172.16.0.2)

      ! invalid になっている

      Origin IGP, localpref 100, valid, external, invalid flowspec-path
      Received Path ID 0, Local Path ID 0, version 0
      Extended community: FLOWSPEC Traffic-rate:0,0

mention-bot を試した

Facebook が公開しているPull Request (PR) レビュー支援ツール.
PR を作ったとき,最適なレビューアーを見つけてきてPR 上でメンションしてくれる.

github.com

試したけど,うまくいかなかった.

ざっくり言うと

セキュリティ上の懸念があって断念した.

  1. 「ボットアカウントはGitHub の2FA をオフにしなさい」と書いてある
    • GitHub API にはないが Web UI にはある機能を使っているから
    • 自動Web ログイン と 2FA の併用がむずかしい
  2. このボット機能のためだけに,1 の状態で動くエンドポイントを持ちたくない
    • PR 作成時にWebHook でトリガーしてもらうのにエンドポイント必要
    • 得られる価値に対してリスク高い

やりたかったこ

PR レビューがコスト高になることがある

Private Repo でGitHub フローを回しており,日常的にPR レビューしている. チーム内のさまざまな事情により,ふと気づくとレビューにすごい時間を使っていることがある.

個人がレビューを嫌って押し付けあうのは避けたいが,チーム全体として効率よくレビューしたい.

日本人だし「〇〇さんレビューお願いします」って言えない

〇〇さんにお願いするのが最適な場合でも「お願いします」ってなかなか言えない.日本人だからね… 急ぎの中ちょっと待っちゃったりして,状況をすこしずつ悪くしてしまう.

また,PR がぽんっと出た瞬間「あいつが見るだろう」みたいな考えがよぎる. たぶんみんな同じことやってて そもそもの着手が遅くなり,ちょっとずつ他メンバーのタスクをブロックしちゃう.

これやめたい.

試したこ

レビューアーをランダムに決めるやつ とかもあるが, やりたいことの半分しか実現できないので mention-bot に興味を持った.

ぱっと見「WebHook トリガーにしないといけないの面倒…」と思ったが, 「PR 作ったあと チャット経由で手動キックすれば動くだろう」と,Hubot スクリプトを書いてみた.

ところが…GitHub API トークンではなくID / パスワードでログインしないと動かないことがわかった.

mention-bot の動き

ここ に書いてあるとおりだが,

  1. WebHook でPR の情報を受け取る.またはPR の情報を渡して手動で呼ぶ
  2. PR 内で,どのファイル / 行にdiff があるか探す
  3. 削除/変更があった行について,git blame でmaster ブランチでの最終更新者を探す
    • いじる行の最終更新者がレビューアーに向く という前提
  4. PR 全体で3 の頻出ランキングを作る
  5. いじるファイル内の変更しない行についても 同様にgit blame ベースのmaster 最終更新者ランキングをつくる
    • いじる行でなくても,同じファイル内の最終更新者がレビューアーに向く という前提
  6. 5 を4 のケツにくっつける
  7. 6 からPR 作成者を除いたトップ N人について,指定のコールバックを呼ぶ

問題は,git blame に相当するGitHub API がないこと. Web UI にはあるのでスクレイピングして代用しているが,これが原因でボットアカウントを2FA にできない. 場合によってはAPI も使うため, さらにトークンが必要.

あきらめた

とても便利そうなんだけれど,セキュリティ的にイマイチ という判断をした.
今回Private Repo なのでやめたが,GitHub Enterprise で社内に閉じてるとか, Public Repo であれば敷居が下がったかもしれない.

git blame をローカルでやればいいだけかもしれない.

Public Repo なら認証不要*1 なので,

var mentionBot = require('./run-mention-bot.js');
var GitHubApi = require('github');

var github = new GitHubApi({ version: '3.0.0' });

var config = {
  maxReviewers: 5,
  findPotentialReviewers: true,
  fileBlacklist: [],
  userBlacklist: [],
  requiredOrgs: []
}

mentionBot
  .guessOwnersForPullRequest(
    'https://github.com/fbsamples/bot-testing',
    98,
    'mkonicek',
    'master',
    false,
    null,
    config,
    github
  )
  .then(function(users) {
    console.log(users);
  })
  .catch(function(err) {
    console.error(err);
  });

こういう感じのが動く.

特定Organization 内からのみレビューアー候補を選びたいときは,API トークンが必要なことがある.

それでもちょっと試したいひと向け

Private Repo でちょっとだけ試す方法.
ES6 のtranspile 環境をつくるのが面倒なので,mention-bot レポジトリ内にスクリプトを置く作戦.

git clone https://github.com/facebook/mention-bot.git
cd mention-bot
npm install
npm install github --save

Web UI 認証をやめて,認証済みCookie をつかう.

# githubAuthCookies.js を書き換え

module.exports = '_octo=GHx.x.xxxxxxxxxx.xxxxxxxxxx; logged_in=yes; dotcom_user=codeout; _gat=1; _gh_sess=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; _ga=GAx.x.xxxxxxxxxx.xxxxxxxxxx; tz=Asia%2FTokyo; user_session=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

私はChrome から抜いた.

Cookie

最後にスクリプト本体.

# mention.js

var mentionBot = require('./run-mention-bot.js');
var GitHubApi = require('github');

var github = new GitHubApi({ version: '3.0.0' });
// Organization 内からのみレビューアー候補を選びたいときのみ必要
github.authenticate({
  type: 'oauth',
  token: '...token...'
});

var config = {
  maxReviewers: 5,
  findPotentialReviewers: true,
  fileBlacklist: [],
  userBlacklist: [],
  requiredOrgs: []  // レビューアー候補がprivate メンバーだと動かない
}

mentionBot
  .guessOwnersForPullRequest(
    'https://github.com/user/private_repo',
    1,          // PR Number
    'codeout',  // PR 作成者
    'master',   // blame するブランチ
    false,  // Private Repo でも false, null でOK
    null,   // Organization 縛りにするときは true, 'organization'
    config,
    github
  )
  .then(function(users) {
    console.log(users);
  })
  .catch(function(err) {
    console.error(err);
  });

実行結果

mention-bot $ time node mention.js
[ 'codeout', 'foo', 'bar', 'baz' ]
node mention.js  0.89s user 0.23s system 11% cpu 9.385 total

Organization 縛りをつけるとAPI コールが増えて遅い.

mention-bot $ time node mention.js
[ 'codeout', 'foo', 'bar' ]
node mention.js  1.01s user 0.25s system 6% cpu 18.059 total

*1:PR 上でメンションしない場合

高速にnetflow を吐く

最近 netflow コレクターをさわる機会が多く「コレクターをベンチマークしたい」と思っている. 「netflow v5 なら何 flow/s まで受けられる」「v9 ならこのくらい」など,性能限界を知っておきたい.

2016/02/20 追記: NetFlow-Generator

戦略

いくつか考えられる方法があって

  1. ネットワークデバイスのサンプリングレートを変え,多めにnetflow export する
  2. netflow データを用意し,高速に送信する

1 は実網に影響あるかもしれないためパス.2 の方法にする. netflow データの作り方もいくつか思いつく.

2-1. ネットワークデバイスが吐いたnetflow を再利用する
2-2. ウソデータをでっち上げる

ネットワークデバイスが吐いたnetflow を再利用する

nfdump というツールを利用する.

$ nfcapd -l . -p <ポート>

データが溜まるのを待つ.1M flowほどあれば十分だが,溜まるのに時間がかかる場合は-t <interval> オプションをつけておく. これはnetflow データを1 ファイルにまとめるため.(デフォルトでは5分でファイルローテートしてしまう)

1M flowも溜めるのは,netflow データストアによって重複netflow を無視する可能性があるため. なるべく重複レコードが少ないほうがよい.

実行すると nfcapd.201602180335 のようなファイルができ,

$ nfreplay -H <IP アドレス> -p <ポート> -r nfcapd.201602180335

のようにするとnetflow パケットを送信できる.デフォルトではnetflow v5 で,-v9 オプションをつけるとnetflow v9 になる.

OSX の場合は

Error sending data: No buffer space available

のようなエラーが出るかもしれない.手元ではLinuxからnetflow を吐き,ベンチマークした.

ウソデータをでっち上げる

nfdump に加え,flow-tools というツールを利用する.

$ nfcapd -l .

しておき,

$ flow-gen -n 1000000 -V 5 | flow-send 0/127.0.0.1/9995

すると各フィールドがシーケンシャルなnetflow データが作れる.

srcIP            dstIP            prot  srcPort  dstPort  octets      packets
0.0.0.0          255.255.0.0      17    0        65280    1           1
0.0.0.1          255.255.0.1      17    1        65281    2           2
0.0.0.2          255.255.0.2      17    2        65282    3           3
0.0.0.3          255.255.0.3      17    3        65283    4           4
0.0.0.4          255.255.0.4      17    4        65284    5           5
0.0.0.5          255.255.0.5      17    5        65285    6           6
0.0.0.6          255.255.0.6      17    6        65286    7           7
0.0.0.7          255.255.0.7      17    7        65287    8           8
0.0.0.8          255.255.0.8      17    8        65288    9           9
0.0.0.9          255.255.0.9      17    9        65289    10          10
0.0.0.10         255.255.0.10     17    10       65290    11          11
0.0.0.11         255.255.0.11     17    11       65291    12          12
0.0.0.12         255.255.0.12     17    12       65292    13          13
  • flow-gen が作るnetflow のタイムスタンプはエポックタイムで0 だが,nfcapd が一定時間シフトさせてしまう (バグの可能性ある)
  • flow-gen が作るnetflow は30 flow/pkt
    • MTU1500 ではこれが限界
    • 変更するオプションはなさそう

これをネットワークデバイスが吐いたnetflow を再利用する と同様にnfreplay すればよい. 手元のFE NIC 環境ではwire rate 出た.(およそ 250k flow/s)

さらにスピードを出す

250k flow/s も出れば十分すぎるが,さらにコレクターをいじめるためにtcpreplay をつかう. 前述のnfreplay 時にパケットキャプチャーしておき,

$ tcpreplay -t -i eth0 nfreplay.pcap

すれば1G NIC 環境でもwire rate 出た.(およそ 2.5M flow/s)

ちなみにdst IP アドレス,dst MAC アドレスを変えつつ送信してもwire rate 出る.

$ tcpreplay-edit -tC -i eth0 -D 0.0.0.0/0:<IP アドレス>/32 --enet-dmac <MAC アドレス> nfreplay.pcap

netmap を使えば10Gbps ちかく出る可能性 はあるが,やめてあげてください.コレクターが動くわけがない.

スピードを調整する

tcpreplay が便利なのは送信スピードを変えられること.

-x string, --multiplier=string
-p number, --pps=number
-M string, --mbps=string

オプションを使う.

課題

nfreplay を使った方法ではIPFIX,sflow を吐けない.対応しているのはnetflow v5/v9 のみ. 実パケットをキャプチャーし,tcpreplay で高速送信する方法ならOK.

2016/02/20 追記: NetFlow-Generator

NetFlow-Generator というのがあるよ」と教えてもらいました.

ウソデータをでっち上げる ときのflow-tools (のflow-gen) の代わりに使える.

  • タイムスタンプをいじれる
  • flow/pkt をいじれる
  • flow-gen → nfcapd でフローデータ保存 → nfreplay と同等のスループットで動く

柔軟でよさそう.

$ flowgen -h
Usage: flowgen [options] [flowrec-options] <collector>
 options:
   -n, --count <num>
   -p, --port <num>
   -V, --version <version>
   -f, --flowrec <# of flow records in packet>
   -d, --debug <debug level>
   -N, --nosend
   -h, --help
 flowrec-options:
   -w, --wait <wait time>
   -i, --interval <interval>
   --enginetype <engine type>
   --engineid <engine id>
   --srcaddr <src ip address>
   --dstaddr <dst ip address>
   --nexthop <nexthop ip address>
   --inputif <input IfIndex>
   --outputif <output IfIndex>
   --packets <# of packets>
   --octets <# of octets>
   --firstseen <first seen>
   --lastseen <last seen>
   --srcport <src port>
   --dstport <dst port>
   --tcpflags <tcp flags>
   --protocol <protocol number>
   --tos <tos value>
   --srcas <src AS#>
   --dstas <dst AS#>
   --srcmask <src subnet mask length>
   --dstmask <dst subnet mask length>

  Numbers can be expressed using the following meta characters:
    111      (static)
    111-222  (sequential)
    111:222  (random)
    100@70,200@20,300@10   (probabilistic)

junoser internals

junoser というRuby gem をメンテしている. これはJUNOS Config のPEG パーサーで,標準入力やファイルを JUNOS Config として構文解析できる.解析後,文法チェックやスタイルの変換もできる.

たとえば,

構文チェック.display | set 形式とデフォルトの構造化スタイル両方をサポート.

$ junoser -c config.invalid
Invalid syntax:  set protocols bgp group e1 neighbor 10.0.26.1 exprot s2b
zsh: exit 1     junoser -c config.invalid

スタイルの変換.display | set スタイル → 構造化スタイル

junoser -s config.set

スタイルの変換.構造化スタイル → display | set スタイル

junoser -d config

さっとチェックしたり,CI に使ったり,レビュー時 display | set スタイルがつらい時に重宝してる.

文法定義はどこから来るか

CLI と,NETCONF 用のXML が構造的に似ていることを利用して,<get-schema> で取れるXML Schema(XSD) を使っている.

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
  <xsd:import schemaLocation="junos.xsd" namespace="http://xml.juniper.net/junos/12.1X47/junos"/>
  <xsd:element name="configuration">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:choice minOccurs="0" maxOccurs="unbounded">
          <xsd:element name="interfaces" minOccurs="0">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:choice minOccurs="0" maxOccurs="unbounded">
                  <xsd:element name="interface" minOccurs="0" maxOccurs="unbounded" type="interfaces-type">
                  </xsd:element>
                </xsd:choice>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
  <xsd:complexType name="interfaces-type">
    <xsd:sequence>
      <xsd:element name="name">
        <xsd:complexType>
          <xsd:simpleContent>
            <xsd:restriction base="key-attribute-string-type">
              <xsd:enumeration value="$junos-interface-ifd-name">
              </xsd:enumeration>
              <xsd:enumeration value="interface-name">
              </xsd:enumeration>
            </xsd:restriction>
          </xsd:simpleContent>
        </xsd:complexType>
      </xsd:element>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="unit" minOccurs="0" maxOccurs="unbounded">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="name">
                <xsd:complexType>
                  <xsd:simpleContent>
                    <xsd:restriction base="key-attribute-ulong-type">
                      <xsd:enumeration value="$junos-underlying-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="$junos-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="interface-unit-number">
                      </xsd:enumeration>
                    </xsd:restriction>
                  </xsd:simpleContent>
                </xsd:complexType>
              </xsd:element>
              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element name="family" minOccurs="0">
                  <xsd:complexType>
                    <xsd:sequence>
                      <xsd:choice minOccurs="0" maxOccurs="unbounded">
                        <xsd:element name="inet" minOccurs="0">
                          <xsd:complexType>
                            <xsd:sequence>
                              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                                <xsd:element name="address" minOccurs="0" maxOccurs="unbounded">
                                  <xsd:complexType>
                                    <xsd:sequence>
                                      <xsd:element name="name">
                                        <xsd:complexType>
                                          <xsd:simpleContent>
                                            <xsd:extension base="ipv4prefix">
                                              <xsd:attribute name="key" type="xsd:string" fixed="key"/>
                                            </xsd:extension>
                                          </xsd:simpleContent>
                                        </xsd:complexType>
                                      </xsd:element>
                                    </xsd:sequence>
                                  </xsd:complexType>
                                </xsd:element>
                              </xsd:choice>
                            </xsd:sequence>
                          </xsd:complexType>
                        </xsd:element>
                      </xsd:choice>
                    </xsd:sequence>
                  </xsd:complexType>
                </xsd:element>
              </xsd:choice>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:schema>

上記のXSD はごく一例*1だが,XML パーサーを使って,これを下のruby コードに変換する. (junoser に含めたパーサージェネレーターで自動生成)

module Junoser
  class Parser < Parslet::Parser
    rule(:configuration) do
      c(
        b(str("interfaces"),
          c(
            interfaces_type
          )
        )
      )
    end

    rule(:interfaces_type) do
      b((arg | str("interface-name")).as(:arg),
        c(
          a(str("unit"), arg | arg | str("interface-unit-number"),
            c(
              b(str("family"),
                c(
                  b(str("inet"),
                    c(
                      b(a(str("address"), arg))
                    )
                  )
                )
              )
            )
          )
        )
      )
    end
  end
end

全体で60万行くらいのXSD が,4万行くらいのPEG パーサーになる.これがjunoser の本体だが,パーサージェネレーターの生成物でもある.

パーサージェネレーターの闇

ここから本題です.ここまでは比較的スッキリと書ける.処理する対象がゴチャゴチャしているため一見ややこしいが,やっていることはシンプル.

問題1: CLI とXSD の微妙なちがい

interfaces {
    ge-0/0/0 {
        description "interface 1";
    }
}

上のようなJUNOS Config がある.よく見ますね. description ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<interfaces>
    <interface>
        <name>ge-0/0/0</name>
        <description>interface 1</description>
    </interface>
</interfaces>

なるほど,<description/> 要素に文字列が入っている.

routing-options {
    autonomous-system 64500;
}

いっぽう,上のようなJUNOS Config がある. autonomous-system ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<routing-options>
    <autonomous-system>
        <as-number>64600</as-number>
    </autonomous-system>
</routing-options>

なるほど,こちらは<autonomous-system/> 要素の中で,新たに<as-number/> 要素で文字列を包んでいて interface description と違う.

では2つのXSD を見てみましょう.

<xsd:complexType name="interfaces-type">
  <xsd:sequence>
    <xsd:choice minOccurs="0" maxOccurs="unbounded">
      <xsd:element name="description" minOccurs="0" type="xsd:string"/>
    </xsd:choice>
  </xsd:sequence>
</xsd:complexType>
<xsd:element name="autonomous-system" minOccurs="0">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="as-number" type="xsd:string"/>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

ほぼ同じなわけです.「as-number はただのラベルであって,CLI 上には現れないこと」がXSD からは分からない.

junoser では文脈をくみとりながら「これはただのラベル要素」「これは必要な要素」と選別していて,そこが泥臭いハックになっている.

問題2: そもそもXSD にない定義

junoser はvSRX のXSD をベースにしているため,vSRX にない機能はパースできない.

<xsd:element name="service-set" minOccurs="0" maxOccurs="unbounded">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

たとえば,上はset services service-set ... に対応するXSD だが,vSRX には定義が存在しない.

問題3: ライセンス

XSD で定義される構文木に泥臭いハックを入れて,CLI にあてはめるためのパーサーを作った.このコードのライセンス,微妙ですよね. いちおうJuniper のかたに確認して公開しているが…大丈夫かな? 詳しいかたがいればアドバイスいただけると嬉しいです.

Juniper はよくてもCisco がNG とかだと,IOS-XR 版が作れなくて困ってしまう.

あと,junoser は個人プロジェクトとして進めているので,Juniper にフィードバックしたくてもできないことがあって困ってる.

おわりに

junoser をメンテするにあたって,けっこう泥臭いことをやっています.でも,けっこう動くレベルのはずです.

もし興味をもってもらえたら ぜひ使ってみてください.パースできないCLI コマンドがあれば,フィードバックよろしくお願いします.自分ではあまり使わないConfig については,問題1 を潰しきれていません.

また,手元にXSD がないのが原因で動かないケース(問題2) もあるので,その際にはXSD の提供に協力してもらえると助かります.

こちらが安定してきたら,IOS-XR 版にも着手予定です.

*1:関係のない要素も省略

netconf / restconf 時代にもxlogin 使いまくってる話

NetOpsCoding Advent Calendar 2015 18日目のエントリーです. ハードウェア製品を使ったネットワーキングの話.

さまざまなコンポーネントについて YANG Model の標準化が進み,NETCONF 使える気運が高まっています.さらにRESTCONF も待っていて,ハードウェアで高速にルーティングしつつ運用を自動化できそうな予感がしています.

私は,条件がそろえばNETCONF を使います.XML といえど エラーや出力が構造化されていて きちんと取れるのはうれしい.

一方で 日常のネットワーク運用はCLI に頼っていて,判断をはさみつつ手続き (= コマンド) を変えます.このような用途にはNETCONF / RESTCONF はたぶん向きません.大がかりすぎです.

今回は,自動化できないときでも便利な xlogin を紹介します.

( ようやく,これ自動化の話じゃないわ と気づきました… 気にせず行きます すみません )

xlogin ?

rancid に同梱されている clogin, jlogin, flogin, ... のことをまとめて xlogin と呼んでいます.これらはrancid のためのサポートツールで,各種デバイスにログインするためのexpect スクリプトセットです.

ssh / telnet を薄くwrap するだけの単純なexpect ですが,めちゃ便利なので, いまはネットワークデバイスにログインするとき ssh / telnet を呼ぶことはほとんどありません.たぶんrancid 本体より便利です.

つかいかた

ログインする例 (IOS 系)

xlogin はデバイス種別ごとに実行ファイルが分かれており,IOS 系にはclogin を使います.

clogin <デバイス名>

ログインする例 (IOS 系)

パスワードを自動入力しつつ telnet でログインしています.よく見れば,ログインと同時にenable になっていることがわかると思います.

上はQuagga の例ですが,IOS でも同じ動きです.

複数デバイスにコマンド実行する例 (Junos)

複数のJunos に対し,show version を実行します.Junos にはjlogin を使います.

jlogin -c "show version" <デバイス名1> <デバイス名2>

複数デバイスにコマンド実行する例 (Junos)

2台のJunos に順にssh ログインし,show version を実行し,ログアウトします.結果は標準出力されるので,grep に渡したり,ファイルにリダイレクトしたり,| vi - して開く → 編集 → 保存したりできます.

よく見ると ssh のまえにtelnet を試しているのがわかると思いますが,xlogin はデフォルトでは「telnet / ssh 両方を試してログインできるほうを使う」という動きをします.

初めてssh するデバイスについては,自動でhostkey のfingerprint を~/.ssh/known_hosts に保存します.fingerprint が違っていることを検出した場合はログインしません.

複数コマンドを実行する例 (IOS-XR)

IOS-XR にログインし,interface description を変更します.

! ./cmd
show interfaces description

configure
interface Gi0/0/0/0
description interface1
commit
exit
exit

show interfaces description
exit

上のような一連のコマンド群を実行する必要がありますが,ファイルに書いておいてxlogin に渡すだけ.

clogin -x <コマンドファイル> <デバイス名>

複数コマンドを実行する例 (IOS-XR)

単純に逐次実行するだけなので,失敗時にrollback しない点に注意してください

  • バイス固有のコメント行をいれておくと便利
  • 引数をふやせば,複数デバイスに対して実行できる
  • jlogin -c "show version; show chassis hardware" router1 のようにコマンドラインからも渡せる
  • vty にコピー&ペーストしたときのバッファ溢れが気になる時など,vi cmd; clogin -x cmd <デバイス名> すれば安心.プロンプトが返ってきたら1行実行,また1行実行…という動きをする

以上,xlogin の使用例をならべてみました.

clogin, jlogin, ... を使い分けるのは 少々めんどうですが,便利そうじゃないですか?

対応デバイス

xlogin は対応デバイスが多くてスゴい.めんどくさいプロンプト処理をやってくれてます.

xlogin Vendor
alogin Alteon WebOS switch login
anlogin Arbor Networks login
avologin Avocent login
blogin Bay Networks(Nortel) login
clogin Cisco login
complogin Compass login
dllogin D-Link login
elogin ADC EZT3 login
flogin foundry login
fnlogin fortinet login
hlogin hp login
htlogin Hitachi router login
jlogin juniper login
mrvlogin MRV optical switch login
mtlogin MikroTik router login
nlogin netscreen login
nslogin Netscaler login
panlogin Palo Alto Networks login (based on alogin)
rivlogin Riverstone (and Enterasys SSR) login
rscmd Riverstone Networks Automated login
tlogin Netopis login
wlogin Cisco Wireless Lan Controller login
xilogin Xirrus login

残念なのは,github などにレポジトリがなくてソーシャルコーディングできないこと. ここにないデバイスをサポートしても,本家にマージしてもらうのがつらい.

認証情報

認証情報の持たせかたについても触れておきましょう.

認証情報は,あらかじめ~/.cloginrc という設定ファイルに書いておかないといけません.

  • ログイン方法 (ssh or telnet)
  • 接続先ポート
  • ユーザー名
  • ログインパスワード / enable シークレット
  • ...

などを書いておきます.ログインする際 「xlogin に渡したデバイス を上から順に探し,最初にマッチした設定を使う」という動きをします. サーチドメインをつけた名前や,A レコードを引いたあとのIP アドレスではないことに注意です.デバイス名にはワイルドカードが使えます.

# ~/.cloginrc

add method    router4  {telnet:2605}
add password  router4  {zebra} {zebra}

# default

add user      *        {koji}
add password  *        {login_passwd} {enable_secret}

~/.cloginrc が他者に読まれそうなパーミッションであれば,xlogin 実行時に警告します.

設定方法の詳細はrancid web サイト にあります.プロンプトのカスタマイズや enable するかどうかなど,けっこう細かく設定できます.

その他のヒント

インストール

  • deb パッケージがあるディストリビューションが多い.Debian, Ubuntu などはapt-get 一発
  • rpm パッケージは (たぶん)ないので,自分でつくらないといけない
  • OSX であれば brew にある
  • ping 127.0.0.1 の応答がないと./configure がコケる.OSXfirewall など注意
  • ソースパッケージからビルドしてもよい.なぜか./configure でxlogin が生成される.「./bin/*login だけ$PATH のどこかにコピー」とかよくやる

~/.cloginrc をやめる

ファイルに認証情報を書くのは セキュリティ上いまいちなので,私はvault に保管しています. 完全ではありませんが,他者からアクセスする機会を大幅に減らせますし,認証情報へのアクセスログが取れます.ずいぶんマシ.

やりかたは別のエントリー を参照してください.

まとめ

xlogin の使い方とヒントについて説明しました.

ログインするとき ssh / telnet がわりに使うのはもちろんですが,自動でなにかするコードを書くときにも重宝しています. そのコードには「なにかする」ことだけを記述し,認証とコマンド実行をxlogin に切り出せば 少ない手数で見通しよくなりますし,認証レイヤーを共通化できます.

NETCONF を使いたくても,サポートしていないデバイスがあったりで xlogin + CLI パーサー を選ぶ局面はまだまだありそうです.残念ですけどね.

条件を満たすとチャット通知するリマインダーをつくる

NetOpsCoding Advent Calendar 2015 3日目のエントリーです.

最近はネットワーク運用をラクにすることに興味があります.

たとえば RESTCONF のようなI-D が標準され,進化すれば ネットワークへの自動デプロイが実現できると思いますが,それはネットワーク運用の一部でしかありません.

スコープをネットワーク運用だとすれば

  1. オーダーをもらう
  2. ネットワークをつくる
  3. 監視する
  4. トラブル対応する
  5. ネットワークを拡張する
  6. 廃止する

このようなワークフロー全体をラクに回したい.デプロイツールは1つのコンポーネントなのですが,コンポーネントの進化に比べて ワークフローが進化していない(っぽい) ことに少し懸念を持っています.

雑に言えば「このコンポーネントと このコンポーネントを組み合わせれば,ほら,こんなに便利」という知見が足りないと感じています.

前置きが長くなりました.今回はそういう流れの話にしたいと思い, いろんなコンポーネントを組み合わせて リマインダーをつくってみようと思います.

やりたいこと

ネットワークを運用していると「次のタイミングでやりましょう」ということが結構あります.多くの場合は やりたいことを覚えておいて,なにかの際に,もしくは ときどき「あれ,やれるかな」というふうに判断しています.

ただ,残念ながら「あれ」が増えれば覚えきれないし 小さいことは忘れてしまいます.「タイミングがきたら絶対にここを見るから,ここに書いておこう」という場所は限られていますし,TODO リストの2ページ目以降は目に触れることすらありません.

次のステップとしては,多数の「〜のときに〜したい」を覚える代わり,「〜になったら,〜になりそうなら 勝手に通知がくる」が望ましいと思っています.

〜になったら / 〜になりそうなら のパターン

通知が欲しいタイミングと,情報ソースについてはいくつかのパターンが考えられます.

  • 定期ポーリングで条件判定すれば十分

    • ネットワークの状態から判定
    • コンフィグから判定
    • ログから判定
    • 手順書から判定
  • リアルタイムイベントをつかまえて条件判定

    • ログから判定
    • メールなどの通知時に判定

これから,いくつかの実装例を紹介します.

ポーリング: ネットワークの状態から判定

特定ポートからリンクがすべて抜けたら,ハードウェアを交換したいというケースがありますね.

というフローを考えます.

準備

Slack の場合,単純な通知であればSlackbot がおすすめです.

Slackbot

ほかにもIncoming Webhook やReal Time Messaging API がありますが,Slackbot のみ 同居しているHubot にコマンドを送れます.ここでは紹介しませんが「Slack に通知があったらHubot が勝手に手順書をつくってくれる」など 次のワークフローを回すことができるよう,Hubot を起こせるほうをおすすめします.*1

次にSlack 通知用のシェルスクリプトを準備します.トークンが含まれているのでchmod 700 しておきましょう.

# notify.sh

#!/bin/sh

notify() {
  curl -sfd "$1" 'https://codeance.slack.com/services/hooks/slackbot?token=ccvIktQ90VrBP03gzsA6WsJK&channel=%23test' > /dev/null
}

最後に条件判定スクリプト.ネットワークデバイスの状態を取ってくる部分は NETCONF でもNetOpsCoding Advent Calendar 2日目の例 でもなんでも構いません.今回はrancid に含まれるjlogin を使いました.

# poll.sh

#!/bin/sh

. ./notify.sh

poll() {
  jlogin -c 'show interfaces ge-0/0/* descriptions' 192.168.0.76 | egrep \^ge-0/0 | grep -v down > /dev/null
}

poll || notify 'PIC 0/0 交換のチャンスです'

実行例

ge-0/0/* がすべてdown であれば通知されます.

Slack

このスクリプトを,cron.daily のようにrun-parts 経由でcrond から実行すればOK.

リアルタイムイベント: syslog から判定

特定のBGP ピアでは,落ちたときに急ぎで手動オペレーションしないといけない場合があります.

  • ルーティングを確認したいが,ホントに落とすと怒られる → 自然に落ちたときに確認したい
  • max-prefix limit で落ちたら手動でclear しないといけない

残念ながら,いまのところsyslog 以上に有力なプッシュ通知はないと思いますので

  • syslog で特定のイベントを受信したら,HipChat に通知

というフローを考えます.zabbix やsensu のような監視システムを使っている場合は イベントハンドラーを書くことで簡単に実装できますが,syslog-ng などでも難しくありません.大人の事情で監視機能が外部組織にあっても大丈夫.

準備

HipChat であれば カスタムIntegration を使います.Slack とは違って,どのAPI でもHubot にコマンドを送ることができます.

HipChat Integration

次にHipChat 通知用のシェルスクリプトを準備します.トークンが含まれているのでchmod 700 しておきましょう.

# /home/codeout/hipchat/notify.sh

#!/bin/sh

notify() {
  curl -fd "{\"color\":\"green\",\"message\":\"$1\",\"notify\":false,\"message_format\":\"text\"}" -H 'Content-Type: application/json' 'https://codeance.hipchat.com/v2/room/1301314/notification?auth_token=0uOU8ATolyOMpBGx9P04bjLytOcGnZVm8FugRUEA'
}

最後に条件判定スクリプト.syslog-ng の外部プログラムとして動作させます.

# /home/codeout/hipchat/watch.sh

#!/bin/sh

. /home/codeout/hipchat/notify.sh

filter() {
  echo "$1" | egrep -v 'peer 192.168.0.73.*changed state from Established' > /dev/null
}

while read l; do
  filter "$l" || notify 'ピア(192.168.0.73) 落ちた.経路確認だ'
done

条件判定が成功して通知 / 条件判定が失敗 のどちらのケースでも0 を返せるよう,egrep something && notify よりegrep -v something || notify にします.

泥臭いパターンマッチングをしなくていいよう,構造化されたデータでもらいたいのはNetOpsCoding Advent Calendar 1日目のエントリー のとおりですね.

最後にsyslog-ng の設定例.

# /etc/syslog-ng/conf.d/watch.conf

source s_router {
  udp();
};

destination watch {
  program("/home/koji/hipchat/watch.sh");
};

log {
  source(s_router);
  destination(watch);
};

実行例

BGP ピアが落ちると,HipChat に通知が飛びます.

HipChat

ポーリング: コンフィグから判定

たとえば,トランジットやピア一覧に変化があった場合にリマインドして欲しい場合があります.「連絡先一覧 アップデートした?」のように管理情報の更新を促してもらいたい.

「コンフィグはGitLab で管理されていて,適宜更新されている」という仮定のもと

  • 定期的にgit fetch
  • トランジットやピア一覧に変化があったらSlack に通知

というフローを考えます.

準備

GitLab 上のレポジトリを,仮にssh://git@192.168.99.101:10022/codeout/netops.git とします.

まず 手元にgit clone

$ git clone ssh://git@192.168.99.101:10022/codeout/netops.git

さらに 空パスフレーズssh key を作り,deploy key として登録しておいてください.

詳しい説明は省略しますが,実装例を diff.rb に置きました. ここではJuniper を仮定していて,description "T: Transit A" のようなタグ(T:) を探す動作になっています.また,display-set 形式のほうがdiff に適しているため,junoser を使って変換しています.

内部にssh private key やSlack トークンを書いているので,chmod 700 しておきましょう.

実行例

トランジットやピア一覧に変化があった場合のみ,diff とともにSlack に通知が飛びます.

Slack

リアルタイム: 手順書から判定

最後に「〜になったら」ではなく「〜になりそうなら」の判定を考えてみます.たとえば「デバイスを再起動するときにあれもやりたい」というのはけっこうあるはず.

仮にGitLab で手順書をつくっているとすると Issue やMerge Request などに手順情報が含まれているはずですので,何かしらの方法でこれを見て判定すればよさそうです.

GitLab にはうまいぐあいにWebHook というしくみがあり,データを更新した際 あらかじめ指定したURL にJSON データをPOST させることができます.JSON データのスキーマはGitLab 独自です.

このWebHook を使えば,リアルタイム判定になってしまいますが手順書から「〜になりそうなら」を抽出することができそうです.*2 このWebHook は,GitHub など 流行のサービスにはだいたい備わっていますので,JSON スキーマにさえ注意すれば同じしくみが使えます.

さて,

  • GitLab のIssue / Merge Request のタイトルやコメントに「再起動」「reboot」の文字があったらSlack に通知する

ということをやってみたいと思います.

準備

GitLab 上でWebHook を設定します.そのURL で待ち受けるWebサービスを,これから作ることになります.

GitLab WebHook

単純な実装例を endpoint.rb に置きました.実際はApache やNginx などと連携して動くWeb アプリが好ましいですが,コンセプトの理解には十分でしょう.

これを実行すると ポート8080 でHTTP リクエストを受け付け,GitLab からPOST されたデータを見て 条件に合う場合のみSlack に通知します.

内部にSlack トークンを書いているため,動かす際にはchmod 700 しておきましょう.

実行例

50行程度の短いコードですが,きちんと動きます.

GitLab WebHook

Merge Request データには2つのブランチ名が入っており,ポーリング: コンフィグから判定 のやりかたの応用でMerge Request 中のdiff を見ることもできます.

まとめ

利用可能なコンポーネントをつかって,人間の代わりにコンピューターに覚えといてもらう しくみをいくつか紹介しました.今回の例ではワークフローをラクに回す ところまでいきませんでしたが,人間が苦手な部分 (=記憶) と,単純判断をコンピューターに任せることができるので 少し幸せになれそうです.

さらに言えば,もう次のステップが見えてますね.

「〜のときに〜したい」の後半.アクション部分.通知の代わりに単純作業をやらせましょう.もしくは 作業ための下準備をやらせましょう.

自動でやっていいこと,結構ありそうじゃないですか?

*1:rate limit にだけ注意.現在 1 メッセージ/s

*2:もちろんポーリング動作にもできますが,実装が複雑になる