February 23, 2009

Gaebarを使ってみた


先日 App Gallery という GAE 上のサービスを始めてみました。サービスをやるからにはデータをバックアップしたいと思い、Gaebar を試してみたのでご紹介します。

なんと Gaebar の画面は django pony です!

Gaebar は Google App Engine Backup And Restore の略で、文字通り Google App Engine 用のバックアップ・リストアツールです。ゲイバーと読むそうです。この名前が良いですねw

Gaebar は Django のアプリケーションで簡単に使えます。GAE上で動く既存の Django アプリケーションにプラグインする事で、バックアップ・リストア機能を追加する事ができます。

残念ながら今は Django で動いているアプリケーションでしか使えませんが、少し頑張れば同じ事は別のフレームワークでも可能だと思います。

準備する

まずは Gaebar をダウンロードします。
Gaebar github から Download 機能を使ってアーカイブをダウンロードするか、下記のように git で clone しても良いです。

$ git clone git://github.com/aral/gaebar.git

またはあなたのプロジェクトが git を使っているなら下記のコマンドが良いかもしれません。

$ git submodule add git://github.com/aral/gaebar.git


方法はともあれ、プロジェクト直下に gaebar というディレクトリができていれば大丈夫です。

次に GAE の SDK にパッチを当てます。これは Gaebar がデータを python のファイルとしてダウンロードしてきてローカルのファイルシステムに保存するために必要なのです。方法が AralのBlog Entry に乗っていますが、この情報は少し古いので、SDK-1.1.9では下記のパッチを使ってください。

--- ../google_appengine/google/appengine/tools/dev_appserver.py.org 2009-02-26 11:33:33.000000000 +0900
+++ ../google_appengine/google/appengine/tools/dev_appserver.py 2009-02-20 17:04:46.000000000 +0900
@@ -1274,6 +1274,7 @@
allowed_symbols = self._WHITE_LIST_PARTIAL_MODULES[module.__name__]
for symbol in set(module.__dict__) - set(allowed_symbols):
if not (symbol.startswith('__') and symbol.endswith('__')):
+ module.__dict__['old_'+symbol] = module.__dict__[symbol]
del module.__dict__[symbol]

if module.__name__ in self._MODULE_OVERRIDES:


注意) app-engine-patch を使う場合は、settings.py で DJANGO_STYLE_MODEL_KIND = False にしないと動きませんでした。(少なくとも私は動かせませんでした。)動かせた方がいたら教えてください :-)

1. settings.py は下記のようにします。

INSTALLED_APPS = (
# Other apps...
'gaebar',
)


2. urls.py は下記のとおりです

urlpatterns = patterns('',

# ...other URLs

url(r'^gaebar/', include('gaebar.urls')),
)


3. app.yaml に static folder を追加します。

# Static: Gaebar
- url: /gaebar/static
static_dir: gaebar/static


4. index.yaml にインデックスを追加します。一回 dev server で Gaebar を動かせば自動で追加されます。

- kind: GaebarCodeShard
properties:
- name: backup
- name: created_at


5. Gaebar の設定を settings.py に追加します。

GAEBAR_LOCAL_URL: ローカル開発サーバの絶対 URL を書きます。
GAEBAR_SECRET_KEY: 秘密の文字列を記入します。誰にも秘密にしてください。
GAEBAR_SERVERS: dict でサーバーを列挙します。
GAEBAR_MODELS: tuple でバックアップするモデルを列挙します。

App Gallery で使用している設定のサンプルを下記に掲載します。

GAEBAR_LOCAL_URL = 'http://localhost:8000'
GAEBAR_SECRET_KEY = 'replace_with_your_secret_key'

GAEBAR_SERVERS = {
u'Deployment': u'http://app-gallery.appspot.com',
u'Staging': u'http://app-gallery-staging.appspot.com',
u'Local Test': u'http://localhost:8080',
}

GAEBAR_MODELS = (
(
'appgallery.models',
(u'Application', u'ThumbnailImage'),
),
(
'tags.models',
(u'Tag', u'TagApplication')
),
(
'favorites.models',
(u'Favorite',)
),
(
'ragendja.auth.custom_models',
(u'User',),
),
)


あと注意点がもう一つ。ローカル開発サーバーで django.contrib.csrf.middleware.CsrfMiddleware を使っているとバックアップがうまく行きませんでした。なので私はローカル開発サーバーでだけ CsrfMiddleware をオフにしています。

やった!これで Gaebar が使用できます。使う前に、Gaebar がどんな風に動くか説明しておきますね。

Gaebar は datastore のデータを Python code としてバックアップします。その Python code を実行する事でリストアします。バックアップは長くかかりますが、GAE はそれを許さないので、Gaebar は バックアップとリストアのプロセスを細切れにして ajax を使って繰り返し呼び出します。

デフォルトでは Gaebar は 5 つの Entity をバックアップし Python code に変換し、1MB の制限を回避するために 300KB くらいのコード断片として datastore に保存します。バックアップの過程では、このすぐ後に、コード断片をローカルサーバー側にダウンロードして保存する事になります。

もしあなたがもっと高い Quota を持っているなら、views.py をいじる事でこれらのデフォルト値を変えられます。

Gaebar を使う時は、十分にテストしてから使ってくださいね。万が一データが消失しても、私や Aral は責任を持てませんので。

使い方

A. リモートバックアップを取るには

1. Gaebar をあなたのアプリケーションと一緒に GAE にデプロイします

注意: デプロイする時に、gaebar/backups フォルダ内のバックアップデータも一緒にアップロードされる事を頭にいれておいてください。不必要ならバックアップデータは別の場所に移動した状態でデプロイすると良いでしょう。

2. 本番サーバで Gaebar にアクセスします。(例: http://myapp.appspot.com/gaebar/ )

3. Create New Backup ボタンをクリックします。

* バックアップをする時に、ローカル開発サーバーが動作している必要があります。

注意: reference error が発生した場合(存在しない Entity への ReferenceProperty)は、バックアップ時には無視されます。リストアすれば reference error は消滅します。

注意: 今までバックアップした一番大きなデータストアは 18995 行で 223 code 断片、35MB ありました。ローカル開発サーバーへのリストアは一晩中かかりました(ローカル開発サーバーの datastore は激遅なので)。もし記録を破ったら aral@aralbalkan.com に教えてあげてください :-)

注意2: バックアップ中の例外を詳しく見たい場合は、本番サーバーの settings.py で DEBUG_PROPAGATE_EXCEPTIONS = True と設定してください。


B. ローカルでリストアする

開発中に、リアル世界のデータを使ってテストするために、ローカルの開発サーバーにデータをリストアできます。

1. ローカル開発サーバの Gaebar ページに行きます。(例: http://localhost:8000/gaebar/ )
2. リストからリストアしたいバックアップデータを選びます。
3. 終わるまで待ちます。

tips: あらかじめ /var/tmp/ にある datastore の history ファイルのバックアップを取った方が良いです。

ローカルのデータストアは非常に遅いので、覚悟してください。

C. ステージング環境にリストアする

GAE にはバージョニング機能がありますが、datastore は別バージョン間でも共通です。ですからステージング用の別アプリを使用するのは良い考えでしょう。

本番とは別のアプリケーションスロットを用意して、そこにリストアしたいバックアップデータと一緒に Gaebar をデプロイします。後はローカル開発サーバーでのリストアと同じです。

D. 本番環境にリストアする

本番環境でトラブルがあった場合などは、バックアップからリストアできます。データストアのデータは上書きされるので注意してください。


実際に App Gallery でもバックアップ・リストアしてみましたが、完璧に動いています。Gaebar はかなり便利なのでみなさんも使ってみてはいかがでしょうか。

February 11, 2009

Mercurial deps extension

Mercurial で svn:externals みたいな事できないのか #mercurial irc で聞いてみたところ、deps extensionを使うと良いらしい。

Mercurial に同梱されてないので別途このへんから落として来て使う。

設定


deps.py を適当なところに置いて、hgrc に下記のように設定する。
[extensions]
hgext.deps =
# 又は deps.py を hgext dir 以外に置いたなら下記のようにする
# deps = /path/to/deps.py


ロケーション設定


依存関係のロケーションを設定する。これも hgrc に書く。ここで紹介する例は libfoo が外部 Mercurial リポジトリにあって、libbar は外部の CVS で管理されている場合。

[deps]
aliases = libfoo, libbar
alias.libfoo = /path/to/libfoo
alias.libbar = :pserver:anonymous@cvs.server.org:/sources/bar
alias.libbar.command = cvs -z3 -d$source co -r $rev -d $dest bar


実際の依存関係を設定


これはコマンドで行う。
$ hg deps -a libfoo -r 3a9b061bada1 -d lib/foo 0.9.1
$ hg deps -a libbar -r v0r7_8 -d lib/bar 0.9.1

$ hg deps -a libfoo -r f24139319bdb -d lib/foo 1.0
$ hg deps -a libbar -r v0r8_0 -d lib/bar 1.0


書式はこう

hg deps -a <ロケーション設定のalias名> -r <外部のリビジョン> -d  <本体のバージョン>


本体のバージョンに応じて外部リポジトリのリビジョンを変えたりできるわけか。

で、これをすると下記のような .hgdeps というファイルが出来る。これも hg add するのが良いみたい。
[1.0]
f24139319bdb libfoo lib/foo
v0r8_0 libbar lib/bar

[0.9.1]
3a9b061bada1 libfoo lib/foo
v0r7_8 libbar lib/bar


依存関係を持ってくる



下記のようにして clone するくらいしか機能は無いみたい。
$ hg depsclone 1.0


この例では、バージョン 1.0 用の外部依存関係を取ってくる事ができる。

まあすごく便利っていうわけでも無いし、使いたい人も限られてるだろうけど、Mercurial で svn:externals みたいな事がしたい人は試してみてください。

January 31, 2009

Google Apps に Group 機能が来ました

いつの間にか Google Apps の有料版にグループ機能が来ました。もしかするとコントロールパネルを拡張版にしないと出てこないかもしれません。おそらく今までの maillist 機能を拡張した機能で、ざっと触ってみたところ


  1. いくらでも入れ子に出来る

  2. メンバーのロールが Owner と Member の二種類ある

  3. ドメイン外のユーザを Owner としても Member としても追加できる

  4. このグループへの送信者を下記のグループ単位で制限できる


    • Owners
    • Members
    • ドメイン内のユーザ
    • Any mail address

  5. どうやら Google Document や Google Sites の invite の時に使えそう


今までは Google Apps を導入するお客さんには別立てでメーリングリストのサーバ(mailmanとか)を勧める事が多かったけど、これからはあまり必要なくなるかもしれませんね。

Python旅館第一回

最近やる気が無かったのですが Python旅館に来たら嘘のようにやる気が復活しました。

本当は原稿を書く予定だったんですが、隣に座った tokibitoIE6 の怖いコレ について教えてくれました。

おお!こないだ公開した My Trivia にもこのセキュリティホールあるし。早速直さないと。この OpenSocial アプリ ではユーザ入力の Trivia をサーバ側から json で返して、OpenSocial Canvas 側で表示しているのですが、とりあえず表示の時に document.createTextNode() してるから大丈夫だろうと思っていました。

しかしこういう時は < や > を下記のように処理してあげないといけないという事ですね。(この文脈では obj にユーザ入力文字が入ってます)


obj = obj.replace('<', r'\u003C')
obj = obj.replace('>', r'\u003E')


しかしこんな簡単な修正に 1h くらいかかってしまって情けない...

January 6, 2009

Blogger で Syntax Highlighting

明けましておめでとうございます。今年はすっかり寝正月でした。

突然ですが Blogger で手軽に Syntax Highlighting を使えたので書いてみます。
テンプレートのヘッダに下記を追加して

<link href='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.css' rel='stylesheet' type='text/css'/>
<script src='http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js' type='text/javascript'/>


body の onload ハンドラを設定します。

<body onload='prettyPrint()'>


実際のコード部には class 指定した pre タグを使います。

<pre class="prettyprint">

October 11, 2008

GeoPt Property 用のカスタムウィジェット

こないだ Maps API hack-a-thon Tokyo に参加してきました。その時作ったのが Google App Engine の GeoPt Property をとても扱いやすくするためのユーティリティクラスです。

この記事では、そのクラスを使って geopoint データを datastore に保存するサンプルコードをお見せします。始めるまえに、こちらのdjangoforms に関する記事を読んでおくとコードがすぐ理解できるでしょう。

まずはこの記事用のディレクトリを作ります。そして拙作の my_geopt.py をその中にコピーし、さらに新しく3つのファイルを作ります。app.yaml, main.py と index.html です。

ファイルを作成したら、開発用サーバーを機動して localhost にアクセスします。すると Google Map をクリックするだけで特定の GeoPt が指定できる事がわかります。

また ライブデモもあります(少し複雑になってますが、基本は同じです)。

Happy coding :-)

app.yaml
application: geotest
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: main.py


main.py
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

import os
import wsgiref.handlers

from google.appengine.ext import db
from google.appengine.ext.webapp import template
from google.appengine.ext.db import djangoforms
import my_geopt

class Place(db.Model):
  name = db.StringProperty()
  description = db.StringProperty()
  geopoint = db.GeoPtProperty()
  date = db.DateTimeProperty(auto_now=True)

class PlaceForm(djangoforms.ModelForm):
  class Meta:
    model = Place

class MainPage(webapp.RequestHandler):
  def post(self):
    data = PlaceForm(data=self.request.POST)
    if data.is_valid():
      # Save the data, and redirect to the view page
      entity = data.save(commit=False)
      entity.put()
      self.redirect('/')
    else:
      # Reprint the form
      contents = {'form': data}
      path = os.path.join(os.path.dirname(__file__), 'index.html')
      self.response.out.write(template.render(path, contents))
  def get(self):
    contents = {'form': PlaceForm()}
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.out.write(template.render(path, contents))

application = webapp.WSGIApplication(
  [('/', MainPage),],
  debug=True)

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()


index.html (Please replace 2 YOURAPIKEYs with your Maps API Key)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>GeoPtWidgets Example</title>
<script src="http://maps.google.com/maps?file=api&v=2&key=YOURAPIKEY"
type="text/javascript"></script>
<script src="http://www.google.com/uds/api?file=uds.js&v=1.0&key=YOURAPIKEY"
type="text/javascript"></script>
<script src="http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js"
type="text/javascript"></script>
<style type="text/css">
@import url("http://www.google.com/uds/css/gsearch.css");
@import url("http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css");
</style>
</head>

<body onunload="GUnload()">
<h1>GeoPtWidgets Example</h1>
<form method="post" action="/">
{{ form.as_p }}
<input type="submit" value="register"/>
</form>
</body> </html>

A custom widgets for GeoPt Property

I attended a Maps API hack-a-thon in Tokyo. There I wrote some utility class which helps us to handle GeoPt Property very easy.

In this article, I'll show you some sample code which store geopoint data by just clicking on Google Map. Before we go, perhaps you can read the djangoforms documentation. This will help you to understand the codes in this article.

Please create a directory for this article, copy my_geopt.py to the directory and create 3 brand new files app.yaml, main.py, and index.html. That's all.

After creating these files, start local dev server, get access to localhost, and see that you can specify a particular GeoPt by just clicking on the Google Map.

And here is a live demo(slightly more complicated, but basically almost the same).

Happy coding :-)

app.yaml
application: geotest
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: main.py


main.py
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

import os
import wsgiref.handlers

from google.appengine.ext import db
from google.appengine.ext.webapp import template
from google.appengine.ext.db import djangoforms
import my_geopt

class Place(db.Model):
  name = db.StringProperty()
  description = db.StringProperty()
  geopoint = db.GeoPtProperty()
  date = db.DateTimeProperty(auto_now=True)

class PlaceForm(djangoforms.ModelForm):
  class Meta:
    model = Place

class MainPage(webapp.RequestHandler):
  def post(self):
    data = PlaceForm(data=self.request.POST)
    if data.is_valid():
      # Save the data, and redirect to the view page
      entity = data.save(commit=False)
      entity.put()
      self.redirect('/')
    else:
      # Reprint the form
      contents = {'form': data}
      path = os.path.join(os.path.dirname(__file__), 'index.html')
      self.response.out.write(template.render(path, contents))
  def get(self):
    contents = {'form': PlaceForm()}
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.out.write(template.render(path, contents))

application = webapp.WSGIApplication(
  [('/', MainPage),],
  debug=True)

def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()


index.html (Please replace 2 YOURAPIKEYs with your Maps API Key)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>GeoPtWidgets Example</title>
<script src="http://maps.google.com/maps?file=api&v=2&key=YOURAPIKEY"
type="text/javascript"></script>
<script src="http://www.google.com/uds/api?file=uds.js&v=1.0&key=YOURAPIKEY"
type="text/javascript"></script>
<script src="http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js"
type="text/javascript"></script>
<style type="text/css">
@import url("http://www.google.com/uds/css/gsearch.css");
@import url("http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css");
</style>
</head>

<body onunload="GUnload()">
<h1>GeoPtWidgets Example</h1>
<form method="post" action="/">
{{ form.as_p }}
<input type="submit" value="register"/>
</form>
</body> </html>