Add Throttle category management

    Partial-Prod: https://mirantis.jira.com/browse/PROD-18575

Change-Id: Iee8ffff2161df5c1506d086778661e49519fb397
diff --git a/README.rst b/README.rst
index 41520e3..d641952 100644
--- a/README.rst
+++ b/README.rst
@@ -733,7 +733,7 @@
             url: https://path-to-my-library
             credential_id: github
 
- Jenkins Global env properties enforcing
+Jenkins Global env properties enforcing
 
  .. code-block:: yaml
 
@@ -745,6 +745,27 @@
              name: "OFFLINE_DEPLOYMENT" # optional, default using dict key
              value: "true"
 
+Throttle categories management from client (requires
+`Throttle Concurrent Builds <https://plugins.jenkins.io/throttle-concurrents>`_
+plugin)
+
+.. code-block:: yaml
+
+    jenkins:
+      client:
+        throttle_category:
+          'My First Category':
+            max_total: 2
+            max_per_node: 1
+          'My Second Category':
+            max_total: 5
+            max_per_node: 2
+            max_per_label:
+              'node_label_1': 1
+              'node_label_2': 2
+          'My Category To Remove:
+            enabled: false
+
 Usage
 =====
 
diff --git a/_states/jenkins_throttle_category.py b/_states/jenkins_throttle_category.py
new file mode 100644
index 0000000..10c9978
--- /dev/null
+++ b/_states/jenkins_throttle_category.py
@@ -0,0 +1,162 @@
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+create_throttle_groovy = u"""\
+import jenkins.model.*
+import hudson.plugins.throttleconcurrents.ThrottleJobProperty
+import net.sf.json.JSONObject
+
+String categoryName = '${categoryName}'
+Integer maxPerNode = ${maxPerNode}
+Integer maxTotal = ${maxTotal}
+String labels = '${labels}'
+
+Boolean categoryExists = true
+Boolean pairsExists = true
+
+def descriptor = Jenkins.getInstance().getDescriptorByType(
+        ThrottleJobProperty.DescriptorImpl.class)
+
+def categories = descriptor.getCategories()
+
+def category = categories.find {it.categoryName == categoryName}
+
+if (! category) {
+    categoryExists = false
+    category = new ThrottleJobProperty.ThrottleCategory(
+        categoryName, maxPerNode, maxTotal, null)
+    categories.add(category)
+}
+
+if ((category.maxConcurrentPerNode != maxPerNode) ||
+    (category.maxConcurrentTotal != maxTotal)) {
+    categoryExists = false
+    category.maxConcurrentPerNode = maxPerNode
+    category.maxConcurrentTotal == maxTotal
+}
+
+if (labels) {
+    def nodeLabeledPairs = category.getNodeLabeledPairs()
+    def _pairs = JSONObject.fromObject(labels)
+    _pairs.each{ key, val ->
+        _pair = nodeLabeledPairs.find{
+            it.throttledNodeLabel == key
+        }
+        if (! _pair) {
+            pairsExists = false
+            nodeLabeledPairs.add(new ThrottleJobProperty.NodeLabeledPair(
+                key, val));
+        } else if (_pair.maxConcurrentPerNodeLabeled != val) {
+            pairsExists = false
+            _pair.maxConcurrentPerNodeLabeled = val
+        }
+    }
+}
+
+if (categoryExists && pairsExists) {
+    print("EXISTS")
+} else {
+    descriptor.save()
+    print("CREATED")
+}
+"""
+
+remove_throttle_groovy = """
+import jenkins.model.*
+import hudson.plugins.throttleconcurrents.ThrottleJobProperty
+
+String categoryName = '${categoryName}'
+Integer index = -1
+
+def descriptor = Jenkins.getInstance().getDescriptorByType(
+        ThrottleJobProperty.DescriptorImpl.class)
+
+def categories = descriptor.getCategories()
+
+categories.eachWithIndex {it, id ->
+    if (it.categoryName == categoryName) {
+        index = id
+    }
+}
+
+if ( index == -1) {
+    print("NOT PRESENT")
+} else {
+    categories.remove(index)
+    descriptor.save()
+    print("REMOVED")
+}
+"""
+
+def __virtual__():
+    '''
+    Only load if jenkins_common module exist.
+    '''
+    if 'jenkins_common.call_groovy_script' not in __salt__:
+        return (
+            False,
+            'The jenkins_plugin state module cannot be loaded: '
+            'jenkins_common not found')
+    return True
+
+def present(name, max_total, max_per_node, labels=[]):
+    """
+    Jenkins Throttle Category state method
+
+    :param name: category name
+    :param max_total: maximum total concurrent builds
+    :param max_per_node: maximum concurrent builds per node
+    :param labels: maximum per labeled node dict
+    :returns: salt-specified state dict
+    """
+
+    return _plugin_call(name, create_throttle_groovy,
+                        ["CREATED", "EXISTS"],
+                        {"categoryName": name,
+                        "maxPerNode": max_per_node if max_per_node else 0,
+                        "maxTotal": max_total if max_total else 0,
+                        "labels": json.dumps(labels) if labels else ""})
+
+def absent(name):
+    """
+    Jenkins Throttle Category absence state method
+
+    :param name: category name
+    :returns: salt-specified state dict
+    """
+    return _plugin_call(name, remove_throttle_groovy,
+                        ["REMOVED", "NOT PRESENT"],
+                        {"categoryName": name})
+
+def _plugin_call(name, template, success_msgs, params):
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+    result = False
+    if test:
+        status = success_msgs[0]
+        ret['changes'][name] = status
+        ret['comment'] = 'Throttle Category "%s" %s' % (name, status.lower())
+    else:
+        call_result = __salt__['jenkins_common.call_groovy_script'](
+            template, params)
+        if call_result["code"] == 200 and call_result["msg"] in success_msgs:
+            status = call_result["msg"]
+            if status == success_msgs[0]:
+                ret['changes'][name] = status
+            ret['comment'] = 'Throttle Category "%s" %s' % (name, status.lower())
+            result = True
+        else:
+            status = 'FAILED'
+            logger.error(
+                'Jenkins API call failure: %s', call_result["msg"])
+            ret['comment'] = 'Jenkins API call failure: %s' % (call_result[
+                "msg"])
+    ret['result'] = None if test else result
+    return ret
diff --git a/jenkins/client/init.sls b/jenkins/client/init.sls
index 081e35b..7b0726f 100644
--- a/jenkins/client/init.sls
+++ b/jenkins/client/init.sls
@@ -44,6 +44,9 @@
 {%- if client.globalenvprop is defined %}
   - jenkins.client.globalenvprop
 {%- endif %}
+{%- if client.throttle_category is defined %}
+  - jenkins.client.throttle_category
+{%- endif %}
 
 # execute job enforcements as last
 {%- if client.job is defined %}
diff --git a/jenkins/client/throttle_category.sls b/jenkins/client/throttle_category.sls
new file mode 100644
index 0000000..de508a0
--- /dev/null
+++ b/jenkins/client/throttle_category.sls
@@ -0,0 +1,17 @@
+{% from "jenkins/map.jinja" import client with context %}
+{% for name, throttle_category in client.get("throttle_category",{}).iteritems() %}
+{% if throttle_category.get('enabled', True) %}
+'throttle_category_{{ name }}':
+  jenkins_throttle_category.present:
+    - name: '{{ throttle_category.get('name', name) }}'
+    - max_total: {{ throttle_category.get('max_total', 0) }}
+    - max_per_node: {{ throttle_category.get('max_per_node', 0) }}
+    - labels: {{ throttle_category.get('max_per_label',[]) }}
+    - require:
+      - jenkins_client_install
+{% else %}
+'throttle_category_{{ name }}_disable':
+   jenkins_throttle_category.absent:
+     - name: '{{ throttle_category.get('name', name) }}'
+{% endif %}
+{%- endfor %}