forked from ansible-collections/community.general
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New module: Jenkins node management (ansible-collections#9016)
* New module: Jenkins node management * Fix sanity errors * (Try to) fix mock import for Python 2.7 * Remove encoding from XML tostring in hopes of appeasing py27 Default encoding is probably always good enough. * Turns out that encoding parameter was important for python3... It's not super obvious how to resolve in a 2 + 3 compatible way, so branch and omit encoding for 2. * Implement review fixes and suggestions
- Loading branch information
Showing
4 changed files
with
965 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,385 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) Ansible Project | ||
# 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 | ||
|
||
from __future__ import absolute_import, division, print_function | ||
__metaclass__ = type | ||
|
||
DOCUMENTATION = ''' | ||
--- | ||
module: jenkins_node | ||
short_description: Manage Jenkins nodes | ||
version_added: 10.0.0 | ||
description: | ||
- Manage Jenkins nodes with Jenkins REST API. | ||
requirements: | ||
- "python-jenkins >= 0.4.12" | ||
author: | ||
- Connor Newton (@phyrwork) | ||
extends_documentation_fragment: | ||
- community.general.attributes | ||
attributes: | ||
check_mode: | ||
support: partial | ||
details: | ||
- Check mode is unable to show configuration changes for a node that is not yet | ||
present. | ||
diff_mode: | ||
support: none | ||
options: | ||
url: | ||
description: | ||
- URL of the Jenkins server. | ||
default: http://localhost:8080 | ||
type: str | ||
name: | ||
description: | ||
- Name of the Jenkins node to manage. | ||
required: true | ||
type: str | ||
user: | ||
description: | ||
- User to authenticate with the Jenkins server. | ||
type: str | ||
token: | ||
description: | ||
- API token to authenticate with the Jenkins server. | ||
type: str | ||
state: | ||
description: | ||
- Specifies whether the Jenkins node should be V(present) (created), V(absent) | ||
(deleted), V(enabled) (online) or V(disabled) (offline). | ||
default: present | ||
choices: ['enabled', 'disabled', 'present', 'absent'] | ||
type: str | ||
num_executors: | ||
description: | ||
- When specified, sets the Jenkins node executor count. | ||
type: int | ||
labels: | ||
description: | ||
- When specified, sets the Jenkins node labels. | ||
type: list | ||
elements: str | ||
''' | ||
|
||
EXAMPLES = ''' | ||
- name: Create a Jenkins node using token authentication | ||
community.general.jenkins_node: | ||
url: http://localhost:8080 | ||
user: jenkins | ||
token: 11eb751baabb66c4d1cb8dc4e0fb142cde | ||
name: my-node | ||
state: present | ||
- name: Set number of executors on Jenkins node | ||
community.general.jenkins_node: | ||
name: my-node | ||
state: present | ||
num_executors: 4 | ||
- name: Set labels on Jenkins node | ||
community.general.jenkins_node: | ||
name: my-node | ||
state: present | ||
labels: | ||
- label-1 | ||
- label-2 | ||
- label-3 | ||
''' | ||
|
||
RETURN = ''' | ||
--- | ||
url: | ||
description: URL used to connect to the Jenkins server. | ||
returned: success | ||
type: str | ||
sample: https://jenkins.mydomain.com | ||
user: | ||
description: User used for authentication. | ||
returned: success | ||
type: str | ||
sample: jenkins | ||
name: | ||
description: Name of the Jenkins node. | ||
returned: success | ||
type: str | ||
sample: my-node | ||
state: | ||
description: State of the Jenkins node. | ||
returned: success | ||
type: str | ||
sample: present | ||
created: | ||
description: Whether or not the Jenkins node was created by the task. | ||
returned: success | ||
type: bool | ||
deleted: | ||
description: Whether or not the Jenkins node was deleted by the task. | ||
returned: success | ||
type: bool | ||
disabled: | ||
description: Whether or not the Jenkins node was disabled by the task. | ||
returned: success | ||
type: bool | ||
enabled: | ||
description: Whether or not the Jenkins node was enabled by the task. | ||
returned: success | ||
type: bool | ||
configured: | ||
description: Whether or not the Jenkins node was configured by the task. | ||
returned: success | ||
type: bool | ||
''' | ||
|
||
import sys | ||
from xml.etree import ElementTree | ||
|
||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible.module_utils.common.text.converters import to_native | ||
from ansible_collections.community.general.plugins.module_utils import deps | ||
|
||
with deps.declare( | ||
"python-jenkins", | ||
reason="python-jenkins is required to interact with Jenkins", | ||
url="https://opendev.org/jjb/python-jenkins", | ||
): | ||
import jenkins | ||
|
||
|
||
IS_PYTHON_2 = sys.version_info[0] <= 2 | ||
|
||
|
||
class JenkinsNode: | ||
def __init__(self, module): | ||
self.module = module | ||
|
||
self.name = module.params['name'] | ||
self.state = module.params['state'] | ||
self.token = module.params['token'] | ||
self.user = module.params['user'] | ||
self.url = module.params['url'] | ||
self.num_executors = module.params['num_executors'] | ||
self.labels = module.params['labels'] | ||
|
||
if self.labels is not None: | ||
for label in self.labels: | ||
if " " in label: | ||
self.module.fail_json("labels must not contain spaces: got invalid label {}".format(label)) | ||
|
||
self.instance = self.get_jenkins_instance() | ||
self.result = { | ||
'changed': False, | ||
'url': self.url, | ||
'user': self.user, | ||
'name': self.name, | ||
'state': self.state, | ||
'created': False, | ||
'deleted': False, | ||
'disabled': False, | ||
'enabled': False, | ||
'configured': False, | ||
'warnings': [], | ||
} | ||
|
||
def get_jenkins_instance(self): | ||
try: | ||
if self.user and self.token: | ||
return jenkins.Jenkins(self.url, self.user, self.token) | ||
elif self.user and not self.token: | ||
return jenkins.Jenkins(self.url, self.user) | ||
else: | ||
return jenkins.Jenkins(self.url) | ||
except Exception as e: | ||
self.module.fail_json(msg='Unable to connect to Jenkins server, %s' % to_native(e)) | ||
|
||
def configure_node(self, present): | ||
if not present: | ||
# Node would only not be present if in check mode and if not present there | ||
# is no way to know what would and would not be changed. | ||
if not self.module.check_mode: | ||
raise Exception("configure_node present is False outside of check mode") | ||
return | ||
|
||
configured = False | ||
|
||
data = self.instance.get_node_config(self.name) | ||
root = ElementTree.fromstring(data) | ||
|
||
if self.num_executors is not None: | ||
elem = root.find('numExecutors') | ||
if elem is None: | ||
elem = ElementTree.SubElement(root, 'numExecutors') | ||
if elem.text is None or int(elem.text) != self.num_executors: | ||
elem.text = str(self.num_executors) | ||
configured = True | ||
|
||
if self.labels is not None: | ||
elem = root.find('label') | ||
if elem is None: | ||
elem = ElementTree.SubElement(root, 'label') | ||
labels = [] | ||
if elem.text: | ||
labels = elem.text.split() | ||
if labels != self.labels: | ||
elem.text = " ".join(self.labels) | ||
configured = True | ||
|
||
if configured: | ||
if IS_PYTHON_2: | ||
data = ElementTree.tostring(root) | ||
else: | ||
data = ElementTree.tostring(root, encoding="unicode") | ||
|
||
self.instance.reconfig_node(self.name, data) | ||
|
||
self.result['configured'] = configured | ||
if configured: | ||
self.result['changed'] = True | ||
|
||
def present_node(self): | ||
def create_node(): | ||
try: | ||
self.instance.create_node(self.name, launcher=jenkins.LAUNCHER_SSH) | ||
except jenkins.JenkinsException as e: | ||
# Some versions of python-jenkins < 1.8.3 has an authorization bug when | ||
# handling redirects returned when posting new resources. If the node is | ||
# created OK then can ignore the error. | ||
if not self.instance.node_exists(self.name): | ||
raise e | ||
|
||
# TODO: Remove authorization workaround. | ||
self.result['warnings'].append( | ||
"suppressed 401 Not Authorized on redirect after node created: see https://review.opendev.org/c/jjb/python-jenkins/+/931707" | ||
) | ||
|
||
present = self.instance.node_exists(self.name) | ||
created = False | ||
if not present: | ||
if not self.module.check_mode: | ||
create_node() | ||
present = True | ||
|
||
created = True | ||
|
||
self.configure_node(present) | ||
|
||
self.result['created'] = created | ||
if created: | ||
self.result['changed'] = True | ||
|
||
return present # Used to gate downstream queries when in check mode. | ||
|
||
def absent_node(self): | ||
def delete_node(): | ||
try: | ||
self.instance.delete_node(self.name) | ||
except jenkins.JenkinsException as e: | ||
# Some versions of python-jenkins < 1.8.3 has an authorization bug when | ||
# handling redirects returned when posting new resources. If the node is | ||
# deleted OK then can ignore the error. | ||
if self.instance.node_exists(self.name): | ||
raise e | ||
|
||
# TODO: Remove authorization workaround. | ||
self.result['warnings'].append( | ||
"suppressed 401 Not Authorized on redirect after node deleted: see https://review.opendev.org/c/jjb/python-jenkins/+/931707" | ||
) | ||
|
||
present = self.instance.node_exists(self.name) | ||
deleted = False | ||
if present: | ||
if not self.module.check_mode: | ||
delete_node() | ||
|
||
deleted = True | ||
|
||
self.result['deleted'] = deleted | ||
if deleted: | ||
self.result['changed'] = True | ||
|
||
def enabled_node(self): | ||
present = self.present_node() | ||
|
||
enabled = False | ||
|
||
if present: | ||
info = self.instance.get_node_info(self.name) | ||
|
||
if info['offline']: | ||
if not self.module.check_mode: | ||
self.instance.enable_node(self.name) | ||
|
||
enabled = True | ||
else: | ||
# Would have created node with initial state enabled therefore would not have | ||
# needed to enable therefore not enabled. | ||
if not self.module.check_mode: | ||
raise Exception("enabled_node present is False outside of check mode") | ||
enabled = False | ||
|
||
self.result['enabled'] = enabled | ||
if enabled: | ||
self.result['changed'] = True | ||
|
||
def disabled_node(self): | ||
present = self.present_node() | ||
|
||
disabled = False | ||
|
||
if present: | ||
info = self.instance.get_node_info(self.name) | ||
|
||
if not info['offline']: | ||
if not self.module.check_mode: | ||
self.instance.disable_node(self.name) | ||
|
||
disabled = True | ||
else: | ||
# Would have created node with initial state enabled therefore would have | ||
# needed to disable therefore disabled. | ||
if not self.module.check_mode: | ||
raise Exception("disabled_node present is False outside of check mode") | ||
disabled = True | ||
|
||
self.result['disabled'] = disabled | ||
if disabled: | ||
self.result['changed'] = True | ||
|
||
|
||
def main(): | ||
module = AnsibleModule( | ||
argument_spec=dict( | ||
name=dict(required=True, type='str'), | ||
url=dict(default='http://localhost:8080'), | ||
user=dict(), | ||
token=dict(no_log=True), | ||
state=dict(choices=['enabled', 'disabled', 'present', 'absent'], default='present'), | ||
num_executors=dict(type='int'), | ||
labels=dict(type='list', elements='str'), | ||
), | ||
supports_check_mode=True, | ||
) | ||
|
||
deps.validate(module) | ||
|
||
jenkins_node = JenkinsNode(module) | ||
|
||
state = module.params.get('state') | ||
if state == 'enabled': | ||
jenkins_node.enabled_node() | ||
elif state == 'disabled': | ||
jenkins_node.disabled_node() | ||
elif state == 'present': | ||
jenkins_node.present_node() | ||
else: | ||
jenkins_node.absent_node() | ||
|
||
module.exit_json(**jenkins_node.result) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Oops, something went wrong.