Merge "Revert "Add internal dependencies in state jenkins.client""
diff --git a/README.rst b/README.rst
index 69a295d..09d860b 100644
--- a/README.rst
+++ b/README.rst
@@ -70,6 +70,7 @@
jenkins:
master:
mode: EXCLUSIVE
+ java_args: -Xms256m -Xmx1g
# Do not manage config.xml from Salt, use UI instead
no_config: true
slaves:
@@ -451,12 +452,16 @@
jenkins:
client:
+ plugin_remove_unwanted: false
+ plugin_force_remove: false
plugin:
- swarm:
- restart: false
- hipchat:
+ plugin1: 1.2.3
+ plugin2:
+ plugin3: {}
+ plugin4:
+ version: 3.2.1
enabled: false
- restart: true
+ plugin5: absent
Adding plugin params to job
@@ -476,11 +481,7 @@
categories:
- my_throuttle_category
plugin:
- swarm:
- restart: false
- hipchat:
- enabled: false
- restart: true
+ throttle-concurrents:
LDAP configuration (depends on LDAP plugin)
diff --git a/_modules/jenkins_common.py b/_modules/jenkins_common.py
index 377a71f..225042f 100644
--- a/_modules/jenkins_common.py
+++ b/_modules/jenkins_common.py
@@ -111,10 +111,14 @@
tokenReq = requests.get("%s/crumbIssuer/api/json" % jenkins_url,
auth=(jenkins_user, jenkins_password) if jenkins_user else None)
if tokenReq.status_code == 200:
+ logger.debug("Got Jenkins API crumb: %s", tokenReq.json())
return tokenReq.json()
- elif tokenReq.status_code in [404, 401]:
+ elif tokenReq.status_code in [404, 401, 502, 503]:
# 404 means CSRF security is disabled, so api crumb is not necessary,
# 401 means unauthorized
+ # 50x means jenkins is unavailabe - fail in call_groovy_script, but
+ # not here, to handle exception in state
+ logger.debug("Got error %s: %s", str(tokenReq.status_code), tokenReq.reason)
return None
else:
raise Exception("Cannot obtain Jenkins API crumb. Status code: %s. Text: %s" %
diff --git a/_states/jenkins_plugin.py b/_states/jenkins_plugin.py
index 0f80d51..a96afed 100644
--- a/_states/jenkins_plugin.py
+++ b/_states/jenkins_plugin.py
@@ -1,81 +1,9 @@
+import json
import logging
+import time
-logger = logging.getLogger(__name__)
-install_plugin_groovy = """\
-import jenkins.model.*
-import java.util.logging.Logger
-
-def logger = Logger.getLogger("")
-def installed = false
-def exists = false
-def pluginName="${plugin}"
-def instance = Jenkins.getInstance()
-def pm = instance.getPluginManager()
-def uc = instance.getUpdateCenter()
-def needUpdateSites(maxOldInSec = 1800){
- long oldestTs = 0
- for (UpdateSite s : Jenkins.instance.updateCenter.siteList) {
- if(oldestTs == 0 || s.getDataTimestamp()<oldestTs){
- oldestTs = s.getDataTimestamp()
- }
- }
- return (System.currentTimeMillis()-oldestTs)/1000 > maxOldInSec
-}
-
-if (!pm.getPlugin(pluginName)) {
- if(needUpdateSites()) {
- uc.updateAllSites()
- }
- def plugin = uc.getPlugin(pluginName)
- if (plugin) {
- plugin.deploy()
- installed = true
- }
-}else{
- exists = true
- print("EXISTS")
-}
-if (installed) {
- instance.save()
- if(${restart}){
- instance.doSafeRestart()
- }
- print("INSTALLED")
-}else if(!exists){
- print("FAILED")
-}
-""" # noqa
-
-remove_plugin_groovy = """
-import jenkins.model.*
-import java.util.logging.Logger
-
-def logger = Logger.getLogger("")
-def installed = false
-def initialized = false
-
-def pluginName="${plugin}"
-def instance = Jenkins.getInstance()
-def pm = instance.getPluginManager()
-
-def actPlugin = pm.getPlugin(pluginName)
-if (!actPlugin) {
- def pluginToInstall = Jenkins.instance.updateCenter.getPlugin(pluginName)
- if(!pluginToInstall){
- print("FAILED")
- }else{
- print("NOT PRESENT")
- }
-} else {
- actPlugin.disable()
- actPlugin.archive.delete()
- if({restart}){
- instance.doSafeRestart()
- }
- print("REMOVED")
-}
-""" # noqa
+log = logging.getLogger(__name__)
def __virtual__():
@@ -85,63 +13,46 @@
if 'jenkins_common.call_groovy_script' not in __salt__:
return (
False,
- 'The jenkins_plugin state module cannot be loaded: '
+ 'The jenkins_node state module cannot be loaded: '
'jenkins_common not found')
return True
-
-def present(name, restart=False):
+def managed(name, plugins, remove_unwanted=False, force_remove=False):
"""
- Jenkins plugin present state method, for installing plugins
+ Manage jenkins plugins
- :param name: plugin name
- :param restart: do you want to restart jenkins after plugin install?
+ :param name: salt resource name (usually 'jenkins_plugin_manage')
+ :param plugins: map containing plugin names and parameters
+ :param remove_unwanted: whether to remove not listed plugins
+ :param force_remove: force removing plugins recursively with all dependent plugins
:returns: salt-specified state dict
"""
- return _plugin_call(name, restart, install_plugin_groovy, [
- "INSTALLED", "EXISTS"])
+ log.info('Managing jenkins plugins')
+ template = __salt__['jenkins_common.load_template'](
+ 'salt://jenkins/files/groovy/plugin.template',
+ __env__)
+ result = __salt__['jenkins_common.api_call'](name, template,
+ [ 'UPDATED', 'NO CHANGES' ],
+ {
+ 'plugin_list': json.dumps(plugins),
+ 'clean_unwanted': remove_unwanted,
+ 'force_remove': force_remove
+ },
+ 'Manage Jenkins plugins')
+ log.debug('Got result: ' + json.dumps(result))
+ log.info('Checking if restart is required...')
+ # While next code is successful, we should wait for jenkins shutdown
+ # either:
+ # - false returned by isQuietingDown()
+ # - any error meaning that jenkins is unavailable (restarting)
+ wait = { 'result': True }
+ while (wait['result']):
+ wait = __salt__['jenkins_common.api_call']('jenkins_restart_wait',
+ 'println Jenkins.instance.isQuietingDown()', [ 'true' ], {},
+ 'Wait for jenkins restart')
+ if (wait['result']):
+ log.debug('Jenkins restart is required. Waiting...')
+ time.sleep(5)
-def absent(name, restart=False):
- """
- Jenkins plugin absent state method, for removing plugins
-
- :param name: plugin name
- :param restart: do you want to restart jenkins after plugin remove?
- :returns: salt-specified state dict
- """
- return _plugin_call(name, restart, remove_plugin_groovy, [
- "REMOVED", "NOT PRESENT"])
-
-
-def _plugin_call(name, restart, template, success_msgs):
- 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'] = 'Jenkins plugin %s %s' % (name, status.lower())
- else:
- call_result = __salt__['jenkins_common.call_groovy_script'](
- template, {"plugin": name, "restart": "true" if restart else "false"})
- 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'] = 'Jenkins plugin %s %s%s' % (name, status.lower(
- ), ", jenkins restarted" if status == success_msgs[0] and restart else "")
- result = True
- else:
- status = 'FAILED'
- logger.error(
- "Jenkins plugin API call failure: %s", call_result["msg"])
- ret['comment'] = 'Jenkins plugin API call failure: %s' % (call_result[
- "msg"])
- ret['result'] = None if test else result
- return ret
+ return result
diff --git a/jenkins/client/plugin.sls b/jenkins/client/plugin.sls
index f511ec0..bb888d2 100644
--- a/jenkins/client/plugin.sls
+++ b/jenkins/client/plugin.sls
@@ -1,15 +1,19 @@
{% from "jenkins/map.jinja" import client with context %}
-{% for name, plug in client.get('plugin',{}).iteritems() %}
-{% if plug.get('enabled', True) %}
-plugin_{{ name }}:
- jenkins_plugin.present:
- - name: {{ plug.get('name', name) }}
- - restart: {{ plug.get('restart', False) }}
-{% else %}
-plugin_{{ name }}_disable:
- jenkins_plugin.absent:
- - name: {{ plug.get('name', name) }}
- - restart: {{ plug.get('restart', False) }}
-{% endif %}
-{% endfor %}
+{%- if client.plugin is defined %}
+jenkins_plugins:
+ jenkins_plugin.managed:
+ - plugins: {{ client.plugin }}
+ - remove_unwanted: {{ client.get('plugin_remove_unwanted', False) }}
+ - force_remove: {{ client.get('plugin_force_remove', False) }}
+jenkins_wait_functional:
+ cmd.script:
+ - source: salt://jenkins/files/wait4jenkins.sh
+ - shell: /bin/bash
+ - env:
+ - JENKINS_URL: "{{ client.master.get('proto', 'http') }}://{{ client.master.get('host', 'localhost') }}:{{ client.master.get('port', '8080') }}/login"
+ - WAIT_TIME: "300"
+ - INTERVAL: "5"
+ - require:
+ - jenkins_plugins
+{%- endif %}
diff --git a/jenkins/files/groovy/plugin.template b/jenkins/files/groovy/plugin.template
new file mode 100644
index 0000000..24591c4
--- /dev/null
+++ b/jenkins/files/groovy/plugin.template
@@ -0,0 +1,163 @@
+#!groovy
+
+import groovy.json.JsonSlurper
+
+// List of plugins to manage
+String pluginListJson = '''${plugin_list}'''
+LinkedHashMap pluginList = new JsonSlurper().parseText(pluginListJson)
+
+// Whether to remove plugins not listed in pluginList
+boolean cleanUnwanted = "${clean_unwanted}".toBoolean()
+
+// Whether to recursively remove plugins with all dependent plugins
+boolean forceRemove = "${force_remove}".toBoolean()
+
+// Removing and enabling/disabling plugins doesn't set isRestartRequiredForCompletion()
+boolean needRestart = false
+
+def pm = Jenkins.instance.pluginManager
+def uc = Jenkins.instance.updateCenter
+
+ArrayList pluginsToInstall = []
+ArrayList pluginsToRemove = []
+
+LinkedHashMap allPluginDeps = [:]
+
+// Compare version strings and return
+// -1 if first parameter is less than second one
+// 0 if both parameters are equal
+// 1 if first parameter is greather than second one
+int versionCmp(String versionOne, String versionTwo){
+
+ List verA = versionOne.tokenize('.')
+ List verB = versionTwo.tokenize('.')
+
+ int commonIndices = Math.min(verA.size(), verB.size())
+
+ for (int i = 0; i < commonIndices; ++i){
+ int numA = verA[i].toInteger()
+ int numB = verB[i].toInteger()
+ if (numA != numB) {
+ return numA <=> numB
+ }
+ }
+
+ // If we got this far then all the common indices are identical, so whichever version is longer must be more recent
+ return verA.size() <=> verB.size()
+}
+
+// Return all dependency plugins for the specified one (recursiverly)
+LinkedHashMap getPluginDeps(String pluginName, String pluginVersion = null){
+ LinkedHashMap pluginDeps = [:]
+ def pluginToProbe = Jenkins.instance.updateCenter.getPlugin(pluginName)
+ pluginToProbe.dependencies.each { p, v ->
+ pluginDeps << getPluginDeps(p, v)
+ }
+ // FIXME: need to get minimal version because Jenkins senses specified version as 'at least'
+ pluginDeps[pluginName] = pluginVersion
+ return pluginDeps
+}
+
+// Remove plugin with all dependent plugins (!!! DANGEROUS !!!)
+ArrayList<String> removePluginRecursive(String pluginName, boolean force = false){
+ ArrayList removedPlugins = []
+ def pluginToRemove = Jenkins.instance.pluginManager.getPlugin(pluginName)
+ if (pluginToRemove) {
+ // Get plugin dependants
+ Set pluginsDependent = pluginToRemove.getDependants()
+ // ... and remove it all
+ if (pluginsDependent && force){
+ pluginsDependent.each { removedPlugins += removePluginRecursive(it) }
+ }
+ // Remove requested plugin if it is still not deleted
+ // and if requested plugin does not have dependants
+ if (! pluginToRemove.isDeleted() && ! pluginToRemove.getDependants()) {
+ pluginToRemove.doDoUninstall()
+ removedPlugins << pluginName
+ }
+ }
+ return removedPlugins
+}
+
+String pluginName
+def pluginInfo
+def pluginAvailable
+def pluginInstalled
+
+for (plugin in pluginList) {
+ pluginName = plugin.key
+ pluginInfo = plugin.value ?: new LinkedHashMap()
+
+ // Guess contents of pluginInfo if it is string
+ if (pluginInfo.getClass() == String){
+ if (pluginInfo == 'absent') {
+ // remove plugin
+ pluginsToRemove << pluginName
+ continue
+ } else if (pluginInfo ==~ /^[0-9.]+$/){
+ // pluginInfo is version
+ pluginInfo = new LinkedHashMap([ version: pluginInfo ])
+ } else {
+ // clean incorrect pluginInfo
+ pluginInfo = new LinkedHashMap()
+ }
+ }
+
+ pluginAvailable = uc.getPlugin(pluginName)
+ pluginInstalled = pluginAvailable.getInstalled()
+ // If plugin installed
+ if (pluginInstalled){
+ // ... and pluginInfo contains version
+ if (pluginInfo.get('version')){
+ // ... and installed plugin version is lower than version from pluginInfo
+ if (versionCmp(pluginInstalled.getVersion(), pluginInfo.get('version')) < 0){
+ // upgrade plugin
+ pluginsToInstall << pluginAvailable
+ }
+ }
+ // ... plugin is active and pluginInfo has enable=false
+ if (pluginInstalled.isActive() && ! pluginInfo.get('enabled', true).toBoolean()){
+ // disable plugin
+ pluginInstalled.disable()
+ needRestart = true
+ }
+ // ... plugin is not active and pluginInfo has enabled=true
+ if (! pluginInstalled.isActive() && pluginInfo.get('enabled', true).toBoolean()){
+ // enable plugin
+ pluginInstalled.enable()
+ needRestart = true
+ }
+ } else {
+ // install plugin
+ pluginsToInstall << pluginAvailable
+ }
+
+ // Collect all plugin dependencies to decide about unwanted installed plugins
+ allPluginDeps << getPluginDeps(pluginName, pluginInfo ? pluginInfo.get('version') : null)
+}
+
+// Deploy plugins by list if any
+pluginsToInstall.each {
+ it.deploy(false).get()
+}
+
+// Remove unwanted plugins
+if (cleanUnwanted){
+ pluginsToRemove += pm.getPlugins()*.getShortName()
+}
+
+// Remove plugins by list if any keeping required by some others
+(pluginsToRemove - allPluginDeps.keySet()).each {
+ if (removePluginRecursive(it, forceRemove)) {
+ needRestart = true
+ }
+}
+
+// Restart Jenkins if needed
+if (uc.isRestartRequiredForCompletion() || needRestart){
+ Jenkins.instance.doSafeRestart()
+ println 'UPDATED'
+} else {
+ // No changes
+ println 'NO CHANGES'
+}
diff --git a/jenkins/files/jenkins b/jenkins/files/jenkins
index 022627b..e311282 100644
--- a/jenkins/files/jenkins
+++ b/jenkins/files/jenkins
@@ -1,3 +1,4 @@
+{%- from "jenkins/map.jinja" import master with context -%}
# defaults for jenkins continuous integration server
# pulled in from the init script; makes things easier.
@@ -8,7 +9,7 @@
# arguments to pass to java
#JAVA_ARGS="-Xmx256m"
-JAVA_ARGS="-Djava.net.preferIPv4Stack=true -Djenkins.install.runSetupWizard=false" # make jenkins listen on IPv4 address and disable setup wizard
+JAVA_ARGS="{{ master.get('java_args', '') }} -Djava.net.preferIPv4Stack=true -Djenkins.install.runSetupWizard=false" # make jenkins listen on IPv4 address and disable setup wizard
PIDFILE=/var/run/jenkins/jenkins.pid
diff --git a/jenkins/files/wait4jenkins.sh b/jenkins/files/wait4jenkins.sh
new file mode 100755
index 0000000..f8eae77
--- /dev/null
+++ b/jenkins/files/wait4jenkins.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+: "${JENKINS_URL:=http://localhost:8080}"
+: "${WAIT_TIME:=300}"
+: "${INTERVAL:=5}"
+
+while [ $((WAIT_TIME -= INTERVAL)) -ge 0 ]; do
+ if curl -fs -o /dev/null "$JENKINS_URL"; then
+ STATUS=0
+ break
+ fi
+ sleep $INTERVAL
+done
+
+exit ${STATUS:-124}
diff --git a/jenkins/master/service.sls b/jenkins/master/service.sls
index 635ba17..845cff0 100644
--- a/jenkins/master/service.sls
+++ b/jenkins/master/service.sls
@@ -88,9 +88,14 @@
- file: {{ master.home }}/hudson.model.UpdateCenter.xml
jenkins_service_running:
- cmd.wait:
- - name: "i=0; while true; do curl -s -f http://localhost:{{ master.http.port }}/login >/dev/null && exit 0; [ $i -gt 60 ] && exit 1; sleep 5; done"
- - watch:
- - service: jenkins_master_service
+ cmd.script:
+ - source: salt://jenkins/files/wait4jenkins.sh
+ - shell: /bin/bash
+ - env:
+ - JENKINS_URL: "http://localhost:{{ master.http.port }}/login"
+ - WAIT_TIME: "300"
+ - INTERVAL: "5"
+ - onchanges:
+ - service: jenkins_master_service
{%- endif %}