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',