Everything's Beta

things I don't get to do at work :)

Archive for the ‘gae’ tag

Get updates when your server’s ip changes

without comments

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:

  1. Poll for changes to self.ip
  2. 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 :)

Written by srijak

February 24th, 2010 at 11:18 pm

Posted in code

Tagged with , ,