Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zendesk_ticket: new module #8543

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4f532c4
Adding in library reqs
elchico2007 Jun 13, 2024
d980342
Adding next part base structure of module
elchico2007 Jun 13, 2024
39d72ba
Adding in first working module for creation
elchico2007 Jun 16, 2024
f020145
Adding in some changes
elchico2007 Jun 17, 2024
a562ea5
Adding in function to close/resolve
elchico2007 Jun 18, 2024
b061c82
Added fully working closed section
elchico2007 Jun 18, 2024
71fbfa1
Adding in body
elchico2007 Jun 18, 2024
7ce3270
Finalizing with comments
elchico2007 Jun 18, 2024
e4e5775
Adding syntax changes to doc
elchico2007 Jun 18, 2024
7c4b514
More doc changes
elchico2007 Jun 18, 2024
39f6b5a
Adding in to BotMeta
elchico2007 Jun 18, 2024
db8218c
Adding info to BOTMETA
elchico2007 Jun 18, 2024
43217a4
Ensuring compatibility with 2.7
elchico2007 Jun 19, 2024
73fd95f
Fixing format string
elchico2007 Jun 20, 2024
ab19871
Update plugins/modules/zendesk_ticket.py
elchico2007 Jun 28, 2024
e2eeea4
Update plugins/modules/zendesk_ticket.py
elchico2007 Jun 28, 2024
5b6c41b
Update plugins/modules/zendesk_ticket.py
elchico2007 Jun 29, 2024
a0a3446
Adding in extra spaces for examples, utilizing AnsibleModule function…
elchico2007 Jun 29, 2024
1c35ac6
Fixing function name
elchico2007 Jun 29, 2024
4730cf8
Adding check to see if ticket exists before closing or resolving
elchico2007 Jun 29, 2024
eb6ab56
Adding in Simple unit tests
elchico2007 Jul 2, 2024
d06da3a
Fixing PEP 8 issues
elchico2007 Jul 2, 2024
80d6b4f
Fixing alphabetical order
elchico2007 Jul 15, 2024
3ddbf2d
Changing host to url for consistency and accuracy
elchico2007 Jul 15, 2024
d339d9c
Adding in module unit tests
elchico2007 Jul 25, 2024
5edc565
Fixing syntax
elchico2007 Jul 25, 2024
7b54a60
Update plugins/modules/zendesk_ticket.py
elchico2007 Aug 20, 2024
137315b
Update plugins/modules/zendesk_ticket.py
elchico2007 Aug 20, 2024
7c82697
Fixing documentation.
elchico2007 Aug 20, 2024
f3d42ab
Clarifying 'new' choice for status.
elchico2007 Aug 20, 2024
e110186
Clarifying on token and password parameters.
elchico2007 Aug 20, 2024
c11ff30
Removing redundant password and token check
elchico2007 Aug 20, 2024
a094157
Making closing and resolving ticket easier in the logic
elchico2007 Aug 20, 2024
709a902
Adding in new line at end of file
elchico2007 Aug 20, 2024
4c1c35b
Changing status option
elchico2007 Aug 20, 2024
9247bc1
Comitting failing unit test for testing purposes
elchico2007 Aug 20, 2024
ff8dd3a
Changing code
elchico2007 Aug 20, 2024
7e7c81e
Update plugins/modules/zendesk_ticket.py
elchico2007 Aug 21, 2024
43a6733
Update plugins/modules/zendesk_ticket.py
elchico2007 Aug 21, 2024
abb1bcc
Update plugins/modules/zendesk_ticket.py
elchico2007 Aug 21, 2024
ade01ad
Chaging class name to be camel case
elchico2007 Aug 21, 2024
54b3eab
Removing mock http library
elchico2007 Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,9 @@ files:
ignore: chrishoffman verkaufer
$modules/yum_versionlock.py:
maintainers: gyptazy aminvakil
$modules/zendesk_ticket.py:
labels: zendesk
maintainers: elchico2007
$modules/zfs:
keywords: beadm dladm illumos ipadm nexenta omnios openindiana pfexec smartos solaris sunos zfs zpool
labels: solaris
Expand Down
301 changes: 301 additions & 0 deletions plugins/modules/zendesk_ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2024, Luis Valle ([email protected])
# 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: zendesk_ticket
short_description: Manages tickets in Zendesk
description:
- This module allows you to create and close tickets in Zendesk.
author: "Luis Valle (@elchico2007)"
elchico2007 marked this conversation as resolved.
Show resolved Hide resolved
version_added: 9.4.0
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
url:
type: str
description:
- The URL of the Zendesk instance.
required: true
username:
type: str
description:
- The Zendesk account username.
required: true
aliases: ['user']
password:
type: str
description:
- The Zendesk account password.
- Optional if token is used.
required: false
aliases: ['pass']
token:
type: str
description:
- The API token for authentication.
- Optional if password is used.
required: false
body:
type: str
description:
- The body of the ticket.
default: ''
priority:
type: str
description:
- The priority of the ticket.
choices: ['urgent', 'high', 'normal', 'low']
default: normal
status:
type: str
description:
- The status of the ticket.
elchico2007 marked this conversation as resolved.
Show resolved Hide resolved
- The V(new) choice is not idempotent and will create a new ticket each time it's used.
- The V(closed) choice will close the ticket. If O(body) is provided, it will mark the ticket as resolved with the given resolution.
choices: ['new', 'closed']
required: true
ticket_id:
type: int
description:
- The ID of the ticket to be closed or resolved.
required: false
subject:
type: str
description:
- The subject of the ticket.
required: false
'''

EXAMPLES = '''
- name: Create a new ticket
community.general.zendesk_ticket:
username: 'your_username'
token: 'your_api_token'
url: 'https://yourcompany.zendesk.com'
body: 'This is a sample ticket'
priority: 'normal'
subject: 'New Ticket'
status: 'new'

- name: Close a ticket
community.general.zendesk_ticket:
username: 'your_username'
token: 'your_api_token'
url: 'https://yourcompany.zendesk.com'
ticket_id: 12345
status: 'closed'

- name: Resolve a ticket
community.general.zendesk_ticket:
username: 'your_username'
token: 'your_api_token'
url: 'https://yourcompany.zendesk.com'
ticket_id: 12345
status: 'closed'
body: 'Issue has been resolved'
'''

import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.urls import Request


class ZendeskAPI:
"""
Handles interactions with the Zendesk API for ticket management.

Attributes:
username (str): Zendesk account username.
password (str): Zendesk account password.
token (str): API token for authentication.
url (str): URL of the Zendesk instance.
headers (dict): Default headers for API requests.
"""
def __init__(self, username, password, token, url):
"""
Initializes the ZendeskAPI object with authentication credentials.

Args:
username (str): Zendesk account username.
password (str): Zendesk account password.
token (str): API token for authentication.
url (str): URL of the Zendesk instance.
"""
self.username = username
self.password = password
self.token = token
self.url = url
self.headers = {
"Content-Type": "application/json",
}

def api_auth(self):
"""
Configures and returns a Request object with authentication headers.

Returns:
Request: Configured Request object for API calls.
"""
if self.token:
request = Request(url_username='{0}/token'.format(self.username), url_password=self.token, headers=self.headers)
else:
request = Request(url_username=self.username, url_password=self.password, headers=self.headers)
return request

def check_ticket(self, ticket_id):
"""
Checks if a ticket exists in Zendesk.
Returns:
bool: True if the ticket exists, False otherwise.
"""
url = '{0}/api/v2/tickets/{1}'.format(self.url, ticket_id)
request = self.api_auth()
try:
response = request.get(url)
except Exception:
return False
Comment on lines +166 to +170
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is not wrong, however, for the user of this module, information is lost: why has it failed? what was the error message? They will have an error and have little idea about what happened.
This could be an improvement for another PR, no need to hold this one back over this.

return response.getcode() == 200

def create_ticket(self, body, priority, subject):
"""
Creates a new ticket in Zendesk.

Args:
body (str): The text body of the ticket.
priority (str): The priority of the ticket (e.g., 'urgent', 'high', 'normal', 'low').
subject (str): The subject of the ticket.

Returns:
dict: A dictionary containing the result of the ticket creation operation.
"""
changed = False
url = '{0}/api/v2/tickets'.format(self.url)
payload = {
"ticket": {
"comment": {
"body": body
},
"priority": priority,
"subject": subject
}
}

request = self.api_auth()

try:
response = request.post(url, data=json.dumps(payload))
if response.getcode() == 201:
changed = True
return {
'changed': changed,
'response': json.load(response),
}
except Exception as e:
return {
'changed': changed,
'msg': to_native(e)
}

def close_ticket(self, ticket_id, status, body):
"""
Closes or resolves a ticket in Zendesk.

Args:
ticket_id (int): The ID of the ticket to be closed or resolved.
status (str): The new status for the ticket ('closed' or 'resolved').
body (str): An optional comment to add to the ticket.

Returns:
dict: A dictionary containing the result of the ticket update operation.
"""
url = '{0}/api/v2/tickets/{1}'.format(self.url, ticket_id)
payload = {
"ticket": {
"status": status,
}
}
if body:
payload["ticket"]["comment"] = {"body": body}

request = self.api_auth()

try:
response = request.patch(url, data=json.dumps(payload))
if response.getcode() in [200, 204]:
return {
'changed': True,
'response': json.load(response),
}
except Exception as e:
return {
'changed': False,
'msg': to_native(e)
}


def main():
module = AnsibleModule(
argument_spec=dict(
url=dict(type='str', required=True),
username=dict(type='str', required=True, aliases=['user']),
password=dict(type='str', required=False, aliases=['pass'], no_log=True),
status=dict(type='str', required=True, choices=['new', 'closed']),
body=dict(type='str', default=''),
priority=dict(type='str', choices=['urgent', 'high', 'normal', 'low'], default='normal'),
subject=dict(type='str'),
token=dict(type='str', required=False, no_log=True),
ticket_id=dict(type='int')
),
required_if=[
('status', 'new', ('subject',)),
('status', 'close', ('ticket_id',)),
],
required_one_of=[
('password', 'token')
],
supports_check_mode=False
)

url = module.params['url']
username = module.params['username']
password = module.params['password']
status = module.params['status']
body = module.params['body']
priority = module.params['priority']
subject = module.params['subject']
token = module.params['token']
ticket_id = module.params['ticket_id']

zendesk_api = ZendeskAPI(username, password, token, url)

result = {} # Initialize result dictionary

if status == 'new':
result = zendesk_api.create_ticket(body, priority, subject)
elif status == 'closed':
if not zendesk_api.check_ticket(ticket_id):
module.fail_json(msg="Ticket ID {0} does not exist.".format(ticket_id))
result = zendesk_api.close_ticket(ticket_id, 'closed', body)

if 'msg' in result:
module.fail_json(**result)
else:
module.exit_json(**result)


if __name__ == '__main__':
main()
69 changes: 69 additions & 0 deletions tests/unit/plugins/modules/test_zendesk_ticket.py
elchico2007 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

@russoz russoz Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only proves that the code handles exceptions, it does not test how the code actually works: that was the idea of mocking.
Since the requests library is not being used (to warrant the use of httmock), maybe the answer is a direct mocking of the ansible.module_utils.urls.Request class, or at the very least, of its methods .get(), .patch(), etc...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess we need a better mechanism for this. For open_url and fetch_url in the same module there are some unit test frameworks in https://github.com/ansible-collections/community.internal_test_tools/tree/main/tests/unit/utils, but not (yet) for the Request class...

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Luis Valle <[email protected]>
# 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

from ansible_collections.community.general.plugins.modules import zendesk_ticket
from ansible_collections.community.general.tests.unit.plugins.modules.utils import ModuleTestCase, set_module_args, AnsibleFailJson


class TestZendeskTicket(ModuleTestCase):
def setUp(self):
super(TestZendeskTicket, self).setUp()
self.module = zendesk_ticket

def tearDown(self):
super(TestZendeskTicket, self).tearDown()

def test_with_no_parameters(self):
"""
Test that the module fails when no parameters are provided.
"""
with self.assertRaises(AnsibleFailJson):
set_module_args({})
self.module.main()

def test_create_ticket_no_password_or_token(self):
"""
Test that the module fails when neither password nor token is provided.
"""
ticket_data = {
'url': 'http://zendesk.com',
'username': 'your_username',
'ticket_id': 28,
'status': 'new',
'body': 'This is a test ticket body',
'priority': 'high',
'subject': 'Test Ticket'
}
set_module_args(ticket_data)
with self.assertRaises(AnsibleFailJson) as exc_info:
self.module.main()
result = exc_info.exception.args[0]
self.assertTrue(result['failed'])
self.assertEqual(result['msg'], 'one of the following is required: password, token')

def test_resolve_ticket_idempotency_with_mock(self):
"""
Test the module's behavior when attempting to resolve a non-existent ticket.
"""
ticket_data = {
'url': 'http://zendesk.com',
'username': 'your_username',
'password': 'your_password',
'ticket_id': 35436,
'status': 'closed',
'body': 'Ticket is resolved.'
}
set_module_args(ticket_data)

with self.assertRaises(AnsibleFailJson) as exc_info:
self.module.main()

result = exc_info.exception.args[0]
self.assertTrue(result['failed'])
self.assertEqual(result['msg'], 'Ticket ID 35436 does not exist.')