aboutsummaryrefslogtreecommitdiff
path: root/cwp/bartlett
diff options
context:
space:
mode:
Diffstat (limited to 'cwp/bartlett')
-rw-r--r--cwp/bartlett/app.yaml22
-rwxr-xr-xcwp/bartlett/server.py153
-rw-r--r--cwp/bartlett/static/favicon.icobin0 -> 198 bytes
-rw-r--r--cwp/bartlett/test/server_tester.py101
-rwxr-xr-xcwp/bartlett/update_appengine_server1
5 files changed, 277 insertions, 0 deletions
diff --git a/cwp/bartlett/app.yaml b/cwp/bartlett/app.yaml
new file mode 100644
index 00000000..60010f70
--- /dev/null
+++ b/cwp/bartlett/app.yaml
@@ -0,0 +1,22 @@
+application: chromeoswideprofiling
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /favicon.ico
+ static_files: static/favicon.ico
+ upload: static/favicon.ico
+- url: /remote_api
+ script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
+ login: admin
+- url: /
+ script: server.py
+- url: /upload
+ script: server.py
+- url: /serve
+ script: server.py
+- url: /serve/.*
+ script: server.py
+- url: /del/.*
+ script: server.py
diff --git a/cwp/bartlett/server.py b/cwp/bartlett/server.py
new file mode 100755
index 00000000..f6b35361
--- /dev/null
+++ b/cwp/bartlett/server.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+# Author: mrdmnd@ (Matt Redmond)
+# Based off of code in //depot/google3/experimental/mobile_gwp
+"""Code to transport profile data between a user's machine and the CWP servers.
+ Pages:
+ "/": the main page for the app, left blank so that users cannot access
+ the file upload but left in the code for debugging purposes
+ "/upload": Updates the datastore with a new file. the upload depends on
+ the format which is templated on the main page ("/")
+ input includes:
+ profile_data: the zipped file containing profile data
+ board: the architecture we ran on
+ chromeos_version: the chromeos_version
+ "/serve": Lists all of the files in the datastore. Each line is a new entry
+ in the datastore. The format is key~date, where key is the entry's
+ key in the datastore and date is the file upload time and date.
+ (Authentication Required)
+ "/serve/([^/]+)?": For downloading a file of profile data, ([^/]+)? means
+ any character sequence so to download the file go to
+ '/serve/$key' where $key is the datastore key of the file
+ you want to download.
+ (Authentication Required)
+ "/del/([^/]+)?": For deleting an entry in the datastore. To use go to
+ '/del/$key' where $key is the datastore key of the entry
+ you want to be deleted form the datastore.
+ (Authentication Required)
+ TODO: Add more extensive logging"""
+
+import cgi
+import logging
+import md5
+import urllib
+
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+logging.getLogger().setLevel(logging.DEBUG)
+
+
+class FileEntry(db.Model):
+ profile_data = db.BlobProperty() # The profile data
+ date = db.DateTimeProperty(auto_now_add=True) # Date it was uploaded
+ data_md5 = db.ByteStringProperty() # md5 of the profile data
+ board = db.StringProperty() # board arch
+ chromeos_version = db.StringProperty() # ChromeOS version
+
+
+class MainPage(webapp.RequestHandler):
+ """Main page only used as the form template, not actually displayed."""
+
+ def get(self, response=''): # pylint: disable-msg=C6409
+ if response:
+ self.response.out.write('<html><body>')
+ self.response.out.write("""<br>
+ <form action="/upload" enctype="multipart/form-data" method="post">
+ <div><label>Profile Data:</label></div>
+ <div><input type="file" name="profile_data"/></div>
+ <div><label>Board</label></div>
+ <div><input type="text" name="board"/></div>
+ <div><label>ChromeOS Version</label></div>
+ <div><input type="text" name="chromeos_version"></div>
+ <div><input type="submit" value="send" name="submit"></div>
+ </form>
+ </body>
+ </html>""")
+
+
+class Upload(webapp.RequestHandler):
+ """Handler for uploading data to the datastore, accessible by anyone."""
+
+ def post(self): # pylint: disable-msg=C6409
+ """Takes input based on the main page's form."""
+ getfile = FileEntry()
+ f1 = self.request.get('profile_data')
+ getfile.profile_data = db.Blob(f1)
+ getfile.data_md5 = md5.new(f1).hexdigest()
+ getfile.board = self.request.get('board')
+ getfile.chromeos_version = self.request.get('chromeos_version')
+ getfile.put()
+ self.response.out.write(getfile.key())
+ #self.redirect('/')
+
+
+class ServeHandler(webapp.RequestHandler):
+ """Given the entry's key in the database, output the profile data file. Only
+ accessible from @google.com accounts."""
+
+ def get(self, resource): # pylint: disable-msg=C6409
+ if Authenticate(self):
+ file_key = str(urllib.unquote(resource))
+ request = db.get(file_key)
+ self.response.out.write(request.profile_data)
+
+
+class ListAll(webapp.RequestHandler):
+ """Displays all files uploaded. Only accessible by @google.com accounts."""
+
+ def get(self): # pylint: disable-msg=C6409
+ """Displays all information in FileEntry, ~ delimited."""
+ if Authenticate(self):
+ query_str = 'SELECT * FROM FileEntry ORDER BY date ASC'
+ query = db.GqlQuery(query_str)
+ delimiter = '~'
+
+ for item in query:
+ display_list = [item.key(), item.date, item.data_md5, item.board,
+ item.chromeos_version]
+ str_list = [cgi.escape(str(i)) for i in display_list]
+ self.response.out.write(delimiter.join(str_list) + '</br>')
+
+
+class DelEntries(webapp.RequestHandler):
+ """Deletes entries. Only accessible from @google.com accounts."""
+
+ def get(self, resource): # pylint: disable-msg=C6409
+ """A specific entry is deleted, when the key is given."""
+ if Authenticate(self):
+ fkey = str(urllib.unquote(resource))
+ request = db.get(fkey)
+ if request:
+ db.delete(fkey)
+
+
+def Authenticate(webpage):
+ """Some urls are only accessible if logged in with a @google.com account."""
+ user = users.get_current_user()
+ if user is None:
+ webpage.redirect(users.create_login_url(webpage.request.uri))
+ elif user.email().endswith('@google.com'):
+ return True
+ else:
+ webpage.response.out.write('Not Authenticated')
+ return False
+
+
+def main():
+ application = webapp.WSGIApplication(
+ [
+ ('/', MainPage),
+ ('/upload', Upload),
+ ('/serve/([^/]+)?', ServeHandler),
+ ('/serve', ListAll),
+ ('/del/([^/]+)?', DelEntries),
+ ],
+ debug=False)
+ run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/cwp/bartlett/static/favicon.ico b/cwp/bartlett/static/favicon.ico
new file mode 100644
index 00000000..19b58c2e
--- /dev/null
+++ b/cwp/bartlett/static/favicon.ico
Binary files differ
diff --git a/cwp/bartlett/test/server_tester.py b/cwp/bartlett/test/server_tester.py
new file mode 100644
index 00000000..585da43a
--- /dev/null
+++ b/cwp/bartlett/test/server_tester.py
@@ -0,0 +1,101 @@
+# Copyright 2012 Google Inc. All Rights Reserved.
+# Author: mrdmnd@ (Matt Redmond)
+"""A unit test for sending data to Bartlett. Requires poster module."""
+
+import cookielib
+import os
+import signal
+import subprocess
+import tempfile
+import time
+import unittest
+import urllib2
+
+from poster.encode import multipart_encode
+from poster.streaminghttp import register_openers
+
+SERVER_DIR = '../.'
+SERVER_URL = 'http://localhost:8080/'
+GET = '_ah/login?email=googler@google.com&action=Login&continue=%s'
+AUTH_URL = SERVER_URL + GET
+
+
+class ServerTest(unittest.TestCase):
+ """A unit test for the bartlett server. Tests upload, serve, and delete."""
+
+ def setUp(self):
+ """Instantiate the files and server needed to test upload functionality."""
+ self._server_proc = LaunchLocalServer()
+ self._jar = cookielib.LWPCookieJar()
+ self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._jar))
+
+ # We need these files to not delete when closed, because we have to reopen
+ # them in read mode after we write and close them.
+ self.profile_data = tempfile.NamedTemporaryFile(delete=False)
+
+ size = 16 * 1024
+ self.profile_data.write(os.urandom(size))
+
+ def tearDown(self):
+ self.profile_data.close()
+ os.remove(self.profile_data.name)
+ os.kill(self._server_proc.pid, signal.SIGINT)
+
+ def testIntegration(self): # pylint: disable-msg=C6409
+ key = self._testUpload()
+ self._testListAll()
+ self._testServeKey(key)
+ self._testDelKey(key)
+
+ def _testUpload(self): # pylint: disable-msg=C6409
+ register_openers()
+ data = {'profile_data': self.profile_data,
+ 'board': 'x86-zgb',
+ 'chromeos_version': '2409.0.2012_06_08_1114'}
+ datagen, headers = multipart_encode(data)
+ request = urllib2.Request(SERVER_URL + 'upload', datagen, headers)
+ response = urllib2.urlopen(request).read()
+ self.assertTrue(response)
+ return response
+
+ def _testListAll(self): # pylint: disable-msg=C6409
+ request = urllib2.Request(AUTH_URL % (SERVER_URL + 'serve'))
+ response = self.opener.open(request).read()
+ self.assertTrue(response)
+
+ def _testServeKey(self, key): # pylint: disable-msg=C6409
+ request = urllib2.Request(AUTH_URL % (SERVER_URL + 'serve/' + key))
+ response = self.opener.open(request).read()
+ self.assertTrue(response)
+
+ def _testDelKey(self, key): # pylint: disable-msg=C6409
+ # There is no response to a delete request.
+ # We will check the listAll page to ensure there is no data.
+ request = urllib2.Request(AUTH_URL % (SERVER_URL + 'del/' + key))
+ response = self.opener.open(request).read()
+ request = urllib2.Request(AUTH_URL % (SERVER_URL + 'serve'))
+ response = self.opener.open(request).read()
+ self.assertFalse(response)
+
+
+def LaunchLocalServer():
+ """Launch and store an authentication cookie with a local server."""
+ proc = subprocess.Popen(
+ ['dev_appserver.py', '--clear_datastore', SERVER_DIR],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ # Wait for server to come up
+ while True:
+ time.sleep(1)
+ try:
+ request = urllib2.Request(SERVER_URL + 'serve')
+ response = urllib2.urlopen(request).read()
+ if response:
+ break
+ except urllib2.URLError:
+ continue
+ return proc
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cwp/bartlett/update_appengine_server b/cwp/bartlett/update_appengine_server
new file mode 100755
index 00000000..f3812057
--- /dev/null
+++ b/cwp/bartlett/update_appengine_server
@@ -0,0 +1 @@
+appcfg.py --oauth2 update .