Merge pull request #16 from jakubjosef/master

First version of jenkins nodes enforcing + fixes.
diff --git a/README.rst b/README.rst
index 1d20ec7..319e0b7 100644
--- a/README.rst
+++ b/README.rst
@@ -306,6 +306,89 @@
             - repository: base
               file: macros/git.groovy
 
+Credentials enforcing from client
+
+.. code-block:: yaml
+    
+    jenkins:
+      client:
+        credential:
+          cred_first:
+            username: admin
+            password: password
+          cred_second:
+            username: salt
+            password: password
+          cred_with_key:
+            username: admin
+            key: SOMESSHKEY
+
+Users enforcing from client
+
+.. code-block:: yaml
+
+    jenkins:
+      client:
+        user:
+          admin:
+            password: admin_password
+            admin: true
+          user01:
+            password: user_password
+
+Node enforcing from client using JNLP launcher
+
+.. code-block:: yaml
+
+    jenkins:
+        client:
+          node:
+            node01:
+              remote_home: /remote/home/path
+              desc: node-description
+              num_executors: 1
+              node_mode: Normal
+              ret_strategy: Always
+              labels:
+                - example
+                - label
+              launcher:
+                 type: jnlp
+
+Node enforcing from client using SSH launcher
+
+.. code-block:: yaml
+
+    jenkins:
+        client:
+          node:
+            node01:
+              remote_home: /remote/home/path
+              desc: node-description
+              num_executors: 1
+              node_mode: Normal
+              ret_strategy: Always
+              labels:
+                - example
+                - label 
+              launcher:
+                 type: ssh
+                 host: test-launcher
+                 port: 22
+                 username: launcher-user
+                 password: launcher-pass
+
+Setting node labels
+
+.. code-block:: yaml
+
+    jenkins:
+        client:
+            label:
+              node-name:
+                lbl_text: label-offline
+                append: false # set true for label append instead of replace
+
 SMTP server settings
 
 .. code-block:: yaml
@@ -328,29 +411,8 @@
         approved_scripts:
         - method groovy.json.JsonSlurperClassic parseText java.lang.String
 
-Credentials enforcing
 
-.. code-block:: yaml
-    
-    jenkins:
-      master:
-        credentials:
-          - type: username_password
-            scope: GLOBAL
-            id: credential-1
-            desc: ""
-            username: admin
-            password: "password"
-          - type: ssh_key
-            scope: GLOBAL
-            id: key-credential
-            desc: ""
-            username: admin
-            password: "key-password"
-            key: |
-               xxxxxxxxxxxxxxxxxxxx
-
-Users enforcing 
+Users enforcing from master
 
 .. code-block:: yaml
 
diff --git a/_modules/jenkins_common.py b/_modules/jenkins_common.py
index e4ae5d8..493aebe 100644
--- a/_modules/jenkins_common.py
+++ b/_modules/jenkins_common.py
@@ -6,12 +6,18 @@
 logger = logging.getLogger(__name__)
 
 
-def call_groovy_script(script, props):
+def call_groovy_script(script, props, username=None, password=None, success_status_codes=[200]):
     """
     Common method for call Jenkins groovy script API
 
-    :param script groovy script template
-    :param props groovy script properties
+    :param script: groovy script template
+    :param props: groovy script properties
+    :param username: jenkins username (optional,
+            if missing creds from sall will be used)
+    :param password: jenkins password (optional,
+            if missing creds from sall will be used)
+    :param success_status_codes: success response status code
+            (optional) in some cases we want to declare error call as success
     :returns: HTTP dict {status,code,msg}
     """
     ret = {
@@ -20,6 +26,11 @@
         "msg": ""
     }
     jenkins_url, jenkins_user, jenkins_password = get_jenkins_auth()
+    if username:
+        jenkins_user = username
+    if password:
+        jenkins_password = password
+
     if not jenkins_url:
         raise SaltInvocationError('No Jenkins URL found.')
 
@@ -33,7 +44,7 @@
                         auth=(jenkins_user, jenkins_password),
                         data=req_data)
     ret["code"] = req.status_code
-    if req.status_code == 200:
+    if req.status_code in success_status_codes:
         ret["status"] = "SUCCESS"
         ret["msg"] = req.text
         logger.debug("Jenkins script API call success: %s", ret)
@@ -72,15 +83,15 @@
                             auth=(jenkins_user, jenkins_password) if jenkins_user else None)
     if tokenReq.status_code == 200:
         return tokenReq.json()
-    elif tokenReq.status_code == 404:
-        # 404 means CSRF security is disabled, so api crumb is not necessary
+    elif tokenReq.status_code in [404, 401]:
+        # 404 means CSRF security is disabled, so api crumb is not necessary,
+        # 401 means unauthorized
         return None
     else:
         raise Exception("Cannot obtain Jenkins API crumb. Status code: %s. Text: %s" %
                         (tokenReq.status_code, tokenReq.text))
 
 
-
 def get_jenkins_auth():
     """
     Get jenkins params from salt
diff --git a/_states/jenkins_credential.py b/_states/jenkins_credential.py
new file mode 100644
index 0000000..ce44fe6
--- /dev/null
+++ b/_states/jenkins_credential.py
@@ -0,0 +1,92 @@
+import logging
+logger = logging.getLogger(__name__)
+
+create_credential_groovy = u"""\
+import jenkins.*;
+import jenkins.model.*;
+import hudson.*;
+import hudson.model.*;
+
+import com.cloudbees.plugins.credentials.domains.Domain;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+
+def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
+        com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
+        Jenkins.instance
+    )
+
+def result = creds.find{{ it.username == "{username}" && it.password.toString() == "{password}" }}
+if(result){{
+    print("EXISTS")
+}}else{{
+    domain = Domain.global()
+    store = Jenkins.instance.getExtensionList(
+      'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
+    )[0].getStore()
+
+    credentials_new = new {clazz}(
+      {params}
+    )
+
+    creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
+          {clazz}.class, Jenkins.instance
+    );
+    ret = store.addCredentials(domain, credentials_new)
+    if (ret) {{
+      print("CREATED");
+    }} else {{
+        print("FAILED");
+    }}
+}}
+"""  # noqa
+
+def present(name, scope, username, password=None, desc="", key=None):
+    """
+    Main jenkins credentials state method
+
+    :param name: credential name
+    :param scope: credential scope
+    :param username: username
+    :param password: password (optional)
+    :param desc: credential description (optional)
+    :param key: credential key (optional)
+    :returns: salt-specified state dict
+    """
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+    result = False
+    if test:
+        status = 'CREATED'
+        ret['changes'][name] = status
+        ret['comment'] = 'Credentials %s %s' % (name, status.lower())
+    else:
+        clazz = ""
+        if key:
+            clazz = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"
+            params = 'CredentialsScope.{}, "{}", "{}", "{}"'.format(
+                scope, name, desc, key)
+        else:
+            clazz = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
+            params = 'CredentialsScope.{}, "{}", "{}", "{}", "{}"'.format(
+                scope, name, desc, username, password)
+
+        call_result = __salt__['jenkins_common.call_groovy_script'](
+            create_credential_groovy, {"username": username, "password": password, "clazz": clazz, "params": params})
+        if call_result["code"] == 200 and call_result["msg"] in ["CREATED", "EXISTS"]:
+            status = call_result["msg"]
+            if call_result["msg"] == "CREATED":
+                ret['changes'][name] = status
+            ret['comment'] = 'Credentials %s %s' % (name, status.lower())
+            result = True
+        else:
+            status = 'FAILED'
+            logger.error(
+                "Jenkins credentials API call failure: %s", call_result["msg"])
+            ret['comment'] = 'Jenkins credentials API call failure: %s' % (call_result["msg"])
+    ret['result'] = None if test else result
+    return ret
diff --git a/_states/jenkins_credentials.py b/_states/jenkins_credentials.py
deleted file mode 100644
index f2d4b03..0000000
--- a/_states/jenkins_credentials.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import logging
-logger = logging.getLogger(__name__)
-
-create_credential_groovy = u"""\
-import jenkins.*;
-import jenkins.model.*;
-import hudson.*;
-import hudson.model.*;
-
-import com.cloudbees.plugins.credentials.domains.Domain;
-import com.cloudbees.plugins.credentials.CredentialsScope;
-
-domain = Domain.global()
-store = Jenkins.instance.getExtensionList(
-  'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
-)[0].getStore()
-
-credentials_new = new {clazz}(
-  {params}
-)
-
-creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
-      {clazz}.class, Jenkins.instance
-);
-updated = false;
-
-for (credentials_current in creds) {{
-  // Comparison does not compare passwords but identity.
-  if (credentials_new == credentials_current) {{
-    store.removeCredentials(domain, credentials_current);
-    ret = store.addCredentials(domain, credentials_new)
-    updated = true;
-    println("OVERWRITTEN");
-    break;
-  }}
-}}
-
-if (!updated) {{
-  ret = store.addCredentials(domain, credentials_new)
-  if (ret) {{
-    println("CREATED");
-  }} else {{
-    println("FAILED");
-  }}
-}}
-"""  # noqa
-
-
-def present(name, scope, username, password=None, desc="", key=None):
-    """
-    Main jenkins credentials state method
-
-    :param name: credential name
-    :param scope: credential scope
-    :param username: username
-    :param password: password (optional)
-    :param desc: credential description (optional)
-    :param key: credential key (optional)
-    :returns: salt-specified state dict
-    """
-    test = __opts__['test']  # noqa
-    ret = {
-        'name': name,
-        'changes': {},
-        'result': False,
-        'comment': '',
-    }
-    result = False
-    if test:
-        status = 'CREATED'
-        ret['changes'][name] = status
-        ret['comment'] = 'Credentials ' + status.lower()
-    else:
-        clazz = ""
-        if key:
-            clazz = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"
-            params = 'CredentialsScope.{}, "{}", "{}", "{}"'.format(scope, name, desc, key)
-        else:
-            clazz = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
-            params = 'CredentialsScope.{}, "{}", "{}", "{}", "{}"'.format(scope, name, desc, username, password)
-
-        call_result = __salt__['jenkins_common.call_groovy_script'](create_credential_groovy, {"clazz": clazz, "params":params})
-        if call_result["code"] == 200 and call_result["msg"].strip() in ["CREATED", "OVERWRITTEN"]:
-            status = call_result["msg"]
-            ret['changes'][name] = status
-            ret['comment'] = 'Credentials ' + status.lower()
-            result = True
-        else:
-            status = 'FAILED'
-            logger.error("Jenkins credentials API call failure: %s", call_result["msg"])
-            ret['comment'] = 'Jenkins credentials API call failure: %s' % (call_result["msg"])
-    ret['result'] = None if test else result
-    return ret
diff --git a/_states/jenkins_node.py b/_states/jenkins_node.py
new file mode 100644
index 0000000..388c9fb
--- /dev/null
+++ b/_states/jenkins_node.py
@@ -0,0 +1,154 @@
+import logging
+logger = logging.getLogger(__name__)
+
+
+create_node_groovy = u"""\
+import jenkins.model.*
+import hudson.model.*
+import hudson.slaves.*
+import hudson.plugins.sshslaves.*
+
+def result=Jenkins.instance.slaves.find{{
+ it.name == '{name}' && 
+ it.numExecutors == {num_executors} &&
+ it.nodeDescription == "{desc}" && 
+ it.remoteFS == "{remote_home}" && 
+ it.labelString == "{label}" && 
+ it.mode == Node.Mode.{node_mode} && 
+ it.launcher.getClass().getName().equals({launcher}.getClass().getName()) &&
+ it.retentionStrategy.getClass().getName().equals(new hudson.slaves.RetentionStrategy.{ret_strategy}().getClass().getName())}}
+if(result){{
+    print("EXISTS")
+}}else{{
+  Slave slave = new DumbSlave(
+                    "{name}",
+                    "{desc}",
+                    "{remote_home}",
+                    "{num_executors}",
+                    Node.Mode.{node_mode},
+                    "{label}",
+                    {launcher},
+                    new RetentionStrategy.{ret_strategy}(),
+                    new LinkedList())
+  Jenkins.instance.addNode(slave)
+  print("CREATED")
+}}
+"""  # noqa
+
+create_lbl_groovy = u"""\
+hudson = hudson.model.Hudson.instance
+updated = false
+hudson.slaves.find {{ slave -> slave.nodeName.equals("{name}")
+  if({append}){{
+    slave.labelString = slave.labelString + " " + "{lbl_text}"
+  }}else{{
+    slave.labelString = "{lbl_text}"
+  }}
+  updated = true
+  print "{lbl_text}"
+}}
+if(!updated){{
+    print "FAILED"
+}}
+hudson.save()
+"""  # noqa
+
+
+def label(name, lbl_text, append=False):
+    """
+    Jenkins node label state method
+
+    :param name: node name
+    :param lbl_text: label text
+    :returns: salt-specified state dict
+    """
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+    result = False
+    if test:
+        status = 'CREATED'
+        ret['changes'][name] = status
+        ret['comment'] = 'Label %s %s' % (name, status.lower())
+    else:
+        call_result = __salt__['jenkins_common.call_groovy_script'](
+            create_lbl_groovy, {'name': name, 'lbl_text': lbl_text, 'append': "true" if append else "false"})
+        if call_result["code"] == 200 and call_result["msg"].strip() == lbl_text:
+            status = "CREATED"
+            ret['changes'][name] = status
+            ret['comment'] = 'Label %s %s ' % (name, status.lower())
+            result = True
+        else:
+            status = 'FAILED'
+            logger.error(
+                "Jenkins label API call failure: %s", call_result["msg"])
+            ret['comment'] = 'Jenkins label API call failure: %s' % (
+                call_result["msg"])
+    ret['result'] = None if test else result
+    return ret
+
+
+def present(name, remote_home, launcher, num_executors="1", node_mode="Normal", desc="", labels=[], ret_strategy="Always"):
+    """
+    Jenkins node state method
+
+    :param name: node name
+    :param remote_home: node remote home path
+    :param launcher: launcher dict with type, name, port, username, password
+    :param num_executors: number of node executurs (optional, default 1)
+    :param node_mode: node mode (optional, default Normal)
+    :param desc: node description (optional)
+    :param labels: node labels list (optional)
+    :param ret_strategy: node retention strategy from RetentionStrategy class
+    :returns: salt-specified state dict
+    """
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+    result = False
+    if test:
+        status = 'CREATED'
+        ret['changes'][name] = status
+        ret['comment'] = 'Node %s %s' % (name, status.lower())
+    else:
+        label_string = " ".join(labels)
+        launcher_string = "new hudson.slaves.JNLPLauncher()"
+        if launcher:
+            if launcher["type"] == "ssh":
+                launcher_string = 'new hudson.plugins.sshslaves.SSHLauncher("{}",{},"{}","{}","","","","","")'.format(
+                    launcher["host"], launcher["port"], launcher["username"], launcher["password"])
+            elif launcher["type"] == "jnlp":
+                launcher_string = "new hudson.slaves.JNLPLauncher()"
+
+        call_result = __salt__['jenkins_common.call_groovy_script'](
+            create_node_groovy,
+            {"name": name,
+                "desc": desc,
+                "label": label_string,
+                "remote_home": remote_home,
+                "num_executors": num_executors,
+                "launcher": launcher_string,
+                "node_mode": node_mode.upper(),
+                "ret_strategy": ret_strategy})
+        if call_result["code"] == 200 and call_result["msg"] in ["CREATED", "EXISTS"]:
+            status = call_result["msg"]
+            if call_result["msg"] == "CREATED":
+                ret['changes'][name] = status
+            ret['comment'] = 'Node %s %s' % (name, status.lower())
+            result = True
+        else:
+            status = 'FAILED'
+            logger.error(
+                "Jenkins node API call failure: %s", call_result["msg"])
+            ret['comment'] = 'Jenkins node API call failure: %s' % (
+                call_result["msg"])
+    ret['result'] = None if test else result
+    return ret
diff --git a/_states/jenkins_user.py b/_states/jenkins_user.py
new file mode 100644
index 0000000..6f37ec3
--- /dev/null
+++ b/_states/jenkins_user.py
@@ -0,0 +1,72 @@
+import logging
+logger = logging.getLogger(__name__)
+
+create_admin_groovy = u"""\
+import jenkins.model.*
+import hudson.security.*
+def instance = Jenkins.getInstance()
+def hudsonRealm = new HudsonPrivateSecurityRealm(false)
+def result=hudsonRealm.createAccount("{username}","{password}")
+instance.setSecurityRealm(hudsonRealm)
+def strategy = new hudson.security.FullControlOnceLoggedInAuthorizationStrategy()
+strategy.setAllowAnonymousRead(false)
+instance.setAuthorizationStrategy(strategy)
+instance.save()
+print(result)
+"""  # noqa
+
+
+create_user_groovy = u"""\
+def result=jenkins.model.Jenkins.instance.securityRealm.createAccount("{username}", "{password}")
+print(result)
+"""  # noqa
+
+def present(name, username, password, admin=False):
+    """
+    Main jenkins users state method
+
+    :param username: user name
+    :param password: user password
+    :param admin:  is admin user flag (username will be always admin)
+    :returns: salt-specified state dict
+    """
+    test = __opts__['test']  # noqa
+    ret = {
+        'name': username,
+        'changes': {},
+        'result': False,
+        'comment': '',
+    }
+
+    result = False
+    if test:
+        status = 'CREATED'
+        ret['changes'][username] = status
+        ret['comment'] = 'User %s %s' % (username, status.lower())
+    else:
+        # try to call jenkins script api with given user and password to prove
+        # his existence
+        user_exists_result = __salt__['jenkins_common.call_groovy_script'](
+            "print(\"TEST\")", {"username": username}, username, password,[200, 401])
+        user_exists = user_exists_result and user_exists_result[
+            "code"] == 200 and user_exists_result["msg"].count("TEST") == 1
+        if not user_exists:
+            call_result = __salt__['jenkins_common.call_groovy_script'](
+                create_admin_groovy if admin else create_user_groovy, {"username": username, "password": password})
+            if call_result["code"] == 200 and call_result["msg"].count(username) == 1:
+                status = "CREATED" if not admin else "ADMIN CREATED"
+                ret['changes'][username] = status
+                ret['comment'] = 'User %s %s' % (username, status.lower())
+                result = True
+            else:
+                status = 'FAILED'
+                logger.error("Jenkins user API call failure: %s",
+                             call_result["msg"])
+                ret['comment'] = 'Jenkins user API call failure: %s' % (call_result[
+                                                                        "msg"])
+        else:
+            status = "EXISTS"
+            ret['comment'] = 'User %s %s' % (username, status.lower())
+            result = True
+    ret['result'] = None if test else result
+    return ret
diff --git a/_states/jenkins_users.py b/_states/jenkins_users.py
deleted file mode 100644
index 29a2f64..0000000
--- a/_states/jenkins_users.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import logging
-logger = logging.getLogger(__name__)
-
-create_admin_groovy = u"""\
-import jenkins.model.*
-import hudson.security.*
-def instance = Jenkins.getInstance()
-def hudsonRealm = new HudsonPrivateSecurityRealm(false)
-def result=hudsonRealm.createAccount("{username}","{password}")
-instance.setSecurityRealm(hudsonRealm)
-def strategy = new hudson.security.FullControlOnceLoggedInAuthorizationStrategy()
-strategy.setAllowAnonymousRead(false)
-instance.setAuthorizationStrategy(strategy)
-instance.save()
-print(result)
-"""  #noqa
-
-
-create_user_groovy = u"""\
-def result=jenkins.model.Jenkins.instance.securityRealm.createAccount("{username}", "{password}")
-print(result)
-"""  # noqa
-
-
-def present(name, username, password, admin=False):
-    """
-    Main jenkins users state method
-
-    :param username: user name
-    :param password: user password
-    :param admin:  is admin user flag (username will be always admin)
-    :returns: salt-specified state dict
-    """
-    test = __opts__['test']  # noqa
-    ret = {
-        'name': username,
-        'changes': {},
-        'result': False,
-        'comment': '',
-    }
-
-    result = False
-    if test:
-        status = 'CREATED'
-        ret['changes'][username] = status
-        ret['comment'] = 'User %s %s' % (username, status.lower())
-    else:
-        call_result = __salt__['jenkins_common.call_groovy_script'](create_admin_groovy if admin else create_user_groovy, {"username": username, "password":password})
-        if call_result["code"] == 200 and call_result["msg"].count(username) == 1:
-            status = "CREATED" if not admin else "ADMIN CREATED"
-            ret['changes'][username] = status
-            ret['comment'] = 'User %s %s' % (username, status.lower())
-            result = True
-        else:
-            status = 'FAILED'
-            logger.error("Jenkins user API call failure: %s", call_result["msg"])
-            ret['comment'] = 'Jenkins user API call failure: %s' % (call_result["msg"])
-    ret['result'] = None if test else result
-    return ret
diff --git a/jenkins/client/credential.sls b/jenkins/client/credential.sls
index 4d5412e..31492bf 100644
--- a/jenkins/client/credential.sls
+++ b/jenkins/client/credential.sls
@@ -1,7 +1,7 @@
 {% from "jenkins/map.jinja" import client with context %}
 {% for name, cred in client.get('credential',{}).iteritems() %}
 credential_{{ name }}:
-  jenkins_credentials.present:
+  jenkins_credential.present:
   - name: {{ cred.get('name', name) }}
   - username: {{ cred.username }}
   - password: {{ cred.get('password', '') }}
diff --git a/jenkins/client/node.sls b/jenkins/client/node.sls
new file mode 100644
index 0000000..8412137
--- /dev/null
+++ b/jenkins/client/node.sls
@@ -0,0 +1,21 @@
+{% from "jenkins/map.jinja" import client with context %}
+{% for name, node in client.get("node",{}).iteritems() %}
+node_{{ name }}:
+  jenkins_node.present:
+    - name: {{ name }}
+    - desc:  {{ node.get('desc','') }}
+    - remote_home: {{ node.remote_home }}
+    - launcher: {{ node.launcher }}
+    - num_executors: {{ node.get('num_executors','1') }}
+    - node_mode: {{ node.get('node_mode','Normal') }}
+    - ret_strategy: {{ node.get('ret_strategy','Always') }}
+    - labels: {{ node.get('labels',[]) }}
+{% endfor %}
+
+{% for node_name, label in client.get("label",{}).iteritems() %}
+label_for_{{ node_name }}:
+  jenkins_node.label:
+    - name: {{ node_name }}
+    - lbl_text: {{ label.lbl_text }}
+    - append: {{ label.get('append', False) }}
+{% endfor %}
\ No newline at end of file
diff --git a/jenkins/map.jinja b/jenkins/map.jinja
index 27c33bd..678bdc3 100644
--- a/jenkins/map.jinja
+++ b/jenkins/map.jinja
@@ -1,14 +1,14 @@
 
 {% set master = salt['grains.filter_by']({
     'Debian': {
-        'pkgs': ['jenkins','python-bcrypt'],
+        'pkgs': ['jenkins','python-bcrypt']
         'service': 'jenkins',
         'config': '/etc/default/jenkins',
         'home': '/var/lib/jenkins',
         'update_site_url': 'http://updates.jenkins-ci.org/update-center.json'
     },
     'RedHat': {
-        'pkgs': ['jenkins'],
+        'pkgs': ['jenkins','python-bcrypt'],
         'service': 'jenkins',
         'config': '/etc/sysconfig/jenkins',
         'home': '/var/lib/jenkins',
@@ -71,7 +71,7 @@
 
 {% set client = salt['grains.filter_by']({
     'default': {
-        'pkgs': ['python-jenkins'],
+        'pkgs': ['python-jenkins', 'python-bcrypt'],
         'job_status': job_status,
         'dir': {
             'jenkins_source_root': '/var/cache/salt/minion/jenkins/source',