azkaban-uncached

Details

diff --git a/src/java/azkaban/webapp/AzkabanWebServer.java b/src/java/azkaban/webapp/AzkabanWebServer.java
index 1af216f..f9f62a5 100644
--- a/src/java/azkaban/webapp/AzkabanWebServer.java
+++ b/src/java/azkaban/webapp/AzkabanWebServer.java
@@ -31,8 +31,14 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.TimeZone;
 
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
 import javax.management.MBeanServer;
 import javax.management.ObjectName;
+import javax.management.ReflectionException;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
@@ -73,6 +79,7 @@ import azkaban.webapp.servlet.AzkabanServletContextListener;
 
 import azkaban.webapp.servlet.AbstractAzkabanServlet;
 import azkaban.webapp.servlet.ExecutorServlet;
+import azkaban.webapp.servlet.JMXHttpServlet;
 import azkaban.webapp.servlet.ScheduleServlet;
 import azkaban.webapp.servlet.HistoryServlet;
 import azkaban.webapp.servlet.IndexServlet;
@@ -179,9 +186,6 @@ public class AzkabanWebServer implements AzkabanServer {
 		}
 		
 		configureMBeanServer();
-		
-		
-		
 	}
 
 	private void setViewerPlugins(List<ViewerPlugin> viewerPlugins) {
@@ -441,6 +445,7 @@ public class AzkabanWebServer implements AzkabanServer {
 		root.addServlet(new ServletHolder(new ExecutorServlet()),"/executor");
 		root.addServlet(new ServletHolder(new HistoryServlet()), "/history");
 		root.addServlet(new ServletHolder(new ScheduleServlet()),"/schedule");
+		root.addServlet(new ServletHolder(new JMXHttpServlet()),"/jmx");
 		
 		String viewerPluginDir = azkabanSettings.getString("viewer.plugin.dir", "plugins/viewer");
 		app.setViewerPlugins(loadViewerPlugins(root, viewerPluginDir, app.getVelocityEngine()));
@@ -723,7 +728,27 @@ public class AzkabanWebServer implements AzkabanServer {
 		} catch (Exception e) {
 			logger.error("Error registering mbean " + mbeanClass.getCanonicalName(), e);
 		}
-
 	}
 	
+	public List<ObjectName> getMbeanNames() {
+		return registeredMBeans;
+	}
+	
+	public MBeanInfo getMBeanInfo(ObjectName name) {
+		try {
+			return mbeanServer.getMBeanInfo(name);
+		} catch (Exception e) {
+			logger.error(e);
+			return null;
+		}
+	}
+	
+	public Object getMBeanAttribute(ObjectName name, String attribute) {
+		 try {
+			return mbeanServer.getAttribute(name, attribute);
+		} catch (Exception e) {
+			logger.error(e);
+			return null;
+		}
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index e257bc5..8810352 100644
--- a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -341,10 +341,14 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
 	 * @throws IOException
 	 */
 	protected void writeJSON(HttpServletResponse resp, Object obj) throws IOException {
-		resp.setContentType(JSON_MIME_TYPE);
-		JSONUtils.toJSON(obj, resp.getOutputStream());
+		writeJSON(resp, obj, false);
 	}
 
+	protected void writeJSON(HttpServletResponse resp, Object obj, boolean pretty) throws IOException {
+		resp.setContentType(JSON_MIME_TYPE);
+		JSONUtils.toJSON(obj, resp.getOutputStream(), true);
+	}
+	
 	/**
 	 * Retrieve the Azkaban application
 	 * 
diff --git a/src/java/azkaban/webapp/servlet/JMXHttpServlet.java b/src/java/azkaban/webapp/servlet/JMXHttpServlet.java
new file mode 100644
index 0000000..6bd1e94
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/JMXHttpServlet.java
@@ -0,0 +1,143 @@
+package azkaban.webapp.servlet;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+
+import javax.management.MBeanInfo;
+import javax.management.ObjectName;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import azkaban.user.Permission;
+import azkaban.user.Role;
+import azkaban.user.User;
+import azkaban.user.UserManager;
+import azkaban.webapp.AzkabanWebServer;
+import azkaban.webapp.session.Session;
+
+/**
+ * Limited set of jmx calls for when you cannot attach to the jvm
+ */
+public class JMXHttpServlet extends LoginAbstractAzkabanServlet {
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private static final Logger logger = Logger.getLogger(JMXHttpServlet.class.getName());
+
+	private UserManager userManager;
+	private AzkabanWebServer server;
+	
+	private static final String GET_MBEANS = "getMBeans";
+	private static final String GET_MBEAN_INFO = "getMBeanInfo";
+	private static final String GET_MBEAN_ATTRIBUTE = "getAttribute";
+	private static final String ATTRIBUTE = "attribute";
+	private static final String MBEAN = "mBean";
+	
+	@Override
+	public void init(ServletConfig config) throws ServletException {
+		super.init(config);
+		
+		server = (AzkabanWebServer)getApplication();
+		userManager = server.getUserManager();
+	}
+	
+	@Override
+	protected void handleGet(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+		if (hasParam(req, "ajax")){
+			HashMap<String,Object> ret = new HashMap<String,Object>();
+
+			if(!hasAdminRole(session.getUser())) {
+				ret.put("error", "User " + session.getUser().getUserId() + " has no permission.");
+			}
+			String ajax = getParam(req, "ajax");
+			if (GET_MBEANS.equals(ajax)) {
+				ret.put("mbeans", server.getMbeanNames());
+			}
+			else if (GET_MBEAN_INFO.equals(ajax)) {
+				if (hasParam(req, MBEAN)) {
+					String mbeanName = getParam(req, MBEAN);
+					try {
+						ObjectName name = new ObjectName(mbeanName);
+						MBeanInfo info = server.getMBeanInfo(name);
+						ret.put("attributes", info.getAttributes());
+						ret.put("description", info.getDescription());
+					} catch (Exception e) {
+						logger.error(e);
+						ret.put("error", "'" + mbeanName + "' is not a valid mBean name");
+					}
+				}
+				else {
+					ret.put("error", "No 'mbean' name parameter specified" );
+				}
+			}
+			else if (GET_MBEAN_ATTRIBUTE.equals(ajax)) {
+				if (!hasParam(req, MBEAN) || !hasParam(req, ATTRIBUTE)) {
+					ret.put("error", "Parameters 'mbean' and 'attribute' must be set");
+				}
+				else {
+					String mbeanName = getParam(req, MBEAN);
+					String attribute = getParam(req, ATTRIBUTE);
+					
+					try {
+						ObjectName name = new ObjectName(mbeanName);
+						Object obj = server.getMBeanAttribute(name, attribute);
+						ret.put("value", obj);
+					} catch (Exception e) {
+						logger.error(e);
+						ret.put("error", "'" + mbeanName + "' is not a valid mBean name");
+					}
+				}
+			}
+			else {
+				ret.put("commands", new String[] {
+						GET_MBEANS, 
+						GET_MBEAN_INFO+"&"+MBEAN+"=<name>", 
+						GET_MBEAN_ATTRIBUTE+"&"+MBEAN+"=<name>&"+ATTRIBUTE+"=<attributename>"}
+				);
+			}
+			this.writeJSON(resp, ret, true);
+		}
+		else {
+			handleJMXPage(req, resp, session);
+		}
+	}
+
+	private void handleJMXPage(HttpServletRequest req, HttpServletResponse resp, Session session) {
+		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/jmxpage.vm");
+		
+		if(!hasAdminRole(session.getUser())) {
+			page.add("errorMsg", "User " + session.getUser().getUserId() + " has no permission.");
+			page.render();
+			return;
+		}
+
+		page.add("mbeans", server.getMbeanNames());
+
+		page.render();
+	}
+	
+	@Override
+	protected void handlePost(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+
+	}
+	
+	private boolean hasAdminRole(User user) {
+		for(String roleName: user.getRoles()) {
+			Role role = userManager.getRole(roleName);
+			Permission perm = role.getPermission();
+			if (perm.isPermissionSet(Permission.Type.ADMIN)) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+}
diff --git a/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm b/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm
new file mode 100644
index 0000000..64fcc9c
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/velocity/jmxpage.vm
@@ -0,0 +1,86 @@
+#*
+ * Copyright 2012 LinkedIn, Inc
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+*#
+
+<!DOCTYPE html> 
+<html>
+	<head>
+#parse( "azkaban/webapp/servlet/velocity/style.vm" )
+		<script type="text/javascript" src="${context}/js/jquery/jquery-1.9.1.js"></script>
+		<script type="text/javascript" src="${context}/js/namespace.js"></script>
+		<script type="text/javascript" src="${context}/js/underscore-1.4.4-min.js"></script>
+		<script type="text/javascript" src="${context}/js/backbone-0.9.10-min.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
+		<script type="text/javascript">
+			var contextURL = "${context}";
+			var currentTime = ${currentTime};
+			var timezone = "${timezone}";
+			var errorMessage = null;
+			var successMessage = null;
+		</script>
+	</head>
+	<body>
+#set($current_page="all")
+#parse( "azkaban/webapp/servlet/velocity/nav.vm" )
+#if($errorMsg)
+<div class="box-error-message"><pre>$errorMsg</pre></div>
+#end
+
+		<div class="content">
+			<div id="all-jobs-content">
+				<div class="section-hd flow-header">
+					<h2><a href="${context}/jmx">Admin JMX Http Page</span></a></h2>
+				</div>
+			</div>
+			
+			<div id="flow-tabs">
+				<table id="all-jobs" class="all-jobs job-table">
+					<thead>
+						<tr>
+							<th>Name</th>
+							<th>Domain</th>
+							<th>Canonical Name</th>
+							<th></th>
+						</tr>
+					</thead>
+					<tbody>
+#foreach($bean in $mbeans)
+					<tr>
+						<td>${bean.keyPropertyList.get("name")}</td>
+						<td>${bean.domain}</td>
+						<td>${bean.canonicalName}</td>
+						<td><div class="btn4 querybtn" id="${bean.canonicalName}">Query</div></td>
+					</tr>
+					<tr class="childrow" id="${bean.canonicalName}-child" style="display: none;">
+						<td class="expandedFlow">
+							<table class="innerTable">
+								<thead>
+									<tr>
+										<th class="tb-name">Attribute Name</th>
+										<th>Value</th>
+									</tr>
+								</thead>
+								<tbody id="${bean.canonicalName}-tbody">
+								</tbody>
+							</table>
+						</td>
+					</tr>
+#end
+					</tbody>
+				</table>
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/src/web/js/azkaban.jmx.view.js b/src/web/js/azkaban.jmx.view.js
new file mode 100644
index 0000000..5c9193a
--- /dev/null
+++ b/src/web/js/azkaban.jmx.view.js
@@ -0,0 +1,189 @@
+$.namespace('azkaban');
+
+var projectTableView;
+azkaban.ProjectTableView= Backbone.View.extend({
+  events : {
+    "click .project-expand": "expandProject"
+  },
+  initialize : function(settings) {
+
+  },
+  expandProject : function(evt) {
+    if (evt.target.tagName!="SPAN") {
+    	return;
+    }
+    
+    var target = evt.currentTarget;
+    var targetId = target.id;
+    var requestURL = contextURL + "/manager";
+    
+    var targetExpanded = $('#' + targetId + '-child');
+    var targetTBody = $('#' + targetId + '-tbody');
+    
+    var createFlowListFunction = this.createFlowListTable;
+    
+    if (target.loading) {
+    	console.log("Still loading.");
+    }
+    else if (target.loaded) {
+    	if($(targetExpanded).is(':visible')) {
+    		$(target).addClass('expand').removeClass('collapse');
+    		$(targetExpanded).fadeOut("fast");
+    	}
+    	else {
+    	    $(target).addClass('collapse').removeClass('expand');
+    		$(targetExpanded).fadeIn();
+    	}
+    }
+    else {
+	    // projectId is available
+	    $(target).addClass('wait').removeClass('collapse').removeClass('expand');
+	    target.loading = true;
+	    
+	    $.get(
+	      requestURL,
+	      {"project": targetId, "ajax":"fetchprojectflows"},
+	      function(data) {
+	        console.log("Success");
+	        target.loaded = true;
+	        target.loading = false;
+	        
+	        createFlowListFunction(data, targetTBody);
+	        
+			$(target).addClass('collapse').removeClass('wait');
+	    	$(targetExpanded).fadeIn("fast");
+	      },
+	      "json"
+	    );
+    }
+  },
+  render: function() {
+  },
+  createFlowListTable : function(data, innerTable) {
+  	var flows = data.flows;
+  	flows.sort(function(a,b){return a.flowId.localeCompare(b.flowId);});
+  	
+  	var requestURL = contextURL + "/manager?project=" + data.project + "&flow=";
+  	for (var i = 0; i < flows.length; ++i) {
+  		var id = flows[i].flowId;
+  		
+  		var tr = document.createElement("tr");
+		var idtd = document.createElement("td");
+		$(idtd).addClass("tb-name");
+  		
+  		var ida = document.createElement("a");
+		ida.project = data.project;
+		$(ida).text(id);
+		$(ida).attr("href", requestURL + id);
+		
+		$(idtd).append(ida);
+		$(tr).append(idtd);
+		$(innerTable).append(tr);
+  	}
+  }
+});
+
+var projectHeaderView;
+azkaban.ProjectHeaderView= Backbone.View.extend({
+  events : {
+    "click #create-project-btn":"handleCreateProjectJob"
+  },
+  initialize : function(settings) {
+    if (settings.errorMsg && settings.errorMsg != "null") {
+      // Chrome bug in displaying placeholder text. Need to hide the box.
+      $('#searchtextbox').hide();
+      $('.messaging').addClass("error");
+      $('.messaging').removeClass("success");
+      $('.messaging').html(settings.errorMsg);
+    }
+    else if (settings.successMsg && settings.successMsg != "null") {
+      $('#searchtextbox').hide();
+      $('.messaging').addClass("success");
+      $('.messaging').removeClass("error");
+      $('#message').html(settings.successMsg);
+    }
+    else {
+      $('#searchtextbox').show();
+      $('.messaging').removeClass("success");
+      $('.messaging').removeClass("error");
+    }
+    
+    $('#messageClose').click(function() {
+      $('#searchtextbox').show();
+      
+      $('.messaging').slideUp('fast', function() {
+        $('.messaging').removeClass("success");
+        $('.messaging').removeClass("error");
+      });
+    });
+  },
+  handleCreateProjectJob : function(evt) {
+    console.log("click create project");
+      $('#create-project').modal({
+          closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+          position: ["20%",],
+          containerId: 'confirm-container',
+          containerCss: {
+            'height': '220px',
+            'width': '565px'
+          },
+          onShow: function (dialog) {
+            var modal = this;
+            $("#errorMsg").hide();
+          }
+        });
+  },
+  render: function() {
+  }
+});
+
+var createProjectView;
+azkaban.CreateProjectView= Backbone.View.extend({
+  events : {
+    "click #create-btn": "handleCreateProject"
+  },
+  initialize : function(settings) {
+    $("#errorMsg").hide();
+  },
+  handleCreateProject : function(evt) {
+	 // First make sure we can upload
+	 var projectName = $('#path').val();
+	 var description = $('#description').val();
+
+     console.log("Creating");
+     $.ajax({
+     	async: "false",
+     	url: "manager",
+     	dataType: "json",
+     	type: "POST",
+     	data: {action:"create", name:projectName, description:description},
+     	success: function(data) {
+     		if (data.status == "success") {
+     			if (data.action == "redirect") {
+     				window.location = data.path;
+     			}
+     		}
+     		else {
+     			if (data.action == "login") {
+ 					window.location = "";
+     			}
+     			else {
+	     			$("#errorMsg").text("ERROR: " + data.message);
+	    			$("#errorMsg").slideDown("fast");
+    			}
+     		}
+     	}
+     });
+
+  },
+  render: function() {
+  }
+});
+
+var tableSorterView;
+$(function() {
+	projectHeaderView = new azkaban.ProjectHeaderView({el:$( '#all-jobs-content'), successMsg: successMessage, errorMsg: errorMessage });
+	projectTableView = new azkaban.ProjectTableView({el:$('#all-jobs')});
+	tableSorterView = new azkaban.TableSorter({el:$('#all-jobs'), initialSort: $('.tb-name')});
+	uploadView = new azkaban.CreateProjectView({el:$('#create-project')});
+});