1
2
3
4
5 """Server for processing client (web-site) requests to NPSGD.
6
7 This module is meant to be run as a daemon process in conjunction with npsgd_queue
8 and npsgd_web. It acts as an HTTP server that serves up html for models
9 (e.g. http://localhost:8000/model/example) and can be proxied behind a server
10 or run as a web service.
11 """
12 import os
13 import sys
14 import time
15 import logging
16 import functools
17 import threading
18 import tornado.web
19 import tornado.ioloop
20 import tornado.escape
21 import tornado.httpclient
22 import tornado.httpserver
23 import urllib
24 from optparse import OptionParser
25 from datetime import datetime
26
27 from npsgd import model_manager
28 from npsgd import model_parameters
29 from npsgd import ui_modules
30
31 from npsgd.model_manager import modelManager
32 from npsgd.model_task import ModelTask
33 from npsgd.model_parameters import ValidationError
34 from npsgd.config import config
35
36
37
38 lastWorkerCheckSuccess = datetime(1,1,1)
41 """HTTP handler for all model related requests."""
42
43 @tornado.web.asynchronous
44 - def get(self, modelName):
45 """Get handler to serve up the html for a specific model.
46
47 This method will either serve up an error if the queue is down or
48 the queue has no workers or return a nice view of the given model
49 using the model template. Requests to the queue are done
50 asynchronously to avoid blocking.
51 """
52 global lastWorkerCheckSuccess
53 model = modelManager.getLatestModel(modelName)
54
55 td = datetime.now() - lastWorkerCheckSuccess
56 checkWorkers = (td.seconds + td.days * 24 * 3600) > config.keepAliveTimeout
57
58 if checkWorkers:
59
60 http = tornado.httpclient.AsyncHTTPClient()
61 request = tornado.httpclient.HTTPRequest(
62 "http://%s:%s/client_queue_has_workers?secret=%s" % (config.queueServerAddress, config.queueServerPort, config.requestSecret),
63 method="GET")
64 callback = functools.partial(self.queueCallback, model=model)
65 http.fetch(request, callback)
66 else:
67 self.renderModel(model)
68
70 """Render the html associated with a given model object."""
71 self.render(config.modelTemplatePath, model=model, errorText=None)
72
74 self.render(config.modelErrorTemplatePath, errorText=errorText)
75
77 """Asynchronous callback from the queue (for checking if it is up, or has workers).
78
79 This method will actually perform the rendering of the model if things
80 look good.
81 """
82 global lastWorkerCheckSuccess
83 if response.error:
84 self.queueErrorRender("We are sorry. Our queuing server appears to be down at the moment, please try again later")
85 return
86
87 try:
88 json = tornado.escape.json_decode(response.body)
89 hasWorkers = json["response"]["has_workers"]
90 if hasWorkers:
91 lastWorkerCheckSuccess = datetime.now()
92 self.renderModel(model)
93 else:
94 self.queueErrorRender("We are sorry, our model worker machines appear to be down at the moment. Please try again later")
95 return
96
97 except KeyError:
98 logging.info("Bad response from queue server")
99 self.queueErrorRender("We are sorry. Our queuing server appears to be having issues communicating at the moment, please try again later")
100 return
101
102 @tornado.web.asynchronous
103 - def post(self, modelName):
104 """Post handler to actually create a model task with given parameters."""
105 modelVersion = self.get_argument("modelVersion")
106 model = modelManager.getModel(modelName, modelVersion)
107 try:
108 task = self.setupModelTask(model)
109 except ValidationError, e:
110 logging.exception(e)
111 self.render(config.modelTemplatePath, model=model, errorText=str(e))
112 return
113 except tornado.web.HTTPError, e:
114 logging.exception(e)
115 self.render(config.modelTemplatePath, model=model, errorText=None)
116 return
117
118 logging.info("Making async request to get confirmation number for task")
119
120 http = tornado.httpclient.AsyncHTTPClient()
121 request = tornado.httpclient.HTTPRequest(
122 "http://%s:%s/client_model_create" % (config.queueServerAddress, config.queueServerPort),
123 method="POST",
124 body=urllib.urlencode({
125 "secret": config.requestSecret,
126 "task_json": tornado.escape.json_encode(task.asDict())}))
127
128 http.fetch(request, self.confirmationNumberCallback)
129
131 """Asynchronous callback when model run is populated to confirmation queue."""
132
133 if response.error: raise tornado.web.HTTPError(500)
134
135 try:
136 json = tornado.escape.json_decode(response.body)
137 logging.info(json)
138 res = json["response"]
139 model = modelManager.getModel(res["task"]["modelName"], res["task"]["modelVersion"])
140 task = model.fromDict(res["task"])
141 code = res["code"]
142 except KeyError:
143 logging.info("Bad response from queue server")
144 raise tornado.web.HTTPError(500)
145
146 self.render(config.confirmTemplatePath, email=task.emailAddress, code=code)
147
149 email = self.get_argument("email")
150
151 paramDict = {}
152 for param in model.parameters:
153 try:
154 argVal = self.get_argument(param.name)
155 except tornado.web.HTTPError:
156 argVal = param.nonExistValue()
157
158 value = param.withValue(argVal)
159 paramDict[param.name] = value.asDict()
160
161 task = model(email, 0, paramDict)
162 return task
163
166 """Index response handler (e.g. http://localhost:8000): do nothing for now."""
167
169 self.write("Find a model!")
170
172 """HTTP confirmation code handler.
173
174 Requests to this server proxy to the queue for actual confirmation code handling.
175 The queue request is called asynchronously.
176 """
177
178 @tornado.web.asynchronous
179 - def get(self, confirmationCode):
180 http = tornado.httpclient.AsyncHTTPClient()
181 request = tornado.httpclient.HTTPRequest(
182 "http://%s:%s/client_confirm/%s?secret=%s" % (config.queueServerAddress, config.queueServerPort, \
183 confirmationCode, config.requestSecret))
184
185 logging.info("Asyncing a confirmation: '%s'", confirmationCode)
186 http.fetch(request, self.confirmationCallback)
187
189 if response.error: raise tornado.web.HTTPError(404)
190
191 json = tornado.escape.json_decode(response.body)
192 res = json["response"]
193
194 if res == "okay":
195 self.render(config.confirmedTemplatePath)
196 elif res == "already_confirmed":
197 self.render(config.alreadyConfirmedTemplatePath)
198 else:
199 logging.info("Bad response from queue server: %s", res)
200 raise tornado.web.HTTPError(500)
201
204 appList = [
205 (r"/", ClientBaseRequest),
206 (r"/confirm_submission/(\w+)", ClientConfirmRequest),
207 (r"/models/(.*)", ClientModelRequest)
208 ]
209
210 settings = {
211 "static_path": os.path.join(os.path.dirname(__file__), "static"),
212 "ui_modules": ui_modules,
213 "template_path": config.htmlTemplateDirectory
214 }
215
216 return tornado.web.Application(appList, **settings)
217
220 parser = OptionParser()
221 parser.add_option('-c', '--config', dest='config',
222 help="Config file", default="config.cfg")
223 parser.add_option('-p', '--client-port', dest='port',
224 help="Http port (for serving html)", default=8000, type="int")
225 parser.add_option('-l', '--log-filename', dest='log',
226 help="Log filename (use '-' for stderr)", default="-")
227
228 (options, args) = parser.parse_args()
229
230 config.loadConfig(options.config)
231 config.setupLogging(options.log)
232 model_manager.setupModels()
233 model_manager.startScannerThread()
234
235 clientHTTP = tornado.httpserver.HTTPServer(setupClientApplication())
236 clientHTTP.listen(options.port)
237
238 logging.info("NPSGD Web Booted up, serving on port %d", options.port)
239 print >>sys.stderr, "NPSGD web server running on port %d" % options.port
240
241 tornado.ioloop.IOLoop.instance().start()
242
243 if __name__ == "__main__":
244 main()
245