2020-03-09 10:11:07 +01:00
|
|
|
#!/usr/bin/python
|
2021-08-08 10:40:22 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2022-08-08 14:24:58 +02:00
|
|
|
# Copyright (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
|
2022-08-05 12:28:29 +02:00
|
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
|
|
|
|
DOCUMENTATION = '''
|
|
|
|
---
|
|
|
|
module: jenkins_plugin
|
|
|
|
author: Jiri Tyr (@jtyr)
|
|
|
|
short_description: Add or remove Jenkins plugin
|
|
|
|
description:
|
|
|
|
- Ansible module which helps to manage Jenkins plugins.
|
|
|
|
|
|
|
|
options:
|
|
|
|
group:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Name of the Jenkins group on the OS.
|
|
|
|
default: jenkins
|
|
|
|
jenkins_home:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: path
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Home directory of the Jenkins user.
|
|
|
|
default: /var/lib/jenkins
|
|
|
|
mode:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: raw
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- File mode applied on versioned plugins.
|
|
|
|
default: '0644'
|
|
|
|
name:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Plugin name.
|
2022-08-24 20:00:11 +02:00
|
|
|
required: true
|
2020-03-09 10:11:07 +01:00
|
|
|
owner:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Name of the Jenkins user on the OS.
|
|
|
|
default: jenkins
|
|
|
|
state:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Desired plugin state.
|
|
|
|
- If the C(latest) is set, the check for new version will be performed
|
|
|
|
every time. This is suitable to keep the plugin up-to-date.
|
|
|
|
choices: [absent, present, pinned, unpinned, enabled, disabled, latest]
|
|
|
|
default: present
|
|
|
|
timeout:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: int
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Server connection timeout in secs.
|
|
|
|
default: 30
|
|
|
|
updates_expiration:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: int
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Number of seconds after which a new copy of the I(update-center.json)
|
|
|
|
file is downloaded. This is used to avoid the need to download the
|
|
|
|
plugin to calculate its checksum when C(latest) is specified.
|
|
|
|
- Set it to C(0) if no cache file should be used. In that case, the
|
|
|
|
plugin file will always be downloaded to calculate its checksum when
|
|
|
|
C(latest) is specified.
|
|
|
|
default: 86400
|
|
|
|
updates_url:
|
2021-06-29 07:56:59 +02:00
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
description:
|
|
|
|
- A list of base URL(s) to retrieve I(update-center.json), and direct plugin files from.
|
|
|
|
- This can be a list since community.general 3.3.0.
|
|
|
|
default: ['https://updates.jenkins.io', 'http://mirrors.jenkins.io']
|
|
|
|
update_json_url_segment:
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
description:
|
|
|
|
- A list of URL segment(s) to retrieve the update center json file from.
|
|
|
|
default: ['update-center.json', 'updates/update-center.json']
|
|
|
|
version_added: 3.3.0
|
|
|
|
latest_plugins_url_segments:
|
|
|
|
type: list
|
|
|
|
elements: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
2021-06-29 07:56:59 +02:00
|
|
|
- Path inside the I(updates_url) to get latest plugins from.
|
|
|
|
default: ['latest']
|
|
|
|
version_added: 3.3.0
|
|
|
|
versioned_plugins_url_segments:
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
description:
|
|
|
|
- Path inside the I(updates_url) to get specific version of plugins from.
|
|
|
|
default: ['download/plugins', 'plugins']
|
|
|
|
version_added: 3.3.0
|
2020-03-09 10:11:07 +01:00
|
|
|
url:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- URL of the Jenkins server.
|
|
|
|
default: http://localhost:8080
|
|
|
|
version:
|
2020-10-31 13:53:57 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
description:
|
|
|
|
- Plugin version number.
|
|
|
|
- If this option is specified, all plugin dependencies must be installed
|
|
|
|
manually.
|
|
|
|
- It might take longer to verify that the correct version is installed.
|
|
|
|
This is especially true if a specific version number is specified.
|
|
|
|
- Quote the version to prevent the value to be interpreted as float. For
|
|
|
|
example if C(1.20) would be unquoted, it would become C(1.2).
|
|
|
|
with_dependencies:
|
|
|
|
description:
|
|
|
|
- Defines whether to install plugin dependencies.
|
|
|
|
- This option takes effect only if the I(version) is not defined.
|
|
|
|
type: bool
|
2022-08-24 20:00:11 +02:00
|
|
|
default: true
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
notes:
|
|
|
|
- Plugin installation should be run under root or the same user which owns
|
|
|
|
the plugin files on the disk. Only if the plugin is not installed yet and
|
|
|
|
no version is specified, the API installation is performed which requires
|
|
|
|
only the Web UI credentials.
|
|
|
|
- It's necessary to notify the handler or call the I(service) module to
|
|
|
|
restart the Jenkins service after a new plugin was installed.
|
|
|
|
- Pinning works only if the plugin is installed and Jenkins service was
|
|
|
|
successfully restarted after the plugin installation.
|
|
|
|
- It is not possible to run the module remotely by changing the I(url)
|
|
|
|
parameter to point to the Jenkins server. The module must be used on the
|
|
|
|
host where Jenkins runs as it needs direct access to the plugin files.
|
|
|
|
extends_documentation_fragment:
|
|
|
|
- url
|
2020-10-31 13:53:57 +01:00
|
|
|
- files
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = '''
|
|
|
|
- name: Install plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: build-pipeline-plugin
|
|
|
|
|
|
|
|
- name: Install plugin without its dependencies
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: build-pipeline-plugin
|
2022-08-24 20:00:11 +02:00
|
|
|
with_dependencies: false
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
- name: Make sure the plugin is always up-to-date
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
state: latest
|
|
|
|
|
|
|
|
- name: Install specific version of the plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
version: "1.15"
|
|
|
|
|
|
|
|
- name: Pin the plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
state: pinned
|
|
|
|
|
|
|
|
- name: Unpin the plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
state: unpinned
|
|
|
|
|
|
|
|
- name: Enable the plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
state: enabled
|
|
|
|
|
|
|
|
- name: Disable the plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: token-macro
|
|
|
|
state: disabled
|
|
|
|
|
|
|
|
- name: Uninstall plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: build-pipeline-plugin
|
|
|
|
state: absent
|
|
|
|
|
|
|
|
#
|
|
|
|
# Example of how to authenticate
|
|
|
|
#
|
|
|
|
- name: Install plugin
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: build-pipeline-plugin
|
|
|
|
url_username: admin
|
|
|
|
url_password: p4ssw0rd
|
|
|
|
url: http://localhost:8888
|
|
|
|
|
|
|
|
#
|
|
|
|
# Example of a Play which handles Jenkins restarts during the state changes
|
|
|
|
#
|
|
|
|
- name: Jenkins Master play
|
|
|
|
hosts: jenkins-master
|
|
|
|
vars:
|
|
|
|
my_jenkins_plugins:
|
|
|
|
token-macro:
|
2022-08-24 20:00:11 +02:00
|
|
|
enabled: true
|
2020-03-09 10:11:07 +01:00
|
|
|
build-pipeline-plugin:
|
|
|
|
version: "1.4.9"
|
2022-08-24 20:00:11 +02:00
|
|
|
pinned: false
|
|
|
|
enabled: true
|
2020-03-09 10:11:07 +01:00
|
|
|
tasks:
|
|
|
|
- name: Install plugins without a specific version
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: "{{ item.key }}"
|
|
|
|
register: my_jenkins_plugin_unversioned
|
|
|
|
when: >
|
|
|
|
'version' not in item.value
|
|
|
|
with_dict: "{{ my_jenkins_plugins }}"
|
|
|
|
|
|
|
|
- name: Install plugins with a specific version
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: "{{ item.key }}"
|
|
|
|
version: "{{ item.value['version'] }}"
|
|
|
|
register: my_jenkins_plugin_versioned
|
|
|
|
when: >
|
|
|
|
'version' in item.value
|
|
|
|
with_dict: "{{ my_jenkins_plugins }}"
|
|
|
|
|
|
|
|
- name: Initiate the fact
|
2020-07-16 13:42:12 +02:00
|
|
|
ansible.builtin.set_fact:
|
2022-08-24 20:00:11 +02:00
|
|
|
jenkins_restart_required: false
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
- name: Check if restart is required by any of the versioned plugins
|
2020-07-16 13:42:12 +02:00
|
|
|
ansible.builtin.set_fact:
|
2022-08-24 20:00:11 +02:00
|
|
|
jenkins_restart_required: true
|
2020-03-09 10:11:07 +01:00
|
|
|
when: item.changed
|
|
|
|
with_items: "{{ my_jenkins_plugin_versioned.results }}"
|
|
|
|
|
|
|
|
- name: Check if restart is required by any of the unversioned plugins
|
2020-07-16 13:42:12 +02:00
|
|
|
ansible.builtin.set_fact:
|
2022-08-24 20:00:11 +02:00
|
|
|
jenkins_restart_required: true
|
2020-03-09 10:11:07 +01:00
|
|
|
when: item.changed
|
|
|
|
with_items: "{{ my_jenkins_plugin_unversioned.results }}"
|
|
|
|
|
|
|
|
- name: Restart Jenkins if required
|
2020-07-14 17:28:08 +02:00
|
|
|
ansible.builtin.service:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: jenkins
|
|
|
|
state: restarted
|
|
|
|
when: jenkins_restart_required
|
|
|
|
|
|
|
|
- name: Wait for Jenkins to start up
|
2020-07-14 17:28:08 +02:00
|
|
|
ansible.builtin.uri:
|
2020-03-09 10:11:07 +01:00
|
|
|
url: http://localhost:8080
|
|
|
|
status_code: 200
|
|
|
|
timeout: 5
|
|
|
|
register: jenkins_service_status
|
|
|
|
# Keep trying for 5 mins in 5 sec intervals
|
|
|
|
retries: 60
|
|
|
|
delay: 5
|
|
|
|
until: >
|
|
|
|
'status' in jenkins_service_status and
|
|
|
|
jenkins_service_status['status'] == 200
|
|
|
|
when: jenkins_restart_required
|
|
|
|
|
|
|
|
- name: Reset the fact
|
2020-07-16 13:42:12 +02:00
|
|
|
ansible.builtin.set_fact:
|
2022-08-24 20:00:11 +02:00
|
|
|
jenkins_restart_required: false
|
2020-03-09 10:11:07 +01:00
|
|
|
when: jenkins_restart_required
|
|
|
|
|
|
|
|
- name: Plugin pinning
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: "{{ item.key }}"
|
|
|
|
state: "{{ 'pinned' if item.value['pinned'] else 'unpinned'}}"
|
|
|
|
when: >
|
|
|
|
'pinned' in item.value
|
|
|
|
with_dict: "{{ my_jenkins_plugins }}"
|
|
|
|
|
|
|
|
- name: Plugin enabling
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.jenkins_plugin:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: "{{ item.key }}"
|
|
|
|
state: "{{ 'enabled' if item.value['enabled'] else 'disabled'}}"
|
|
|
|
when: >
|
|
|
|
'enabled' in item.value
|
|
|
|
with_dict: "{{ my_jenkins_plugins }}"
|
|
|
|
'''
|
|
|
|
|
|
|
|
RETURN = '''
|
|
|
|
plugin:
|
|
|
|
description: plugin name
|
|
|
|
returned: success
|
|
|
|
type: str
|
|
|
|
sample: build-pipeline-plugin
|
|
|
|
state:
|
|
|
|
description: state of the target, after execution
|
|
|
|
returned: success
|
|
|
|
type: str
|
|
|
|
sample: "present"
|
|
|
|
'''
|
|
|
|
|
|
|
|
import hashlib
|
2021-04-26 06:56:21 +02:00
|
|
|
import io
|
2020-03-09 10:11:07 +01:00
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import tempfile
|
|
|
|
|
2022-11-17 06:55:46 +01:00
|
|
|
from ansible.module_utils.basic import AnsibleModule, to_bytes
|
|
|
|
from ansible.module_utils.six.moves import http_cookiejar as cookiejar
|
|
|
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
|
|
|
from ansible.module_utils.urls import fetch_url, url_argument_spec
|
|
|
|
from ansible.module_utils.six import text_type, binary_type
|
|
|
|
from ansible.module_utils.common.text.converters import to_native
|
|
|
|
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.jenkins import download_updates_file
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
class FailedInstallingWithPluginManager(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
class JenkinsPlugin(object):
|
|
|
|
def __init__(self, module):
|
|
|
|
# To be able to call fail_json
|
|
|
|
self.module = module
|
|
|
|
|
|
|
|
# Shortcuts for the params
|
|
|
|
self.params = self.module.params
|
|
|
|
self.url = self.params['url']
|
|
|
|
self.timeout = self.params['timeout']
|
|
|
|
|
|
|
|
# Crumb
|
|
|
|
self.crumb = {}
|
|
|
|
# Cookie jar for crumb session
|
|
|
|
self.cookies = None
|
|
|
|
|
|
|
|
if self._csrf_enabled():
|
|
|
|
self.cookies = cookiejar.LWPCookieJar()
|
|
|
|
self.crumb = self._get_crumb()
|
|
|
|
|
|
|
|
# Get list of installed plugins
|
|
|
|
self._get_installed_plugins()
|
|
|
|
|
|
|
|
def _csrf_enabled(self):
|
|
|
|
csrf_data = self._get_json_data(
|
|
|
|
"%s/%s" % (self.url, "api/json"), 'CSRF')
|
|
|
|
|
|
|
|
if 'useCrumbs' not in csrf_data:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Required fields not found in the Crumbs response.",
|
|
|
|
details=csrf_data)
|
|
|
|
|
|
|
|
return csrf_data['useCrumbs']
|
|
|
|
|
|
|
|
def _get_json_data(self, url, what, **kwargs):
|
|
|
|
# Get the JSON data
|
|
|
|
r = self._get_url_data(url, what, **kwargs)
|
|
|
|
|
|
|
|
# Parse the JSON data
|
|
|
|
try:
|
|
|
|
json_data = json.loads(to_native(r.read()))
|
|
|
|
except Exception as e:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Cannot parse %s JSON data." % what,
|
|
|
|
details=to_native(e))
|
|
|
|
|
|
|
|
return json_data
|
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
def _get_urls_data(self, urls, what=None, msg_status=None, msg_exception=None, **kwargs):
|
|
|
|
# Compose default messages
|
|
|
|
if msg_status is None:
|
|
|
|
msg_status = "Cannot get %s" % what
|
|
|
|
|
|
|
|
if msg_exception is None:
|
|
|
|
msg_exception = "Retrieval of %s failed." % what
|
|
|
|
|
|
|
|
errors = {}
|
|
|
|
for url in urls:
|
|
|
|
err_msg = None
|
|
|
|
try:
|
|
|
|
self.module.debug("fetching url: %s" % url)
|
|
|
|
response, info = fetch_url(
|
|
|
|
self.module, url, timeout=self.timeout, cookies=self.cookies,
|
|
|
|
headers=self.crumb, **kwargs)
|
|
|
|
|
|
|
|
if info['status'] == 200:
|
|
|
|
return response
|
|
|
|
else:
|
|
|
|
err_msg = ("%s. fetching url %s failed. response code: %s" % (msg_status, url, info['status']))
|
|
|
|
if info['status'] > 400: # extend error message
|
|
|
|
err_msg = "%s. response body: %s" % (err_msg, info['body'])
|
|
|
|
except Exception as e:
|
|
|
|
err_msg = "%s. fetching url %s failed. error msg: %s" % (msg_status, url, to_native(e))
|
|
|
|
finally:
|
|
|
|
if err_msg is not None:
|
|
|
|
self.module.debug(err_msg)
|
|
|
|
errors[url] = err_msg
|
|
|
|
|
|
|
|
# failed on all urls
|
|
|
|
self.module.fail_json(msg=msg_exception, details=errors)
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
def _get_url_data(
|
|
|
|
self, url, what=None, msg_status=None, msg_exception=None,
|
2021-06-29 07:56:59 +02:00
|
|
|
dont_fail=False, **kwargs):
|
2020-03-09 10:11:07 +01:00
|
|
|
# Compose default messages
|
|
|
|
if msg_status is None:
|
|
|
|
msg_status = "Cannot get %s" % what
|
|
|
|
|
|
|
|
if msg_exception is None:
|
|
|
|
msg_exception = "Retrieval of %s failed." % what
|
|
|
|
|
|
|
|
# Get the URL data
|
|
|
|
try:
|
|
|
|
response, info = fetch_url(
|
|
|
|
self.module, url, timeout=self.timeout, cookies=self.cookies,
|
|
|
|
headers=self.crumb, **kwargs)
|
|
|
|
|
|
|
|
if info['status'] != 200:
|
2021-06-29 07:56:59 +02:00
|
|
|
if dont_fail:
|
|
|
|
raise FailedInstallingWithPluginManager(info['msg'])
|
|
|
|
else:
|
|
|
|
self.module.fail_json(msg=msg_status, details=info['msg'])
|
2020-03-09 10:11:07 +01:00
|
|
|
except Exception as e:
|
2021-06-29 07:56:59 +02:00
|
|
|
if dont_fail:
|
|
|
|
raise FailedInstallingWithPluginManager(e)
|
|
|
|
else:
|
|
|
|
self.module.fail_json(msg=msg_exception, details=to_native(e))
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
def _get_crumb(self):
|
|
|
|
crumb_data = self._get_json_data(
|
|
|
|
"%s/%s" % (self.url, "crumbIssuer/api/json"), 'Crumb')
|
|
|
|
|
|
|
|
if 'crumbRequestField' in crumb_data and 'crumb' in crumb_data:
|
|
|
|
ret = {
|
|
|
|
crumb_data['crumbRequestField']: crumb_data['crumb']
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Required fields not found in the Crum response.",
|
|
|
|
details=crumb_data)
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def _get_installed_plugins(self):
|
|
|
|
plugins_data = self._get_json_data(
|
|
|
|
"%s/%s" % (self.url, "pluginManager/api/json?depth=1"),
|
|
|
|
'list of plugins')
|
|
|
|
|
|
|
|
# Check if we got valid data
|
|
|
|
if 'plugins' not in plugins_data:
|
|
|
|
self.module.fail_json(msg="No valid plugin data found.")
|
|
|
|
|
|
|
|
# Create final list of installed/pined plugins
|
|
|
|
self.is_installed = False
|
|
|
|
self.is_pinned = False
|
|
|
|
self.is_enabled = False
|
|
|
|
|
|
|
|
for p in plugins_data['plugins']:
|
|
|
|
if p['shortName'] == self.params['name']:
|
|
|
|
self.is_installed = True
|
|
|
|
|
|
|
|
if p['pinned']:
|
|
|
|
self.is_pinned = True
|
|
|
|
|
|
|
|
if p['enabled']:
|
|
|
|
self.is_enabled = True
|
|
|
|
|
|
|
|
break
|
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
def _install_with_plugin_manager(self):
|
|
|
|
if not self.module.check_mode:
|
|
|
|
# Install the plugin (with dependencies)
|
|
|
|
install_script = (
|
|
|
|
'd = Jenkins.instance.updateCenter.getPlugin("%s")'
|
|
|
|
'.deploy(); d.get();' % self.params['name'])
|
|
|
|
|
|
|
|
if self.params['with_dependencies']:
|
|
|
|
install_script = (
|
|
|
|
'Jenkins.instance.updateCenter.getPlugin("%s")'
|
|
|
|
'.getNeededDependencies().each{it.deploy()}; %s' % (
|
|
|
|
self.params['name'], install_script))
|
|
|
|
|
|
|
|
script_data = {
|
|
|
|
'script': install_script
|
|
|
|
}
|
|
|
|
data = urlencode(script_data)
|
|
|
|
|
|
|
|
# Send the installation request
|
|
|
|
r = self._get_url_data(
|
|
|
|
"%s/scriptText" % self.url,
|
|
|
|
msg_status="Cannot install plugin.",
|
|
|
|
msg_exception="Plugin installation has failed.",
|
|
|
|
data=data,
|
|
|
|
dont_fail=True)
|
|
|
|
|
|
|
|
hpi_file = '%s/plugins/%s.hpi' % (
|
|
|
|
self.params['jenkins_home'],
|
|
|
|
self.params['name'])
|
|
|
|
|
|
|
|
if os.path.isfile(hpi_file):
|
|
|
|
os.remove(hpi_file)
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
def install(self):
|
|
|
|
changed = False
|
|
|
|
plugin_file = (
|
|
|
|
'%s/plugins/%s.jpi' % (
|
|
|
|
self.params['jenkins_home'],
|
|
|
|
self.params['name']))
|
|
|
|
|
|
|
|
if not self.is_installed and self.params['version'] in [None, 'latest']:
|
2021-06-29 07:56:59 +02:00
|
|
|
try:
|
|
|
|
self._install_with_plugin_manager()
|
|
|
|
changed = True
|
|
|
|
except FailedInstallingWithPluginManager: # Fallback to manually downloading the plugin
|
|
|
|
pass
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
if not changed:
|
2020-03-09 10:11:07 +01:00
|
|
|
# Check if the plugin directory exists
|
|
|
|
if not os.path.isdir(self.params['jenkins_home']):
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Jenkins home directory doesn't exist.")
|
|
|
|
|
2020-07-27 11:33:08 +02:00
|
|
|
checksum_old = None
|
2020-03-09 10:11:07 +01:00
|
|
|
if os.path.isfile(plugin_file):
|
|
|
|
# Make the checksum of the currently installed plugin
|
2020-07-27 11:33:08 +02:00
|
|
|
with open(plugin_file, 'rb') as plugin_fh:
|
|
|
|
plugin_content = plugin_fh.read()
|
|
|
|
checksum_old = hashlib.sha1(plugin_content).hexdigest()
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
if self.params['version'] in [None, 'latest']:
|
|
|
|
# Take latest version
|
2021-06-29 07:56:59 +02:00
|
|
|
plugin_urls = self._get_latest_plugin_urls()
|
2020-03-09 10:11:07 +01:00
|
|
|
else:
|
|
|
|
# Take specific version
|
2021-06-29 07:56:59 +02:00
|
|
|
plugin_urls = self._get_versioned_plugin_urls()
|
2020-03-09 10:11:07 +01:00
|
|
|
if (
|
|
|
|
self.params['updates_expiration'] == 0 or
|
|
|
|
self.params['version'] not in [None, 'latest'] or
|
2020-07-27 11:33:08 +02:00
|
|
|
checksum_old is None):
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Download the plugin file directly
|
2021-06-29 07:56:59 +02:00
|
|
|
r = self._download_plugin(plugin_urls)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Write downloaded plugin into file if checksums don't match
|
2020-07-27 11:33:08 +02:00
|
|
|
if checksum_old is None:
|
2020-03-09 10:11:07 +01:00
|
|
|
# No previously installed plugin
|
|
|
|
if not self.module.check_mode:
|
|
|
|
self._write_file(plugin_file, r)
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
else:
|
|
|
|
# Get data for the MD5
|
|
|
|
data = r.read()
|
|
|
|
|
|
|
|
# Make new checksum
|
2020-07-27 11:33:08 +02:00
|
|
|
checksum_new = hashlib.sha1(data).hexdigest()
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# If the checksum is different from the currently installed
|
|
|
|
# plugin, store the new plugin
|
2020-07-27 11:33:08 +02:00
|
|
|
if checksum_old != checksum_new:
|
2020-03-09 10:11:07 +01:00
|
|
|
if not self.module.check_mode:
|
|
|
|
self._write_file(plugin_file, data)
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
elif self.params['version'] == 'latest':
|
|
|
|
# Check for update from the updates JSON file
|
|
|
|
plugin_data = self._download_updates()
|
|
|
|
|
|
|
|
# If the latest version changed, download it
|
2020-07-27 11:33:08 +02:00
|
|
|
if checksum_old != to_bytes(plugin_data['sha1']):
|
2020-03-09 10:11:07 +01:00
|
|
|
if not self.module.check_mode:
|
2021-06-29 07:56:59 +02:00
|
|
|
r = self._download_plugin(plugin_urls)
|
2020-03-09 10:11:07 +01:00
|
|
|
self._write_file(plugin_file, r)
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
# Change file attributes if needed
|
|
|
|
if os.path.isfile(plugin_file):
|
|
|
|
params = {
|
|
|
|
'dest': plugin_file
|
|
|
|
}
|
|
|
|
params.update(self.params)
|
|
|
|
file_args = self.module.load_file_common_arguments(params)
|
|
|
|
|
|
|
|
if not self.module.check_mode:
|
|
|
|
# Not sure how to run this in the check mode
|
|
|
|
changed = self.module.set_fs_attributes_if_different(
|
|
|
|
file_args, changed)
|
|
|
|
else:
|
|
|
|
# See the comment above
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
return changed
|
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
def _get_latest_plugin_urls(self):
|
|
|
|
urls = []
|
|
|
|
for base_url in self.params['updates_url']:
|
|
|
|
for update_segment in self.params['latest_plugins_url_segments']:
|
|
|
|
urls.append("{0}/{1}/{2}.hpi".format(base_url, update_segment, self.params['name']))
|
|
|
|
return urls
|
|
|
|
|
|
|
|
def _get_versioned_plugin_urls(self):
|
|
|
|
urls = []
|
|
|
|
for base_url in self.params['updates_url']:
|
|
|
|
for versioned_segment in self.params['versioned_plugins_url_segments']:
|
|
|
|
urls.append("{0}/{1}/{2}/{3}/{2}.hpi".format(base_url, versioned_segment, self.params['name'], self.params['version']))
|
|
|
|
return urls
|
|
|
|
|
|
|
|
def _get_update_center_urls(self):
|
|
|
|
urls = []
|
|
|
|
for base_url in self.params['updates_url']:
|
|
|
|
for update_json in self.params['update_json_url_segment']:
|
|
|
|
urls.append("{0}/{1}".format(base_url, update_json))
|
|
|
|
return urls
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
def _download_updates(self):
|
2022-11-17 06:55:46 +01:00
|
|
|
try:
|
|
|
|
updates_file, download_updates = download_updates_file(self.params['updates_expiration'])
|
|
|
|
except OSError as e:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Cannot create temporal directory.",
|
|
|
|
details=to_native(e))
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Download the updates file if needed
|
|
|
|
if download_updates:
|
2021-06-29 07:56:59 +02:00
|
|
|
urls = self._get_update_center_urls()
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Get the data
|
2021-06-29 07:56:59 +02:00
|
|
|
r = self._get_urls_data(
|
|
|
|
urls,
|
2020-03-09 10:11:07 +01:00
|
|
|
msg_status="Remote updates not found.",
|
|
|
|
msg_exception="Updates download failed.")
|
|
|
|
|
|
|
|
# Write the updates file
|
2022-11-17 06:55:46 +01:00
|
|
|
tmp_update_fd, tmp_updates_file = tempfile.mkstemp()
|
|
|
|
os.write(tmp_update_fd, r.read())
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
try:
|
2022-11-17 06:55:46 +01:00
|
|
|
os.close(tmp_update_fd)
|
2020-03-09 10:11:07 +01:00
|
|
|
except IOError as e:
|
|
|
|
self.module.fail_json(
|
2022-11-17 06:55:46 +01:00
|
|
|
msg="Cannot close the tmp updates file %s." % tmp_updates_file,
|
2020-03-09 10:11:07 +01:00
|
|
|
details=to_native(e))
|
|
|
|
|
|
|
|
# Open the updates file
|
|
|
|
try:
|
2022-11-17 06:55:46 +01:00
|
|
|
f = io.open(tmp_updates_file, encoding='utf-8')
|
|
|
|
|
|
|
|
# Read only the second line
|
|
|
|
dummy = f.readline()
|
|
|
|
data = json.loads(f.readline())
|
2020-03-09 10:11:07 +01:00
|
|
|
except IOError as e:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Cannot open temporal updates file.",
|
|
|
|
details=to_native(e))
|
2022-11-17 06:55:46 +01:00
|
|
|
except Exception as e:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Cannot load JSON data from the tmp updates file.",
|
|
|
|
details=to_native(e))
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Move the updates file to the right place if we could read it
|
|
|
|
if download_updates:
|
2022-11-17 06:55:46 +01:00
|
|
|
self.module.atomic_move(tmp_updates_file, updates_file)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# Check if we have the plugin data available
|
2022-11-17 06:55:46 +01:00
|
|
|
if not data.get('plugins', {}).get(self.params['name']):
|
|
|
|
self.module.fail_json(msg="Cannot find plugin data in the updates file.")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
return data['plugins'][self.params['name']]
|
|
|
|
|
2021-06-29 07:56:59 +02:00
|
|
|
def _download_plugin(self, plugin_urls):
|
2020-03-09 10:11:07 +01:00
|
|
|
# Download the plugin
|
2021-06-29 07:56:59 +02:00
|
|
|
|
|
|
|
return self._get_urls_data(
|
|
|
|
plugin_urls,
|
2020-03-09 10:11:07 +01:00
|
|
|
msg_status="Plugin not found.",
|
|
|
|
msg_exception="Plugin download failed.")
|
|
|
|
|
|
|
|
def _write_file(self, f, data):
|
|
|
|
# Store the plugin into a temp file and then move it
|
|
|
|
tmp_f_fd, tmp_f = tempfile.mkstemp()
|
|
|
|
|
|
|
|
if isinstance(data, (text_type, binary_type)):
|
|
|
|
os.write(tmp_f_fd, data)
|
|
|
|
else:
|
|
|
|
os.write(tmp_f_fd, data.read())
|
|
|
|
|
|
|
|
try:
|
|
|
|
os.close(tmp_f_fd)
|
|
|
|
except IOError as e:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg='Cannot close the temporal plugin file %s.' % tmp_f,
|
|
|
|
details=to_native(e))
|
|
|
|
|
|
|
|
# Move the file onto the right place
|
|
|
|
self.module.atomic_move(tmp_f, f)
|
|
|
|
|
|
|
|
def uninstall(self):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
# Perform the action
|
|
|
|
if self.is_installed:
|
|
|
|
if not self.module.check_mode:
|
|
|
|
self._pm_query('doUninstall', 'Uninstallation')
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
def pin(self):
|
|
|
|
return self._pinning('pin')
|
|
|
|
|
|
|
|
def unpin(self):
|
|
|
|
return self._pinning('unpin')
|
|
|
|
|
|
|
|
def _pinning(self, action):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
# Check if the plugin is pinned/unpinned
|
|
|
|
if (
|
|
|
|
action == 'pin' and not self.is_pinned or
|
|
|
|
action == 'unpin' and self.is_pinned):
|
|
|
|
|
|
|
|
# Perform the action
|
|
|
|
if not self.module.check_mode:
|
|
|
|
self._pm_query(action, "%sning" % action.capitalize())
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
def enable(self):
|
|
|
|
return self._enabling('enable')
|
|
|
|
|
|
|
|
def disable(self):
|
|
|
|
return self._enabling('disable')
|
|
|
|
|
|
|
|
def _enabling(self, action):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
# Check if the plugin is pinned/unpinned
|
|
|
|
if (
|
|
|
|
action == 'enable' and not self.is_enabled or
|
|
|
|
action == 'disable' and self.is_enabled):
|
|
|
|
|
|
|
|
# Perform the action
|
|
|
|
if not self.module.check_mode:
|
|
|
|
self._pm_query(
|
|
|
|
"make%sd" % action.capitalize(),
|
|
|
|
"%sing" % action[:-1].capitalize())
|
|
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
def _pm_query(self, action, msg):
|
|
|
|
url = "%s/pluginManager/plugin/%s/%s" % (
|
|
|
|
self.params['url'], self.params['name'], action)
|
|
|
|
|
|
|
|
# Send the request
|
|
|
|
self._get_url_data(
|
|
|
|
url,
|
|
|
|
msg_status="Plugin not found. %s" % url,
|
2021-05-26 07:00:53 +02:00
|
|
|
msg_exception="%s has failed." % msg,
|
|
|
|
method="POST")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
# Module arguments
|
|
|
|
argument_spec = url_argument_spec()
|
|
|
|
argument_spec.update(
|
2020-10-31 13:53:57 +01:00
|
|
|
group=dict(type='str', default='jenkins'),
|
|
|
|
jenkins_home=dict(type='path', default='/var/lib/jenkins'),
|
2020-03-09 10:11:07 +01:00
|
|
|
mode=dict(default='0644', type='raw'),
|
2020-10-31 13:53:57 +01:00
|
|
|
name=dict(type='str', required=True),
|
|
|
|
owner=dict(type='str', default='jenkins'),
|
2020-03-09 10:11:07 +01:00
|
|
|
state=dict(
|
|
|
|
choices=[
|
|
|
|
'present',
|
|
|
|
'absent',
|
|
|
|
'pinned',
|
|
|
|
'unpinned',
|
|
|
|
'enabled',
|
|
|
|
'disabled',
|
|
|
|
'latest'],
|
|
|
|
default='present'),
|
|
|
|
timeout=dict(default=30, type="int"),
|
|
|
|
updates_expiration=dict(default=86400, type="int"),
|
2021-06-29 07:56:59 +02:00
|
|
|
updates_url=dict(type="list", elements="str", default=['https://updates.jenkins.io',
|
|
|
|
'http://mirrors.jenkins.io']),
|
|
|
|
update_json_url_segment=dict(type="list", elements="str", default=['update-center.json',
|
|
|
|
'updates/update-center.json']),
|
|
|
|
latest_plugins_url_segments=dict(type="list", elements="str", default=['latest']),
|
|
|
|
versioned_plugins_url_segments=dict(type="list", elements="str", default=['download/plugins', 'plugins']),
|
2020-03-09 10:11:07 +01:00
|
|
|
url=dict(default='http://localhost:8080'),
|
|
|
|
url_password=dict(no_log=True),
|
|
|
|
version=dict(),
|
|
|
|
with_dependencies=dict(default=True, type='bool'),
|
|
|
|
)
|
|
|
|
# Module settings
|
|
|
|
module = AnsibleModule(
|
|
|
|
argument_spec=argument_spec,
|
|
|
|
add_file_common_args=True,
|
|
|
|
supports_check_mode=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Force basic authentication
|
|
|
|
module.params['force_basic_auth'] = True
|
|
|
|
|
|
|
|
# Convert timeout to float
|
|
|
|
try:
|
|
|
|
module.params['timeout'] = float(module.params['timeout'])
|
|
|
|
except ValueError as e:
|
|
|
|
module.fail_json(
|
|
|
|
msg='Cannot convert %s to float.' % module.params['timeout'],
|
|
|
|
details=to_native(e))
|
|
|
|
|
|
|
|
# Set version to latest if state is latest
|
|
|
|
if module.params['state'] == 'latest':
|
|
|
|
module.params['state'] = 'present'
|
|
|
|
module.params['version'] = 'latest'
|
|
|
|
|
|
|
|
# Create some shortcuts
|
|
|
|
name = module.params['name']
|
|
|
|
state = module.params['state']
|
|
|
|
|
|
|
|
# Initial change state of the task
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
# Instantiate the JenkinsPlugin object
|
|
|
|
jp = JenkinsPlugin(module)
|
|
|
|
|
|
|
|
# Perform action depending on the requested state
|
|
|
|
if state == 'present':
|
|
|
|
changed = jp.install()
|
|
|
|
elif state == 'absent':
|
|
|
|
changed = jp.uninstall()
|
|
|
|
elif state == 'pinned':
|
|
|
|
changed = jp.pin()
|
|
|
|
elif state == 'unpinned':
|
|
|
|
changed = jp.unpin()
|
|
|
|
elif state == 'enabled':
|
|
|
|
changed = jp.enable()
|
|
|
|
elif state == 'disabled':
|
|
|
|
changed = jp.disable()
|
|
|
|
|
|
|
|
# Print status of the change
|
|
|
|
module.exit_json(changed=changed, plugin=name, state=state)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|