ヒアリングとかtry&errorも自動化したい。¶
チームで運用している、めっちゃカオスだったシステムが、id:t9md さん製のツールによって、サーバ毎や設定毎に微妙に異なっていた手順が統一の手順でできるようになり、基本はシーケンシャルにしかできなかったものが、並列化されるようになり、めっちゃ時間も短縮され、作業的にも本当に楽になりました。
さて、そのシステムで基本的に残るのは設定内容自体の生成の自動化。現状、定型フォームで依頼をもらい、設定していますが、微妙に違う要件が割りと多いので結果見て、わずかな修正を再依頼されては修正、場合によってはその場でヒアリングして修正、というプロセスがあります。このプロセスは依頼する方もされる方も結構時間を割かれるので、何とか自動化したいワケです。
今朝風呂入りながらどうやったら自動化できるかなぁと考えていましたが、現時点で思いついたのはプレビュー機能。他もっも良い方法はあるのでしょう…? また、チーム内で私が本来やるべきタスクは別にあります。でも、本来やるべきことに時間を割けるようにするため、やっつけたいというジレンマ。何とか時間作って実装してみますかね…。
E4B8BBが文字化け。¶
UTF-8だとE4B8BB、実体参照だと主が、縦に点三つの記号に化ける。音読みだと”シュ”、訓読みだと”あるじ”とか”ぬし”とか”おも(に)”という漢字です。今のところ、Debianでのみの様子。バグかなぁ?
リバースプロキシするとIndexesの対象にならない。¶
ApacheでIndexesで表示させているディレクトリが沢山あってですね、一部のファイルは別のサーバで生成していて、さらに過去のファイルが結構あったので、横着して集約せずにリバースプロキシで表示させようと思ったのです。
つまり、/var/www/の下をOptions Indexesで表示させていて、hogeとかmogeなどがそこにあるわけです。そこにfugaとかいうディレクトリがある時、普通ならfugaも表示されますね。
ここで、
ProxyPass /fuga http://otherhost/fuga
ProxyPassReverse /fuga http://foo.example.com/fuga
と設定してやると、hogeとかmogeは(関係無いので)ちゃんと表示されますが、fugaは表示されない!知らんかった…。
とりあえず、そんだけ。
両親にiPad2。¶
来月おまめちゃんの出産を控えているものの、父親の体調があまり良くないので遠出するのは無理だなぁと思っていました。また、なにか調べ物するのにも、いちいちでかいノートPCを引っ張り出してテーブルの席に座って、というのも大変そうでした。なので、iPad2をあげることにしました。無線LANの設定だけワシがして、基本的な操作方法やApple IDの登録は一緒にやって教えること約1時間半。すぐに慣れたようです。しんどくて寝ている時とかでもパソコン使わなくても大丈夫ですな。母親には親父が教えてあげるんでしょうね。良かった良かった。
これで、おまめちゃんが産まれたあとの当面孫の顔を見られない問題も写真や動画を共有できるので、なかなか出来ない親孝行と考えれば、iPadはなかなか良い買い物ですね。さて、おまめちゃんが産まれる前に、CouchDBで写真&動画共有用のアプリ作るべ 1 。
- 1
既存サービス使わないのは、容量制限とか同時アップロードの制限とかを気にしたくないのですよ
あんどきゅめんてっどTonicDNS。¶
私にもかつてPowerDNS用のAPIを、管理ツールとして以前導入したPowerDNS GUIを拡張して作ろうとしたことがありました 1 。ですが、pdns guiは現状ではapplication/x-www-form-urlencoded形式でしかデータを受け付けないこと、特定のゾーンのレコードだけの登録、更新、削除などができないという仕様です 2 。ですので、JSONで受け付けてやるようにして、特定のレコードだけ登録、更新、削除できるようにしようかとも考えました。が、pdns-guiはフレームワークとしてsymfony1.0を使っています。独自に拡張するとsymfony1.4もしくは2.0にバージョンアップした場合に追従するのがめんどいという問題があります 3 。一方、自分でsymfony1.0から1.1→1.2→1.3→1.4、もしくはさらに2.0とアップデートするのは結構ハードルが高いので、いずれにしても微妙だなぁと思いつつも、PowerDNS GUIを拡張してAPIを作ることにしようかとtweetしたところ、 TonicDNSは見た? と教えてもらったので試してみることにしました 4 。
TonicDNSとは。¶
Github で公開されている、PowerDNS用のRESTful APIです。ロゴが結構かっこいいですね!TonicDNS自体は、 tonic というRESTfulなWebアプリを書くためのPHPのライブラリ&フレームワークを使っているようです 5 。導入自体は、README.markdownのQuick Install Guideのとおりにすればできます。この導入途中で、初めて mpm-itk モジュールなんてあるのを知りました。DebianやUbuntuではapache2-mpm-itkパッケージを導入すれば使えます。
さて使ってみよう…、アレ?¶
上記のとおり、導入自体はあっさりできました。が、肝心の使い方については一切ドキュメントがありません!APIなのに使い方が分からんのでは使えんやないの…。ただし、ソースコードには結構コメントが書いてありますし、結構読みやすい&分かりやすいコードです。ソース嫁ということなのでしょう。なので、実際にソースコードを読んで、使い方を調べてみました。
ユーザを作成する。¶
TonicDNSにはユーザ認証の仕組みがあります。しかしユーザ登録の仕組みはありません。ユーザ用のテーブルスキーマは下記のようになっていますが、
mysql> desc users;
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(16) | NO | | NULL | |
| password | varchar(34) | NO | | NULL | |
| fullname | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | | NULL | |
| description | varchar(1024) | NO | | NULL | |
| perm_templ | tinyint(4) | NO | | 0 | |
| active | tinyint(4) | NO | | 0 | |
+-------------+---------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)
passwordのフォーマットがなんなのか分からないので困ってしまいます。lib/pdo_token_backend.phpを見ると、
if (($result = $stat1->execute(array(":username" => $token->username, ":password" => md5($token->password)))) !== false) {
となっているので、パスワード文字列をmd5()処理してやります。なので、パスワードをdummypasswordとする場合には、
<?php
printf("%s\n", md5("dummypassword"));
?>
としてやり、phpコマンドで実行した結果である
$ php md5pw.php
60da11eb799d6a8da47e5cd6e4aa2273
をパスワード文字列としてusersテーブルにinsertしてやります。
mysql> insert into users values (null,'testuser','60da11eb799d6a8da47e5cd6e4aa2273','test user','testuser@example.org','test user',0,0);
Query OK, 1 row affected (0.00 sec)
このあと、Tokenを作ります。Tokenの作成方法は、classes/AuthenticationResource.class.phpを見ると分かります。
/**
* Corresponds to login.
*
* Request:
*
* {
* "username": <username>,
* "password": <password>,
* "local_user": <username>
* }
*
* Response:
*
* {
* "username": <string>,
* "valid_until": <int>,
* "hash": <string>,
* "token": <string>
* }
*
* Errors:
*
* 500 - Invalid request or missing username/password.
* 403 - Username/password incorrect.
*
* @access public
* @param mixed $request Request parameters
* @return Response Authentication Token if successful, error message if false.
*/
public function put($request) {
//(snip)
$token = new Token();
$token->username = $data->username;
$token->password = $data->password;
$token = $this->backend->createToken($token);
if ($token == null) {
$response->code = Response::FORBIDDEN;
$response->error = "Username and/or password was invalid.";
return $response;
}
$response->code = Response::OK;
$response->body = $token->toArray();
$response->log_message = "Token was successfully created.";
return $response;
}
まず、上記のコメントにある形式でJSONファイルを作成します。
{
"username": "testuser",
"password": "dummypassword",
"local_user": "testuser"
}
これを/authenticateにPUTメソッドで送信します。
$ curl -k -X PUT https://localhost/authenticate -d @./testuser.json
{"username":"testuser","valid_until":1327146727,"hash":"5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1","token":"5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1"}
コメントの期待値のレスポンスが返りましたね。これでTokenの登録ができました。なお、このTokenはしばらくすると無効になるので、リクエスト前に必ず実行するようにすると良いでしょう 6 。
tokenの使い方。¶
tokenの使い方はコメントには一切書いていませんが、lib/tonic.phpの下記の部分を見ると分かります。
// get HTTP request type
$raw_headers = array();
if (function_exists("apache_request_headers")) {
$raw_headers = apache_request_headers();
} else if (function_exists("nsapi_request_headers")) {
$raw_headers = nsapi_request_headers();
}
foreach ($raw_headers as $k => $h) {
switch (strtolower($k)) {
case "content-type":
$this->requestType = $h;
break;
case "x-authentication-token":
$this->requestToken = $h;
break;
}
}
curlコマンドを使う場合は、 -H “x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1” とすれば、Tokenを渡す事ができます。
ゾーンの参照。¶
ゾーンの取得は、/zone/:identifierでGETメソッドで取得します。:itentifierにはドメインを指定します。test.localドメインが既に登録されている場合、下記のように実行します。
$ curl -s -k -H 'x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1' -X GET https://localhost/zone/test.local | sed '
s/\[{/\[\n{/g
s/},{/},\n{/g
'
{"name":"test.local","type":"MASTER","notified_serial":"2012011801","records":[
{"name":"ns.test.local","type":"A","content":"192.168.0.10","ttl":"86400","priority":null},
{"name":"ns2.test.local","type":"A","content":"192.168.0.11","ttl":"86400","priority":null},
{"name":"test.local","type":"SOA","content":"ns.test.local hostmaster.test.local 2012011801","ttl":"86400","priority":null},
{"name":"test.local","type":"NS","content":"ns.test.local","ttl":"86400","priority":null},
{"name":"test.local","type":"NS","content":"ns2.test.local","ttl":"86400","priority":null},
{"name":"test.local","type":"MX","content":"mx.test.local","ttl":"86400","priority":"0"},
{"name":"test.local","type":"MX","content":"mx2.test.local","ttl":"86400","priority":"10"},
{"name":"www.test.local","type":"A","content":"192.168.0.1","ttl":"86400","priority":null}]}
レコードの登録。¶
すでに登録済みのゾーンに対しレコードを登録する場合には、下記のようなJSONファイルを用意します。
{"records": [
{ "name": "mx.test.local", "type": "A", "content": "11.11.11.11" },
{ "name": "mx2.test.local", "type": "A", "content": "11.11.11.12" },
{ "name": "test.local", "type": "MX", "content": "mx3.test.local", "priority": 30 },
{ "name": "mx3.test.local", "type": "A", "content": "11.11.11.13" }]}
これを/zone/:identifierに対しPUTメソッドで送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X PUT https://localhost/zone/test.local -d @./add_record.json
true
レコード情報を取得すると登録されていることが分かります。
{"name":"test.local","type":"MASTER","notified_serial":"2012011801","records":[
{"name":"mx.test.local","type":"A","content":"11.11.11.11","ttl":"86400","priority":"0","change_date":"1327755951"},
{"name":"mx2.test.local","type":"A","content":"11.11.11.12","ttl":"86400","priority":"0","change_date":"1327755951"},
{"name":"mx3.test.local","type":"A","content":"11.11.11.13","ttl":"86400","priority":"0","change_date":"1327755951"},
{"name":"ns.test.local","type":"A","content":"192.168.0.10","ttl":"86400","priority":null},
{"name":"ns2.test.local","type":"A","content":"192.168.0.11","ttl":"86400","priority":null},
{"name":"test.local","type":"SOA","content":"ns.test.local hostmaster.test.local 2012011801","ttl":"86400","priority":null},
{"name":"test.local","type":"NS","content":"ns.test.local","ttl":"86400","priority":null},
{"name":"test.local","type":"NS","content":"ns2.test.local","ttl":"86400","priority":null},
{"name":"test.local","type":"MX","content":"mx.test.local","ttl":"86400","priority":"0"},
{"name":"test.local","type":"MX","content":"mx2.test.local","ttl":"86400","priority":"10"},
{"name":"test.local","type":"MX","content":"mx3.test.local","ttl":"86400","priority":"30","change_date":"1327755951"},
{"name":"www.test.local","type":"A","content":"192.168.0.1","ttl":"86400","priority":null}]}
MXとSRVレコード以外はpriorityは必要ありませんが、上記のように指定しなかった場合、conf/database.conf.phpでconst DNS_DEFAULT_RECORD_PRIORITYにデフォルト値として設定されている0が登録されます。0ではなく、nullを設定しておくとprirityはnullになります。が、これはまた現時点ではこうしてしまうと次に説明するレコードの削除のときに問題になります。
レコードの削除。¶
test.localゾーンのレコードの削除を行うためには、次のようなJSONを用意します。
{ "name": "test.local", "records": [
{ "name": "test.local", "type": "MX", "content": "mx3.test.local", "priority": 30 },
{ "name": "mx.test.local", "type": "A", "content": "11.11.11.11" },
{ "name": "mx2.test.local", "type": "A", "content": "11.11.11.12" },
{ "name": "mx3.test.local", "type": "A", "content": "11.11.11.13"} ]}
これを/zone/に対しDELETEメソッドで送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X DELETE https://localhost/zone/ -d @./delete_record.json
true
この実行結果はtrueが返ってきます。ところが、上記で削除できるのは一番最初のMXレコードだけです。他の3つは、priorityを指定していないため、レコードの削除ができないのです。
public function delete_records($response, $identifier, $data, &$out = null) {
//(snip)
$statement = $connection->prepare(sprintf(
"DELETE FROM `%s` WHERE name = :name AND type = :type AND prio = :priority AND content = :content;", PowerDNSConfig::DB_RECORD_TABLE
));
$statement->bindParam(":name", $r_name);
$statement->bindParam(":type", $r_type);
$statement->bindParam(":content", $r_content);
$statement->bindParam(":priority", $r_prio);
foreach ($data->records as $record) {
if (!isset($record->name) || !isset($record->type) || !isset($record->priority) || !isset($record->content)) {
continue;
}
$r_name = $record->name;
$r_type = $record->type;
$r_content = $record->content;
$r_prio = $record->priority;
if ($statement->execute() === false) {
$response->code = Response::INTERNALSERVERERROR;
$response->error = sprintf("Rolling back transaction, failed to delete zone record - name: '%s', type: '%s', prio: '%s'", $r_name, $r_type, $r_prio);
$connection->rollback();
$out = false;
return $response;
}
}
上記のとおり、レコード単位ではpriorityが設定されていない場合には処理がスキップされるだけでエラーにはならないためです。TonicDNSだけでPowerDNSを使うのなら問題ないかもしれませんが、他の管理ツールと一緒に使う場合は、ここは不整合が生じるのでパッチを作成中です 7 。
レコードの更新。¶
残念ながら現時点でレコードの更新は未実装のためできません。
テンプレートの作成。¶
ゾーンの登録と行きたいところですが、ゾーンの作成には元にするテンプレートが必要です。テンプレートの作成には、下記のようなJSONを用意します。
{
"identifier": "sample1",
"description": "sample template",
"entries": [ {
"name": "test2.local",
"type": "NS",
"content": "ns.test2.local",
"ttl": 86400,
"priority": 0
},{
"name": "ns.test2.local",
"type": "A",
"content": "10.10.10.1",
"ttl": 86400,
"priority": 0
}
]
}
これを/template/:identifierにPUTメソッドで送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X PUT https://localhost/template/sample1 -d @./create_template.json
true
テンプレートの参照。¶
テンプレートの参照は、/template/にGETメソッドでアクセスします。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X GET https://localhost/template/
[
{"identifier":"sample1","entries":[
{"name":"test2.local","type":"NS","content":"ns.test2.local","ttl":"86400","priority":"0"},
{"name":"ns.test2.local","type":"A","content":"10.10.10.1","ttl":"86400","priority":"0"}],"description":"sample template"}]
複数ある場合は列挙されます。
特定のテンプレートだけを表示する場合には、/template/:identifierをGETメソッドでアクセスします。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X GET https://localhost/template/sample1
{"identifier":"sample1","entries":[
{"name":"test2.local","type":"NS","content":"ns.test2.local","ttl":"86400","priority":"0"},
{"name":"ns.test2.local","type":"A","content":"10.10.10.1","ttl":"86400","priority":"0"}],"description":"sample template"}
テンプレートの更新。¶
先ほどの作成したテンプレートを更新してみましょう。まず、下記のような一部変更したJSONを用意します。
{
"identifier": "sample1",
"description": "sample template",
"entries": [ {
"name": "test2.local",
"type": "NS",
"content": "ns.test2.local",
"ttl": 86400,
"priority": 0
},{
"name": "ns.test2.local",
"type": "A",
"content": "10.10.10.2",
"ttl": 86400,
"priority": 0
},{
"name": "test2.local",
"type": "A",
"content": "10.10.10.1",
"ttl": 86400,
"priority": 0
},{
"name": "test2.local",
"type": "SOA",
"content": "ns.test2.local hostmaster.test2.local 2012012901 10800 3600 604800 3600",
"ttl": 86400,
"priority": 0
}
]
}
これを/template/:identifierにPOSTメソッドで送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X POST https://localhost/template/sample1 -d @./update_template.json
true
テンプレートを参照しなおしてみると、更新できていることが確認できます。
{"identifier":"sample1","entries":[
{"name":"test2.local","type":"NS","content":"ns.test2.local","ttl":"86400","priority":"0"},
{"name":"ns.test2.local","type":"A","content":"10.10.10.2","ttl":"86400","priority":"0"},
{"name":"test2.local","type":"A","content":"10.10.10.1","ttl":"86400","priority":"0"},
{"name":"test2.local","type":"SOA","content":"ns.test2.local hostmaster.test2.local 2012012901 10800 3600 604800 3600","ttl":"86400","priority":"0"}],"description":"sample template"}
テンプレートの削除。¶
これは/template/:identifierにDELETEメソッドを送信するだけです。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X DELETE https://localhost/template/sample1
true
ゾーンの登録。¶
さて、テンプレートが用意できたので、ゾーンを登録してみます。まず、次にようなJSONを用意します。
{
"name": "test2.local",
"type": "MASTER",
"master": null,
"templates": [{
"identifier": "sample1"
}],
"records": [{
"name": "moge.test2.local",
"type": "A",
"content": "11.11.11.11"
}]
}
これを/zone/にPUTメソッドで送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X PUT https://localhost/zone/ -d@./create_zone.json
true
ゾーンを参照してみると、登録できていることが確認できます。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X GET https://localhost/zone/test2.local
{"name":"test2.local","type":"MASTER","notified_serial":"2012012901","records":[
{"name":"moge.test2.local","type":"A","content":"11.11.11.11","ttl":"86400","priority":null,"change_date":"1327768827"},
{"name":"ns.test2.local","type":"A","content":"10.10.10.2","ttl":"86400","priority":"0","change_date":"1327768827"},
{"name":"test2.local","type":"SOA","content":"ns.test2.local hostmaster.test2.local 2012012901 10800 3600 604800 3600","ttl":"86400","priority":"0","change_date":"1327768827"},
{"name":"test2.local","type":"NS","content":"ns.test2.local","ttl":"86400","priority":"0","change_date":"1327768827"},
{"name":"test2.local","type":"A","content":"10.10.10.1","ttl":"86400","priority":"0","change_date":"1327768827"}]}
ゾーンの更新。¶
ゾーンの更新は、MASTER, SLAVE, NATIVEへの変更ができます。SLAVEに変更するときは、PowerDNSの仕様として、masterにmasterサーバのIPアドレスを指定する必要があります。変更するためには、
{
"name": "test2.local",
"type": "SLAVE",
"master": "10.10.10.1"
}
という感じのJSONを用意し、/zone/:identifierにPOSTメソッドで送信すれば良いはずです。ただし、PowerDNS自体の設定にも依存するので、PowerDNSの設定がmasterサーバなのにゾーンはSLAVEにする、という処理は失敗します 8 。
ゾーンの削除。¶
ゾーンの削除は/zone/:identifierにDELETEメソッドを送信します。
$ curl -s -k -H "x-authentication-token: 5790245d3bcd19c055b2c83d56f25f8a1ceeb9e1" -X DELETE https://localhost/zone/test.local
true
まとめ。¶
とりあえず、現状ではレコードの登録、参照、削除ができるので、最低限やりたいことはできそうです。ですが、
使い方のドキュメントが無い
レコードの更新ができない
MX, SRVレコード以外のレコード登録にpriorityが設定されるのはイケてない
ユーザ作成ができない
レコード更新してもSOAレコードのserialが更新されない
といった問題は不便なので、パッチ書いて git format-patch
で送付しようと思います。 9
あとは独自要件として、PowerDNS GUIとの整合性を取るためにautitテーブルの更新も行う必要があるので、その辺のパッチも作らなくてはですね。
- 1
私にも、というかワシだけだろう…
- 2
ゾーンに登録されているレコードを全部変更する、というのは可能です
- 3
ただ、upstreamでは開発止まっているんじゃないかなぁ…。
- 4
pdns-gui自体は、APIが無いことを除けば、現状必要な管理ツールとしての要件としては満たしているので変更したくない、ということも試してみようかと思った理由の一つです。
- 5
なので、TonicDNS自体もまたPHPで書かれています…。まぁ、ええわ。
- 6
無効になるタイミングは、ユーザ作成時のパラメータに依ります。
- 7
ちなみに、以前ブログでも書いたPowerDNS GUIの場合は、MX, SRVレコード以外ではpriorityはnullになります。
- 8
それだけ確認済み
- 9
Githubだからpull requestとか使うんかな。まぁformat-patchでええやろ。