1#!/usr/bin/env python
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Status UI for Google App Engine Pipeline API."""
18
19import logging
20import os
21import pkgutil
22import traceback
23
24from google.appengine.api import users
25from google.appengine.ext import webapp
26
27try:
28  import json
29except ImportError:
30  import simplejson as json
31
32# Relative imports
33import util
34
35
36class _StatusUiHandler(webapp.RequestHandler):
37  """Render the status UI."""
38
39  _RESOURCE_MAP = {
40    '/status': ('ui/status.html', 'text/html'),
41    '/status.css': ('ui/status.css', 'text/css'),
42    '/status.js': ('ui/status.js', 'text/javascript'),
43    '/list': ('ui/root_list.html', 'text/html'),
44    '/list.css': ('ui/root_list.css', 'text/css'),
45    '/list.js': ('ui/root_list.js', 'text/javascript'),
46    '/common.js': ('ui/common.js', 'text/javascript'),
47    '/common.css': ('ui/common.css', 'text/css'),
48    '/jquery-1.4.2.min.js': ('ui/jquery-1.4.2.min.js', 'text/javascript'),
49    '/jquery.treeview.min.js': ('ui/jquery.treeview.min.js', 'text/javascript'),
50    '/jquery.cookie.js': ('ui/jquery.cookie.js', 'text/javascript'),
51    '/jquery.timeago.js': ('ui/jquery.timeago.js', 'text/javascript'),
52    '/jquery.ba-hashchange.min.js': (
53        'ui/jquery.ba-hashchange.min.js', 'text/javascript'),
54    '/jquery.json.min.js': ('ui/jquery.json.min.js', 'text/javascript'),
55    '/jquery.treeview.css': ('ui/jquery.treeview.css', 'text/css'),
56    '/treeview-default.gif': ('ui/images/treeview-default.gif', 'image/gif'),
57    '/treeview-default-line.gif': (
58        'ui/images/treeview-default-line.gif', 'image/gif'),
59    '/treeview-black.gif': ('ui/images/treeview-black.gif', 'image/gif'),
60    '/treeview-black-line.gif': (
61        'ui/images/treeview-black-line.gif', 'image/gif'),
62    '/images/treeview-default.gif': (
63        'ui/images/treeview-default.gif', 'image/gif'),
64    '/images/treeview-default-line.gif': (
65        'ui/images/treeview-default-line.gif', 'image/gif'),
66    '/images/treeview-black.gif': (
67        'ui/images/treeview-black.gif', 'image/gif'),
68    '/images/treeview-black-line.gif': (
69        'ui/images/treeview-black-line.gif', 'image/gif'),
70  }
71
72  def get(self, resource=''):
73    import pipeline  # Break circular dependency
74    if pipeline._ENFORCE_AUTH:
75      if users.get_current_user() is None:
76        logging.debug('User is not logged in')
77        self.redirect(users.create_login_url(self.request.url))
78        return
79
80      if not users.is_current_user_admin():
81        logging.debug('User is not admin: %r', users.get_current_user())
82        self.response.out.write('Forbidden')
83        self.response.set_status(403)
84        return
85
86    if resource not in self._RESOURCE_MAP:
87      logging.debug('Could not find: %s', resource)
88      self.response.set_status(404)
89      self.response.out.write("Resource not found.")
90      self.response.headers['Content-Type'] = 'text/plain'
91      return
92
93    relative_path, content_type = self._RESOURCE_MAP[resource]
94    path = os.path.join(os.path.dirname(__file__), relative_path)
95    if not pipeline._DEBUG:
96      self.response.headers["Cache-Control"] = "public, max-age=300"
97    self.response.headers["Content-Type"] = content_type
98    try:
99      data = pkgutil.get_data(__name__, relative_path)
100    except AttributeError:  # Python < 2.6.
101      data = None
102    self.response.out.write(data or open(path, 'rb').read())
103
104
105class _BaseRpcHandler(webapp.RequestHandler):
106  """Base handler for JSON-RPC responses.
107
108  Sub-classes should fill in the 'json_response' property. All exceptions will
109  be returned.
110  """
111
112  def get(self):
113    import pipeline  # Break circular dependency
114    if pipeline._ENFORCE_AUTH:
115      if not users.is_current_user_admin():
116        logging.debug('User is not admin: %r', users.get_current_user())
117        self.response.out.write('Forbidden')
118        self.response.set_status(403)
119        return
120
121    # XSRF protection
122    if (not pipeline._DEBUG and
123        self.request.headers.get('X-Requested-With') != 'XMLHttpRequest'):
124      logging.debug('Request missing X-Requested-With header')
125      self.response.out.write('Request missing X-Requested-With header')
126      self.response.set_status(403)
127      return
128
129    self.json_response = {}
130    try:
131      self.handle()
132      output = json.dumps(self.json_response, cls=util.JsonEncoder)
133    except Exception, e:
134      self.json_response.clear()
135      self.json_response['error_class'] = e.__class__.__name__
136      self.json_response['error_message'] = str(e)
137      self.json_response['error_traceback'] = traceback.format_exc()
138      output = json.dumps(self.json_response, cls=util.JsonEncoder)
139
140    self.response.set_status(200)
141    self.response.headers['Content-Type'] = 'application/json'
142    self.response.headers['Cache-Control'] = 'no-cache'
143    self.response.out.write(output)
144
145  def handle(self):
146    raise NotImplementedError('To be implemented by sub-classes.')
147
148
149class _TreeStatusHandler(_BaseRpcHandler):
150  """RPC handler for getting the status of all children of root pipeline."""
151
152  def handle(self):
153    import pipeline  # Break circular dependency
154    self.json_response.update(
155        pipeline.get_status_tree(self.request.get('root_pipeline_id')))
156
157
158class _ClassPathListHandler(_BaseRpcHandler):
159  """RPC handler for getting the list of all Pipeline classes defined."""
160
161  def handle(self):
162    import pipeline  # Break circular dependency
163    self.json_response['classPaths'] = pipeline.get_pipeline_names()
164
165
166class _RootListHandler(_BaseRpcHandler):
167  """RPC handler for getting the status of all root pipelines."""
168
169  def handle(self):
170    import pipeline  # Break circular dependency
171    self.json_response.update(
172        pipeline.get_root_list(
173            class_path=self.request.get('class_path'),
174            cursor=self.request.get('cursor')))
175