JunosのConfiguration Archival機能を使ってコンフィグを自動でGitにPushしてみる

# はじめに

今回は、Junosの自動コンフィグバックアップ機能である Configuration Archivalを使って、Commitと同時にConfigを自動でGitにPushしてみます。

# 環境

このメモの内容は以下のような環境でテストしています。

* サーバ
    * FreeBSD 9.3
    * Ruby 2.0
        * Gem: ftpd 1.1.1
* ルータ
    * vSRX 12.1X47-D20.7

# Configuration Archivalとは

詳しくは[公式ドキュメント](http://www.juniper.net/techpubs/en_US/junos14.2/topics/task/configuration/junos-software-system-management-router-configuration-archiving.html)を読んで頂きたいところですが、一言で言うと、自動でConfigをリモートのサーバにバックアップできる機能です

バックアップのタイミングは、一定間隔毎か、Commit直後を選択可能です。また、リモートのサーバとしては、FTPあるいはSCPを選択可能です。

Junos上の設定は以下のようにして行います。

```sample.junos
[edit system archival configuration]
archive-sites {
    ftp://username:password@host/path;
}
transfer-on-commit;
```

転送されるファイル名は以下のようになります。このファイルはGzipで圧縮されてサーバに保存されます。

* 14.1以前
    * `<ホスト名>_juniper.conf.n.gz_YYYYMMDD_HHMMSS`
* 14.2 or 14.1X53-D30以降
    * `<ホスト名>_YYYYMMDD_HHMMSS_juniper.conf.n.gz`

# FTP to Git ゲートウェイを作る

さて、この機能をただ使うだけでは、あくまでFTP/SCPにConfigをアップロードすることしかできません。単純なバックアップであれば十分かもしれませんが、やはりGit等での管理を考えたいところです。

そこで、FTPで受信したファイルをGitに自動的にPushする簡単なゲートウェイを実装してみました(今のところ、あくまで自宅で使ってるだけなので、テキトーです)。動作の流れとしては以下の様な感じです。

 1. FTPサーバとしてファイルを待ち受ける
 1. FTPでファイルを受信したら、ファイル名をパースして、ホスト名と日時を抽出
 1. Gzip圧縮されているか確認し、圧縮されていたら展開
 1. Gitのリポジトリをクローン
 1. ホスト名のConfigを更新
 1. Commit messageを生成
 1. Commit & Push

エラー処理とかは全くもって適当なので、実運用に使う場合は真面目に書き直すことをオススメします。認証も行っていません。**というか、多分OSコマンドインジェクションとか出来ると思います……。**

```rb:ftp-server.rb
#!/usr/bin/env ruby

require 'ftpd'
require 'zlib'
require 'logger'
require 'tmpdir'
require 'json'

$log = Logger.new('ftp-server.log')
$log.level = Logger::DEBUG

GIT_URL = ENV['GIT_URL']
GIT_CONFIG_DIR = ENV['GIT_CONFIG_DIR']

class GitFileSystem
  def initialize
  end

  #PathExpansion
  def set_data_dir(datadir)
  end

  def expand_ftp_path(ftp_path)
    $log.debug 'FS: called expand_ftp_path with '+ftp_path
    return '/'
  end

  #Accessors
  def accessible?(ftp_path)
    $log.debug 'FS: called accessible? with '+ftp_path
    return true
  end

  def exists?(ftp_path)
    $log.debug 'FS: called exists? with '+ftp_path
    return true
  end

  def directory?(ftp_path)
    $log.debug 'FS: called directory? with '+ftp_path
    return true if ftp_path == '/'
    return false
  end

  #FileWriting
  def write_file(ftp_path, stream, mode)
    # todo
    $log.debug 'FS: write_file: ' + ftp_path
  end

  def write(ftp_path, stream)
    $log.debug 'WRITE: called write with ' + ftp_path
    $log.info 'Receiving a file: ' + ftp_path
    file_name = File.basename(ftp_path)
    content = stream.read
    if content[0,4].unpack("H*")[0] == '1f8b0800'
      $log.debug 'WRITE: detect gziped'
      begin
        content = Zlib::GzipReader.new(StringIO.new(content)).read
      rescue => e
        $log.info 'WRITE: zlib exception...' + e.to_s
        raise e
      end
      file_name.gsub!('.gz','')
    end

    $log.debug 'content: ' + content.to_s

    hostname = nil
    datetime = nil

    if file_name=~ /.+_\d{8}_\d{6}\_juniper\.conf/
      tmp = file_name.split('_')
      hostname = tmp.shift(tmp.length - 3).join('_')
      datetime = [tmp[0],tmp[1]].join('_')
    elsif file_name=~ /.+_juniper\.conf_\d{8}_\d{6}/
      tmp = file_name.split('_juniper.conf_')
      hostname= tmp[0]
      datetime = tmp[1]
    else
      return nil
    end
    $log.debug 'hostname: ' + hostname
    $log.debug 'datetime: ' + datetime

    conf_name = hostname + '.conf'

    commit_message = {:hostname => hostname, :datetime => datetime}.to_json.to_s

    $log.debug 'commit_message: ' + commit_message

    Dir.mktmpdir do |dir|
      Dir.chdir(dir) do
        $log.debug 'Current dir: ' + Dir.pwd
        $log.debug 'Clonning git repos...'
        $log.debug `git clone #{GIT_URL} .`
        raise 'GitCloneFailedException' if $? != 0
        if GIT_CONFIG_DIR
          Dir.mkdir(GIT_CONFIG_DIR) unless Dir.exists?(GIT_CONFIG_DIR)
          Dir.chdir(GIT_CONFIG_DIR)
        end

        if File.exist?(conf_name)
          $log.debug 'Found a current configuration'
          current = File.read(conf_name)
        end

        $log.debug 'Writing a configuration'
        File.write(conf_name, content)

        $log.debug `git add #{conf_name}`
        raise 'GitAddFailedException' if $? != 0

        $log.debug 'Committing'

        $log.debug `git commit -m "#{commit_message}" #{conf_name}`
        raise 'GitCommitFailedException' if $? != 0

        $log.debug 'Pushing'

        $log.debug `git push origin master`
        raise 'GitPushFailedException' if $? != 0
      end
    end
  end
end

class Driver
  def initialize()
    $log.debug 'called Driver.initialize'
  end

  def authenticate(user, pass)
    $log.debug 'called Driver.authenticate with ' + user
    true
  end

  def file_system(user)
    $log.debug 'called Driver.file_system with ' + user
    GitFileSystem.new
  end
end

raise 'GitRepositoryUndefinedException' unless GIT_URL

driver = Driver.new()
server = Ftpd::FtpServer.new(driver)
server.exception_handler = Proc.new{|e| puts e.to_s}
server.interface ='0.0.0.0'
server.port = 2221
server.log = $log
server.start
$log.info "Server listening on port #{server.bound_port}"

while true
  sleep 0.1
end
```

コードの内容については特に説明はしませんが、まあ読み解くのに特に難しい所はないかと思います。

# 動作確認

実際に試してみましょう。適当なGitリポジトリを用意しておきます。ここでは例として、`http://git.example.com/test.git` に認証無しでPushできるリポジトリがあることとします。

以下のコマンドで起動します。
`GIT_URL="http://git.example.com/test.git" ruby ftp-server.rb`

Junos機器に以下のようなConfigを投入し、Commitします。

```auto-archive.junos
# set system archival configuration transfer-on-commit
# set system archival configuration archive-sites pasvftp://<ftp-server>:2221/
# commit
```

Commitして少しすると、JunosからFTPサーバにConfigが送信され、GitへのPushが行われます。リポジトリを確認してみると…

```
$ git log
commit 6460ff47572d66926450fdcb33cee424f28efd86
Author: Junos <home-junos@home.example.net>
Date:   Sun Dec 27 21:11:47 2015 +0900

    {hostname:rt01,datetime:20151227_121124}

commit 26b6d8b57b1d56ee86c390e25b2f068cda87c987
Author: Kazubu <kazubu@git.example.net>
Date:   Sun Dec 27 20:24:55 2015 +0900

    initial commit

$ cat rt01.conf
## Last changed: 2015-12-27 21:11:20 JST
version 12.1X47-D20.7;
system {
    host-name rt01;
    time-zone Asia/Tokyo;
...<snip>...
```

Configが自動的にGitにPushされていることが確認できます。
ここで、Configを適当に編集して、再度Commitしてみましょう。

```
# set interfaces ge-0/0/0 description "Home Flets NGN"
# commit
```

そして、Gitリポジトリを確認してみると…

```
$ git log
commit ced44dea7bf623393b86a8dc84d4bad9d87dbcbf
Author: Junos <home-junos@home.example.net>
Date:   Sun Dec 27 21:17:36 2015 +0900

    {hostname:rt01,datetime:20151227_121708}


commit 6460ff47572d66926450fdcb33cee424f28efd86
Author: Junos <home-junos@home.example.net>
Date:   Sun Dec 27 21:11:47 2015 +0900

    {hostname:rt01,datetime:20151227_121124}

commit 26b6d8b57b1d56ee86c390e25b2f068cda87c987
Author: Kazubu <kazubu@git.example.net>
Date:   Sun Dec 27 20:24:55 2015 +0900

    initial commit

$ git diff 6460ff ced44d
diff --git a/rt01.conf b/rt01.conf
index 0ab3412..59e61cd 100644
--- a/rt01.conf
+++ b/rt01.conf
@@ -1,4 +1,4 @@
-## Last changed: 2015-12-27 21:11:20 JST
+## Last changed: 2015-12-27 21:16:45 JST
 version 12.1X47-D20.7;
 system {
     host-name rt01;
@@ -56,6 +56,7 @@ system {
 }
 interfaces {
     ge-0/0/0 {
+        description "Home Flets NGN";
         unit 0 {
             encapsulation ppp-over-ether;
         }
```

こんな感じで、変更が正しくCommit & Pushされているはずです

# さいごに

テキトーではありますが、JunosでのCommitをトリガーにして、GitリポジトリのConfigを更新する事が実現できました。やったね！
[Junoser](https://github.com/codeout/junoser/)や、Commit Scriptを組み合わせる事によって、もっといろいろ楽しいことができるはずです。
是非参考にして頂けると幸いです。