Google App Engineサイトを活用して家のWANアドレス管理サーバーを構築する
ごく簡単なことなのでAppEngineの知識はチュートリアルレベルで十分。
アプリケーションを定義するapp.yamlは以下の様な感じ。
[app.yaml]
application: アプリ名
version: 1
runtime: python27
api_version: 1
threadsafe: yes
handlers:
- url: /favicon\.ico
static_files: favicon.ico
upload: favicon\.ico
- url: .*
script: main.app
# login: required
libraries:
- name: webapp2
version: "2.5.1"
アプリにアクセスした時に起動するメインプログラムがこちら。
[main.py]
#!/usr/bin/env python # # Copyright 2007 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import webapp2 from google.appengine.api import users from google.appengine.ext import db from datetime import datetime,timedelta class AppUser(db.Model): account = db.UserProperty() lanaddr = db.StringProperty() address = db.StringProperty() apppath = db.StringProperty() updated = db.DateTimeProperty() class RootHandler(webapp2.RequestHandler): def get(self): self.response.headers['Content-Type'] = 'text/plain' self.response.out.write( self.request.remote_addr ) class MyHandler(webapp2.RequestHandler): def get(self): self.response.headers['Content-Type'] = 'text/html' user = users.get_current_user() if user: self.response.out.write(''+user.email()+'
') app_users = AppUser.gql("where account = :1",user) for au in app_users: if self.request.remote_addr == au.address: url = "http://"+au.lanaddr+au.apppath non = "http://"+au.address+au.apppath else: url = "http://"+au.address+au.apppath non = "http://"+au.lanaddr+au.apppath self.response.out.write(""+url+" ") self.response.out.write(non+" ") else: self.redirect(users.create_login_url(self.request.uri)) class CommitHandler(webapp2.RequestHandler): def get(self): self.response.headers['Content-Type'] = 'text/plain' user = users.get_current_user() if user: app_users = AppUser.gql("where account = :1",user) if app_users.count() == 0: ip = AppUser() else: ip = app_users[0] ip.account = user ip.updated = datetime.now() ip.address = self.request.remote_addr ip.apppath = self.request.get('path') ip.lanaddr = self.request.get('lan') if ip.lanaddr == "": ip.lanaddr = "localhost" ip.put() self.response.out.write(ip.address+" "+ip.lanaddr+" "+ip.apppath+" COMMITED") else: self.redirect(users.create_login_url(self.request.uri)) class AdminHandler(webapp2.RequestHandler): def get(self): self.response.headers['Content-Type'] = 'text/plain' user = users.get_current_user() if user: if users.is_current_user_admin(): self.response.out.write('AppUsers') for u in AppUser().all(): self.response.out.write("\n"+u.account.email()) self.response.out.write(" "+u.lanaddr) self.response.out.write(" "+u.address) self.response.out.write(" "+u.apppath) self.response.out.write(" "+str(u.updated+timedelta(hours=9))) else: self.redirect(users.create_login_url(self.request.uri)) app = webapp2.WSGIApplication([('/', RootHandler), ('/my', MyHandler), ('/commit', CommitHandler), ('/admin', AdminHandler) ], debug=True)
Apacheライセンスである事が記述されてますが、サンプルがそうなっていたのでそのまま残してます。自分はそんなつもりはないんだけど。
順を追って説明しておくと
import webapp2
'webapp2'フレームワークを使いますってこと。サンプルがそうなってた。
from google.appengine.api import users
ユーザサービスというアカウント認証関係のAPIを使用する。WANアドレスをこのアカウントと紐付けるため。
from google.appengine.ext import db
データストアというデータベースを使用する。これ使ってデータ管理する。
from datetime import datetime,timedelta
これがちょっと悩んだんですが、サーバー側の時間が全部UTCなんですね。そこからJSTへ変換するための最小モジュールをインポート。pythonモジュールで時間関係の便利なモジュールは沢山あるけれど、やりたいことはたった一つ
self.response.out.write(" "+str(u.updated+timedelta(hours=9)))
の部分なので、timedelta使うことに。
この辺は、'google app engine utc jst'でググれば沢山親切ページが現れる。
以下の様な定義を入れておくことで、データストアにAppUserという自分専用のデータテーブルが作成される
class AppUser(db.Model):
account = ユーザアカウント(Googleアカウントオブジェクトインスタンス)
lanaddr = LANアドレス(コミット時に自分でパラメータ指定する)
address = WANアドレス(self.request.remote_addrで得られる。目的の物!)
apppath = 自分ちのサーバーアプリへのURL(コミット時に自分でパラメータ指定する)
updated = コミット時の更新日時
プログラム上では、この'AppUser'クラスを使ってDBへアクセスする。最初に実行されるフレームワークアプリケーションオブジェクト生成部分でハンドラの定義をする。
app = webapp2.WSGIApplication([('/', RootHandler),
('/my', MyHandler),
('/commit', CommitHandler),
('/admin', AdminHandler)
http://[app-name].appspot.com だったら、RootHandler
http://[app-name].appspot.com/my だったら、MyHandler
http://[app-name].appspot.com/commit だったら、CommitHandler
http://[app-name].appspot.com/admin だったら、AdminHandler
で、それぞれの'get'メソッドが呼ばれる。Railsでいうところのroutesかな。class RootHandler(webapp2.RequestHandler):
ここはとにかく単純にWANアドレスを返す。そういうサイトよくあるよね。
バッチ処理とかで使いやすいようにプレーンテキストでアドレスのみを返すようにした。結構便利。
class MyHandler(webapp2.RequestHandler):
自分のアドレスを返すところ。
ログインユーザのアカウントに紐付いたデータが登録されていれば、それをリンクで表示。これがやりたかっただけなんだけど、道のり長いっす。
ログインしていないなら、ログインページへ飛ばす。ログインしたら戻ってくる。
self.redirect(users.create_login_url(self.request.uri))
がフレームワークとユーザサービスの便利な組み合わせ。
これもチュートリアルの範囲内。
class CommitHandler(webapp2.RequestHandler):
"http://[app-name].appspot.com/commit?lan=XXXX&path=YYYY" ってアクセスする。
アカウント認証されたユーザならば、DB検索してマッチしたデータエンティティに、なければ新たなエンティティにデータを記録するところ。ログインしてなければ同じようにログインページへ飛ぶ。
class AdminHandler(webapp2.RequestHandler):
ここはデータテーブルの一覧を表示するところ。管理者(自分)のみが観覧できる。
チュートリアルに従って作って、ブラウザ上からアクセスして機能テスト。問題なし!
実際にはここまで作るだけでも試行錯誤したけど・・・本題とは遠いところで詰まった。
例えば、UTC=>JSTのシンプル解決に悩んだりとか。
適当なモジュールをインポートしてもサーバーになかったりとか。
途中からDBモデルにlanaddrを追加したんだけれど、なぜか更新されないとか。
'https://appengine.google.com'のアプリ管理からデータテーブルを削除したり、ログ見たり・・・
だけど、一番苦労したのは、ログイン認証だ。
サーバーは出来たが誰でもアクセスできちゃう。管理ユーザ(自分)だけしかアクセスできないパーミッション設定が可能だけど、それはそれで面倒だ。
個人のデータを保持するにはやはり個人を特定する認証が必要なのだ。
(もし利用希望者がおられれば、URLお教えしますが・・・)
ブラウザからならログインページが出て、ログインしたら後はブラウザがよろしくやってくれるけど、やりたいのは自分ちのUbuntuServerから、cronタスクで処理したいので、ブラウザがやってることを自分でやらんとならん。
googleアカウント認証は、こちらを参考にしましたよ
http://johannilsson.com/2011/04/13/authenticated-requests-on-app-engine.html
http://maimon-it.blogspot.jp/2010/04/authenticating-google-app-engine-apps.html
スクリプトでのログイン認証と、認証付きアクセスを記述したものがこちら
[gauth.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-
from subprocess import *
from os import path
mail = 'Googleメールアドレス'
password = 'パスワード'
app = 'アプリ名'
app_url = 'http://'+app+'.appspot.com/'
cookie_file = path.expanduser("~")+'/.google_myapp_cookie'
def login():
try:
# API認証キーを取得
print 'Get Auth-Key...\n'
auth = Popen(['curl','-f','-s',
'https://www.google.com/accounts/ClientLogin',
'-d','accountType=HOSTED_OR_GOOGLE',
'-d','Email=' + mail,
'-d','Passwd=' + password,
'-d','source=' + app,
'-d','service=ah'],
stdout=PIPE).communicate()[0]
# ログインしてクッキー取得
print 'Login...\n'
login = Popen(['curl','-c','-',
app_url+'_ah/login?auth='+auth[ auth.rindex("Auth=")+5:-1]],
stdout=PIPE).communicate()[0]
cookie = login[login.rindex("ACSID")+6:-1]
# クッキーの保存
f = open(cookie_file,'w')
f.write(cookie)
f.close()
return cookie
except:
return ""
def get(controller):
try:
f = open(cookie_file)
cookie = f.read()
f.close()
except:
cookie = login()
try:
return Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
stdout=PIPE).communicate()[0]
except:
return ''
pythonのsubprocessや、外部コマンド'curl'については、親切解説しているところが沢山あるんで。そちらで、よろしくです。
処理内容を簡単に説明する
loginメソッド:(ログインしてクッキーを得るまで)
'https://www.google.com/accounts/ClientLogin' へ所定のパラメータ付きでアクセスしてアカウント認証データを取得。
'http://[app-name].appspot.com/_ah/login?auth=認証データのAuth=以降のデータ列' ってやるとログイン出来る。
ログインした時に得られるデータ内に"ACSID"というキーワードのデータ列があるんで、その部分を切り出して保存。
これがこのサイトへのログインクッキーデータ。
ドキュメントによると、Googleアカウントだけじゃなく、OpenIDでの認証も出来るらしいけど、その辺は無視。
getメソッド:(クッキー使ってアクセス)
クッキーデータをファイルから読んで、認証付きで指定URLへGETアクセスする。
手順がやっつけっぽいが、一応機能する。
Cookieが無効になる期限がどれくらいなのかイマイチ不明なので、その辺調べてはっきりしてから、ちゃんとした手順に直そうかな。
・・・
ということで、修正した getメソッドがこちら
def get(controller):
for i in range(2):
try:
f = open(cookie_file)
cookie = f.read()
f.close()
ret = Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
stdout=PIPE).communicate()[0]
if ret.find('COMMITED')>0:
return 'ok'
else:
raise
except:
cookie = login()
return 'err'
Cookieが無効になるのは、https://appengine.google.com から、アプリ設定ページでCookie Expirationというところで設定するのだった。1日、1週間、2週間が設定できる。デフォルトは1日だった。最長の2週間に設定したがそれでも短いので、コミットに失敗したらログインからやり直しするように変更した。
cronで実行するスクリプト
[gcommith.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-
import gauth
# コミット
#gauth.get('commit?lan=192.168.X.Y&path=/foo')
gauth.get('commit?lan=192.168.X.Y')
cronタスク
crontabでユーザレベルタスクで回してます。
$ crontab -e
ってやって、自分の好きなエディタで開いたら
0 3 * * * python ~/Grive/gcommit.py
って書いておく。毎日3時にコミット実行です。crontabで回すと自分にメールでタスク結果が飛んでくる。開発中は飛んでくるのが便利だけど、落ち着くとうざいね。止めちゃおっかな。
この記事をよく読んだ奇特な方はおや?と思ったかも。
gcommit.pyの冒頭に、#!/usr/bin/pythonって書いてあるのに、なぜに「python ~/Grive/gcommit.py」なのかと。その謎は、パス名[/Grive]に隠されております。
どうでも良い話ですが・・・次回はその話にちょっと脱線します。