Ansible Amenities: Manage Jenkins Nodes with Ansible and the Jenkins API
Ansible Amenities is a series of semi-regularly published posts documenting different uses of Ansible in the spirit of Hubert Klein Ikkinks Groovy Goodness.
At work we’re replacing and rebuilding a lot of heterogenously configured systems. One of the recurring tasks was creating Nodes in Jenkins. Once again, Python and Ansible are a great help developing automation for an annoying task.
Jenkins API in Python
Working with the Jenkins API in Python works much like Gitlab.
import jenkins
username = "automation"
password = "token"
jenkins_url = "jenkins.example.org"
jenkins_api = jenkins.Jenkins(jenkins_url, username=username, password=password)
To find out if a Jenkins Node already exists we can ask Jenkins:
name = "my-node"
jenkins_api.node_exists(name)
Which should return False unless that node was already set up.
Sometimes Nodes aren’t used for a while or are out of service, so we don’t use them and can thus disable the node
name = "my-node"
jenkins_api.disable_node(name)
Enabling nodes works just the same way
name = "my-node"
jenkins_api.enable_node(self.name)
Creating nodes allows us to tell a bunch of things about the node to Jenkins.
name = "my-node"
jenkins_api.create_node(name,
numExecutors=1,
nodeDescription=description,
labels=labels,
remoteFS=remoteFS,
exclusive=exclusive,
launcher=launcher_type,
launcher_params=launcher_params)
launcher_type is based on the Jenkins launcher definitions:
jenkins.LAUNCHER_SSH
jenkins.LAUNCHER_COMMAND
jenkins.LAUNCHER_JNLP
jenkins.LAUNCHER_WINDOWS_SERVICE
Each designating a different way of connecting and interacting with the Jenkins node.
remoteFStells Jenkins the path to the workspace it will work in on the node and where the Agent is supposed to run.numExecutors=1limits the amount of jobs run at the same time. The Jenkins documentation describes this really well.labelsapplies a collection of labels to the node that Jenkins will refer to in the Pipelines executed.exclusivedesignates that the node will only be used for the job assigned to it and no other jobs.launcher_paramsdescribe the options passed to the Jenkins agent when starting it. In our case the Jenkins Agent is launched via SSH so most of the time we’re using the following params:port: Port number to connect to, which for SSH would be22username: User to connect as on the nodecredentialsId: ID of credentials to use to authenticate with, which should be in your Jenkins credentials collectionhost: Hostname to connect to (In our case we know this through Ansible facts or variablesjavaPath: Path to Jenkins’ JAVA. This needs to point to a valid Java distribution so usually your JAVA_HOME if you use a compatible Java version
Ansible Module
import traceback
from time import sleep
JENKINS_IMP_ERR = None
try:
import jenkins
python_jenkins_installed = True
except ImportError:
JENKINS_IMP_ERR = traceback.format_exc()
python_jenkins_installed = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native
class JenkinsNodes:
def __init__(self, module):
self.jenkins_str_to_launcher_map = dict(ssh=jenkins.LAUNCHER_SSH,
cmd=jenkins.LAUNCHER_COMMAND,
jnlp=jenkins.LAUNCHER_JNLP,
win=jenkins.LAUNCHER_WINDOWS_SERVICE)
self.module = module
self.jenkins_url=module.params.get('jenkins_url')
self.username=module.params.get('username')
self.password=module.params.get('password')
self.token=module.params.get('token')
self.name=module.params.get('name')
self.args=module.params.get('args')
self.description=module.params.get('description')
self.state=module.params.get('state')
self.exclusive=module.params.get('exclusive')
self.labels=module.params.get('labels')
self.launcher_params=module.params.get('launcher_params')
self.server = self.get_jenkins_connection()
self.remoteFS = module.params.get("remoteFS")
try:
self.launcher_type=self.jenkins_str_to_launcher_map.get(module.params.get('launcher_type'))
except Exception as e:
self.module.fail_json(msg='Failed to lookup launchertype "%s" in launcher type map, %s' % (module.params.get('launcher_type'), to_native(e)))
self.result = {
'changed': False,
'url': self.jenkins_url,
'name': self.name,
'user': self.username,
'state': self.state,
}
def get_jenkins_connection(self):
try:
if (self.username and self.password):
return jenkins.Jenkins(self.jenkins_url, username=self.username, password=self.password)
elif (self.username and self.token):
return jenkins.Jenkins(self.jenkins_url, username=self.username, password=self.token)
elif (self.username and not (self.password or self.token)):
return jenkins.Jenkins(self.jenkins_url, username=self.username)
else:
return jenkins.Jenkins(self.jenkins_url)
except Exception as e:
self.module.fail_json(msg='Unable to connect to Jenkins server, %s' % to_native(e))
def create_node(self):
try:
if self.server.node_exists(self.name):
self.module.fail_json(msg='Failed to create node "%s". It already exists' % self.name)
return dict(Changed=False,
url=self.jenkins_url,
name=self.name)
self.server.create_node(self.name,
numExecutors=1,
nodeDescription=self.description,
labels=self.labels,
remoteFS=self.remoteFS,
exclusive=self.exclusive,
launcher=self.launcher_type,
launcher_params=self.launcher_params)
return dict(Changed=True,
url=self.jenkins_url,
name=self.name,
numExecutors=1,
nodeDescription=self.description,
labels=self.labels,
exclusive=self.exclusive,
launcher=self.launcher_type,
launcher_params=self.launcher_params)
except Exception as e:
self.module.fail_json(msg='Failed to create node, %s' % to_native(e))
def delete_node(self):
try:
if not self.server.node_exists(self.name):
self.module.fail_json(msg='Failed to delete node "%s". It does not exist' % self.name)
return dict(Changed=False,
url=self.jenkins_url,
name=self.name)
self.server.delete_node(self.name)
return dict(Changed=True,
url=self.jenkins_url,
name=self.name)
except Exception as e:
self.module.fail_json(msg='Failed to delete node, %s' % to_native(e))
def enable_node(self):
try:
self.server.enable_node(self.name)
return dict(Changed=True,
url=self.jenkins_url,
name=self.name)
except Exception as e:
self.module.fail_json(msg='Failed to enable node, %s' % to_native(e))
def disable_node(self):
try:
self.server.disable_node(self.name)
return dict(Changed=True,
url=self.jenkins_url,
name=self.name)
except Exception as e:
self.module.fail_json(msg='Failed to disable node, %s' % to_native(e))
def test_dependencies(module):
if not python_jenkins_installed:
module.fail_json(
msg=missing_required_lib("python-jenkins",
url="https://python-jenkins.readthedocs.io/en/latest/install.html"),
exception=JENKINS_IMP_ERR)
def main():
module = AnsibleModule(
argument_spec=dict(
jenkins_url=dict(type='str'),
username=dict(type='str'),
password=dict(type='str', no_log=True),
token=dict(type='str', no_log=True),
name=dict(type='str', required=True),
description=dict(type='str'),
exclusive=dict(type='bool'),
labels=dict(type='str'),
remoteFS=dict(type='str',default="/var/lib/jenkins"),
launcher_params=dict(type='dict'),
state=dict(required=False, choices=['present', 'absent', 'enabled', 'disabled'], default='present'),
launcher_type=dict(required=False, choices=['ssh','cmd','jnlp','win'])
),
mutually_exclusive=[['password', 'token']]
)
test_dependencies(module)
jenkins_node = JenkinsNodes(module)
result = dict()
if module.params.get('state') == 'present':
result = jenkins_node.create_node()
elif module.params.get('state') == 'absent':
result = jenkins_node.delete_node()
elif module.params.get('state') == 'enabled':
result = jenkins_node.enable_node()
elif module.params.get('state') == 'disabled':
result = jenkins_node.disable_node()
module.exit_json(**result)
if __name__ == '__main__':
main()
With the script in your Ansible module path you can work with it in your playbooks.
Delete a Node from the Jenkins Server
- name: Delete Node
jenkins_nodes:
name: MyNode
jenkins_url: {{ jenkins_url }}
username: {{ username }}
password: {{ password }}
state: absent
Enable a Node on Jenkins Server
- name: Enable Node
jenkins_nodes:
name: MyNode
jenkins_url: {{ jenkins_url }}
username: {{ username }}
password: {{ password }}
state: enabled
Disable a Node on Jenkins Server
- name: Enable Node
jenkins_nodes:
name: MyNode
jenkins_url: {{ jenkins_url }}
username: {{ username }}
password: {{ password }}
state: disabled
Create a Node on Jenkins:
- name: Enable Node
jenkins_nodes:
name: MyNode
jenkins_url: {{ jenkins_url }}
username: {{ username }}
password: {{ password }}
launcher_type: 'ssh'
remoteFS: "/home/jenkins"
launcher_params:
port: '22'
username: 'jenkins'
credentialsId: 'abcdef'
host: '{{ inventory_hostname }}'
exclusive: false
labels:
- build-server
state: present
We use this at the end of our instance setup to get the server online and ready to receive deployments from Jenkins.