読者です 読者をやめる 読者になる 読者になる

もろず blog

もろちゃんがITに関しての様々なトピックを解説します


Python でダミーサーバーを立てよう!

Python SMTP Upstart



f:id:chanmoro999:20150119172107j:plain

2015年一発目の記事です!
今年もよろしくお願いします


さて、開発していく中で外部APIとの連携、メール送信などのテストのためにダミーのサーバーが必要になる場面はよくあると思います

最近仕事でまた必要になったんですが、わざわざ専用のサーバーを入れるほどでもなかったので Python で動くスタブのサーバーをつくりました

いろいろインストールして準備する必要がないように CentOS 6.4 にデフォルトで入っている Python 2.6.6 上で動かせるようにします



この記事では
1. ダミーサーバーの立て方
2. ダミーサーバーをカスタムする
3. ダミーサーバーをデーモン化する
4. まとめ
について説明します


1. ダミーサーバーの立て方

今回は以下の2つのダミーサーバーを用意します

  • 外部 API をシミュレートする Web サーバー
  • メール送信時に接続する SMTP サーバー

まずは、一番簡単にできそうな方法を調べました

Web サーバー

Python には標準で用意されている Web サーバー機能があります
単純に静的ファイル をホストするだけの Web サーバーなら以下のコマンドで起動できます

python -u -m SimpleHTTPServer 8000

引数にはポート番号を指定します
1000番以下はルート権限で実行する必要があるので sudo をつけましょう


SimpleHTTPServer は GET、HEAD のメソッドに対応していますが、POST や PUT の機能には対応していません
なので API のダミーとして使うのは厳しいですね

SMPT サーバー

Python には標準で用意されているテスト用の SMTP サーバー機能があります
受け取ったメールの内容を標準出力に吐く機能だけを持ったテスト用の SMTP サーバーであれば以下のコマンドで起動できます

python -u -m smtpd -c DebuggingServer :8025

引数にポート番号を指定しますが、SimpleHTTPServer と違ってポート番号の前にコロンが必要です


DebuggingServer には base64 デコードする機能がついていないので、メール本文が base64 エンコードされている場合に base64 エンコードされた文字列がそのまま出力されてしまいます
メールを受信できるのはいいものの、base64 エンコードされた文字列をコピペしてデコードしないと中身が確認できません



SimpleHTTPServer、smtpd.DebuggingServer をそのまま使っても今回欲しかった機能にちょっと足りないです

そんな時に、欲しい機能を追加したダミーサーバーをサクっと作りたいですよね
どんな感じでカスタムできるのかを見ていきましょう


記事の先頭に戻る


2. ダミーサーバーをカスタムする

WebAPI のダミーとするには POST や PUT に対応した Web サーバーが必要です
base64 エンコードされたメールの中身も確認したいので、 base64 デコードの機能を SMTP サーバーに付けたいです

また、どちらも受け取ったリクエストの内容を確認したいので、データをログに記録する機能も欲しいですね


そんな機能を追加したサーバーを作りました
実装したソースは GitHub にアップしています
Chanmoro/python · GitHub

こんな感じで起動します

# Web サーバー
$ python dummy_webapi.py

# SMTP サーバー
$ python dummy_smtp.py

ログは python ファイルと同じディレクトリに server.log という名前で出力するようにしています

Web サーバー

python/dummy_webapi.py at master · Chanmoro/python · GitHub

以下の機能を追加しています
- GET と POST に対応した固定の JSON を返す
- ログファイル出力


BaseHTTPServer.BaseHTTPRequestHandler を継承して、各 HTTP リクエストのメソッドに対応した関数を実装します
POST メソッドに対応した処理を追加したければ do_POST メソッドを実装する、というような命名規則になってます

今回はこんな感じで実装しました

def do_POST(self):
    f = open("post_response.json")
    response_body = f.read()
    
    self.send_response(200)
    self.send_header('Content-type', 'application/json; charset=UTF-8')
    self.send_header('Content-length', len(response_body))
    self.end_headers()
    self.wfile.write(response_body)
    logging.info('[Request method] POST')
    logging.info('[Request headers]\n' + str(self.headers))

    content_len = int(self.headers.getheader('content-length', 0))
    post_body = self.rfile.read(content_len)
    logging.info('[Request doby]\n' + post_body)


ちなみにルーティングの機能は用意されていないので、もし複数API を用意する必要があればリクエスト URL 毎に振り分ける処理を書かないといけないです

自分でルーティングの機能を追加して MVC を実装するサンプルが以下の記事に載っていました
Aventures Logicielles: A very simple HTTP server with basic MVC in Python

正直、ここまでやるなら Django を入れた方が良いんじゃないかと思いますが・・・

SMPT サーバー

python/dummy_smtp.py at master · Chanmoro/python · GitHub

以下の機能を追加しています
- Content-Transfer-Encoding の値を見てメール本文をデコードする
- ログファイル出力

DebuggingServer の実装を見てみると以下のようになっていて、SMTPServer の process_message をオーバーライドしてるだけでした

class DebuggingServer(SMTPServer):
    # Do something with the gathered message
    def process_message(self, peer, mailfrom, rcpttos, data):
        inheaders = 1
        lines = data.split('\n')
        print '---------- MESSAGE FOLLOWS ----------'
        for line in lines:
            # headers first
            if inheaders and not line:
                print 'X-Peer:', peer[0]
                inheaders = 0
            print line
        print '------------ END MESSAGE ------------'

なので、メールをコンソールに出力するだけじゃなくて何かの処理もカマしたい!という時は、これと同じように SMTPServer を継承して process_message をオーバーライドすれば作れます

Python 2.7 系のソースですが DebuggingServer の部分は 2.6 系と同じです
releasing/2.7.9: 753a8f457ddc Lib/smtpd.py


ついでにテストメールを送信するコードもつくりました
特筆することはありませんが smtplib.SMTP( ) のところに接続先のメールサーバーを書きます
python/test_mail_sender.py at master · Chanmoro/python · GitHub

# Test mail send sample.
import smtplib
import email.utils
from email.mime.text import MIMEText

mailTo = 'test_to@example.com'
mailFrom = 'test_from@example.com'

mail = MIMEText('This is the body of the test message.')
mail['To'] = mailTo
mail['From'] = mailFrom
mail['Subject'] = 'Test message'

server = smtplib.SMTP('localhost', 8025)
server.set_debuglevel(True)
try:
    server.sendmail(mailTo , [mailFrom], mail.as_string())
finally:
    server.quit()


いろいろな文字コードエンコードで送信してみるのは以下の記事が参考になります
Pythonで日本語メールを送る方法をいろいろ試した - Qiita


記事の先頭に戻る


3. ダミーサーバーをデーモン化する

テストしたい時だけ Python コードを都度実行するというのでもいいんですが、意識せずに起動しっぱなしにしておきたい!というニーズもあると思います

処理をデーモン化するやり方を調べたところ Upstart が簡単にできそうでした
conf ファイルを置くだけでサービスとして認識してくれます


例としてダミーの SMTP サーバーをデーモン化するやり方を見てみましょう


まず /etc/init/dummy_smtp.conf ファイルを作成し以下の内容を記述します
script 〜 end script の間にデーモン化したい処理を書きます

## Upstart Job Defines
description     "Dummy SMTP Server"

script
 python -u dummy_smtp.py
end script


conf ファイルを配置したらすぐ動かせるので、以下のようにして操作します

# conf ファイルの設定反映
initctl reload-configuration

# サービスの起動
$ initctl start dummy-smtpd
dummy-smtpd start/running, process 29022

# サービスの停止
$ initctl stop dummy-smtpd
dummy-smtpd stop/waiting

# サービスの状態確認
$ initctl list
〜省略〜
dummy-smtpd start/running, process 29022
〜省略〜


Upstart を試しているときにちょっとした問題がありました
以下のように標準出力をリダイレクトする処理を設定した時、なぜかファイルに出力されるまでの間にタイムラグが発生していて出力をうまく確認できませんでした

python -m smtpd -c DebuggingServer :8025 >> /var/log/dummy-smtpd.log 2>&1 


結局、Python が標準出力のバッファリング機能を持っているのが原因で、バックグラウンドで動いたときにバッファのせいでタイムラグが発生していました
-u オプションをつけることで Python のバッファリングが無効になりすぐにファイルに出力されるようになりました

python -u -m smtpd -c DebuggingServer :8025 >> /var/log/dummy-smtpd.log 2>&1 


ちなみに、バージョンが 1.4 以降の Upstart が入っていれば /var/log/upstart 以下に標準出力のログが吐かれるそうです
CentOS に入っている Upstart はバージョン 0.6.5 というものすごく古いのが入っているので、ログの機能は使えないバージョンでした

# Upstart のバージョン確認
initctl --version


本番用のサービスは init スクリプトで定義したほうがいいですね
CentOS 7 では systemd を主に使う方向になっているそうです


記事の先頭に戻る


4. まとめ

Python はデフォルトの状態でもいろいろなライブラリが入っています
"Battery Included" (電池つき!) という思想があって、汎用的に必要そうな機能は標準ライブラリに含まれているんです

ざっと見た感じでも使えそうな機能がいろいろ用意されているのがわかります
Python 標準ライブラリ — Python 2.7ja1 documentation


bash で書いていたスクリプトをより高機能にするのにもサクッと使えそうでいいですね

今回は Python でダミーサーバーを立てる方法についての記事でした


記事の先頭に戻る