Merge "Removed hardcode in the LDAP server name"
diff --git a/_grains/jenkins_plugins.py b/_grains/jenkins_plugins.py
new file mode 100644
index 0000000..f5f90dc
--- /dev/null
+++ b/_grains/jenkins_plugins.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+try:
+    import jenkins
+    HAS_JENKINS = True
+except ImportError:
+    HAS_JENKINS = False
+import salt.config
+
+def main():
+    if not HAS_JENKINS:
+        return {}
+
+    output = { "jenkins_plugins" : {} }
+    opts = salt.config.minion_config('/etc/salt/minion')
+    try:
+        url = opts['jenkins']['url']
+    except KeyError:
+        return {}
+
+    try:
+        user = opts['jenkins']['user']
+        password = opts['jenkins']['password']
+    except KeyError:
+        user = None
+        password = None
+
+    server = jenkins.Jenkins(url, username=user, password=password)
+    plugins = server.get_plugins(depth=1)
+    for plugin_name, plugin_dict in plugins.iteritems():
+        output["jenkins_plugins"][plugin_name[0]] = {"version" : (plugin_dict["version"] or 0)}
+    return output
diff --git a/_states/jenkins_job.py b/_states/jenkins_job.py
index 74be81c..6f72d9d 100644
--- a/_states/jenkins_job.py
+++ b/_states/jenkins_job.py
@@ -11,6 +11,14 @@
 
 # Import XML parser
 import xml.etree.ElementTree as ET
+import hashlib
+
+# Jenkins
+try:
+  import jenkins
+  HAS_JENKINS = True
+except ImportError:
+  HAS_JENKINS = False
 
 log = logging.getLogger(__name__)
 
@@ -19,7 +27,7 @@
     '''
     Only load if jenkins_common module exist.
     '''
-    if 'jenkins_common.call_groovy_script' not in __salt__:
+    if HAS_JENKINS and 'jenkins_common.call_groovy_script' not in __salt__:
         return (
             False,
             'The jenkins_job state module cannot be loaded: '
@@ -28,17 +36,7 @@
 
 
 def _elements_equal(e1, e2):
-    if e1.tag != e2.tag:
-        return False
-    if e1.text != e2.text:
-        return False
-    if e1.tail != e2.tail:
-        return False
-    if e1.attrib != e2.attrib:
-        return False
-    if len(e1) != len(e2):
-        return False
-    return all(_elements_equal(c1, c2) for c1, c2 in zip(e1, e2))
+    return hashlib.md5(e1).hexdigest() == hashlib.md5(e2).hexdigest()
 
 
 def present(name,
@@ -63,16 +61,27 @@
         ret['changes'][name] = status
         ret['comment'] = 'Job %s %s' % (name, status.lower())
     else:
-        _job_exists = __salt__['jenkins.job_exists'](name)
-        if _job_exists:
+        _current_job_config = ''
+        _job_exists = True
+        try:
             _current_job_config = __salt__['jenkins.get_job_config'](name)
+        except salt.exceptions.SaltInvocationError as e:
+            if 'does not exists.' in str(e):
+                _job_exists = False
+            else:
+                raise e
+
+
+        if _job_exists:
             buf = six.moves.StringIO(_current_job_config)
-            oldXML = ET.fromstring(buf.read())
+            oldXMLstring = buf.read()
 
             cached_source_path = __salt__['cp.cache_file'](config, __env__)
             with salt.utils.fopen(cached_source_path) as _fp:
-                newXML = ET.fromstring(_fp.read())
-            if not _elements_equal(oldXML, newXML):
+                newXMLstring = _fp.read()
+            if not _elements_equal(oldXMLstring.strip(), newXMLstring.strip()):
+                oldXML = ET.fromstring(oldXMLstring)
+                newXML = ET.fromstring(newXMLstring)
                 diff = difflib.unified_diff(
                     ET.tostringlist(oldXML, encoding='utf8', method='xml'),
                     ET.tostringlist(newXML, encoding='utf8', method='xml'), lineterm='')
diff --git a/jenkins/files/jobs/_common.xml b/jenkins/files/jobs/_common.xml
index b93135f..e62d08b 100644
--- a/jenkins/files/jobs/_common.xml
+++ b/jenkins/files/jobs/_common.xml
@@ -12,9 +12,13 @@
     <org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/>
     {%- endif %}
     {%- if job.get('scm', {}).github is defined %}
-    <com.coravy.hudson.plugins.github.GithubProjectProperty plugin="github@1.21.1">
+    <com.coravy.hudson.plugins.github.GithubProjectProperty plugin="github@{{ salt['grains.get']('jenkins_plugins:github:version', '1.21.1') }}">
       <projectUrl>{{ job.scm.github.url }}</projectUrl>
-      <displayName>{{ job.scm.github.name|default("") }}</displayName>
+      {%- if job.scm.github.name is defined %}
+      <displayName>{{ job.scm.github.name }}</displayName>
+      {%- else %}
+      <displayName/>
+      {%- endif %}
     </com.coravy.hudson.plugins.github.GithubProjectProperty>
     {%- endif %}
     <org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
@@ -23,7 +27,7 @@
         {%- if trigger.enabled|default(True) %}
         {%- if type == 'reverse' %}
         <jenkins.triggers.ReverseBuildTrigger>
-          <spec></spec>
+          <spec/>
           <upstreamProjects>{{ trigger.projects|join(',') if trigger.projects is defined else trigger.project }}</upstreamProjects>
           <threshold>
             <name>{{ trigger.state|default('SUCCESS') }}</name>
@@ -33,8 +37,8 @@
           </threshold>
         </jenkins.triggers.ReverseBuildTrigger>
         {%- elif type == 'github' %}
-        <com.cloudbees.jenkins.GitHubPushTrigger plugin="github@1.21.1">
-          <spec></spec>
+        <com.cloudbees.jenkins.GitHubPushTrigger plugin="github@{{ salt['grains.get']('jenkins_plugins:github:version', '1.21.1') }}">
+          <spec/>
         </com.cloudbees.jenkins.GitHubPushTrigger>
         {%- elif type == 'pollscm' %}
         <hudson.triggers.SCMTrigger>
@@ -42,8 +46,8 @@
           <ignorePostCommitHooks>false</ignorePostCommitHooks>
         </hudson.triggers.SCMTrigger>
         {%- elif type == 'gerrit' %}
-        <com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritTrigger plugin="gerrit-trigger@2.23.0">
-          <spec></spec>
+        <com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritTrigger plugin="gerrit-trigger@{{ salt['grains.get']('jenkins_plugins:gerrit-trigger:version', '2.23.0') }}">
+          <spec/>
           <gerritProjects>
             {%- for pname, project in trigger.project.iteritems() %}
             <com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.data.GerritProject>
@@ -72,21 +76,56 @@
             <onNotBuilt>{%- if "not_built" in trigger.get('skip_vote',[]) %}true{%- else %}false{%- endif %}</onNotBuilt>
           </skipVote>
           <silentMode>{{ trigger.get('silent_mode', False)|lower }}</silentMode>
-          <notificationLevel></notificationLevel>
+          <notificationLevel/>
           <silentStartMode>{{ trigger.get('silent_start_mode', False)|lower }}</silentStartMode>
           <escapeQuotes>true</escapeQuotes>
           <nameAndEmailParameterMode>PLAIN</nameAndEmailParameterMode>
-          <dependencyJobsNames></dependencyJobsNames>
+          <dependencyJobsNames/>
           <commitMessageParameterMode>BASE64</commitMessageParameterMode>
           <changeSubjectParameterMode>PLAIN</changeSubjectParameterMode>
           <commentTextParameterMode>BASE64</commentTextParameterMode>
-          <buildStartMessage>{{ trigger.get('message', {}).build_start|default("") }}</buildStartMessage>
-          <buildFailureMessage>{{ trigger.get('message', {}).build_failure|default("") }}</buildFailureMessage>
-          <buildSuccessfulMessage>{{ trigger.get('message', {}).build_successful|default("") }}</buildSuccessfulMessage>
-          <buildUnstableMessage>{{ trigger.get('message', {}).build_unstable|default("") }}</buildUnstableMessage>
-          <buildNotBuiltMessage>{{ trigger.get('message', {}).build_not_built|default("") }}</buildNotBuiltMessage>
-          <buildUnsuccessfulFilepath>{{ trigger.get('message', {}).build_unsuccessful_filepath|default("") }}</buildUnsuccessfulFilepath>
-          <customUrl>{{ trigger.custom_url|default('') }}</customUrl>
+          {%- set message = trigger.get('message', {}).build_start|default("") %}
+          {%- if message != "" %}
+          <buildStartMessage>{{ message }}</buildStartMessage>
+          {%- else %}
+          <buildStartMessage/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).build_failure|default("") %}
+          {%- if message != "" %}
+          <buildFailureMessage>{{ message }}</buildFailureMessage>
+          {%- else %}
+          <buildFailureMessage/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).build_successful|default("") %}
+          {%- if message != "" %}
+          <buildSuccessfulMessage>{{ message }}</buildSuccessfulMessage>
+          {%- else %}
+          <buildSuccessfulMessage/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).build_unstable|default("") %}
+          {%- if message != "" %}
+          <buildUnstableMessage>{{ message }}</buildUnstableMessage>
+          {%- else %}
+          <buildUnstableMessage/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).build_not_built|default("") %}
+          {%- if message != "" %}
+          <buildNotBuiltMessage>{{ message }}</buildNotBuiltMessage>
+          {%- else %}
+          <buildNotBuiltMessage/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).build_unsuccessful_filepath|default("") %}
+          {%- if message != "" %}
+          <buildUnsuccessfulFilepath>{{ message }}</buildUnsuccessfulFilepath>
+          {%- else %}
+          <buildUnsuccessfulFilepath/>
+          {%- endif %}
+          {%- set message = trigger.get('message', {}).custom_url|default("") %}
+          {%- if message != "" %}
+          <customUrl>{{ message }}</customUrl>
+          {%- else %}
+          <customUrl/>
+          {%- endif %}
           <serverName>{{ trigger.server|default('__ANY__') }}</serverName>
           <triggerOnEvents>
             {%- for ename, actions in trigger.event.iteritems() %}
@@ -120,12 +159,16 @@
             {%- endfor %}
           </triggerOnEvents>
           <dynamicTriggerConfiguration>false</dynamicTriggerConfiguration>
-          <triggerConfigURL></triggerConfigURL>
+          <triggerConfigURL/>
           <triggerInformationAction/>
         </com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritTrigger>
         {%- elif type == 'timer' and trigger.spec != '' %}
         <hudson.triggers.TimerTrigger>
+          {%- if trigger.spec is defined and trigger.spec != '' %}
           <spec>{{ trigger.spec }}</spec>
+          {%- else %}
+          <spec/>
+          {%- endif %}
         </hudson.triggers.TimerTrigger>
         {%- endif %}
         {%- endif %}
diff --git a/jenkins/files/jobs/_parameters.xml b/jenkins/files/jobs/_parameters.xml
index 1a0100d..0f63ad5 100755
--- a/jenkins/files/jobs/_parameters.xml
+++ b/jenkins/files/jobs/_parameters.xml
@@ -4,7 +4,11 @@
         {%- for param_name, param in job.param.iteritems() %}
         <hudson.model.{{ param.get('type', 'string')|capitalize }}ParameterDefinition>
           <name>{{ param_name }}</name>
-          <description>{{ param.get('description', '')|e }}</description>
+          {%- if param.description is defined %}
+          <description>{{ param.description }}</description>
+          {%- else %}
+          <description/>
+          {%- endif %}
           {%- if param.get('type', 'string')|lower == "choice" %}
           <choices class="java.util.Arrays$ArrayList">
             <a class="string-array">
@@ -14,7 +18,11 @@
             </a>
           </choices>
           {%- endif %}
-          <defaultValue>{{ param.get('default', '')|e }}</defaultValue>
+          {%- if param.default is defined %}
+          <defaultValue>{{ param.get('default') }}</defaultValue>
+          {%- else %}
+          <defaultValue/>
+          {%- endif %}
         </hudson.model.{{ param.get('type', 'string')|capitalize }}ParameterDefinition>
         {%- endfor %}
       </parameterDefinitions>
diff --git a/jenkins/files/jobs/workflow-scm.xml b/jenkins/files/jobs/workflow-scm.xml
index 57c3756..2aca88c 100644
--- a/jenkins/files/jobs/workflow-scm.xml
+++ b/jenkins/files/jobs/workflow-scm.xml
@@ -2,13 +2,11 @@
 {%- if job is not defined -%}
   {%- set job = salt['pillar.get']('jenkins:client:job:'+job_name) -%}
 {%- endif -%}
-
-<?xml version='1.0' encoding='UTF-8'?>
-<flow-definition plugin="workflow-job@2.6">
+<?xml version="1.0" encoding="UTF-8"?><flow-definition plugin="workflow-job@{{ salt['grains.get']('jenkins_plugins:workflow-job:version', '2.6') }}">
   {%- include "jenkins/files/jobs/_common.xml" %}
-  <definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps@2.13">
+  <definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps@{{ salt['grains.get']('jenkins_plugins:workflow-cps:version', '2.13') }}">
     {%- if job.scm.get('type', 'git') == 'git' %}
-    <scm class="hudson.plugins.git.GitSCM" plugin="git@2.5.3">
+    <scm class="hudson.plugins.git.GitSCM" plugin="git@{{ salt['grains.get']('jenkins_plugins:git:version', '2.5.3') }}">
       <configVersion>2</configVersion>
       <userRemoteConfigs>
         <hudson.plugins.git.UserRemoteConfig>
diff --git a/jenkins/files/jobs/workflow.xml b/jenkins/files/jobs/workflow.xml
index 0469729..43f97a5 100644
--- a/jenkins/files/jobs/workflow.xml
+++ b/jenkins/files/jobs/workflow.xml
@@ -14,9 +14,9 @@
 {%- endmacro -%}
 
 <?xml version='1.0' encoding='UTF-8'?>
-<flow-definition plugin="workflow-job@2.5">
+<flow-definition plugin="workflow-job@{{ salt['grains.get']('jenkins_plugins:workflow-job:version', '2.5') }}">
   {%- include "jenkins/files/jobs/_common.xml" %}
-  <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.12">
+  <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@{{ salt['grains.get']('jenkins_plugins:workflow-cps:version', '2.12') }}">
     <script>// libraries
 {%- if job.libs is defined %}
 {%- for lib in job.libs %}
diff --git a/jenkins/job_builder.sls b/jenkins/job_builder.sls
index 15ada34..c71ad67 100644
--- a/jenkins/job_builder.sls
+++ b/jenkins/job_builder.sls
@@ -25,7 +25,7 @@
 jenkins_job_builder_install:
   pip.installed:
   - names:
-    - jenkins-job-builder
+    - 'jenkins-job-builder{{ job_builder.source.version|default("") }}'
   - require:
     - pkg: jenkins_job_builder_packages
   - require_in:
@@ -62,7 +62,7 @@
 
 jenkins_job_builder_jobs_update:
   cmd.run:
-  - name: jenkins-jobs update /srv/jenkins/job_builder_config
+  - name: jenkins-jobs update /srv/jenkins/job_builder_config/{{ job_builder.config.path|default("") }}
   - require:
     - git: {{ job_builder.config.address }}
     - file: {{ job_builder.dir.conf }}/jenkins_jobs.ini