diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index c49080bc94..935fe11664 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -762,6 +762,8 @@ files:
maintainers: sermilrod
$modules/jenkins_job_info.py:
maintainers: stpierre
+ $modules/jenkins_node.py:
+ maintainers: phyrwork
$modules/jenkins_plugin.py:
maintainers: jtyr
$modules/jenkins_script.py:
diff --git a/plugins/modules/jenkins_node.py b/plugins/modules/jenkins_node.py
new file mode 100644
index 0000000000..2ee4a481a5
--- /dev/null
+++ b/plugins/modules/jenkins_node.py
@@ -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()
diff --git a/tests/unit/plugins/modules/test_jenkins_node.py b/tests/unit/plugins/modules/test_jenkins_node.py
new file mode 100644
index 0000000000..33e7ca0f13
--- /dev/null
+++ b/tests/unit/plugins/modules/test_jenkins_node.py
@@ -0,0 +1,575 @@
+# 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
+
+import jenkins
+import json
+
+from xml.etree import ElementTree as et
+
+import pytest
+
+from ansible.module_utils import basic
+from ansible.module_utils.common.text.converters import to_bytes
+from ansible_collections.community.general.tests.unit.compat.mock import patch, call
+from ansible_collections.community.general.plugins.modules import jenkins_node
+from pytest import fixture, raises, mark, param
+
+
+def xml_equal(x, y):
+ # type: (et.Element | str, et.Element | str) -> bool
+ if isinstance(x, str):
+ x = et.fromstring(x)
+
+ if isinstance(y, str):
+ y = et.fromstring(y)
+
+ if x.tag != y.tag:
+ return False
+
+ if x.attrib != y.attrib:
+ return False
+
+ if (x.text or "").strip() != (y.text or "").strip():
+ return False
+
+ x_children = list(x)
+ y_children = list(y)
+
+ if len(x_children) != len(y_children):
+ return False
+
+ for x, y in zip(x_children, y_children):
+ if not xml_equal(x, y):
+ return False
+
+ return True
+
+
+def assert_xml_equal(x, y):
+ if xml_equal(x, y):
+ return True
+
+ if not isinstance(x, str):
+ x = et.tostring(x)
+
+ if not isinstance(y, str):
+ y = et.tostring(y)
+
+ raise AssertionError("{} != {}".format(x, y))
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __getitem__(self, item):
+ return self.value[item]
+
+
+def exit_json(*args, **kwargs):
+ if 'changed' not in kwargs:
+ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+class AnsibleFailJson(Exception):
+ pass
+
+
+def fail_json(*args, **kwargs):
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+
+@fixture(autouse=True)
+def module():
+ with patch.multiple(
+ "ansible.module_utils.basic.AnsibleModule",
+ exit_json=exit_json,
+ fail_json=fail_json,
+ ):
+ yield
+
+
+@fixture
+def instance():
+ with patch("jenkins.Jenkins", autospec=True) as instance:
+ yield instance
+
+
+@fixture
+def get_instance(instance):
+ with patch(
+ "ansible_collections.community.general.plugins.modules.jenkins_node.JenkinsNode.get_jenkins_instance",
+ autospec=True,
+ ) as mock:
+ mock.return_value = instance
+ yield mock
+
+
+def test_get_jenkins_instance_with_user_and_token(instance):
+ instance.node_exists.return_value = False
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ "url": "https://localhost:8080",
+ "user": "admin",
+ "token": "password",
+ })
+
+ with pytest.raises(AnsibleExitJson):
+ jenkins_node.main()
+
+ assert instance.call_args == call("https://localhost:8080", "admin", "password")
+
+
+def test_get_jenkins_instance_with_user(instance):
+ instance.node_exists.return_value = False
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ "url": "https://localhost:8080",
+ "user": "admin",
+ })
+
+ with pytest.raises(AnsibleExitJson):
+ jenkins_node.main()
+
+ assert instance.call_args == call("https://localhost:8080", "admin")
+
+
+def test_get_jenkins_instance_with_no_credential(instance):
+ instance.node_exists.return_value = False
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ "url": "https://localhost:8080",
+ })
+
+ with pytest.raises(AnsibleExitJson):
+ jenkins_node.main()
+
+ assert instance.call_args == call("https://localhost:8080")
+
+
+PRESENT_STATES = ["present", "enabled", "disabled"]
+
+
+@mark.parametrize(["state"], [param(state) for state in PRESENT_STATES])
+def test_state_present_when_absent(get_instance, instance, state):
+ instance.node_exists.return_value = False
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": state,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.create_node.call_args == call("my-node", launcher=jenkins.LAUNCHER_SSH)
+
+ assert result.value["created"] is True
+ assert result.value["changed"] is True
+
+
+@mark.parametrize(["state"], [param(state) for state in PRESENT_STATES])
+def test_state_present_when_absent_check_mode(get_instance, instance, state):
+ instance.node_exists.return_value = False
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": state,
+ "_ansible_check_mode": True,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.create_node.called
+
+ assert result.value["created"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_present_when_present(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.create_node.called
+
+ assert result.value["created"] is False
+ assert result.value["changed"] is False
+
+
+def test_state_absent_when_present(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.delete_node.call_args == call("my-node")
+
+ assert result.value["deleted"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_absent_when_present_check_mode(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ "_ansible_check_mode": True,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.delete_node.called
+
+ assert result.value["deleted"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_absent_when_absent(get_instance, instance):
+ instance.node_exists.return_value = False
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "absent",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.delete_node.called
+
+ assert result.value["deleted"] is False
+ assert result.value["changed"] is False
+
+
+def test_state_enabled_when_offline(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": True}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "enabled",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.enable_node.call_args == call("my-node")
+
+ assert result.value["enabled"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_enabled_when_offline_check_mode(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": True}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "enabled",
+ "_ansible_check_mode": True,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.enable_node.called
+
+ assert result.value["enabled"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_enabled_when_not_offline(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": False}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "enabled",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.enable_node.called
+
+ assert result.value["enabled"] is False
+ assert result.value["changed"] is False
+
+
+def test_state_disabled_when_not_offline(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": False}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "disabled",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.disable_node.call_args == call("my-node")
+
+ assert result.value["disabled"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_disabled_when_not_offline_check_mode(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": False}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "disabled",
+ "_ansible_check_mode": True,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.disable_node.called
+
+ assert result.value["disabled"] is True
+ assert result.value["changed"] is True
+
+
+def test_state_disabled_when_offline(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+ instance.get_node_info.return_value = {"offline": True}
+
+ set_module_args({
+ "name": "my-node",
+ "state": "disabled",
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.disable_node.called
+
+ assert result.value["disabled"] is False
+ assert result.value["changed"] is False
+
+
+def test_configure_num_executors_when_not_configured(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "num_executors": 3,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.reconfig_node.call_args[0][0] == "my-node"
+ assert_xml_equal(instance.reconfig_node.call_args[0][1], """
+
+ 3
+
+""")
+
+ assert result.value["configured"] is True
+ assert result.value["changed"] is True
+
+
+def test_configure_num_executors_when_not_equal(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = """
+
+ 3
+
+"""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "num_executors": 2,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert_xml_equal(instance.reconfig_node.call_args[0][1], """
+
+ 2
+
+""")
+
+ assert result.value["configured"] is True
+ assert result.value["changed"] is True
+
+
+def test_configure_num_executors_when_equal(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = """
+
+ 2
+
+"""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "num_executors": 2,
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.reconfig_node.called
+
+ assert result.value["configured"] is False
+ assert result.value["changed"] is False
+
+
+def test_configure_labels_when_not_configured(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "labels": [
+ "a",
+ "b",
+ "c",
+ ],
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.reconfig_node.call_args[0][0] == "my-node"
+ assert_xml_equal(instance.reconfig_node.call_args[0][1], """
+
+
+
+""")
+
+ assert result.value["configured"] is True
+ assert result.value["changed"] is True
+
+
+def test_configure_labels_when_not_equal(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = """
+
+
+
+"""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "labels": [
+ "a",
+ "z",
+ "c",
+ ],
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert instance.reconfig_node.call_args[0][0] == "my-node"
+ assert_xml_equal(instance.reconfig_node.call_args[0][1], """
+
+
+
+""")
+
+ assert result.value["configured"] is True
+ assert result.value["changed"] is True
+
+
+def test_configure_labels_when_equal(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = """
+
+
+
+"""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "labels": [
+ "a",
+ "b",
+ "c",
+ ],
+ })
+
+ with raises(AnsibleExitJson) as result:
+ jenkins_node.main()
+
+ assert not instance.reconfig_node.called
+
+ assert result.value["configured"] is False
+ assert result.value["changed"] is False
+
+
+def test_configure_labels_fail_when_contains_space(get_instance, instance):
+ instance.node_exists.return_value = True
+ instance.get_node_config.return_value = ""
+
+ set_module_args({
+ "name": "my-node",
+ "state": "present",
+ "labels": [
+ "a error",
+ ],
+ })
+
+ with raises(AnsibleFailJson):
+ jenkins_node.main()
+
+ assert not instance.reconfig_node.called
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
index 218fe45673..cfc8493912 100644
--- a/tests/unit/requirements.txt
+++ b/tests/unit/requirements.txt
@@ -54,3 +54,6 @@ proxmoxer ; python_version > '3.6'
#requirements for nomad_token modules
python-nomad < 2.0.0 ; python_version <= '3.6'
python-nomad >= 2.0.0 ; python_version >= '3.7'
+
+# requirement for jenkins_build, jenkins_node, jenkins_plugin modules
+python-jenkins >= 0.4.12
\ No newline at end of file