http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/controllers.js ---------------------------------------------------------------------- diff --git a/src/webui/app/controllers.js b/src/webui/app/controllers.js new file mode 100644 index 0000000..7c228a1 --- /dev/null +++ b/src/webui/app/controllers.js @@ -0,0 +1,1179 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +(function() { + 'use strict'; + + var mesosApp = angular.module('mesos'); + + function hasSelectedText() { + if (window.getSelection) { // All browsers except IE before version 9. + var range = window.getSelection(); + return range.toString().length > 0; + } + return false; + } + + // Returns the URL prefix for an agent, there are two cases + // to consider: + // + // (1) Some endpoints for the agent process itself require + // the agent PID.name (processId) in the path, this is + // to ensure that the webui works correctly when running + // against mesos-local or other instances of multiple agents + // running within the same "host:port": + // + // //hostname:port/slave(1) + // //hostname:port/slave(2) + // ... + // + // (2) Some endpoints for other components in the agent + // do not require the agent PID.name in the path, since + // a single endpoint serves multiple agents within the + // same process. In this case we just return: + // + // //hostname:port + // + // Note that there are some clashing issues in mesos-local + // (e.g., hosting '/slave/log' for each agent log, we don't + // namespace metrics within '/metrics/snapshot', etc). + function agentURLPrefix(agent, includeProcessId) { + var port = agent.pid.substring(agent.pid.lastIndexOf(':') + 1); + var processId = agent.pid.substring(0, agent.pid.indexOf('@')); + + var url = '//' + agent.hostname + ':' + port; + + if (includeProcessId) { + url += '/' + processId; + } + + return url; + } + + // Invokes the pailer, building the endpoint URL with the specified urlPrefix + // and path. + function pailer(urlPrefix, path, window_title) { + var url = urlPrefix + '/files/read?path=' + path; + + // The randomized `storageKey` is removed from `localStorage` once the + // pailer window loads the URL into its `sessionStorage`, therefore + // the probability of collisions is low and we do not use a uuid. + var storageKey = Math.random().toString(36).substr(2, 8); + + // Store the target URL in `localStorage` which is + // accessed by the pailer window when opened. + localStorage.setItem(storageKey, url); + + var pailer = + window.open('app/shared/pailer.html', storageKey, 'width=580px, height=700px'); + + // Need to use window.onload instead of document.ready to make + // sure the title doesn't get overwritten. + pailer.onload = function() { + pailer.document.title = window_title; + }; + } + + + function updateInterval(num_agents) { + // TODO(bmahler): Increasing the update interval for large clusters + // is done purely to mitigate webui performance issues. Ideally we can + // keep a consistently fast rate for updating statistical information. + // For the full system state updates, it may make sense to break + // it up using pagination and/or splitting the endpoint. + if (num_agents < 500) { + return 10000; + } else if (num_agents < 1000) { + return 20000; + } else if (num_agents < 5000) { + return 60000; + } else if (num_agents < 10000) { + return 120000; + } else if (num_agents < 15000) { + return 240000; + } else if (num_agents < 20000) { + return 480000; + } else { + return 960000; + } + } + + // Set the task sandbox directory for use by the WebUI. + function setTaskSandbox(executor) { + _.each( + [executor.tasks, executor.queued_tasks, executor.completed_tasks], + function(tasks) { + _.each(tasks, function(task) { + if (executor.type === 'DEFAULT') { + task.directory = executor.directory + '/tasks/' + task.id; + } else { + task.directory = executor.directory; + } + }); + }); + } + + + // Update the outermost scope with the new state. + function updateState($scope, $timeout, state) { + // Don't do anything if the state hasn't changed. + if ($scope.state == state) { + return true; // Continue polling. + } + + $scope.state = state; + + // A cluster is named if the state returns a non-empty string name. + // Track whether this cluster is named in a Boolean for display purposes. + $scope.clusterNamed = !!$scope.state.cluster; + + // Check for selected text, and allow up to 20 seconds to pass before + // potentially wiping the user highlighted text. + // TODO(bmahler): This is to avoid the annoying loss of highlighting when + // the tables update. Once we can have tighter granularity control on the + // angular.js dynamic table updates, we should remove this hack. + $scope.time_since_update += $scope.delay; + + if (hasSelectedText() && $scope.time_since_update < 20000) { + return true; + } + + // Pass this pollTime to all relativeDate calls to make them all relative to + // the same moment in time. + // + // If relativeDate is called without a reference time, it instantiates a new + // Date to be the reference. Since there can be hundreds of dates on a given + // page, they would all be relative to slightly different moments in time. + $scope.pollTime = new Date(); + + // Update the maps. + $scope.agents = {}; + $scope.frameworks = {}; + $scope.offers = {}; + $scope.completed_frameworks = {}; + $scope.active_tasks = []; + $scope.unreachable_tasks = []; + $scope.completed_tasks = []; + + // Update the stats. + $scope.cluster = $scope.state.cluster; + $scope.total_cpus = 0; + $scope.total_gpus = 0; + $scope.total_mem = 0; + $scope.total_disk = 0; + $scope.used_cpus = 0; + $scope.used_gpus = 0; + $scope.used_mem = 0; + $scope.used_disk = 0; + $scope.offered_cpus = 0; + $scope.offered_gpus = 0; + $scope.offered_mem = 0; + $scope.offered_disk = 0; + + $scope.activated_agents = $scope.state.activated_slaves; + $scope.deactivated_agents = $scope.state.deactivated_slaves; + $scope.unreachable_agents = $scope.state.unreachable_slaves; + + _.each($scope.state.slaves, function(agent) { + $scope.agents[agent.id] = agent; + $scope.total_cpus += agent.resources.cpus; + $scope.total_gpus += agent.resources.gpus; + $scope.total_mem += agent.resources.mem; + $scope.total_disk += agent.resources.disk; + }); + + var setTaskMetadata = function(task) { + if (!task.executor_id) { + task.executor_id = task.id; + } + if (task.statuses.length > 0) { + var firstStatus = task.statuses[0]; + if (!isStateTerminal(firstStatus.state)) { + task.start_time = firstStatus.timestamp * 1000; + } + var lastStatus = task.statuses[task.statuses.length - 1]; + if (isStateTerminal(task.state)) { + task.finish_time = lastStatus.timestamp * 1000; + } + task.healthy = lastStatus.healthy; + } + }; + + var isStateTerminal = function(taskState) { + var terminalStates = [ + 'TASK_ERROR', + 'TASK_FAILED', + 'TASK_FINISHED', + 'TASK_KILLED', + 'TASK_LOST', + 'TASK_DROPPED', + 'TASK_GONE', + 'TASK_GONE_BY_OPERATOR' + ]; + return terminalStates.indexOf(taskState) > -1; + }; + + _.each($scope.state.frameworks, function(framework) { + $scope.frameworks[framework.id] = framework; + + // Fill in the `roles` field for non-MULTI_ROLE schedulers. + if (framework.role) { + framework.roles = [framework.role]; + } + + _.each(framework.offers, function(offer) { + $scope.offers[offer.id] = offer; + $scope.offered_cpus += offer.resources.cpus; + $scope.offered_gpus += offer.resources.gpus; + $scope.offered_mem += offer.resources.mem; + $scope.offered_disk += offer.resources.disk; + offer.framework_name = $scope.frameworks[offer.framework_id].name; + offer.hostname = $scope.agents[offer.slave_id].hostname; + }); + + $scope.used_cpus += framework.resources.cpus; + $scope.used_gpus += framework.resources.gpus; + $scope.used_mem += framework.resources.mem; + $scope.used_disk += framework.resources.disk; + + framework.cpus_share = 0; + if ($scope.total_cpus > 0) { + framework.cpus_share = framework.used_resources.cpus / $scope.total_cpus; + } + + framework.gpus_share = 0; + if ($scope.total_gpus > 0) { + framework.gpus_share = framework.used_resources.gpus / $scope.total_gpus; + } + + framework.mem_share = 0; + if ($scope.total_mem > 0) { + framework.mem_share = framework.used_resources.mem / $scope.total_mem; + } + + framework.disk_share = 0; + if ($scope.total_disk > 0) { + framework.disk_share = framework.used_resources.disk / $scope.total_disk; + } + + framework.max_share = Math.max( + framework.cpus_share, + framework.gpus_share, + framework.mem_share, + framework.disk_share); + + // If the executor ID is empty, this is a command executor with an + // internal executor ID generated from the task ID. + // TODO(brenden): Remove this once + // https://issues.apache.org/jira/browse/MESOS-527 is fixed. + _.each(framework.tasks, setTaskMetadata); + _.each(framework.unreachable_tasks, setTaskMetadata); + _.each(framework.completed_tasks, setTaskMetadata); + + // TODO(bmahler): Add per-framework metrics to the master so that + // the webui does not need to loop over all tasks! + framework.running_tasks = 0; + framework.staging_tasks = 0; + framework.starting_tasks = 0; + framework.killing_tasks = 0; + + _.each(framework.tasks, function(task) { + switch (task.state) { + case "TASK_STAGING": framework.staging_tasks += 1; break; + case "TASK_STARTING": framework.starting_tasks += 1; break; + case "TASK_RUNNING": framework.running_tasks += 1; break; + case "TASK_KILLING": framework.killing_tasks += 1; break; + default: break; + } + }) + + framework.finished_tasks = 0; + framework.killed_tasks = 0; + framework.failed_tasks = 0; + framework.lost_tasks = 0; + + _.each(framework.completed_tasks, function(task) { + switch (task.state) { + case "TASK_FINISHED": framework.finished_tasks += 1; break; + case "TASK_KILLED": framework.killed_tasks += 1; break; + case "TASK_FAILED": framework.failed_tasks += 1; break; + case "TASK_LOST": framework.lost_tasks += 1; break; + default: break; + } + }) + + $scope.active_tasks = $scope.active_tasks.concat(framework.tasks); + $scope.unreachable_tasks = $scope.unreachable_tasks.concat(framework.unreachable_tasks); + $scope.completed_tasks = + $scope.completed_tasks.concat(framework.completed_tasks); + }); + + _.each($scope.state.completed_frameworks, function(framework) { + $scope.completed_frameworks[framework.id] = framework; + + // Fill in the `roles` field for non-MULTI_ROLE schedulers. + if (framework.role) { + framework.roles = [framework.role]; + } + + _.each(framework.completed_tasks, setTaskMetadata); + }); + + $scope.used_cpus -= $scope.offered_cpus; + $scope.used_gpus -= $scope.offered_gpus; + $scope.used_mem -= $scope.offered_mem; + $scope.used_disk -= $scope.offered_disk; + + $scope.idle_cpus = $scope.total_cpus - ($scope.offered_cpus + $scope.used_cpus); + $scope.idle_gpus = $scope.total_gpus - ($scope.offered_gpus + $scope.used_gpus); + $scope.idle_mem = $scope.total_mem - ($scope.offered_mem + $scope.used_mem); + $scope.idle_disk = $scope.total_disk - ($scope.offered_disk + $scope.used_disk); + + $scope.time_since_update = 0; + $scope.$broadcast('state_updated'); + + return true; // Continue polling. + } + + // Update the outermost scope with the metrics/snapshot endpoint. + function updateMetrics($scope, $timeout, metrics) { + $scope.staging_tasks = metrics['master/tasks_staging']; + $scope.starting_tasks = metrics['master/tasks_starting']; + $scope.running_tasks = metrics['master/tasks_running']; + $scope.killing_tasks = metrics['master/tasks_killing']; + $scope.finished_tasks = metrics['master/tasks_finished']; + $scope.killed_tasks = metrics['master/tasks_killed']; + $scope.failed_tasks = metrics['master/tasks_failed']; + $scope.lost_tasks = metrics['master/tasks_lost']; + + return true; // Continue polling. + } + + + // Main controller that can be used to handle "global" events. E.g.,: + // $scope.$on('$afterRouteChange', function() { ...; }); + // + // In addition, the MainCtrl encapsulates the "view", allowing the + // active controller/view to easily access anything in scope (e.g., + // the state). + mesosApp.controller('MainCtrl', [ + '$scope', '$http', '$location', '$timeout', '$modal', + function($scope, $http, $location, $timeout, $modal) { + $scope.doneLoading = true; + + // Adding bindings into scope so that they can be used from within + // AngularJS expressions. + $scope._ = _; + $scope.stringify = JSON.stringify; + $scope.encodeURIComponent = encodeURIComponent; + $scope.basename = function(path) { + // This is only a basic version of basename that handles the cases we care + // about, rather than duplicating unix basename functionality perfectly. + if (path === '/') { + return path; // Handle '/'. + } + + // Strip a trailing '/' if present. + if (path.length > 0 && path.lastIndexOf('/') === (path.length - 1)) { + path = path.substr(0, path.length - 1); + } + return path.substr(path.lastIndexOf('/') + 1); + }; + + $scope.$location = $location; + $scope.delay = 2000; + $scope.retry = 0; + $scope.time_since_update = 0; + $scope.isErrorModalOpen = false; + + // Ordered Array of path => activeTab mappings. On successful route changes, + // the `pathRegexp` values are matched against the current route. The first + // match will be used to set the active navbar tab. + var NAVBAR_PATHS = [ + { + pathRegexp: /^\/agents/, + tab: 'agents' + }, + { + pathRegexp: /^\/frameworks/, + tab: 'frameworks' + }, + { + pathRegexp: /^\/roles/, + tab: 'roles' + }, + { + pathRegexp: /^\/offers/, + tab: 'offers' + }, + { + pathRegexp: /^\/maintenance/, + tab: 'maintenance' + } + ]; + + // Set the active tab on route changes according to NAVBAR_PATHS. + $scope.$on('$routeChangeSuccess', function(event, current) { + var path = current.$$route.originalPath; + + // Use _.some so the loop can exit on the first `pathRegexp` match. + var matched = _.some(NAVBAR_PATHS, function(nav) { + if (path.match(nav.pathRegexp)) { + $scope.navbarActiveTab = nav.tab; + return true; + } + }); + + if (!matched) $scope.navbarActiveTab = null; + }); + + var leadingMasterURL = function(path) { + // Use current location as address in case we could not find the + // leading master. + var address = location.hostname + ':' + location.port; + if ($scope.state && $scope.state.leader_info) { + address = $scope.state.leader_info.hostname + ':' + + $scope.state.leader_info.port; + } + + return '//' + address + path; + } + + var popupErrorModal = function() { + if ($scope.delay >= 128000) { + $scope.delay = 2000; + } else { + $scope.delay = $scope.delay * 2; + } + + $scope.isErrorModalOpen = true; + + var errorModal = $modal.open({ + controller: function($scope, $modalInstance, scope) { + // Give the modal reference to the root scope so it can access the + // `retry` variable. It needs to be passed by reference, not by + // value, since its value is changed outside the scope of the + // modal. + $scope.rootScope = scope; + }, + resolve: { + scope: function() { return $scope; } + }, + templateUrl: "template/dialog/master-gone.html" + }); + + // Make it such that everytime we hide the error-modal, we stop the + // countdown and restart the polling. + errorModal.result.then(function() { + $scope.isErrorModalOpen = false; + + if ($scope.countdown != null) { + if ($timeout.cancel($scope.countdown)) { + // Restart since they cancelled the countdown. + $scope.delay = 2000; + } + } + + // Start polling again, but do it asynchronously (and wait at + // least a second because otherwise the error-modal won't get + // properly shown). + $timeout(pollState, 1000); + $timeout(pollMetrics, 1000); + }); + + $scope.retry = $scope.delay; + var countdown = function() { + if ($scope.retry === 0) { + errorModal.close(); + } else { + $scope.retry = $scope.retry - 1000; + $scope.countdown = $timeout(countdown, 1000); + } + }; + countdown(); + }; + + var pollState = function() { + // When the current master is not the leader, the request is redirected to + // the leading master automatically. This would cause a CORS error if we + // use XMLHttpRequest here. To avoid the CORS error, we use JSONP as a + // workaround. Please refer to MESOS-5911 for further details. + $http.jsonp(leadingMasterURL('/master/state?jsonp=JSON_CALLBACK')) + .success(function(response) { + if (updateState($scope, $timeout, response)) { + $scope.delay = updateInterval(_.size($scope.agents)); + $timeout(pollState, $scope.delay); + } + }) + .error(function() { + if ($scope.isErrorModalOpen === false) { + popupErrorModal(); + } + }); + }; + + var pollMetrics = function() { + $http.jsonp(leadingMasterURL('/metrics/snapshot?jsonp=JSON_CALLBACK')) + .success(function(response) { + if (updateMetrics($scope, $timeout, response)) { + $scope.delay = updateInterval(_.size($scope.agents)); + $timeout(pollMetrics, $scope.delay); + } + }) + .error(function(message, code) { + if ($scope.isErrorModalOpen === false) { + // If return code is 401 or 403 the user is unauthorized to reach + // the endpoint, which is not a connection error. + if ([401, 403].indexOf(code) < 0) { + popupErrorModal(); + } + } + }); + }; + + pollState(); + pollMetrics(); + }]); + + mesosApp.controller('HomeCtrl', function($scope, $http) { + var hostname = $scope.$location.host() + ':' + $scope.$location.port(); + + var update = function() { + $scope.streamLogs = function(_$event) { + pailer( + '//' + hostname, + '/master/log', + 'Mesos Master (' + hostname + ')'); + }; + + // We must query the '/flags' endpoint of *this* master since + // `$scope.state` contains the leader master state. + $http.jsonp('//' + hostname + '/flags?jsonp=JSON_CALLBACK') + .success(function (response) { + // The master attaches a "/master/log" file when either + // of these flags are set. + $scope.log_file_attached = response.flags.external_log_file || response.flags.log_dir; + }) + .error(function(reason) { + $scope.alert_message = 'Failed to get master flags: ' + reason; + $('#alert').show(); + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }); + + mesosApp.controller('FrameworksCtrl', function() {}); + + mesosApp.controller('RolesCtrl', function($scope, $http) { + var update = function() { + // TODO(haosdent): Send requests to the leading master directly + // once `leadingMasterURL` is public. + $http.jsonp('master/roles?jsonp=JSON_CALLBACK') + .success(function(response) { + $scope.roles = response; + }) + .error(function() { + if ($scope.isErrorModalOpen === false) { + popupErrorModal(); + } + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }); + + mesosApp.controller('OffersCtrl', function() {}); + + mesosApp.controller('MaintenanceCtrl', function($scope, $http) { + var update = function() { + // TODO(haosdent): Send requests to the leading master directly + // once `leadingMasterURL` is public. + $http.jsonp('master/maintenance/schedule?jsonp=JSON_CALLBACK') + .success(function(response) { + $scope.maintenance = response; + }) + .error(function() { + if ($scope.isErrorModalOpen === false) { + popupErrorModal(); + } + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }); + + mesosApp.controller('FrameworkCtrl', function($scope, $routeParams) { + var update = function() { + if ($routeParams.id in $scope.completed_frameworks) { + $scope.framework = $scope.completed_frameworks[$routeParams.id]; + $scope.alert_message = 'This framework has terminated!'; + $('#alert').show(); + $('#framework').show(); + } else if ($routeParams.id in $scope.frameworks) { + $scope.framework = $scope.frameworks[$routeParams.id]; + $('#framework').show(); + } else { + $scope.alert_message = 'No framework found with ID: ' + $routeParams.id; + $('#alert').show(); + } + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }); + + + mesosApp.controller('AgentsCtrl', function() {}); + + + mesosApp.controller('AgentCtrl', [ + '$scope', '$routeParams', '$http', '$q', '$timeout', 'top', + function($scope, $routeParams, $http, $q, $timeout, $top) { + $scope.agent_id = $routeParams.agent_id; + + var update = function() { + if (!($routeParams.agent_id in $scope.agents)) { + $scope.alert_message = 'No agent found with ID: ' + $routeParams.agent_id; + $('#alert').show(); + return; + } + + var agent = $scope.agents[$routeParams.agent_id]; + + $scope.streamLogs = function(_$event) { + pailer(agentURLPrefix(agent, false), '/slave/log', 'Mesos Agent (' + agent.id + ')'); + }; + + + // Set up polling for the monitor if this is the first update. + if (!$top.started()) { + $top.start( + agentURLPrefix(agent, true) + '/monitor/statistics?jsonp=JSON_CALLBACK', + $scope + ); + } + + $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK') + .success(function (response) { + $scope.state = response; + + $scope.agent = {}; + $scope.agent.frameworks = {}; + $scope.agent.completed_frameworks = {}; + $scope.agent.url_prefix = agentURLPrefix(agent, false); + + // The agent attaches a "/slave/log" file when either + // of these flags are set. + $scope.agent.log_file_attached = $scope.state.external_log_file || $scope.state.log_dir; + + // Convert the reserved resources map into an array for inclusion + // in an `ng-repeat` table. + $scope.agent.reserved_resources_as_array = _($scope.state.reserved_resources) + .map(function(reservation, role) { + reservation.role = role; + return reservation; + }); + + // Computes framework stats by setting new attributes on the 'framework' + // object. + function computeFrameworkStats(framework) { + framework.num_tasks = 0; + framework.cpus = 0; + framework.gpus = 0; + framework.mem = 0; + framework.disk = 0; + + _.each(framework.executors, function(executor) { + framework.num_tasks += _.size(executor.tasks); + framework.cpus += executor.resources.cpus; + framework.gpus += executor.resources.gpus; + framework.mem += executor.resources.mem; + framework.disk += executor.resources.disk; + }); + } + + // Compute framework stats and update agent's mappings of those + // frameworks. + _.each($scope.state.frameworks, function(framework) { + $scope.agent.frameworks[framework.id] = framework; + computeFrameworkStats(framework); + + // Fill in the `roles` field for non-MULTI_ROLE schedulers. + if (framework.role) { + framework.roles = [framework.role]; + } + }); + + _.each($scope.state.completed_frameworks, function(framework) { + $scope.agent.completed_frameworks[framework.id] = framework; + computeFrameworkStats(framework); + + // Fill in the `roles` field for non-MULTI_ROLE schedulers. + if (framework.role) { + framework.roles = [framework.role]; + } + }); + + $scope.state.allocated_resources = {}; + $scope.state.allocated_resources.cpus = 0; + $scope.state.allocated_resources.gpus = 0; + $scope.state.allocated_resources.mem = 0; + $scope.state.allocated_resources.disk = 0; + + // Currently the agent does not expose the total allocated + // resources across all frameworks, so we sum manually. + _.each($scope.state.frameworks, function(framework) { + $scope.state.allocated_resources.cpus += framework.cpus; + $scope.state.allocated_resources.gpus += framework.gpus; + $scope.state.allocated_resources.mem += framework.mem; + $scope.state.allocated_resources.disk += framework.disk; + }); + + $('#agent').show(); + }) + .error(function(reason) { + $scope.alert_message = 'Failed to get agent usage / state: ' + reason; + $('#alert').show(); + }); + + $http.jsonp(agentURLPrefix(agent, false) + '/metrics/snapshot?jsonp=JSON_CALLBACK') + .success(function (response) { + $scope.staging_tasks = response['slave/tasks_staging']; + $scope.starting_tasks = response['slave/tasks_starting']; + $scope.running_tasks = response['slave/tasks_running']; + $scope.killing_tasks = response['slave/tasks_killing']; + $scope.finished_tasks = response['slave/tasks_finished']; + $scope.killed_tasks = response['slave/tasks_killed']; + $scope.failed_tasks = response['slave/tasks_failed']; + $scope.lost_tasks = response['slave/tasks_lost']; + }) + .error(function(reason) { + $scope.alert_message = 'Failed to get agent metrics: ' + reason; + $('#alert').show(); + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }]); + + + mesosApp.controller('AgentFrameworkCtrl', [ + '$scope', '$routeParams', '$http', '$q', '$timeout', 'top', + function($scope, $routeParams, $http, $q, $timeout, $top) { + $scope.agent_id = $routeParams.agent_id; + $scope.framework_id = $routeParams.framework_id; + + var update = function() { + if (!($routeParams.agent_id in $scope.agents)) { + $scope.alert_message = 'No agent found with ID: ' + $routeParams.agent_id; + $('#alert').show(); + return; + } + + var agent = $scope.agents[$routeParams.agent_id]; + + // Set up polling for the monitor if this is the first update. + if (!$top.started()) { + $top.start( + agentURLPrefix(agent, true) + '/monitor/statistics?jsonp=JSON_CALLBACK', + $scope + ); + } + + $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK') + .success(function (response) { + $scope.state = response; + + $scope.agent = {}; + + function matchFramework(framework) { + return $scope.framework_id === framework.id; + } + + // Find the framework; it's either active or completed. + $scope.framework = + _.find($scope.state.frameworks, matchFramework) || + _.find($scope.state.completed_frameworks, matchFramework); + + if (!$scope.framework) { + $scope.alert_message = 'No framework found with ID: ' + $routeParams.framework_id; + $('#alert').show(); + return; + } + + // Fill in the `roles` field for non-MULTI_ROLE schedulers. + if ($scope.framework.role) { + $scope.framework.roles = [$scope.framework.role]; + } + + // Compute the framework stats. + $scope.framework.num_tasks = 0; + $scope.framework.cpus = 0; + $scope.framework.gpus = 0; + $scope.framework.mem = 0; + $scope.framework.disk = 0; + + _.each($scope.framework.executors, function(executor) { + $scope.framework.num_tasks += _.size(executor.tasks); + $scope.framework.cpus += executor.resources.cpus; + $scope.framework.gpus += executor.resources.gpus; + $scope.framework.mem += executor.resources.mem; + $scope.framework.disk += executor.resources.disk; + + // If 'role' is not present in executor, we are talking + // to a non-MULTI_ROLE capable agent. This means that we + // can use the 'role' of the framework. + executor.role = executor.role || $scope.framework.role; + }); + + $('#agent').show(); + }) + .error(function (reason) { + $scope.alert_message = 'Failed to get agent usage / state: ' + reason; + $('#alert').show(); + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }]); + + + mesosApp.controller('AgentExecutorCtrl', [ + '$scope', '$routeParams', '$http', '$q', '$timeout', 'top', + function($scope, $routeParams, $http, $q, $timeout, $top) { + $scope.agent_id = $routeParams.agent_id; + $scope.framework_id = $routeParams.framework_id; + $scope.executor_id = $routeParams.executor_id; + + var update = function() { + if (!($routeParams.agent_id in $scope.agents)) { + $scope.alert_message = 'No agent found with ID: ' + $routeParams.agent_id; + $('#alert').show(); + return; + } + + var agent = $scope.agents[$routeParams.agent_id]; + + // Set up polling for the monitor if this is the first update. + if (!$top.started()) { + $top.start( + agentURLPrefix(agent, true) + '/monitor/statistics?jsonp=JSON_CALLBACK', + $scope + ); + } + + $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK') + .success(function (response) { + $scope.state = response; + + $scope.agent = {}; + + function matchFramework(framework) { + return $scope.framework_id === framework.id; + } + + // Find the framework; it's either active or completed. + $scope.framework = + _.find($scope.state.frameworks, matchFramework) || + _.find($scope.state.completed_frameworks, matchFramework); + + if (!$scope.framework) { + $scope.alert_message = 'No framework found with ID: ' + $routeParams.framework_id; + $('#alert').show(); + return; + } + + function matchExecutor(executor) { + return $scope.executor_id === executor.id; + } + + function setRole(tasks) { + _.each(tasks, function(task) { + task.role = $scope.framework.role; + }); + } + + function setHealth(tasks) { + _.each(tasks, function(task) { + var lastStatus = _.last(task.statuses); + if (lastStatus) { + task.healthy = lastStatus.healthy; + } + }) + } + + // Look for the executor; it's either active or completed. + $scope.executor = + _.find($scope.framework.executors, matchExecutor) || + _.find($scope.framework.completed_executors, matchExecutor); + + if (!$scope.executor) { + $scope.alert_message = 'No executor found with ID: ' + $routeParams.executor_id; + $('#alert').show(); + return; + } + + // If 'role' is not present in the task, we are talking + // to a non-MULTI_ROLE capable agent. This means that we + // can use the 'role' of the framework. + if (!("role" in $scope.executor)) { + $scope.executor.role = $scope.framework.role; + + setRole($scope.executor.tasks); + setRole($scope.executor.queued_tasks); + setRole($scope.executor.completed_tasks); + } + + setHealth($scope.executor.tasks); + setTaskSandbox($scope.executor); + + $('#agent').show(); + }) + .error(function (reason) { + $scope.alert_message = 'Failed to get agent usage / state: ' + reason; + $('#alert').show(); + }); + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }]); + + + // Reroutes requests like: + // * '/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/browse' + // * '/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/tasks/:task_id/browse' + // to the sandbox directory of the executor or the task respectively. This + // requires a second request because the directory to browse is known by the + // agent but not by the master. Request the directory from the agent, and then + // redirect to it. + // + // TODO(ssorallen): Add `executor.directory` to the master's state endpoint + // output so this controller of rerouting is no longer necessary. + mesosApp.controller('AgentTaskAndExecutorRerouterCtrl', + function($alert, $http, $location, $routeParams, $scope, $window) { + + function goBack(flashMessageOrOptions) { + if (flashMessageOrOptions) { + $alert.danger(flashMessageOrOptions); + } + + if ($window.history.length > 1) { + // If the browser has something in its history, just go back. + $window.history.back(); + } else { + // Otherwise navigate to the framework page, which is likely the + // previous page anyway. + $location.path('/frameworks/' + $routeParams.framework_id).replace(); + } + } + + var reroute = function() { + var agent = $scope.agents[$routeParams.agent_id]; + + // If the agent doesn't exist, send the user back. + if (!agent) { + return goBack("Agent with ID '" + $routeParams.agent_id + "' does not exist."); + } + + // Request agent details to get access to the route executor's "directory" + // to navigate directly to the executor's sandbox. + var url = agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK'; + $http.jsonp(url) + .success(function(response) { + + function matchFramework(framework) { + return $routeParams.framework_id === framework.id; + } + + var framework = + _.find(response.frameworks, matchFramework) || + _.find(response.completed_frameworks, matchFramework); + + if (!framework) { + return goBack( + "Framework with ID '" + $routeParams.framework_id + + "' does not exist on agent with ID '" + $routeParams.agent_id + + "'." + ); + } + + function matchExecutor(executor) { + return $routeParams.executor_id === executor.id; + } + + var executor = + _.find(framework.executors, matchExecutor) || + _.find(framework.completed_executors, matchExecutor); + + if (!executor) { + return goBack( + "Executor with ID '" + $routeParams.executor_id + + "' does not exist on agent with ID '" + $routeParams.agent_id + + "'." + ); + } + + var sandboxDirectory = executor.directory; + + function matchTask(task) { + return $routeParams.task_id === task.id; + } + + // Continue to navigate to the task's sandbox if the task id is + // specified in route parameters. + if ($routeParams.task_id) { + setTaskSandbox(executor); + + var task = + _.find(executor.tasks, matchTask) || + _.find(executor.queued_tasks, matchTask) || + _.find(executor.completed_tasks, matchTask); + + if (!task) { + return goBack( + "Task with ID '" + $routeParams.task_id + + "' does not exist on agent with ID '" + $routeParams.agent_id + + "'." + ); + } + + sandboxDirectory = task.directory; + } + + // Navigate to a path like '/agents/:id/browse?path=%2Ftmp%2F', the + // recognized "browse" endpoint for an agent. + $location.path('/agents/' + $routeParams.agent_id + '/browse') + .search({path: sandboxDirectory}) + .replace(); + }) + .error(function(_response) { + $alert.danger({ + bullets: [ + "The agent is not accessible", + "The agent timed out or went offline" + ], + message: "Potential reasons:", + title: "Failed to connect to agent '" + $routeParams.agent_id + + "' on '" + url + "'." + }); + + // Is the agent dead? Navigate home since returning to the agent might + // end up in an endless loop. + $location.path('/').replace(); + }); + }; + + // When navigating directly to this page, e.g. pasting the URL into the + // browser, the previous page is not a page in Mesos. The agents + // information may not ready when loading this page, we start to reroute + // the sandbox request after the agents information loaded. + if ($scope.state) { + reroute(); + } + + // `reroute` is expected to always route away from the current page + // and the listener would be removed after the first state update. + var removeListener = $scope.$on('state_updated', reroute); + $scope.$on('$routeChangeStart', removeListener); + }); + + + mesosApp.controller('BrowseCtrl', function($scope, $routeParams, $http) { + var update = function() { + if ($routeParams.agent_id in $scope.agents && $routeParams.path) { + $scope.agent_id = $routeParams.agent_id; + $scope.path = $routeParams.path; + + var agent = $scope.agents[$routeParams.agent_id]; + + // This variable is used in 'browse.html' to generate the '/files' + // links, so we have to pass `includeProcessId=false` (see + // `agentURLPrefix`for more details). + $scope.agent_url_prefix = agentURLPrefix(agent, false); + + $scope.pail = function($event, path) { + pailer( + agentURLPrefix(agent, false), + path, + decodeURIComponent(path) + ' (' + agent.id + ')'); + }; + + var url = agentURLPrefix(agent, false) + '/files/browse?jsonp=JSON_CALLBACK'; + + // TODO(bmahler): Try to get the error code / body in the error callback. + // This wasn't working with the current version of angular. + $http.jsonp(url, {params: {path: $routeParams.path}}) + .success(function(data) { + $scope.listing = data; + $('#listing').show(); + }) + .error(function() { + $scope.alert_message = 'Error browsing path: ' + $routeParams.path; + $('#alert').show(); + }); + } else { + if (!($routeParams.agent_id in $scope.agents)) { + $scope.alert_message = 'No agent found with ID: ' + $routeParams.agent_id; + } else { + $scope.alert_message = 'Missing "path" request parameter.'; + } + $('#alert').show(); + } + }; + + if ($scope.state) { + update(); + } + + var removeListener = $scope.$on('state_updated', update); + $scope.$on('$routeChangeStart', removeListener); + }); +})();
http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/frameworks/framework.html ---------------------------------------------------------------------- diff --git a/src/webui/app/frameworks/framework.html b/src/webui/app/frameworks/framework.html new file mode 100644 index 0000000..82f6b27 --- /dev/null +++ b/src/webui/app/frameworks/framework.html @@ -0,0 +1,261 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Framework</span> + {{framework.id}} + </li> +</ol> + +<div class="alert alert-error hidden" id="alert"> + <button class="close" data-dismiss="alert">Ã</button> + <strong>{{alert_message}}</strong> +</div> + +<div class="row" id="framework"> + <div class="col-md-3"> + <div class="well"> + <dl class="inline clearfix"> + <dt>Name:</dt> + <dd>{{framework.name}}</dd> + <dt ng-show="framework.webui_url">Web UI:</dt> + <dd ng-show="framework.webui_url"><a href="{{framework.webui_url}}">{{framework.webui_url}}</a></dd> + <dt>User:</dt> + <dd>{{framework.user}}</dd> + <!-- TODO(bmahler): Consider having a break between each role + in order to increase readability. Also, this doesn't + display well when there are a lot of roles (e.g. a large + organization with a lot of teams & services, using roles + like /engineering/frontend/webserver, etc). --> + <dt>Roles:</dt> + <dd>{{framework.roles.toString()}}</dd> + <dt>Principal:</dt> + <dd>{{framework.principal}}</dd> + <dt>Registered:</dt> + <dd> + <m-timestamp value="{{framework.registered_time * 1000}}"></m-timestamp> + </dd> + <dt>Re-registered:</dt> + <dd ng-if="!framework.reregistered_time">-</dd> + <dd ng-if="framework.reregistered_time"> + <m-timestamp value="{{framework.reregistered_time * 1000}}"></m-timestamp> + </dd> + <dt>Active tasks:</dt> + <dd>{{framework.tasks.length | number}}</dd> + </dl> + + <h4>Tasks</h4> + <table class="table table-condensed"> + <tbody> + <tr> + <td>Staging</td> + <td class="text-right">{{framework.staging_tasks | number}}</td> + </tr> + <tr> + <td>Starting</td> + <td class="text-right">{{framework.starting_tasks | number}}</td> + </tr> + <tr> + <td>Running</td> + <td class="text-right">{{framework.running_tasks | number}}</td> + </tr> + <tr> + <td>Unreachable</td> + <td class="text-right">{{framework.unreachable_tasks.length | number}}</td> + </tr> + <tr> + <td>Killing</td> + <td class="text-right">{{framework.killing_tasks | number}}</td> + </tr> + <tr> + <td>Finished</td> + <td class="text-right">{{framework.finished_tasks | number}}</td> + </tr> + <tr> + <td>Killed</td> + <td class="text-right">{{framework.killed_tasks | number}}</td> + </tr> + <tr> + <td>Failed</td> + <td class="text-right">{{framework.failed_tasks | number}}</td> + </tr> + <tr> + <td>Lost</td> + <td class="text-right">{{framework.lost_tasks | number}}</td> + </tr> + </tbody> + </table> + + <h4>Resources</h4> + <table class="table table-condensed"> + <thead> + <tr> + <td></td> + <td class="text-right">Allocated</td> + <td class="text-right">Offered</td> + </tr> + </thead> + <tbody> + <tr> + <td>CPUs</td> + <td class="text-right"> + {{framework.used_resources.cpus | number}} + </td> + <td class="text-right"> + {{framework.offered_resources.cpus | number}} + </td> + </tr> + <tr> + <td>GPUs</td> + <td class="text-right"> + {{framework.used_resources.gpus | number}} + </td> + <td class="text-right"> + {{framework.offered_resources.gpus | number}} + </td> + </tr> + <tr> + <td>Memory</td> + <td class="text-right"> + {{framework.used_resources.mem * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{framework.offered_resources.mem * (1024 * 1024) | dataSize}} + </td> + </tr> + <tr> + <td>Disk</td> + <td class="text-right"> + {{framework.used_resources.mem * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{framework.offered_resources.mem * (1024 * 1024) | dataSize}} + </td> + </tr> + </tbody> + </table> + </div> + </div> + + <div class="col-md-9"> + <table m-table table-content="framework.tasks" title="Active Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="healthy">Health</th> + <th data-key="start_time">Started</th> + <th data-key="host">Host</th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="task in $data"> + <td> + <a href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}"> + {{task.id}} + </a> + </td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td>{{task.state | truncateMesosState}}</td> + <td class="task-{{task.healthy | taskHealth}}">{{task.healthy | taskHealth}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td> + <span data-ng-show="agents[task.slave_id]"> + {{agents[task.slave_id].hostname}} + </span> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + <td> + <a data-ng-show="agents[task.slave_id]" href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse"> + Sandbox + </a> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + </tr> + </tbody> + </table> + + <table m-table table-content="framework.unreachable_tasks" title="Unreachable Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="start_time">Started</th> + <th data-key="agent_id">Agent ID</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="task in $data"> + <td>{{task.id}}</td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td>{{task.slave_id}}</td> + </tr> + </tbody> + </table> + + <table m-table table-content="framework.completed_tasks" title="Completed Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="start_time">Started</th> + <th data-key="finish_time">Stopped</th> + <th data-key="host">Host</th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="task in $data"> + <td>{{task.id}}</td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td>{{task.state | truncateMesosState}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td> + <m-timestamp value="{{task.finish_time}}"></m-timestamp> + </td> + <td> + <a data-ng-show="agents[task.slave_id]" + href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}"> + {{agents[task.slave_id].hostname}} + </a> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + <td> + <a data-ng-show="agents[task.slave_id]" href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse"> + Sandbox + </a> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + </tr> + </tbody> + </table> + </div> +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/frameworks/frameworks.html ---------------------------------------------------------------------- diff --git a/src/webui/app/frameworks/frameworks.html b/src/webui/app/frameworks/frameworks.html new file mode 100644 index 0000000..d37c613 --- /dev/null +++ b/src/webui/app/frameworks/frameworks.html @@ -0,0 +1,179 @@ +<ol class="breadcrumb"> + <li><a class="badge badge-type" href="#">Master</a></li> + <li class="active"> + <span class="badge badge-type">Frameworks</span> + </li> +</ol> + +<table m-table table-content="frameworks" title="Active Frameworks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="hostname">Host</th> + <th data-key="user">User</th> + <th data-key="name">Name</th> + <th data-key="roles">Roles</th> + <th data-key="principal">Principal</th> + <th data-key="tasks.length">Active Tasks</th> + <th data-key="used_resources.cpus">CPUs</th> + <th data-key="used_resources.gpus">GPUs</th> + <th data-key="used_resources.mem">Mem</th> + <th data-key="used_resources.disk">Disk</th> + <th data-key="max_share">Max Share</th> + <th data-key="registered_time">Registered</th> + <th data-key="reregistered_time">Re-Registered</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="framework in $data | filter: { active: true }"> + <td> + <a href="{{'#/frameworks/' + framework.id}}"> + {{framework.id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{framework.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td ng-switch="!!framework.webui_url"> + <a ng-href="{{framework.webui_url}}" ng-switch-when="true">{{framework.hostname}}</a> + <span ng-switch-when="false">{{framework.hostname}}</span> + </td> + <td>{{framework.user}}</td> + <td>{{framework.name}}</td> + <!-- TODO(bmahler): This doesn't display well when there are a lot + of roles (e.g. a large organization with a lot of teams & + services, using roles like /engineering/frontend/webserver, etc). + Figure out a way to display this without bloating the table. --> + <td>{{framework.roles.toString()}}</td> + <td>{{framework.principal}}</td> + <td>{{framework.tasks.length}}</td> + <td>{{framework.used_resources.cpus | number}}</td> + <td>{{framework.used_resources.gpus | number}}</td> + <td>{{framework.used_resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{framework.used_resources.disk * (1024 * 1024) | dataSize}}</td> + <td>{{framework.max_share * 100 | number}}%</td> + <td> + <m-timestamp value="{{framework.registered_time * 1000}}"></m-timestamp> + </td> + <td ng-show="!framework.reregistered_time">-</td> + <td ng-show="framework.reregistered_time"> + <m-timestamp value="{{framework.reregistered_time * 1000}}"></m-timestamp> + </td> + </tr> + </tbody> +</table> + +<table m-table table-content="frameworks" title="Inactive Frameworks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="hostname">Host</th> + <th data-key="user">User</th> + <th data-key="name">Name</th> + <th data-key="roles">Roles</th> + <th data-key="principal">Principal</th> + <th data-key="tasks.length">Active Tasks</th> + <th data-key="used_resources.cpus">CPUs</th> + <th data-key="used_resources.gpus">GPUs</th> + <th data-key="used_resources.mem">Mem</th> + <th data-key="used_resources.disk">Disk</th> + <th data-key="max_share">Max Share</th> + <th data-key="registered_time">Registered</th> + <th data-key="reregistered_time">Re-Registered</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="framework in $data | filter: { active: false }"> + <td> + <a href="{{'#/frameworks/' + framework.id}}"> + {{framework.id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{framework.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td ng-switch="!!framework.webui_url"> + <a ng-href="{{framework.webui_url}}" ng-switch-when="true">{{framework.hostname}}</a> + <span ng-switch-when="false">{{framework.hostname}}</span> + </td> + <td>{{framework.user}}</td> + <td>{{framework.name}}</td> + <!-- TODO(bmahler): This doesn't display well when there are a lot + of roles (e.g. a large organization with a lot of teams & + services, using roles like /engineering/frontend/webserver, etc). + Figure out a way to display this without bloating the table. --> + <td>{{framework.roles.toString()}}</td> + <td>{{framework.principal}}</td> + <td>{{framework.tasks.length}}</td> + <td>{{framework.used_resources.cpus | number}}</td> + <td>{{framework.used_resources.gpus | number}}</td> + <td>{{framework.used_resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{framework.used_resources.disk * (1024 * 1024) | dataSize}}</td> + <td>{{framework.max_share * 100 | number}}%</td> + <td> + <m-timestamp value="{{framework.registered_time * 1000}}"></m-timestamp> + </td> + <td ng-show="!framework.reregistered_time">-</td> + <td ng-show="framework.reregistered_time"> + <m-timestamp value="{{framework.reregistered_time * 1000}}"></m-timestamp> + </td> + </tr> + </tbody> +</table> + +<table m-table table-content="completed_frameworks" title="Completed Frameworks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="hostname">Host</th> + <th data-key="user">User</th> + <th data-key="name">Name</th> + <th data-key="roles">Roles</th> + <th data-key="principal">Principal</th> + <th data-key="registered_time">Registered</th> + <th data-key="unregistered_time">Unregistered</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="framework in $data"> + <td> + <a href="{{'#/frameworks/' + framework.id}}" title="{{framework.id}}"> + {{framework.id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{framework.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td>{{framework.hostname}}</td> + <td>{{framework.user}}</td> + <td>{{framework.name}}</td> + <!-- TODO(bmahler): This doesn't display well when there are a lot + of roles (e.g. a large organization with a lot of teams & + services, using roles like /engineering/frontend/webserver, etc). + Figure out a way to display this without bloating the table. --> + <td>{{framework.roles.toString()}}</td> + <td>{{framework.principal}}</td> + <td> + <m-timestamp value="{{framework.registered_time * 1000}}"></m-timestamp> + </td> + <td> + <m-timestamp value="{{framework.unregistered_time * 1000}}"></m-timestamp> + </td> + </tr> + </tbody> +</table> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/home.html ---------------------------------------------------------------------- diff --git a/src/webui/app/home.html b/src/webui/app/home.html new file mode 100644 index 0000000..ff96595 --- /dev/null +++ b/src/webui/app/home.html @@ -0,0 +1,334 @@ +<ol class="breadcrumb"> + <li class="active"> + <span class="badge badge-type">Master</span> + {{state.id}} + </li> +</ol> + +<div class="row"> + <div class="col-sm-5 col-md-4 col-lg-3"> + <div class="well"> + <dl class="inline inline-toggle clearfix"> + <dt>Cluster:</dt> + <dd> + <span ng-show="clusterNamed">{{state.cluster}}</span> + <span ng-show="!clusterNamed"> + (Unnamed) + <i class="icon-info-sign" + tooltip="To name this cluster, set the --cluster flag when starting the master." + tooltip-placement="right"></i> + </span> + </dd> + <dt>Leader:</dt> + <dd> + <a ng-show="state.leader_info" + href="//{{state.leader_info.hostname}}:{{state.leader_info.port}}"> + {{state.leader_info.hostname + ':' + state.leader_info.port}} + </a> + <span ng-show="!state.leader_info">(Unknown)</span> + </dd> + <dt>Version:</dt> + <dd>{{state.version}}</dd> + <dt>Built:</dt> + <dd> + <m-timestamp value="{{state.build_time * 1000}}"> + by <i>{{state.build_user}}</i> + </m-timestamp> + </dd> + <dt>Started:</dt> + <dd> + <m-timestamp value="{{state.start_time * 1000}}"></m-timestamp> + </dd> + <dt>Elected:</dt> + <dd> + <m-timestamp value="{{state.elected_time * 1000}}"></m-timestamp> + </dd> + </dl> + + <p ng-if="log_file_attached"> + <b>Master Log:</b> + <span class="btn-group"> + <!-- Links can look like buttons using Bootstrap classes. --> + <a class="btn btn-xs btn-default" href="//{{$location.host()}}:{{$location.port()}}/files/download?path=/master/log"> + Download + </a> + <button class="btn btn-xs btn-default" ng-click="streamLogs($event)"> + View + </button> + </span> + </p> + + <h4>Agents</h4> + <table class="table table-condensed"> + <tbody> + <tr> + <td>Activated</td> + <td class="text-right">{{activated_agents | number}}</td> + </tr> + <tr> + <td>Deactivated</td> + <td class="text-right">{{deactivated_agents | number}}</td> + </tr> + <tr> + <td>Unreachable</td> + <td class="text-right">{{unreachable_agents | number}}</td> + </tr> + </tbody> + </table> + + <h4>Tasks</h4> + <table class="table table-condensed"> + <tbody> + <tr> + <td>Staging</td> + <td class="text-right">{{staging_tasks | number}}</td> + </tr> + <tr> + <td>Starting</td> + <td class="text-right">{{starting_tasks | number}}</td> + </tr> + <tr> + <td>Running</td> + <td class="text-right">{{running_tasks | number}}</td> + </tr> + <tr> + <td>Unreachable</td> + <td class="text-right">{{unreachable_tasks.length | number}}</td> + </tr> + <tr> + <td>Killing</td> + <td class="text-right">{{killing_tasks | number}}</td> + </tr> + <tr> + <td>Finished</td> + <td class="text-right">{{finished_tasks | number}}</td> + </tr> + <tr> + <td>Killed</td> + <td class="text-right">{{killed_tasks | number}}</td> + </tr> + <tr> + <td>Failed</td> + <td class="text-right">{{failed_tasks | number}}</td> + </tr> + <tr> + <td>Lost</td> + <td class="text-right">{{lost_tasks | number}}</td> + </tr> + </tbody> + </table> + + <h4>Resources</h4> + <table class="table table-condensed"> + <thead> + <tr> + <td></td> + <td class="text-right">CPUs</td> + <td class="text-right">GPUs</td> + <td class="text-right">Mem</td> + <td class="text-right">Disk</td> + </tr> + </thead> + <tbody> + <tr> + <td>Total</td> + <td class="text-right">{{total_cpus | decimalFloat}}</td> + <td class="text-right">{{total_gpus | decimalFloat}}</td> + <td class="text-right">{{total_mem * (1024 * 1024) | dataSize}}</td> + <td class="text-right">{{total_disk * (1024 * 1024) | dataSize}}</td> + </tr> + <tr> + <td>Allocated</td> + <td class="text-right">{{used_cpus | decimalFloat}}</td> + <td class="text-right">{{used_gpus | decimalFloat}}</td> + <td class="text-right">{{used_mem * (1024 * 1024) | dataSize}}</td> + <td class="text-right">{{used_disk * (1024 * 1024) | dataSize}}</td> + </tr> + <tr> + <td>Offered</td> + <td class="text-right">{{offered_cpus | decimalFloat}}</td> + <td class="text-right">{{offered_gpus | decimalFloat}}</td> + <td class="text-right">{{offered_mem * (1024 * 1024) | dataSize}}</td> + <td class="text-right">{{offered_disk * (1024 * 1024) | dataSize}}</td> + </tr> + <tr> + <td>Idle</td> + <td class="text-right">{{idle_cpus | decimalFloat}}</td> + <td class="text-right">{{idle_gpus | decimalFloat}}</td> + <td class="text-right">{{idle_mem * (1024 * 1024) | dataSize}}</td> + <td class="text-right">{{idle_disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> + </table> + </div> + </div> + + <div class="col-sm-7 col-md-8 col-lg-9"> + <table m-table table-content="active_tasks" title="Active Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="framework_id">Framework ID</th> + <th data-key="id">Task ID</th> + <th data-key="name">Task Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="healthy">Health</th> + <th data-key="start_time" data-sort>Started</th> + <th data-key="host">Host</th> + <th></th> + </tr> + </thead> + <tbody> + <tr data-ng-if="active_tasks.length === 0"> + <td colspan="8">No active tasks.</td> + </tr> + <tr ng-repeat="task in $data"> + <td> + <a href="#/frameworks/{{task.framework_id}}/"> + {{task.framework_id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{task.framework_id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td> + <a href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}"> + {{task.id}} + </a> + </td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td>{{task.state | truncateMesosState}}</td> + <td class="task-{{task.healthy | taskHealth}}">{{task.healthy | taskHealth}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td> + <span data-ng-show="agents[task.slave_id]"> + {{agents[task.slave_id].hostname}} + </span> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + <td> + <a data-ng-show="agents[task.slave_id]" href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse"> + Sandbox + </a> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + </tr> + </tbody> + </table> + + <table m-table table-content="unreachable_tasks" title="Unreachable Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="framework_id">Framework ID</th> + <th data-key="id">Task ID</th> + <th data-key="name">Task Name</th> + <th data-key="role">Role</th> + <th data-key="start_time" data-sort>Started</th> + <th data-key="agent_id">Agent ID</th> + </tr> + </thead> + <tbody> + <tr data-ng-if="unreachable_tasks.length === 0"> + <td colspan="8">No unreachable tasks.</td> + </tr> + <tr ng-repeat="task in $data"> + <td> + <a href="#/frameworks/{{task.framework_id}}/"> + {{task.framework_id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{task.framework_id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td>{{task.id}}</td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td>{{task.slave_id}}</td> + </tr> + </tbody> + </table> + + <table m-table table-content="completed_tasks" title="Completed Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="framework_id">Framework ID</th> + <th data-key="id">Task ID</th> + <th data-key="name">Task Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="start_time" data-sort>Started</th> + <th data-key="finish_time">Stopped</th> + <th data-key="host">Host</th> + <th></th> + </tr> + </thead> + <tbody> + <tr data-ng-if="completed_tasks.length === 0"> + <td colspan="8">No completed tasks.</td> + </tr> + <tr ng-repeat="task in $data"> + <td> + <a href="#/frameworks/{{task.framework_id}}/"> + {{task.framework_id | truncateMesosID}} + </a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{task.framework_id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td>{{task.id}}</td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td>{{task.state | truncateMesosState}}</td> + <td> + <m-timestamp value="{{task.start_time}}"></m-timestamp> + </td> + <td> + <m-timestamp value="{{task.finish_time}}"></m-timestamp> + </td> + <td> + <a data-ng-show="_.has(agents, task.slave_id)" + href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}"> + {{agents[task.slave_id].hostname}} + </a> + <span class="text-muted" data-ng-show="!_.has(agents, task.slave_id)"> + Agent offline + </span> + </td> + <td> + <a data-ng-show="agents[task.slave_id]" href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse"> + Sandbox + </a> + <span class="text-muted" data-ng-show="!agents[task.slave_id]"> + Agent offline + </span> + </td> + </tr> + </tbody> + </table> + </div> +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/maintenance/maintenance.html ---------------------------------------------------------------------- diff --git a/src/webui/app/maintenance/maintenance.html b/src/webui/app/maintenance/maintenance.html new file mode 100644 index 0000000..d06ddd2 --- /dev/null +++ b/src/webui/app/maintenance/maintenance.html @@ -0,0 +1,36 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#/">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Maintenance Schedule</span> + </li> +</ol> + +<table m-table table-content="maintenance.windows" title="Maintenance Schedule" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="unavailability.start.nanoseconds" data-sort>Begin</th> + <th>End</th> + <th>Hosts</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="window in $data"> + <td> + <m-timestamp value="{{window.unavailability.start.nanoseconds / 1000000}}"> + </m-timestamp> + </td> + <td> + <m-timestamp value="{{(window.unavailability.duration.nanoseconds + window.unavailability.start.nanoseconds) / 1000000}}"> + </m-timestamp> + </td> + <td> + <div ng-repeat="machine_ids in window.machine_ids"> + {{machine_ids.hostname ? machine_ids.hostname : machine_ids.ip}} + </div> + </td> + </tr> + </tbody> +</table> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/offers/offers.html ---------------------------------------------------------------------- diff --git a/src/webui/app/offers/offers.html b/src/webui/app/offers/offers.html new file mode 100644 index 0000000..0586a21 --- /dev/null +++ b/src/webui/app/offers/offers.html @@ -0,0 +1,47 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#/">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Outstanding Offers</span> + </li> +</ol> + +<table m-table table-content="offers" title="Outstanding Offers" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="allocation_info.role">Role</th> + <th data-key="framework_name">Framework</th> + <th data-key="hostname">Host</th> + <th data-key="resources.cpus">CPUs</th> + <th data-key="resources.gpus">GPUs</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk">Disk</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="offer in $data"> + <td> + <abbr title="{{offer.id}}">{{offer.id | truncateMesosID}}</abbr> + </td> + <!-- Make this a link to the role overview --> + <td>{{offer.allocation_info.role}}</td> + <td> + <a href="{{'#/frameworks/' + offer.framework_id}}"> + {{offer.framework_name}} + </a> + </td> + <td> + <a href="#/agents/{{agents[offer.slave_id].id}}"> + {{offer.hostname}} + </a> + </td> + <td>{{offer.resources.cpus | number}}</td> + <td>{{offer.resources.gpus | number}}</td> + <td>{{offer.resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{offer.resources.disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> +</table> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/roles/roles.html ---------------------------------------------------------------------- diff --git a/src/webui/app/roles/roles.html b/src/webui/app/roles/roles.html new file mode 100644 index 0000000..c8ac1ef --- /dev/null +++ b/src/webui/app/roles/roles.html @@ -0,0 +1,60 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#/">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Roles</span> + </li> +</ol> + +<table m-table table-content="roles.roles" title="Roles" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="name" data-sort class="ascending vertically-center" rowspan="2">Role</th> + <th data-key="weight" class="vertically-center" rowspan="2">Weight</th> + <th data-key="frameworks.length" class="vertically-center" rowspan="2">Frameworks</th> + <th data-key="resources.cpus" class="group-column" colspan="4">Allocation</th> + <th data-key="resources.gpus" class="group-column" colspan="4">Guarantee</th> + <th data-key="resources.mem" class="group-column" colspan="4">Limit</th> + </tr> + <tr> + <th data-key="resources.cpus" class="begin-group-column">CPU</th> + <th data-key="resources.gpus">GPU</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk" class="end-group-column">Disk</th> + <th data-key="resources.cpus" class="begin-group-column">CPU</th> + <th data-key="resources.gpus">GPU</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk" class="end-group-column">Disk</th> + <th data-key="resources.cpus" class="begin-group-column">CPU</th> + <th data-key="resources.gpus">GPU</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk" class="end-group-column">Disk</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="role in $data"> + <td>{{role.name}}</td> + <td>{{role.weight | number}}</td> + <td>{{role.frameworks.length | number}}</td> + <td class="begin-group-column">{{role.resources.cpus | decimalFloat}}</td> + <td>{{role.resources.gpus | decimalFloat}}</td> + <td>{{role.resources.mem * (1024 * 1024) | dataSize}}</td> + <td class="end-group-column">{{role.resources.disk * (1024 * 1024) | dataSize}}</td> + + <td class="begin-group-column">{{(role.quota.guarantee.cpus | decimalFloat) || "-"}}</td> + <td>{{(role.quota.guarantee.gpus | decimalFloat) || "-"}}</td> + <td>{{(role.quota.guarantee.mem * (1024 * 1024) | dataSize) || "-"}}</td> + <td class="end-group-column">{{(role.quota.guarantee.disk * (1024 * 1024) | dataSize) || "-"}}</td> + + <!-- TODO(ArmandGrillet): Replace by + (role.quota.limit.<> | decimalFloat) || (role.quota.guarantee.<> | decimalFloat) || "-" + once the limit field is introduced. --> + <td class="begin-group-column">{{(role.quota.guarantee.cpus | decimalFloat) || "-"}}</td> + <td>{{(role.quota.guarantee.gpus | decimalFloat) || "-"}}</td> + <td>{{(role.quota.guarantee.mem * (1024 * 1024) | dataSize) || "-"}}</td> + <td class="end-group-column">{{(role.quota.guarantee.disk * (1024 * 1024) | dataSize) || "-"}}</td> + </tr> + </tbody> +</table>