Archive for the ‘gae’ tag
Get updates when your server’s ip changes
It’s a simple problem; I want to get updates when my home server’s ip address changes. It’s such a simple and pervasive problem that I’m sure there are loads of solutions out there. But, I figured it would be faster/easier to roll my own rather than evaluate+integrate w/e else is out there.
My first instinct at a solution comprised of 2 parts:
- Poll for changes to self.ip
- Publish updated ip using/to w/e
Let’s tackle part1. The main question here is how the server should get its external ip. The first thing that came to mind is a web page that echoes the visitor’s ip. So I wrote a simple Google appengine app that echoes your ipv4 address when you hit it. My script on the server GETs its external ip by hitting http://my-ipaddr.appspot.com/ and if its changed publishes it using the passed-in function. Here’s the code:
# ipwatcher.py import urllib2 import socket import time root_url = 'http://my-ipaddr.appspot.com' def get_ip(): ip = urllib2.urlopen(root_url).read() try: socket.inet_aton(ip) return ip except socket.error: return None def watch(publisher): if not callable(publisher): raise Exception('The publisher needs to be a callable function') last_ip = None while 1: current_ip = get_ip() if current_ip != last_ip: publisher(current_ip) last_ip = current_ip time.sleep(5*60) def publish(ip): # publish however print ip if __name__ == '__main__': watch(publish) # google app engine part # simpler than simple. It's just here for completeness from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app class MainPage(webapp.RequestHandler): def get(self): # echo user ip self.response.headers['Content-Type'] = 'text/plain' self.response.out.write(self.request.remote_addr) application = webapp.WSGIApplication( [('/', MainPage)], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
Yeah, it’s crummy that I do GET requests every 5 minutes, but with the GAE cap at 500requests/s I’m not too worried yet. I don’t know if there is a better way to get the external IP, but as this works and it’s a means to an end, I’m going to resist getting distracted.
So, part1 done with no sweat. Let’s move on to part2 where we actually publish the ip. My first instinct was just to use email. But I get too many emails anyway. Second I thought of scp or ftp to push the ip out. Meh. Been there done that.
Since I have something running on GAE, why not use it? Let’s do it.
GAE’s datastore makes it brain dead easy way to persist data. We could just do this:
name = "my-ip" class IpAddress(db.Model): ip = db.StringProperty(required=True) name = db.StringProperty(required=True) class MainPage(webapp.RequestHandler): def get(self): # echo user ip ip = IpAddress.gql("where name = :1", name).get() if not kv: ip = IpAddress(ip = self.request.remote_addr, name = name) ip.put() else: ip.ip = self.request.remote_addr ip.put() self.response.headers['Content-Type'] = 'text/plain' self.response.out.write(ip.ip)
This would probably work just fine. At least until random people/bots start hitting it. But, with maybe a hundred views a week on my blog, I don’t forsee too many people hitting the random appspot site.
However, since I am playing around with the GAE, might as well make it a little more interesting. Let’s try to make a simple key value store.
class KeyValue(db.Model): name = db.StringProperty(required=False) passwd = db.StringProperty(required=False) value = db.TextProperty(required=False) modified = db.DateProperty(auto_now=True)
The password field is there just so random people who guess a key wont be able to edit the data. Yup, it’s not encrypted. Why? Because I already know the password of everyone who’s going to use it (me
)
If you are dying to use it and don’t want me to see your password, let me know and I can hash it or something.
Anyway, adding simple CRUD is easy too:
class KVStoreUpdate(webapp.RequestHandler): def post(self): # post to an existing key self.response.headers['Content-Type'] = 'text/plain' (name, passwd) = (self.request.get('name'), self.request.get('passwd')) content = self.request.get('content') kv = KeyValue.gql("where name = :1 and passwd = :2", name, passwd).get() if not kv: self.response.out.write('-1') else: kv.value = content kv.put() self.response.out.write(kv.name) def get_name_passwd(self, request): return class KVStoreCreate(webapp.RequestHandler): def post(self): # reserve a new key or confirm old key. self.response.headers['Content-Type'] = 'text/plain' (name, passwd) = (self.request.get('name'), self.request.get('passwd')) kv = KeyValue.gql("where name = :1", name).get() if not kv: newKv = KeyValue (name = name, passwd = passwd) newKv.put() self.response.out.write(name) elif kv.passwd == passwd: self.response.out.write(name) else: self.response.out.write('-1') class KVStoreRead(webapp.RequestHandler): def post(self): # return stored value for the given key self.response.headers['Content-Type'] = 'text/plain' (name, passwd) = (self.request.get('name'), self.request.get('passwd')) kv = KeyValue.gql("where name = :1 and passwd = :2", name, passwd).get() if not kv: self.response.out.write('-1') else: self.response.out.write(kv.value) class KVStoreDelete(webapp.RequestHandler): def post(self): # delete kv self.response.headers['Content-Type'] = 'text/plain' (name, passwd) = (self.request.get('name'), self.request.get('passwd')) kv = KeyValue.gql("where name = :1 and passwd = :2", name, passwd).get() if not kv: self.response.out.write('-1') else: kv.delete() self.response.out.write(name)
Now all we need is the client code:
import urllib2 import urllib import random import string # inbox_id is the just the key. I was going to have versioning + # addressing but decided not to since it was starting to look # like a hybrid of a queuing system and a key value store. class KeyValuePublisher: def __init__(self, inbox_id, passwd, url): self.inbox_id = inbox_id self.passwd = passwd self.url = url self.reserve_inbox() def reserve_inbox(self): self.create_inbox() def create_inbox(self): response = self.make_request({}, "/c") if (response == '-1'): raise Exception('Name already taken, or password invalid') def publish(self, content): response = self.make_request({'content': content}, "/u") if (response == '-1'): raise Exception('Invalid update') def read(self): return self.make_request({}, "/r") def delete(self): response = self.make_request({}, "/d") if (response == '-1'): raise Exception('Can\'t delete') def make_request(self, data, resource = "", method = 'POST'): data['name'] = self.inbox_id data['passwd'] = self.passwd opener = urllib2.build_opener(urllib2.HTTPHandler) request = urllib2.Request(self.url + resource, data=urllib.urlencode(data)) request.add_header('Content-Type', 'application/x-www-form-urlencoded') request.get_method = lambda: method return opener.open(request).read() if __name__ == "__main__": # publish strings to a valid mailbox kvs = KeyValuePublisher("srijak0", "password", 'http://my-ipaddr.appspot.com') for i in range(20): len = random.randint(0,10000) content = ''.join(random.choice(string.letters) for i in xrange(len)) kvs.publish(content) assert content == kvs.read() # initialize with taken mailbox name passed = False try: kvs = KeyValuePublisher("srijak0", "not_password", 'http://my-ipaddr.appspot.com') except: passed = True assert passed # delete mailbox kvs = KeyValuePublisher("srijak0", "password", 'http://my-ipaddr.appspot.com') kvs.delete() assert kvs.read() == '-1' print "[Tests passed]"
Pretty self explanatory.
And there you have it. Not the prettiest code I’ve ever written but it gets the job done well enough for <1hour of work