Prechádzať zdrojové kódy

AMBARI-11697. Automatic RM URL Config (Erik Bergenholtz via rlevas)

Erik Bergenholtz 10 rokov pred
rodič
commit
7d793c2b33
33 zmenil súbory, kde vykonal 813 pridanie a 85 odobranie
  1. 7 1
      contrib/views/capacity-scheduler/pom.xml
  2. 2 17
      contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java
  3. 4 8
      contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/PropertyValidator.java
  4. 5 6
      contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
  5. 4 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/data/all.json
  6. 6 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js
  7. 18 10
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js
  8. 12 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueBadge.js
  9. 3 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js
  10. 1 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js
  11. 38 7
      contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js
  12. 1 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/helpers/timeAgo.js
  13. 10 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js
  14. 9 7
      contrib/views/capacity-scheduler/src/main/resources/ui/app/serializers.js
  15. 25 3
      contrib/views/capacity-scheduler/src/main/resources/ui/app/store.js
  16. 1 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/styles/application.less
  17. 5 1
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueContainer.hbs
  18. 4 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueListItem.hbs
  19. 2 2
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/userGroupInput.hbs
  20. 63 0
      contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/schedulerPanel.hbs
  21. 1 1
      contrib/views/capacity-scheduler/src/main/resources/ui/config.coffee
  22. 51 0
      contrib/views/capacity-scheduler/src/main/resources/ui/test/unit/controllers/queue_test.js
  23. 2 2
      contrib/views/capacity-scheduler/src/main/resources/view.xml
  24. 131 0
      contrib/views/capacity-scheduler/src/test/java/org/apache/ambari/view/capacityscheduler/PropertyValidatorTest.java
  25. 6 1
      contrib/views/utils/pom.xml
  26. 116 0
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ViewUserLocal.java
  27. 13 1
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ambari/AmbariApi.java
  28. 131 0
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ambari/Services.java
  29. 30 2
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/ConfigurationBuilder.java
  30. 10 5
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/HdfsApi.java
  31. 4 1
      contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/HdfsUtil.java
  32. 98 0
      contrib/views/utils/src/test/java/org/apache/ambari/view/utils/ViewUserLocalTest.java
  33. 0 2
      contrib/views/utils/src/test/java/org/apache/ambari/view/utils/hdfs/ConfigurationBuilderTest.java

+ 7 - 1
contrib/views/capacity-scheduler/pom.xml

@@ -19,7 +19,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.apache.ambari.contrib.views</groupId>
     <artifactId>capacity-scheduler</artifactId>
-    <version>0.4.0-SNAPSHOT</version>
+    <version>1.0.0-SNAPSHOT</version>
     <name>Capacity Scheduler</name>
 
     <parent>
@@ -72,6 +72,12 @@
             <artifactId>ambari-views-utils</artifactId>
             <version>0.0.1-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
+            <version>3.2</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <properties>

+ 2 - 17
contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java

@@ -98,9 +98,8 @@ public class ConfigurationService {
   private static final String VERSION_TAG_URL = "?fields=Clusters/desired_configs/capacity-scheduler";
   private static final String CONFIGURATION_URL = "configurations?type=capacity-scheduler";
   private static final String CONFIGURATION_URL_BY_TAG = "configurations?type=capacity-scheduler&tag=%s";
-  private static final String RM_HOST_URL = "services/YARN/components/RESOURCEMANAGER?fields=host_components/host_name";
 
-  private static final String RM_GET_NODE_LABEL_URL = "http://%s:8088/ws/v1/cluster/get-node-labels";
+  private static final String RM_GET_NODE_LABEL_URL = "%s/ws/v1/cluster/get-node-labels";
 
   // ================================================================================
   // Privilege Reading
@@ -483,21 +482,7 @@ public class ConfigurationService {
   }
 
   private String getRMHost() {
-    String rmHost = null;
-    JSONObject rmData = readFromCluster(RM_HOST_URL);
-    if (rmData == null)
-      throw new ServiceFormattedException("Cannot retrieve ResourceManager host");
-    JSONArray components = (JSONArray) rmData.get("host_components");
-    for(Object component : components) {
-      JSONObject roles = (JSONObject) ((JSONObject) component).get("HostRoles");
-      if (roles.get("component_name").equals("RESOURCEMANAGER")) {
-        rmHost = (String) roles.get("host_name");
-        break;
-      }
-    }
-    if (rmHost == null)
-      throw new ServiceFormattedException("Can't find ResourceManager host");
-    return rmHost;
+    return ambariApi.getServices().getRMUrl();
   }
 
 } // end ConfigurationService

+ 4 - 8
contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/PropertyValidator.java

@@ -43,14 +43,10 @@ public class PropertyValidator implements Validator {
 
     if (property.equals(AMBARI_SERVER_URL)) {
       String ambariServerUrl = viewInstanceDefinition.getPropertyMap().get(AMBARI_SERVER_URL);
-      URL url = null;
-      try {
-        url = new URL(ambariServerUrl);
-        url.toURI();
-      } catch (MalformedURLException e) {
-        return new InvalidPropertyValidationResult(false, "Must be valid URL");
-      } catch (URISyntaxException e) {
-        return new InvalidPropertyValidationResult(false, "Must be valid URL");
+
+      if (!ambariServerUrl.matches("^https?://[\\w\\d\\.]+:\\d+/api/v1/clusters/\\w+$")) {
+        return new InvalidPropertyValidationResult(false,
+            "URL should contain protocol, hostname, port, cluster name, e.g. http://ambari.server:8080/api/v1/clusters/MyCluster");
       }
     }
     return ValidationResult.SUCCESS;

+ 5 - 6
contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js

@@ -207,12 +207,8 @@ App.QueueAdapter = DS.Adapter.extend({
           return {name:label};
         }));
       }, function(jqXHR) {
-        if (jqXHR.status === 404) {
-          Ember.run(null, resolve, []);
-        } else {
-          jqXHR.then = null;
-          Ember.run(null, reject, jqXHR);
-        }
+        jqXHR.then = null;
+        Ember.run(null, reject, jqXHR);
       });
     }.bind(this),'App: QueueAdapter#getNodeLabels');
   },
@@ -306,6 +302,9 @@ App.TagAdapter = App.QueueAdapter.extend({
     var adapter = this;
     var uri = [_getCapacitySchedulerViewUri(this),'all'].join('/');
 
+    if (App.testMode)
+      uri = uri + ".json";
+
     return new Ember.RSVP.Promise(function(resolve, reject) {
       adapter.ajax(uri ,'GET').then(function(data) {
         Ember.run(null, resolve, data);

+ 4 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/data/all.json

@@ -0,0 +1,4 @@
+{
+  "items":[
+  ]
+}

+ 6 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js

@@ -44,7 +44,12 @@ App.ExpandableInputComponent = Em.TextField.extend({
   },
   focusOut:function  (argument) {
     this.$().parent().removeClass('expanded').parent().removeClass('expanded-wrap');
-  }
+  },
+  checkBlank:function () {
+    if (Em.isBlank(this.get('value')) && !Em.isNone(this.get('value'))) {
+      this.set('value', null);
+    }
+  }.observes('value')
 });
 
 App.IntInputComponent = Ember.TextField.extend({

+ 18 - 10
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js

@@ -24,15 +24,20 @@ App.PathInputComponent = Em.Component.extend({
     add:function () {
       var currentBasedir = this.get('currentBasedir'),
           path = this.get('path'),
-          basedir = path.substr(0,path.lastIndexOf('.')) || currentBasedir;
-      if (!path) {
-        return this.set('isError',true);
+          basedir = path.substr(0,path.lastIndexOf('.')) || currentBasedir,
+          queuePath = [basedir,path.substr(path.lastIndexOf('.')+1)].join('.'),
+          queueName = path.substr(path.lastIndexOf('.')+1),
+          alreadyExists = this.get('queues.firstObject.store').hasRecordForId('queue',queuePath.toLowerCase());
+
+      if (!path || !queueName) {
+        return this.setProperties({'isError':true,'errorMessage':'Enter queue name.'});
       }
-      if (this.get('pathMap').contains(path)) {
-        this.sendAction('action',path);
-        this.set('activeFlag',false);
-      } else if (this.get('pathMap').contains(basedir)) {
-        this.sendAction('action',basedir,path.substr(path.lastIndexOf('.')+1));
+      if (alreadyExists) {
+        return this.setProperties({'isError':true,'errorMessage':'Queue already exists.'});
+      }
+
+      if (this.get('pathMap').contains(basedir) && !alreadyExists) {
+        this.sendAction('action',basedir,queueName);
         this.set('activeFlag',false);
       }
     },
@@ -48,6 +53,7 @@ App.PathInputComponent = Em.Component.extend({
   }),
   path:'',
   isError:false,
+  errorMessage:'',
   didChangePath:function () {
     return this.set('isError',false);
   }.observes('path'),
@@ -69,11 +75,13 @@ App.PathInputComponent = Em.Component.extend({
     }.observes('pathSource').on('didInsertElement'),
     tooltipInit:function () {
       this.$().tooltip({
-        title:'Enter queue name.',
+        title:function() {
+          return this.get('parentView.errorMessage');
+        }.bind(this),
         placement:'bottom',
         trigger:'manual'
       });
-    }.on('didInsertElement'),
+    }.on('didInsertElement').observes('parentView.errorMessage'),
     tooltipToggle:function (e,o) {
       this.$().tooltip((e.get(o)?'show':'hide'));
     }.observes('parentView.isError'),

+ 12 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueBadge.js

@@ -18,6 +18,18 @@
 
 var App = require('app');
 
+App.WarnBadgeComponent = Em.Component.extend({
+  layout:Em.Handlebars.compile('<i class="fa fa-exclamation"></i>'),
+  classNames:['label','label-warning'],
+  tagName:'span',
+  initTooltip: function(){
+    this.$().tooltip({
+      title:'No capacity',
+      placement:'bottom'
+    });
+  }.on('didInsertElement'),
+});
+
 App.QueueBadgeComponent = Em.Component.extend({
   layoutName:'components/queueBadge',
   tagName:'span',

+ 3 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js

@@ -275,7 +275,9 @@ App.TotalCapacityComponent = Ember.Component.extend({
      * Returns true if total capacity of node labels in leaf are greater than 100.
      * @type {Boolean}
      */
-    warning:Em.computed.gt('capacityValue',100),
+    warning:Em.computed('capacityValue',function() {
+      return this.get('capacityValue') != 100;
+    }),
 
     // OBSERVABLES
 

+ 1 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js

@@ -142,7 +142,7 @@ App.QueueController = Ember.ObjectController.extend({
    * @return {App.Queue}
    */
   parentQueue: function () {
-    return this.store.getById('queue',this.get('content.parentPath'));
+    return this.store.getById('queue',this.get('content.parentPath').toLowerCase());
   }.property('content.parentPath'),
 
   /**

+ 38 - 7
contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js

@@ -38,22 +38,38 @@ App.QueuesController = Ember.ArrayController.extend({
       }
       name = name || '';
       var newQueue,
-          existed = this.get('store.deletedQueues').findBy('path',parentPath+'.'+name);
+          store = this.get('store'),
+          existed = store.get('deletedQueues').findBy('path',[parentPath,name].join('.')),
+          leafQueueNames = store.getById('queue',parentPath.toLowerCase()).get('queuesArray'),
+          newInLeaf = Em.isEmpty(leafQueueNames),
+          totalLeafCapacity,
+          freeLeafCapacity;
 
       if (existed) {
-        newQueue = this.store.createFromDeleted(existed);
+        newQueue = store.createFromDeleted(existed);
       } else {
-        newQueue = this.store.createRecord('queue', {
+
+        if (!newInLeaf) {
+          totalLeafCapacity = leafQueueNames.reduce(function (capacity,queueName) {
+            return store.getById('queue', [parentPath,queueName].join('.').toLowerCase()).get('capacity') + capacity;
+          },0);
+
+          freeLeafCapacity = (totalLeafCapacity < 100) ? 100 - totalLeafCapacity : 0;
+        }
+
+        newQueue = store.createRecord('queue', {
           name:name,
           parentPath: parentPath,
           depth: parentPath.split('.').length,
-          isNewQueue:true
+          isNewQueue:true,
+          capacity: (newInLeaf) ? 100 : freeLeafCapacity,
+          maximum_capacity: (newInLeaf) ? 100: freeLeafCapacity
         });
         this.set('newQueue',newQueue);
       }
 
       if (name) {
-        this.get('store').saveAndUpdateQueue(newQueue,existed)
+        store.saveAndUpdateQueue(newQueue,existed)
           .then(Em.run.bind(this,'transitionToRoute','queue'))
           .then(Em.run.bind(this,'set','newQueue',null));
       } else {
@@ -68,6 +84,9 @@ App.QueuesController = Ember.ArrayController.extend({
       this.get('store').saveAndUpdateQueue(record, updates);
     },
     delQ:function (record) {
+      if (record.get('isNew')) {
+        this.set('newQueue',null);
+      }
       if (record.isCurrent) {
         this.transitionToRoute('queue',record.get('parentPath').toLowerCase())
           .then(Em.run.schedule('afterRender', function () {
@@ -99,6 +118,10 @@ App.QueuesController = Ember.ArrayController.extend({
     },
     clearAlert:function () {
       this.set('alertMessage',null);
+    },
+    toggleProperty:function (property,target) {
+      target = target || this;
+      target.toggleProperty(property);
     }
   },
 
@@ -216,7 +239,7 @@ App.QueuesController = Ember.ArrayController.extend({
           return +queue.get('capacity') + prev;
         },0);
 
-      leaf.setEach('overCapacity',total>100);
+      leaf.setEach('overCapacity',total != 100);
     }.bind(this));
   }.observes('content.length','content.@each.capacity'),
 
@@ -305,7 +328,7 @@ App.QueuesController = Ember.ArrayController.extend({
    * check if can save configs
    * @type {bool}
    */
-  canNotSave: cmp.any('hasOverCapacity', 'hasUncompetedAddings','hasNotValid'),
+  canNotSave: cmp.any('hasOverCapacity', 'hasUncompetedAddings','hasNotValid','hasNotValidLabels'),
 
   /**
    * List of not valid queues.
@@ -319,6 +342,14 @@ App.QueuesController = Ember.ArrayController.extend({
    */
   hasNotValid:cmp.notEmpty('notValid.[]'),
 
+  /**
+   * True if queues have not valid labels.
+   * @type {Boolean}
+   */
+  hasNotValidLabels:function(){
+    return this.get('content').anyBy('hasNotValidLabels',true);
+  }.property('content.@each.hasNotValidLabels'),
+
   /**
    * List of queues with excess of capacity
    * @type {Array}

+ 1 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/helpers/timeAgo.js

@@ -31,7 +31,7 @@ Ember.Handlebars.registerHelper('timeAgo', function(property, options) {
       var value = Em._HandlebarsBoundView.prototype.normalizedValue.call(this);
 
       return function(value, options) {
-        return (value)?moment(value).fromNow(false):'';
+        return (moment.isMoment(value))?moment(value).fromNow(false):value;
       }.call(this, value, options);
     }
   });

+ 10 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js

@@ -43,6 +43,8 @@ App.Scheduler = DS.Model.extend({
   maximum_applications: DS.attr('number', { defaultValue: 0 }),
   node_locality_delay: DS.attr('number', { defaultValue: 0 }),
   resource_calculator: DS.attr('string', { defaultValue: '' }),
+  queue_mappings: DS.attr('string'),
+  queue_mappings_override_enable: DS.attr('boolean'),
   isAnyDirty:Em.computed.alias('isDirty')
 });
 
@@ -57,7 +59,7 @@ App.Tag = DS.Model.extend({
     return this.get('tag') === this.get('store.current_tag');
   }.property('store.current_tag'),
   changed:function () {
-    return (this.get('tag').match(/version[1-9]+/))?moment(+this.get('tag').replace('version','')):'';
+    return (this.get('tag').match(/version[1-9][0-9][0-9]+/))?moment(+this.get('tag').replace('version','')):this.get('tag');
   }.property('tag')
 });
 
@@ -73,6 +75,10 @@ App.Queue = DS.Model.extend({
   sortBy:['name'],
   sortedLabels:Em.computed.sort('labels','sortBy'),
 
+  hasNotValidLabels: function(attribute){
+    return this.get('labels').anyBy('isValid',false);
+  }.property('labels.@each.isValid'),
+
   _accessAllLabels:DS.attr('boolean'),
   accessAllLabels:function (key,val) {
     var labels = this.get('store.nodeLabels').map(function(label) {
@@ -84,7 +90,7 @@ App.Queue = DS.Model.extend({
 
       if (this.get('_accessAllLabels')) {
           labels.forEach(function (lb) {
-            var containsByParent = (Em.isEmpty(this.get('parentPath')))?true:this.store.getById('queue',this.get('parentPath')).get('labels').findBy('name',lb.get('name'));
+            var containsByParent = (Em.isEmpty(this.get('parentPath')))?true:this.store.getById('queue',this.get('parentPath').toLowerCase()).get('labels').findBy('name',lb.get('name'));
             if (!this.get('labels').contains(lb) && !!containsByParent) {
               this.get('labels').pushObject(lb);
             }
@@ -157,6 +163,8 @@ App.Queue = DS.Model.extend({
     }));
   }.property('initialLabels', 'labels.[]', '_accessAllLabels'),
 
+  noCapacity:Em.computed.equal('capacity',0),
+
   name: DS.attr('string'),
   parentPath: DS.attr('string'),
   depth: DS.attr('number'),

+ 9 - 7
contrib/views/capacity-scheduler/src/main/resources/ui/app/serializers.js

@@ -106,7 +106,7 @@ App.SerializerMixin = Em.Mixin.create({
           ordering_policy:               props[base_path + ".ordering-policy"] || null,
           enable_size_based_weight:      props[base_path + ".ordering-policy.fair.enable-size-based-weight"] || null,
           default_node_label_expression: props[base_path + ".default-node-label-expression"] || null,
-          labelsEnabled: props.hasOwnProperty(labelsPath)
+          labelsEnabled:                 props.hasOwnProperty(labelsPath)
         };
 
     switch ((props.hasOwnProperty(labelsPath))?props[labelsPath]:'') {
@@ -141,10 +141,12 @@ App.SerializerMixin = Em.Mixin.create({
 
     var scheduler = [{
       id:                          'scheduler',
-      maximum_am_resource_percent: properties[this.PREFIX + ".maximum-am-resource-percent"]*100 || null, // convert to percent
-      maximum_applications:        properties[this.PREFIX + ".maximum-applications"] || null,
-      node_locality_delay:         properties[this.PREFIX + ".node-locality-delay"] || null,
-      resource_calculator:         properties[this.PREFIX + ".resource-calculator"] || null
+      maximum_am_resource_percent:    properties[this.PREFIX + ".maximum-am-resource-percent"]*100 || null, // convert to percent
+      maximum_applications:           properties[this.PREFIX + ".maximum-applications"] || null,
+      node_locality_delay:            properties[this.PREFIX + ".node-locality-delay"] || null,
+      resource_calculator:            properties[this.PREFIX + ".resource-calculator"] || null,
+      queue_mappings:                 properties[this.PREFIX + ".queue-mappings"] || null,
+      queue_mappings_override_enable: properties[this.PREFIX + ".queue-mappings-override.enable"] || null
     }];
     _recurseQueues(null, "root", 0, properties, queues, this.get('store'));
     this._setupLabels(properties,queues,labels,this.PREFIX);
@@ -185,6 +187,8 @@ App.SchedulerSerializer = DS.RESTSerializer.extend(App.SerializerMixin,{
     json[this.PREFIX + ".maximum-applications"] = record.get('maximum_applications');
     json[this.PREFIX + ".node-locality-delay"] = record.get('node_locality_delay');
     json[this.PREFIX + ".resource-calculator"] = record.get('resource_calculator');
+    json[this.PREFIX + ".queue-mappings"] = record.get('queue_mappings') || null;
+    json[this.PREFIX + ".queue-mappings-override.enable"] = record.get('queue_mappings_override_enable');
 
     return json;
   }
@@ -267,10 +271,8 @@ App.QueueSerializer = DS.RESTSerializer.extend(App.SerializerMixin,{
     }
 
     recordLabels.forEach(function (l) {
-      if (!record.get('store.nodeLabels').findBy('name',l.get('name')).notExist) {
         json[[accessible_node_labels_key, l.get('name'), 'capacity'].join('.')] = l.get('capacity');
         json[[accessible_node_labels_key, l.get('name'), 'maximum-capacity'].join('.')] = l.get('maximum_capacity');
-      }
     });
   }
 });

+ 25 - 3
contrib/views/capacity-scheduler/src/main/resources/ui/app/store.js

@@ -89,7 +89,16 @@ function _fetchTagged(adapter, store, type, sinceToken) {
       store.didUpdateAll(type);
       return store.recordForId('scheduler','scheduler');
     }).then(function (scheduler) {
-      scheduler.setProperties(config.scheduler.objectAt(0));
+      var props = config.scheduler.objectAt(0);
+
+      scheduler.eachAttribute(function (attr,meta) {
+        if (meta.type === 'boolean') {
+          this.set(attr, (props[attr] === 'false' || !props[attr])?false:true);
+        } else {
+          this.set(attr, props[attr]);
+        }
+      },scheduler);
+
       scheduler.set('version',v);
     });
 
@@ -271,12 +280,25 @@ App.ApplicationStore = DS.Store.extend({
   },
 
   nodeLabels: function () {
-    var adapter = this.get('defaultAdapter');
+    var adapter = this.get('defaultAdapter'),
+        store = this,
+        promise = new Ember.RSVP.Promise(function(resolve, reject) {
+          adapter.getNodeLabels().then(function(data) {
+            store.set('isRmOffline',false);
+            resolve(data);
+          }, function() {
+            store.set('isRmOffline',true);
+            resolve([]);
+          });
+        });
+
     return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin).create({
-      promise: adapter.getNodeLabels()
+      promise: promise
     });
   }.property(),
 
+  isRmOffline:false,
+
   isInitialized: Ember.computed.and('tag', 'clusterName'),
 
   markForRefresh:function () {

+ 1 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/styles/application.less

@@ -574,7 +574,6 @@
             .labels-toggle {
               z-index: 1;
               margin-bottom: 5px;
-              display: inline-table;
 
               label {
                 span {
@@ -823,7 +822,7 @@
       top: 25px;
     }
     @media (min-width: @screen-lg-min) {
-      left: 40%;
+      left: 43.5%;
       top: -8px;
     }
     position: absolute;

+ 5 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueContainer.hbs

@@ -65,7 +65,11 @@
           <div class="queue-capacity">
             {{#if label.isNotExist}}
               <span {{bind-attr class=":label label.overCapacity:label-danger:label-warning"}}>{{label.name}}</span>
-              <small>Label is not exist on cluster</small>
+              {{#if label.store.isRmOffline}}
+                <small>Unable to obtain information  about node label from the resource manager</small>
+              {{else}}
+                <small>Label is not exist on cluster</small>
+              {{/if}}
             {{else}}
               <span {{bind-attr class=":label label.overCapacity:label-danger:label-success"}}>{{label.name}}</span>
               <span {{bind-attr class=":label :label-default label.isDefault::hide"}}>default</span>

+ 4 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/queueListItem.hbs

@@ -29,6 +29,10 @@
         <span class="label label-info">v{{queue.version}}</span>
     {{/if}}
 
+    {{#if queue.noCapacity}}
+      {{warn-badge}}
+    {{/if}}
+
     {{queue-badge q=queue class="badge pull-right"}}
 
     {{#if queue.isDeletedQueue}}

+ 2 - 2
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/components/userGroupInput.hbs

@@ -28,6 +28,6 @@
   <div class="col-sm-8"> 
     {{input keyDown=noSpace disabledBinding="disabled" class="form-control input-sm" value=groups placeholder="Comma-separated list of groups"}} 
     <i class="fa fa-users form-control-feedback"></i>
-  </div> 
+    <div class="help-block pull-right"><small>leave blank to deny access for everyone</small></div>
+  </div>
 </div>
-

+ 63 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/app/templates/schedulerPanel.hbs

@@ -118,6 +118,69 @@
             </div>
           </div>
       {{/if}}
+      <div class="form-group">
+        {{tooltip-label
+          label='Queue Mappings'
+          class="col-xs-5 control-label"
+          message='This configuration specifies the mapping of user or group to aspecific queue. You can map a single user or a list of users to queues. Syntax: [u or g]:[name]:[queue_name][,next_mapping]*. Here, u or g indicates whether the mapping is for a user or group. The value is u for user and g for group. name indicates the user name or group name. To specify the user who has submitted the application, %user can be used. queue_name indicates the queue name for which the application has to be mapped. To specify queue name same as user name, %user can be used. To specify queue name same as the name of the primary group for which the user belongs to, %primary_group can be used.'
+        }}
+        {{#if isOperator}}
+          <div class="col-xs-7 control-value">
+            {{expandable-input value=scheduler.queue_mappings class="input-sm form-control input-expand"}}
+            {{#if schedulerDirtyFilelds.queue_mappings}}
+              <div class="btn-group btn-group-xs" >
+                  <a {{action 'rollbackProp' 'queue_mappings' scheduler}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+              </div>
+            {{/if}}
+          </div>
+        {{else}}
+          <div class="col-xs-6">
+            {{#if scheduler.queue_mappings}}
+              <p class="form-control-static">{{scheduler.queue_mappings}}</p>
+            {{else}}
+              <p class="form-control-static">-</p>
+            {{/if}}
+          </div>
+        {{/if}}
+      </div>
+      <div class="form-group">
+        {{tooltip-label
+          class="col-xs-5 control-label"
+          label='Queue Mappings Override'
+          message='This function is used to specify whether the user specified queues can be overridden. This is a Boolean value and the default value is false.'
+        }}
+          {{#if isOperator}}
+          <div class="col-xs-7 control-value input-percent-wrap">
+            <div>
+              <div class="btn-group btn-group-sm pull-right">
+                <a href="#" {{action 'toggleProperty' 'queue_mappings_override_enable' scheduler}} class="btn btn-default">
+                  <i {{bind-attr class=":fa scheduler.queue_mappings_override_enable:fa-check-square-o:fa-square-o"}}></i>
+                  {{#if scheduler.queue_mappings_override_enable}}
+                    Enabled
+                  {{else}}
+                    Disabled
+                  {{/if}}
+                </a>
+              </div>
+            </div>
+            {{#if schedulerDirtyFilelds.queue_mappings_override_enable}}
+              <div class="btn-group btn-group-xs" >
+                <a {{action 'rollbackProp' 'queue_mappings_override_enable' scheduler}} href="#" class="btn btn-default btn-warning"><i class="fa fa-undo"></i></a>
+              </div>
+            {{/if}}
+          </div>
+          {{else}}
+          <div class="col-xs-6">
+            <p class="form-control-static">
+            {{#if scheduler.queue_mappings_override_enable}}
+              Enabled
+            {{else}}
+              Disabled
+            {{/if}}
+            </p>
+          </div>
+          {{/if}}
+      </div>
     </form>
   </div>
 </div>

+ 1 - 1
contrib/views/capacity-scheduler/src/main/resources/ui/config.coffee

@@ -67,5 +67,5 @@ exports.config =
   overrides:
     development:
       paths:
-        public: '/usr/lib/ambari-server/web/views-debug/CAPACITY-SCHEDULER/0.4.0/CS_1/'
+        public: '/usr/lib/ambari-server/web/views-debug/CAPACITY-SCHEDULER/0.4.0/AUTO_CS_INSTANCE/'
 

+ 51 - 0
contrib/views/capacity-scheduler/src/main/resources/ui/test/unit/controllers/queue_test.js

@@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+
+moduleFor('controller:queue', 'App.QueueController',
+  {
+  needs:[ 'controller:queues' ],
+    teardown:function (container) {
+      App.reset();
+    }
+  },
+  function (container) {
+    container.register('model:queue', App.Queue);
+    container.register('model:label', App.Label);
+    container.register('store:main', App.ApplicationStore);
+    container.register('adapter:application', DS.FixtureAdapter);
+    container.register('serializer:application', DS.RESTSerializer);
+  }
+);
+
+test('can get parentQueue',function () {
+  var controller = this.subject(),
+      store = this.container.lookup('store:main'),
+      queues;
+
+
+  Em.run(function() {
+    queues = store.pushMany('queue',[
+      {id:'root.a',parentPath:'root',path:'root.A'},
+      {id:'root.a.b',parentPath:'root.a',path:'root.A.B'}
+    ]);
+    controller.set('store', store );
+    controller.set('model', queues.objectAt(1) );
+  });
+
+  deepEqual(controller.get('parentQueue'),queues.objectAt(0));
+});

+ 2 - 2
contrib/views/capacity-scheduler/src/main/resources/view.xml

@@ -17,7 +17,7 @@
 <view>
     <name>CAPACITY-SCHEDULER</name>
     <label>Capacity Scheduler</label>
-    <version>0.4.0</version>
+    <version>1.0.0</version>
 
     <min-ambari-version>2.1.*</min-ambari-version>
 
@@ -48,7 +48,7 @@
         <masked>true</masked>
     </parameter>
 
-    <resource>
+  <resource>
       <name>scheduler</name>
       <service-class>org.apache.ambari.view.capacityscheduler.CapacitySchedulerService</service-class>
     </resource>

+ 131 - 0
contrib/views/capacity-scheduler/src/test/java/org/apache/ambari/view/capacityscheduler/PropertyValidatorTest.java

@@ -0,0 +1,131 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.capacityscheduler;
+
+import org.apache.ambari.view.ViewInstanceDefinition;
+import org.apache.ambari.view.validation.Validator;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.*;
+
+public class PropertyValidatorTest {
+  @Test
+  public void testValidatePropertyOk() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("http://hostname.com:8080/api/v1/clusters/Cluster");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertTrue(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyHttps() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("https://hostname.com:8080/api/v1/clusters/Cluster");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertTrue(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyNoPort() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("http://hostname.com/api/v1/clusters/Cluster");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyNoProtocol() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("hostname.com:8080/api/v1/clusters/Cluster");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyNoCluster() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("http://hostname.com:8080");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyNoClusterName() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("http://hostname.com:8080/api/v1/clusters/");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyMisspell() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("http://hostname.com:8080/api/v1/clAsters/MyCluster");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  @Test
+  public void testValidatePropertyOnlyHostname() throws Exception {
+    ViewInstanceDefinition instanceDefinition =
+        getViewInstanceDefinition("hostname.com");
+    PropertyValidator propertyValidator = new PropertyValidator();
+
+    assertFalse(propertyValidator.validateProperty(
+        PropertyValidator.AMBARI_SERVER_URL, instanceDefinition,
+        Validator.ValidationContext.PRE_CREATE).isValid());
+  }
+
+  private ViewInstanceDefinition getViewInstanceDefinition(String ambariServerUrl) {
+    ViewInstanceDefinition instanceDefinition = createNiceMock(ViewInstanceDefinition.class);
+    Map<String, String> map = new HashMap<String, String>();
+    expect(instanceDefinition.getPropertyMap()).andReturn(map).anyTimes();
+    replay(instanceDefinition);
+
+    map.put(PropertyValidator.AMBARI_SERVER_URL, ambariServerUrl);
+    return instanceDefinition;
+  }
+}

+ 6 - 1
contrib/views/utils/pom.xml

@@ -39,6 +39,11 @@
       <artifactId>hadoop-common</artifactId>
       <version>${hadoop-version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-yarn-common</artifactId>
+      <version>${hadoop-version}</version>
+    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
@@ -115,7 +120,7 @@
 
   <properties>
     <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>
-    <hadoop-version>2.2.0</hadoop-version>
+    <hadoop-version>2.6.0</hadoop-version>
   </properties>
   <build>
   </build>

+ 116 - 0
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ViewUserLocal.java

@@ -0,0 +1,116 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.utils;
+
+import org.apache.ambari.view.ViewContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages end user specific objects.
+ * Ensures that the instance of specific class is exists only in one instance for
+ * specific user of specific instance.
+ * @param <T> user-local class
+ */
+public class ViewUserLocal<T> {
+  private static Map<Class, Map<String, Object>> viewSingletonObjects = new HashMap<Class, Map<String, Object>>();
+  private final Class<T> tClass;
+
+  public ViewUserLocal(Class<T> tClass) {
+    this.tClass = tClass;
+  }
+
+  /**
+   * Initial value of user-local class. Value can be set either by initialValue() on first get() call,
+   * or directly by calling set() method. Default initial value is null, it can be changed by overriding
+   * this method.
+   * @param context initial value usually based on user properties provided by View Context
+   * @return initial value of user-local variable
+   */
+  protected synchronized T initialValue(ViewContext context) {
+    return null;
+  }
+
+  /**
+   * Returns user-local instance.
+   * If instance of class is not present yet for user, calls initialValue to create it.
+   * @param context View context that provides instance and user names.
+   * @return instance
+   */
+  public T get(ViewContext context) {
+    if (!viewSingletonObjects.containsKey(tClass)) {
+      viewSingletonObjects.put(tClass, new HashMap<String, Object>());
+    }
+
+    Map<String, Object> instances = viewSingletonObjects.get(tClass);
+
+    if (!instances.containsKey(getTagName(context))) {
+      instances.put(getTagName(context), initialValue(context));
+    }
+    return (T) instances.get(getTagName(context));
+  }
+
+  /**
+   * Method for directly setting user-local singleton instances.
+   * @param obj new variable value for current user
+   * @param context ViewContext that provides username and instance name
+   */
+  public void set(T obj, ViewContext context) {
+    if (!viewSingletonObjects.containsKey(tClass)) {
+      viewSingletonObjects.put(tClass, new HashMap<String, Object>());
+    }
+
+    Map<String, Object> instances = viewSingletonObjects.get(tClass);
+    instances.put(getTagName(context), obj);
+  }
+
+  /**
+   * Returns unique key for Map to store a user-local variable.
+   * @param context ViewContext
+   * @return Unique identifier of pair instance-user.
+   */
+  private String getTagName(ViewContext context) {
+    if (context == null) {
+      return "<null>";
+    }
+    return String.format("%s:%s", context.getInstanceName(), context.getUsername());
+  }
+
+  /**
+   * Method for testing purposes, intended to clear the cached user-local instances.
+   * Method should not normally be called from production code.
+   * @param tClass classname instances of which should be dropped
+   */
+  public static void dropAllConnections(Class tClass) {
+    Map<String, Object> instances = viewSingletonObjects.get(tClass);
+    if (instances != null) {
+      viewSingletonObjects.get(tClass).clear();
+    }
+  }
+
+  /**
+   * Method for testing purposes, intended to clear the cached user-local instances.
+   * Drops all classes of user-local variables.
+   * Method should not normally be called from production code.
+   */
+  public static void dropAllConnections() {
+    viewSingletonObjects.clear();
+  }
+}

+ 13 - 1
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ambari/AmbariApi.java

@@ -18,7 +18,6 @@
 
 package org.apache.ambari.view.utils.ambari;
 
-import org.apache.ambari.view.AmbariStreamProvider;
 import org.apache.ambari.view.URLStreamProvider;
 import org.apache.ambari.view.ViewContext;
 import org.apache.ambari.view.cluster.Cluster;
@@ -47,6 +46,8 @@ public class AmbariApi {
 
   private Cluster cluster;
   private ViewContext context;
+  private Services services;
+
   private String remoteUrlCluster;
   private String remoteUsername;
   private String remotePassword;
@@ -285,4 +286,15 @@ public class AmbariApi {
     urlStreamProviderBasicAuth.setRequestedBy(requestedBy);
     return urlStreamProviderBasicAuth;
   }
+
+  /**
+   * Provides access to service-specific utilities
+   * @return object with service-specific methods
+   */
+  public Services getServices() {
+    if (services == null) {
+      services = new Services(this, context);
+    }
+    return services;
+  }
 }

+ 131 - 0
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/ambari/Services.java

@@ -0,0 +1,131 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.utils.ambari;
+
+import org.apache.ambari.view.ViewContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Utilities for specific Hadoop services and util functions for them
+ */
+public class Services {
+  public static final String HTTPS_ONLY = "HTTPS_ONLY";
+  public static final String HTTP_ONLY = "HTTP_ONLY";
+  public static final String YARN_SITE = "yarn-site";
+  public static final String YARN_HTTP_POLICY = "yarn.http.policy";
+
+  private final AmbariApi ambariApi;
+  private ViewContext context;
+
+  protected final static Logger LOG = LoggerFactory.getLogger(Services.class);
+
+  public Services(AmbariApi ambariApi, ViewContext context) {
+    this.ambariApi = ambariApi;
+    this.context = context;
+  }
+
+  /**
+   * Returns URL to Resource Manager.
+   * If cluster associated, returns HTTP or HTTPS address based on "yarn.http.policy" property value.
+   * If not associated, retrieves RM URL from view instance properties by "yarn.resourcemanager.url" property.
+   * @return url of RM
+   */
+  public String getRMUrl() {
+    String url;
+
+    if (ambariApi.isClusterAssociated()) {
+      String protocol;
+
+      String httpPolicy = ambariApi.getCluster().getConfigurationValue(YARN_SITE, YARN_HTTP_POLICY);
+      if (httpPolicy.equals(HTTPS_ONLY)) {
+        protocol = "https";
+        url = ambariApi.getCluster().getConfigurationValue(YARN_SITE, "yarn.resourcemanager.webapp.https.address");
+
+      } else {
+        protocol = "http";
+        url = ambariApi.getCluster().getConfigurationValue(YARN_SITE, "yarn.resourcemanager.webapp.address");
+        if (!httpPolicy.equals(HTTP_ONLY))
+          LOG.error(String.format("RA030 Unknown value %s of yarn-site/yarn.http.policy. HTTP_ONLY assumed.", httpPolicy));
+      }
+
+      url = addProtocolIfMissing(url, protocol);
+    } else {
+      url = context.getProperties().get("yarn.resourcemanager.url");
+      if (!hasProtocol(url)) {
+        throw new AmbariApiException(
+            "RA070 View is not cluster associated. Resource Manager URL should contain protocol.");
+      }
+    }
+    return url;
+  }
+
+  /**
+   * Returns URL to WebHCat in format like http://<hostname>:<port>/templeton/v1
+   * @return url to WebHCat
+   */
+  public String getWebHCatURL() {
+    String host = null;
+
+    if (ambariApi.isClusterAssociated()) {
+      List<String> hiveServerHosts = ambariApi.getHostsWithComponent("WEBHCAT_SERVER");
+
+      if (!hiveServerHosts.isEmpty()) {
+        host = hiveServerHosts.get(0);
+        LOG.info("WEBHCAT_SERVER component was found on host " + host);
+      } else {
+        LOG.warn("No host was found with WEBHCAT_SERVER component. Using hive.host property to get hostname.");
+      }
+    }
+
+    if (host == null) {
+      host = context.getProperties().get("webhcat.hostname");
+      if (host == null || host.isEmpty()) {
+        throw new AmbariApiException(
+            "RA080 Can't determine WebHCat hostname neither by associated cluster nor by webhcat.hostname property.");
+      }
+    }
+
+    String port = context.getProperties().get("webhcat.port");
+    if (port == null || port.isEmpty()) {
+      throw new AmbariApiException(
+          "RA090 Can't determine WebHCat port neither by associated cluster nor by webhcat.port property.");
+    }
+
+    return String.format("http://%s:%s/templeton/v1", host, port);
+  }
+
+  public static String addProtocolIfMissing(String url, String protocol) throws AmbariApiException {
+    if (!hasProtocol(url)) {
+      url = protocol + "://" + url;
+    }
+    return url;
+  }
+
+  /**
+   * Checks if URL has the protocol
+   * @param url url
+   * @return true if protocol is present
+   */
+  public static boolean hasProtocol(String url) {
+    return url.matches("^[^:]+://.*$");
+  }
+}

+ 30 - 2
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/ConfigurationBuilder.java

@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Map;
 
 /**
  * Builds the Configuration of HDFS based on ViewContext.
@@ -63,6 +64,8 @@ public class ConfigurationBuilder {
   private Configuration conf = new Configuration();
   private ViewContext context;
   private AmbariApi ambariApi = null;
+  private AuthConfigurationBuilder authParamsBuilder;
+  private Map<String, String> authParams;
 
   /**
    * Constructor of ConfigurationBuilder based on ViewContext
@@ -70,7 +73,8 @@ public class ConfigurationBuilder {
    */
   public ConfigurationBuilder(ViewContext context) {
     this.context = context;
-    ambariApi = new AmbariApi(context);
+    this.ambariApi = new AmbariApi(context);
+    this.authParamsBuilder = new AuthConfigurationBuilder(context);
   }
 
   private void parseProperties() throws HdfsApiException {
@@ -180,13 +184,25 @@ public class ConfigurationBuilder {
     return defaultFs;
   }
 
+  /**
+   * Set properties relevant to authentication parameters to HDFS Configuration
+   * @param authParams list of auth params of View
+   */
+  public void setAuthParams(Map<String, String> authParams) {
+    String auth = authParams.get("auth");
+    if (auth != null) {
+      conf.set("hadoop.security.authentication", auth);
+    }
+  }
+
   /**
    * Build the HDFS configuration
    * @return configured HDFS Configuration object
    * @throws HdfsApiException if configuration parsing failed
    */
-  public Configuration build() throws HdfsApiException {
+  public Configuration buildConfig() throws HdfsApiException {
     parseProperties();
+    setAuthParams(buildAuthenticationConfig());
 
     conf.set("fs.hdfs.impl", DistributedFileSystem.class.getName());
     conf.set("fs.webhdfs.impl", WebHdfsFileSystem.class.getName());
@@ -194,4 +210,16 @@ public class ConfigurationBuilder {
 
     return conf;
   }
+
+  /**
+   * Builds the authentication configuration
+   * @return map of HDFS auth params for view
+   * @throws HdfsApiException
+   */
+  public Map<String, String> buildAuthenticationConfig() throws HdfsApiException {
+    if (authParams == null) {
+      authParams = authParamsBuilder.build();
+    }
+    return authParams;
+  }
 }

+ 10 - 5
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/HdfsApi.java

@@ -25,12 +25,19 @@ import org.apache.hadoop.fs.permission.FsPermission;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.security.PrivilegedExceptionAction;
 import java.util.*;
 
+import org.apache.hadoop.hdfs.DistributedFileSystem;
+import org.apache.hadoop.hdfs.web.WebHdfsFileSystem;
+import org.apache.hadoop.security.Credentials;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.json.simple.JSONArray;
 
+import javax.security.auth.Subject;
+
 /**
  * Hdfs Business Delegate
  */
@@ -44,15 +51,13 @@ private final Configuration conf;
   /**
    * Constructor
    * @param configurationBuilder hdfs configuration builder
-   * @param authParams map of parameters
    * @throws IOException
    * @throws InterruptedException
    */
-  public HdfsApi(ConfigurationBuilder configurationBuilder, String username, AuthConfigurationBuilder authParams) throws IOException,
+  public HdfsApi(ConfigurationBuilder configurationBuilder, String username) throws IOException,
       InterruptedException, HdfsApiException {
-    this.authParams = authParams.build();
-    conf = configurationBuilder.build();
-
+    this.authParams = configurationBuilder.buildAuthenticationConfig();
+    conf = configurationBuilder.buildConfig();
     ugi = UserGroupInformation.createProxyUser(username, getProxyUser());
 
     fs = ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {

+ 4 - 1
contrib/views/utils/src/main/java/org/apache/ambari/view/utils/hdfs/HdfsUtil.java

@@ -119,8 +119,11 @@ public class HdfsUtil {
     ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(context);
     AuthConfigurationBuilder authConfigurationBuilder = new AuthConfigurationBuilder(context);
 
+    Map<String, String> authParams = authConfigurationBuilder.build();
+    configurationBuilder.setAuthParams(authParams);
+
     try {
-      api = new HdfsApi(configurationBuilder, getHdfsUsername(context), authConfigurationBuilder);
+      api = new HdfsApi(configurationBuilder, getHdfsUsername(context));
       LOG.info("HdfsApi connected OK");
     } catch (IOException e) {
       String message = "HDFS040 Couldn't open connection to HDFS";

+ 98 - 0
contrib/views/utils/src/test/java/org/apache/ambari/view/utils/ViewUserLocalTest.java

@@ -0,0 +1,98 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.utils;
+
+import org.apache.ambari.view.ViewContext;
+import org.junit.Test;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.*;
+
+public class ViewUserLocalTest {
+  @Test
+  public void testDifferentUsers() throws Exception {
+    ViewContext viewContext = createNiceMock(ViewContext.class);
+    expect(viewContext.getInstanceName()).andReturn("INSTANCE1").anyTimes();
+    expect(viewContext.getUsername()).andReturn("luke").anyTimes();
+
+    ViewContext viewContext2 = createNiceMock(ViewContext.class);
+    expect(viewContext2.getInstanceName()).andReturn("INSTANCE1").anyTimes();
+    expect(viewContext2.getUsername()).andReturn("leia").anyTimes();
+    replay(viewContext, viewContext2);
+
+    ViewUserLocal<Object> test = new ViewUserLocal<Object>(Object.class) {
+      @Override
+      protected synchronized Object initialValue(ViewContext context) {
+        return new Object();
+      }
+    };
+
+    Object obj1 = test.get(viewContext);
+    Object obj2 = test.get(viewContext2);
+    assertNotSame(obj1, obj2);
+  }
+
+  @Test
+  public void testDifferentInstances() throws Exception {
+    ViewContext viewContext = createNiceMock(ViewContext.class);
+    expect(viewContext.getInstanceName()).andReturn("INSTANCE1").anyTimes();
+    expect(viewContext.getUsername()).andReturn("luke").anyTimes();
+
+    ViewContext viewContext2 = createNiceMock(ViewContext.class);
+    expect(viewContext2.getInstanceName()).andReturn("INSTANCE2").anyTimes();
+    expect(viewContext2.getUsername()).andReturn("luke").anyTimes();
+    replay(viewContext, viewContext2);
+
+    ViewUserLocal<Object> test = new ViewUserLocal<Object>(Object.class) {
+      @Override
+      protected synchronized Object initialValue(ViewContext context) {
+        return new Object();
+      }
+    };
+
+    Object obj1 = test.get(viewContext);
+    Object obj2 = test.get(viewContext2);
+    assertNotSame(obj1, obj2);
+  }
+
+  @Test
+  public void testSameUsers() throws Exception {
+    ViewContext viewContext = createNiceMock(ViewContext.class);
+    expect(viewContext.getInstanceName()).andReturn("INSTANCE1").anyTimes();
+    expect(viewContext.getUsername()).andReturn("luke").anyTimes();
+
+    ViewContext viewContext2 = createNiceMock(ViewContext.class);
+    expect(viewContext2.getInstanceName()).andReturn("INSTANCE1").anyTimes();
+    expect(viewContext2.getUsername()).andReturn("luke").anyTimes();
+    replay(viewContext, viewContext2);
+
+    ViewUserLocal<Object> test = new ViewUserLocal<Object>(Object.class) {
+      @Override
+      protected synchronized Object initialValue(ViewContext context) {
+        return new Object();
+      }
+    };
+
+    Object obj1 = test.get(viewContext);
+    Object obj2 = test.get(viewContext2);
+    assertSame(obj1, obj2);
+  }
+}

+ 0 - 2
contrib/views/utils/src/test/java/org/apache/ambari/view/utils/hdfs/ConfigurationBuilderTest.java

@@ -20,8 +20,6 @@ package org.apache.ambari.view.utils.hdfs;
 
 import org.junit.Test;
 
-import java.net.URI;
-
 import static org.junit.Assert.*;
 
 public class ConfigurationBuilderTest {