ルーターのコンフィグを git にぶち込んで世代管理してみたはなし

この記事を三行で

  • みんな好き勝手な名前でコンフィグファイルをアップロードしてサーバーがパンク&どれが最新版かわからない状況に
  • 命名規則を作り、サーバーに置いておくコンフィグを最新世代1つのみとした
  • さらにファイルを社内 GitLab に自動アップロードし履歴確認ができるようにした

※ GitLab を入れ物として使うだけで、git を使ったワークフローを導入したとかのカッコイイ話じゃありません

あらまし

とある通信会社の委託でIPネットワークの監視作業員をやっています(非エンジニア)。うちの会社ではルーター・スイッチ類のコンフィグをTFTPサーバーへアップロードしてバックアップとしていますが、先日そのサーバーで反応が5分くらい返ってこなかったり、ファイルのアップロードに失敗したりと動作が不安定に・・・。HDDでも壊れたかな?と思いながら調べてみると...

$ df -h
Filesystem          サイズ  使用  残り 使用% マウント位置
/dev/cciss/c0d0p3     195G  195G     0 100% /
/dev/cciss/c0d0p1      99M   13M   81M  14% /boot

Σ(;゙゚ω゚) ディスクフル!!!!

$ ll /var/tftp/ | wc -l
239662

Σ(;゙゚ω゚) 24万ファイル!!!!

この異常なまでの膨大なファイルは、いろんな組織や人間が好き勝手な名前でコンフィグをアップロードしてきたなれの果てです。ホスト1つを取っても、こんな感じでファイルがうじゃうじゃ出てきます。

$ ll /var/tftp | grep tkycore01
-rw-rw-rw- 1 nobody nobody  124862  10月  10  2015  20140303_tkycore01-confg
-rw-rw-rw- 1 nobody nobody  127522  10月  10  2015  after_tkycore01.config
-rw-rw-rw- 1 nobody nobody  123456  10月  10  2015  before_tkycore01.config
-rw-rw-rw- 1 nobody nobody  122899  10月  10  2015  tkycore01
-rw-rw-rw- 1 nobody nobody  123456  10月  10  2015  tkycore01-confg
-rw-rw-rw- 1 nobody nobody  126984  10月  10  2015  tkycore01-confg.txt
-rw-rw-rw- 1 nobody nobody  119853  10月  10  2015  tkycore01-confg-old
-rw-rw-rw- 1 nobody nobody  127556  10月  10  2015  tkycore01-confg_20130712.config
-rw-rw-rw- 1 nobody nobody  126574  10月  10  2015  tkycore01-config

しかも以前に行ったサーバーの引っ越し時にタイムスタンプが狂ってしまった(属性のコピーを忘れた)らしく、もはやどれが最新のファイルかわからない状態に・・・。しかもこれが原因で誤って古いコンフィグで設備を復旧させてしまい、通信障害を起こしてしまった事故が何度も起きています。(ちなみに社内では、ヒューマンエラーに起因する障害を "人為故障" と呼ぶ)

対策方針

ふつうなら RANCID あたりで管理すべきところですが、うちの会社は優れたオープンソースソフトウェアを導入したり、ワークフローを大きく変えることに強い抵抗があるため、あくまで従来の暖かみのあるマニュアルオペレーション()を踏襲しつつ、差分管理のみを自動化することにしました。というわけで立てた方針がコチラ。

  1. TFTPサーバーに保存しておくコンフィグは最新世代一つのみとし、ファイル名は Cisco ライクに ホスト名-confg に統一する
  2. 定期的にTFTPサーバー上の変更されたファイルを 社内GitLab へ自動アップロードし、バックアップ兼ヒストリーとして活用する

これによって、作業者のオペレーションは次のようになります。

Before

f:id:miyahan:20160713225607p:plain

  • ダウンロード : TFTPサーバーをホスト名で検索し、中身を見比べて最新ファイルを探して取得する = オペミスの恐れ!!
  • アップロード : 特にルールがなく、上書きしたり、ファイル名に日付をつけたり様々 = オートメーション化できない

After

f:id:miyahan:20160714202657p:plain

  • ダウンロード:ホスト名-confg で取ってくる
  • アップロード:ホスト名-confg で上げる

と、シンプルなオペレーションになりました。

サーバー大掃除

さっそくTFTPサーバー上から不要なファイルを削除し、最新ファイルをリネームしなければならないのですが、どのファイルが最新か調べるのは大変です。さらにディスク溢れのせいか、破損しているファイルもいくつか見つかったため、この際すべて取得し直すことにしました。

私が以前作った Telnetter というバッチ実行環境を使い、自担当が管轄している全ホストに Telnet して copy running-config tftp: でTFTPサーバーへ現用コンフィグをアップロードするシナリオファイルを作成して実行。そのあと命名規則に則っていないゴミファイルをサーバーから削除。これによって約14万ファイルを削減できました!!

$ ll /var/tftp | grep tkycore01
-rw-rw-rw- 1 nobody nobody     124862   5月  30  00:00 tkycore01-confg

(*´ω`*) すてき

ファイルが最新版1つになったことによって、サーバーが軽くなりましたし、コンフィグ復旧時に誤って古いコンフィグをダウンロードしてしまうといったヒューマンエラーも防止することができます。

git を使って履歴管理

ただもしかすると今後、以前のコンフィグを参照したいニーズが出てくるかもしれません。でもここで、またファイル名に日付を付けだしたりすると、せっかくの大掃除が水の泡になってしまいます。よい方法を考えていた矢先、「そうだ、履歴管理といえばバージョン管理システムじゃん」と気づき、git を使ってみようと思い立ちました。

バージョン管理システムを使うことで、いわゆる「いつ、だれが、何のために」変更を行ったかを記録できるメリットが得られるのですが、うちの作業員(ノンエンジニア)に git を使ってもらうのは困難なので、定期的に更新されたファイルをリモートリポジトリ(GitLab)に push させるスクリプトを動かし、今回は GitLab を単なる "時間軸をもった入れ物" として使うことに。

私は PHP しか書いたことのないゆとりちゃんなのですが、さすがにシェルスクリプト的なものを PHP で書くのもアレなので、今回人生で初めて Python を使ってみることにしました。

全体の流れは次のとおり

f:id:miyahan:20160531061945p:plain

  1. TFTPサーバーから rsync でファイルを取ってくる
  2. ファイル名を解析し、フォルダ分けを行う
  3. 追加・削除・変更があったファイルを git add / git rm して社内GitLabサーバーへ push

1. rsync でファイルサーバーからファイルを取ってくる

os.chdir(os.path.dirname(os.path.abspath(__file__))+'/')

args = [
    'sshpass', '-p', rsync_remote_pass, 'rsync', '-auvz', '--delete', 
    '--include-from', 'rsync_include.list',
    '--exclude', "*", rsync_remote_uri, 'tftp/'
]
subprocess.call(args)

python から rsync コマンドを叩いているだけです。ポイントは次の通り

  • sshpass を使って対話式ログイン処理を省略
  • --include-from を使って自担当が管轄する装置のコンフィグファイルだけを転送させる
  • --delete オプションを使ってサーバー上のファイルが削除されたら、作業環境のファイルも同期して削除するようにする

2. ホスト名をもとにフォルダ分け

このまま数万のファイルを git にぶちこんでしまうと、リポジトリが大変なことになってしまうので階層化します。今回はロケーション(都道府県名/データセンタービル名)で分けることにしました。

# 既存ファイル一覧を取得 (削除対象フラグとして使う)
arealist = [
    'tokyo', 'kanagawa', 'chiba', 'saitama', 'ibaraki',
    'tochigi', 'gunma', 'yamanashi', 'nagano', 'nigata',
    'miyagi', 'fukushima', 'iwate', 'aomori', 'yamagata',
    'akita', 'hokkaido'
]

old_files = {}
for area in arealist:
    for dirpath, dirnames, filenames in os.walk(area):
        for file in filenames:
            old_files[file] = dirpath+'/'+file

まずは分類済みのファイルリストを生成します。これはのちに削除されたファイルの検出に用います。(都道府県で会社がバレますな)

# TFTPディレクトリから一覧を取得、ファイル名解析しビルごとのディレクトリへコピー
pattern = r"^(secret)$"
host_re = re.compile(pattern)

for file in os.listdir('tftp'):
    # ファイル名解析
    match = host_re.search(file)
    
    if match is None:
        print('\033[91m**WARNING**\033[0m Invalid filename format : '+file)
        continue
    
    bldg  = match.group(2)
    area  = areacode[match.group(3)]
    src = 'tftp/'+file
    dst = area+'/'+bldg+'/'
    
    # 削除フラグを消す
    if file in old_files: old_files.pop(file)
    
    # すでに同じファイルがある場合はスキップ
    if os.path.isfile(dst+'/'+file) and filecmp.cmp(src, dst+'/'+file): continue
    
    # コピー先ディレクトリが無ければ作成
    if not os.path.isdir(dst): os.makedirs(dst)
    
    # ファイルをビルディレクトリへコピー
    shutil.copy2(src, dst)
    args = ['git', 'add', dst+'/'+file]
    subprocess.call(args)

つぎにさきほど rsync で取ってきたファイルに対し、ファイル名を正規表現で解析して適切なディレクトリへコピーしします。

# TFTPディレクトリから無くなったファイルを削除
for path in old_files.values():
    args = ['git', 'rm', path]
    subprocess.call(args)
    print('Removed deleted file: '+path)

ファイルの走査が終わった後も分類済みファイルリストに残っているファイルは、オリジナルのTFTPサーバーに存在しない、すなわち削除されたファイルなので、git rm で分類先ディレクトリから削除します

3. git push

d = datetime.datetime.today()
today = '%s/%s/%s' % (d.year, d.month, d.day)

args = ['git', 'commit', '-m', 'auto backup at '+today]
subprocess.call(args)

args = ['git', 'push', '-u', 'origin', 'master']
subprocess.call(args)

python から git コマンドをコールし、社内 GitLab サーバへ push します。

このスクリプトを1日1回動かし、その日に変更されたファイルを自動で GitLab へ push するようにしました。

git を使ったコンフィグ履歴確認

その結果、こんな感じでWebブラウザからコンフィグの閲覧や差分の確認ができるようになりました。

f:id:miyahan:20160531014639p:plain f:id:miyahan:20160531014648p:plain

また、git コマンドを使えばさらに高度な操作ができます。

  • 特定ホストのコンフィグ変更履歴をみたい : git log <ファイルパス> f:id:miyahan:20160605185659p:plain
  • 特定ホストの過去のコンフィグと現在のコンフィグを比較したい : git diff <コミットID> <コミットID> <ファイルパス> f:id:miyahan:20160605185654p:plain
  • 特定の期間にコンフィグが変更されたホスト一覧を調べたい : git log --since=<開始日時> --until=<終了日時> f:id:miyahan:20160605185658p:plain

ほんと git は何でもできますね。恐るべし!

今後やりたいこと

  • えらい人に本施策の正式な許可をもらう(もらってないのかよ!)
  • 他部署も巻き込んでコンフィグの管理ルール・命名規則を決め標準化する
  • ゆくゆくは作業者に git を使ってもらう
  • ルーター内のフラッシュメモリカードも同様にぐちゃぐちゃになっているので対策したい
  • サーバーがディスクフルなのを誰も気づいてなかったので監視ツール入れたい(私の管轄じゃないけど)

余談 | Python 初体験の感想

結論:ググればなんとかなった

  • 変なことが起きたらとりあえず例外を投げてくれるので、PHP でありがちな「何も言わずに落ちた」・「正常に終わったと見せかけて、正常に終わってない」ということが起きにくい
  • ネストの {} や文末の ;、変数の $ など、PHP と比べて記号が少なく全体的にすっきりしている
  • Python のリスト(配列)の検索はめちゃくちゃ遅い。PHPの「とりあえず連想配列にぶち込む」的なノリで使ったら痛い目に遭った。Python で配列を検索するなら辞書かセットを使ってキーを検索する(ハッシュ照合なので速い)
  • 今回ディレクトリ一覧の取得やファイルのコピーなどファイルシステムまわりの操作を多用したが、PHP よりかゆいところに手が届く感
  • subprocess.call が引数を自動でエスケープしてくれることに驚き。(PHPだと自分でやらないとOSコマンドインジェクションが容易に行えてしまう)
  • 文字列の連結で、どうしても PHP 式の foo.bar 表記にしてしまい毎回怒られてしまう(慣れてくると、今度は PHPfoo+bar って書いちゃうんだろうな・・・)

典型的な食わず嫌いでした。

後日談

後日、説明資料を作って本社の管理部門の方とお話したのですが、「ギットとかよくわかんないし、組織間で協議してルール作るのもめんどくさいので、これまで通り好きな名前でどんどんアップロードしてくれ」とやんわり全面却下されました。

たくさんファイルが作られることで、またサーバーが重くなったり、ヒューマンエラーが起きてしまうリスクについては「課題としては認識している(が対応はしない)」とのことです。

(´-`)

さらに後日談

ルールを決めてもやっぱり誤った命名規則でコンフィグをアップロードする人や、違うディレクトリにアップロードする人が後を絶たなかったため、全ファイルを定期的にチェックしてチャット(Mattermost)にアラートをあげるように改良しました。

やっぱりここを自動化しないとダメだよなぁ・・・。


本記事では「いらすとや」さんのイラストを使わせていただきました。

かわいいフリー素材集 いらすとや


  • 2016/6/10 : 新規ファイルがあるとファイル比較時にエラーで止まるバグがあったのでコード修正
    • -if filecmp.cmp(src, dst+'/'+file):
    • +if os.path.isfile(dst+'/'+file) and filecmp.cmp(src, dst+'/'+file):