First version of jenkins nodes enforcing.
Fixed python-bcrypt dependency definition.
Fixed plurals in state file names.
diff --git a/README.rst b/README.rst
index 1d20ec7..896a95b 100644
--- a/README.rst
+++ b/README.rst
@@ -306,6 +306,85 @@
             - 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
+              label: 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
+              label: 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 +407,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/_states/jenkins_credentials.py b/_states/jenkins_credential.py
similarity index 100%
rename from _states/jenkins_credentials.py
rename to _states/jenkins_credential.py
diff --git a/_states/jenkins_node.py b/_states/jenkins_node.py
new file mode 100644
index 0000000..b80fd4d
--- /dev/null
+++ b/_states/jenkins_node.py
@@ -0,0 +1,139 @@
+import logging
+logger = logging.getLogger(__name__)
+
+
+create_node_groovy = u"""\
+import jenkins.model.*
+import hudson.model.*
+import hudson.slaves.*
+import hudson.plugins.sshslaves.*
+import java.util.ArrayList;
+import hudson.slaves.EnvironmentVariablesNodeProperty.Entry;
+
+  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 "{name}"
+"""  # 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="", label="", 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 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:
+        launcher_string = "new JNLPLauncher()"
+        if launcher:
+            if launcher["type"] == "ssh":
+                launcher_string = 'new SSHLauncher("{}",{},"{}","{}","","","","","")'.format(
+                    launcher["host"], launcher["port"], launcher["username"], launcher["password"])
+            elif launcher["type"] == "jnlp":
+                launcher_string = "new JNLPLauncher()"
+
+        call_result = __salt__['jenkins_common.call_groovy_script'](
+            create_node_groovy,
+            {"name": name,
+                "desc": desc,
+                "label": label,
+                "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"].strip() == name:
+            status = "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_users.py b/_states/jenkins_user.py
similarity index 100%
rename from _states/jenkins_users.py
rename to _states/jenkins_user.py
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..d618a2f
--- /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') }}
+    - label: {{ node.get('label','') }}
+{% 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