FreeDNSに登録しているDNSレコードを更新するDynamicDNSクライアントを作成_Second

はじめに

DynamicDNSサービスに元々MyDNS.JPを使用していましたが、なぜか1レコード分以外のレコードがTXTレコードになってしまい、外部から自宅サーバへアクセスできなくなっていたので、FreeDNSに乗り換えてみました。

追記:エキスパートPythonPython v2.6.2のマニュアルにも目を通し、おかしなところとか、Pythonは、GIL(Global Intepreter Lock)の問題とかで、同時実行できるスレッドが一つに限定されてしまうので、マルチスレッドの代わりに、マルチプロセスをよく利用するとなっているので、それも変更しました。

またDNS更新クライアントとして使用していた機器が故障し、普通のPCから更新することにしました。

FreeDNSの特徴
  • MXレコード、Aレコード、CNAMEレコード、AAAAレコード等ほぼすべて対応している
  • DNSレコードのバックアップサーバとして自分でたてたDNSサーバを指定できる
  • wget等HTTPアクセスでDNSレコードを更新できる
  • XML APIが用意されており、自分でDynamicDNSクライアントを作成できる

手順

前提
  • 外部公開とかメンテとかで外部からサーバにアクセスしたいが、固定グローバルIPなんぞは不要っていう人
  • FreeDNSサービスにユーザ登録し、DNSのレコードを登録していること(Aレコード,MXレコード, AAAAレコードでも)
DynamicDNSクライアント環境
  • sheevaplug(電源プラグ型LinuxPC: FlashROM) 常時電源ONにするとデーモンがダウンしたりするため除外
  • DesktopPC(2002年くらいのノーブランド)
  • Ubuntu10.04
  • python v2.6
XML API取得

下記のURLにアクセスすると、XML形式でレコード更新用URLが取得できます。
(shaの部分は、登録ユーザ名+パスワードを使用してSHA-1で暗号化した文字列が入ります。)

URL: http://freedns.afraid.org/api/?action=getdyndns&sha=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&style=xml

SHA-1暗号化

暗号化文字列は、username + pipe + passwordをSHA-1で暗号化すればよいようなので、
下記のような関数で暗号化文字列を作成します。

def gethashstr(username, password):

    import hashlib

    hashobj = hashlib.sha1()
    encstr = '|'.join([username, password])
    hashobj.update(encstr)
    hashstring = hashobj.hexdigest()
    return hashstring
更新用URLの取得

下記のように必要なパラメータを用意して

HASH_ARGO = "sha"
TARGET_PARAMS = {'action': "getdyndns", HASH_ARGO: None, 'style': "xml"}
TARGET_API = "http://freedns.afraid.org/api/"
TIMEOUT = 300 

urllib2を使ってDNSレコード更新用URLが記述されているXMLを取得しました。

def request(url, timeout, **params):

    query = ''
    for k, v in params.items():
        query += '?' if query == '' else '&' 
        query += '{key}={value}'.format(key=k, value=v)

    url += query

    import urllib2
    print 'Connecting URL: {url} ...'.format(url=url)
    res = urllib2.urlopen(url=url, timeout=timeout)
    print 'URL: {url} contents download finished.'.format(url=url)
    responsetext = res.read()
    return responsetext

TARGET_PARAMS[HASH_ARGO] = gethashstr(hoge, hogepass) 
response = request(TARGET_API, TIMEOUT, TARGET_PARAMS)

XMLからレコード更新用URL抽出

responsetextに取得したXMLが格納されているとして、XMLから更新用URLを抽出します。
XMLの解析モジュールは、dom,saxとかいろいろあったのですが、xml.etree.ElementTreeが一番簡単でしたので、それを使用しました。

利点

  • xpath風に目的のノードを取得できる
    • 下記のXMLから複数のurlのテキストを抽出する場合["item/url"](イテレータオブジェクトが返却される)

欠点

  • XML文字列をそのまま渡すと例外が発生する(ファイルオブジェクトを渡す必要あり)
    • しかし、StringIOで渡せば問題なく対処できる。
    • 通常のPCサーバであればXMLをtempfileにリダイレクトして渡せばよいと思います。
<root>
  <item>
    <host>www.hoge.com</host>
    <address>71.1.1.1</address>
    <url>http://freedns.afraid.org/dynamic/update.php?T3faGkljdax==</url>
  </item>
  <item>
    {...}
    <url>http://freedns.afraid.org/dynamic/update.php?jklIIUadaaa==</url>
  </item>
</root>
def update(responsetext, timeout):
    from xml.etree.ElementTree import ElementTree
    from StringIO import StringIO

    try:
        # xml.etree.ElementTree requires FileObject
        output = StringIO(responsetext)
        tree = ElementTree()
        tree.parse(output)
        urls = tree.findall("item/url")
DNSレコード更新

XMLから抽出した更新用URLにアクセスするだけでレコードの更新ができます。

for url in urls:
    res = urllib2.urlopen(url)

あとは、これをcrontabに1時間に1回動作するように設定しておけばいいのではないかと。
$ crontab -l
0 * * * * python /var/cron/freedns/freednsupdate.py

追記:

マルチプロセスを扱うワーカークラスを作成

#!/usr/bin/env python
# -*= coding:utf-8 -*-
from multiprocessing import Process
import urllib2


class AsyncWorker(Process):
    def __init__(self, url, timeout):
        Process.__init__(self)
        self.url = url 
        self.timeout = timeout

    def run(self):
        try:
            print 'Connecting URL: {url} ...'.format(url=self.url)
            res = urllib2.urlopen(url=self.url, timeout=self.timeout)
        except Exception, e:
            raise e
        else:
            print 'URL: {url} update successfully.'.format(url=self.url)
            return True

ワーカーを使用して処理を行う関数

# Update the dynamic dns records using API
def update(responsetext, timeout):
    from xml.etree.ElementTree import ElementTree
    from cStringIO import StringIO

    try:
        # xml.etree.ElementTree requires FileObject
        output = StringIO(responsetext)
        tree = ElementTree()
        tree.parse(output)
        urls = tree.findall("item/url")

        # Record update
        from worker import AsyncWorker
        tasks = [AsyncWorker(u.text, timeout) for u in urls]
        for t in tasks:
            t.start()

        # wait for the background tasks to finish
        for t in tasks:
            t.join()
        print 'Dynamic dns update tasks has finished.'
    except Exception, e:
        raise e
    finally:
        output.close()

リスト内包表記とか使用しているものの、ワーカーの書き方とかSJC-P(Java)とかで勉強したものそのままひっぱてきた表現になってしまいました。

設定ファイルを読み込むクラスのメソッドの返却値も自分自身(self)を返却するようにして、メソッドチェーンっぽく続けて処理できるようにしてみました。

作成したもの

今回作成したものは下記にあります。ここ変だよとかありましたら突っ込みとかお願いします。

404 · GitHub

参考にしたもの

404 Not Found

pythonまじめに覚えようと思って、注文してみました。
ISBN:4048686291