Jul 04 2020

Register a Jenkins Slave with Ansible

Published by at 7:21 pm under Ansible

We saw how to add a Jenkins slave automatically in a previous post calling a REST API with Curl. Let’s automate this with Ansible to get a new node ready within minutes!

You will first need a Jenkins user and associated token with the proper rights. Agent connect and create should be sufficient. I’ll simply call that user “node”. Log on Jenkins as this user and add a new token.


The new node details need to be passed on to the REST url. We’ll fit these settings into a Jinja2 template as follow:

{
   "name": "{{ jenkins_node }}",
   "nodeDescription": "slave {{ jenkins_node }}",
   "numExecutors": "{{ jenkins_numExecutors }}",
   "remoteFS": "{{ jenkins_remoteFS }}",
   "labelString": "{{ jenkins_label }}",
   "mode": "EXCLUSIVE",
   "": [
      "hudson.slaves.JNLPLauncher",
      "hudson.slaves.RetentionStrategy$Always"
   ],
   "launcher": {
      "stapler-class": "hudson.slaves.JNLPLauncher",
      "$class": "hudson.slaves.JNLPLauncher",
      "workDirSettings": {
         "disabled": true,
         "workDirPath": "",
         "internalDir": "remoting",
         "failIfWorkDirIsMissing": false
      },
      "tunnel": "",
      "vmargs": ""
   },
   "retentionStrategy": {
      "stapler-class": "hudson.slaves.RetentionStrategy$Always",
      "$class": "hudson.slaves.RetentionStrategy$Always"
   },
   "nodeProperties": {
      "stapler-class-bag": "true",
      "hudson-slaves-EnvironmentVariablesNodeProperty": {
         "env": [
            {
               "key": "JAVA_HOME",
               "value": "{{ java_home }}"
            }
         ]
      },
      "_comment:": {
         "hudson-tools-ToolLocationNodeProperty": {
           "locations": [
               {
                  "key": "hudson.model.JDK$DescriptorImpl@JAVA-8",
                  "home": "/usr/bin/java"
               }
            ]
         }
      }
   }
}

Adapt the template to your needs, if you’d like a SSH agent for instance.

The variables that will be replaced in the template can be defined in the following “default” file:

jenkins_slave_user: jenkins-slave
jenkins_token: xxxxxxde2152xxxxxxaa339exxxxxx48d6
jenkins_user: node
jenkins_url: https://jenkins.domain.lan
jenkins_node: "{{ansible_hostname}}"
jenkins_numExecutors: 4
jenkins_remoteFS: /home/jenkins-slave
jenkins_label: "label_1 label_2 label_3"
java_home: /usr/lib/jvm/java-8-openjdk-amd64/

jenkins_user will connect to the master and create the new node, authenticating with jenkins_token created beforehand. jenkins_slave_user is the system user that will launch Jenkins service on the node.


We can now add Ansible tasks to build our role. We first call the REST API:

- name: create node on Jenkins master
  uri:
    url: "{{jenkins_url}}/computer/doCreateItem?name={{ jenkins_node }}&type=hudson.slaves.DumbSlave"
    method: POST
    body_format: form-urlencoded
    force_basic_auth: yes
    user: "{{ jenkins_user }}"
    password: "{{jenkins_token }}"
    body: "json={{ lookup('template', 'node.json.j2', convert_data=False) }}"
    return_content: yes
    status_code: 200, 302, 400
  register: webpage

I added return code 400 in case the node already exists but you may remove it if you think it’s best. I make it fail if the error is anything else than ‘already exists’:
- name: Go through if agent already exists error
  fail:
  when: >
          webpage.status == '400'
          and 'already exists' not in webpage.x_error

The Jenkins agent service needs a secret from the master to get started. The secret is embedded in the agent’s page in XML format.
- name: get node page content
  uri:
    url: "{{jenkins_url}}/computer/{{jenkins_node}}/slave-agent.jnlp"
    method: POST
    body_format: form-urlencoded
    force_basic_auth: yes
    user: "{{ jenkins_user }}"
    password: "{{ jenkins_token }}"
    return_content: yes
    status_code: 200
  register: slavepage

- name: get secret from xml
  xml:
    xmlstring: "{{slavepage.content}}"
    xpath: /jnlp/application-desc/argument
    content: text
  register: secretxml

It will be stored in the system user’s default file /etc/default/jenkins-slave that is loaded by the startup script.
Here’s the template:
JENKINS_USER="jenkins-slave"
JENKINS_WORKDIR=$(eval echo "~$JENKINS_USER")
JENKINS_URL={{ jenkins_url }}
JENKINS_NODENAME=$(hostname)
JENKINS_SECRET={{ jenkins_secret }}
JAVA_ARGS="-Xmx6g"

And the init script. We can now upload these two files:
- name: Copy jenkins-slave default file template
  template:
    src: jenkins-slave-default.j2
    dest: /etc/default/jenkins-slave
    owner: jenkins-slave
    group: jenkins-slave
    mode: 0600
  vars:
    jenkins_secret: "{{secretxml.matches[0].argument}}"
  register: jenkins_config

- name: Copy jenkins-slave init file
  copy:
    src: jenkins-slave-init
    dest: /etc/init.d/jenkins-slave
    owner: root
    group: root
    mode: 0755

And we make sure the service is started:
- name: restart and enable jenkins-slave service if needed
  service:
    name: jenkins-slave
    enabled: yes
    state: restarted
  when: jenkins_config.changed

- name: start and enable jenkins-slave service
  service:
    name: jenkins-slave
    enabled: yes
    state: started

These steps were the basics but you can do a lot more like adding the Jenkins system user creation, adding your own CA certificate if you’re on a private IP, and so on


No responses yet

Trackback URI | Comments RSS

Leave a Reply