2020-03-09 10:11:07 +01:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
2022-08-08 14:24:58 +02:00
# Copyright (c) 2014, Steve Smith <ssmith@atlassian.com>
2020-03-09 10:11:07 +01:00
# Atlassian open-source approval reference OSR-76.
#
2022-08-08 14:24:58 +02:00
# Copyright (c) 2020, Per Abildgaard Toft <per@minfejl.dk> Search and update function
# Copyright (c) 2021, Brandon McNama <brandonmcnama@outlook.com> Issue attachment functionality
2020-04-09 09:29:26 +02:00
#
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
2020-12-19 17:43:41 +01:00
DOCUMENTATION = r """
2020-03-09 10:11:07 +01:00
module : jira
2022-11-09 07:38:36 +01:00
short_description : Create and modify issues in a JIRA instance
2020-03-09 10:11:07 +01:00
description :
- Create and modify issues in a JIRA instance .
2023-02-20 17:46:37 +01:00
extends_documentation_fragment :
- community . general . attributes
attributes :
check_mode :
support : none
diff_mode :
support : none
2020-03-09 10:11:07 +01:00
options :
uri :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : true
description :
- Base URI for the JIRA instance .
operation :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : true
aliases : [ command ]
2021-04-13 07:41:31 +02:00
choices : [ attach , comment , create , edit , fetch , link , search , transition , update ]
2020-03-09 10:11:07 +01:00
description :
- The operation to perform .
username :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
description :
- The username to log - in with .
2021-12-09 21:24:24 +01:00
- Must be used with I ( password ) . Mutually exclusive with I ( token ) .
2020-03-09 10:11:07 +01:00
password :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
description :
- The password to log - in with .
2021-12-09 21:24:24 +01:00
- Must be used with I ( username ) . Mutually exclusive with I ( token ) .
token :
type : str
description :
- The personal access token to log - in with .
- Mutually exclusive with I ( username ) and I ( password ) .
version_added : 4.2 .0
2020-03-09 10:11:07 +01:00
project :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- The project for this operation . Required for issue creation .
summary :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- The issue summary , where appropriate .
2021-04-07 08:14:03 +02:00
- Note that JIRA may not allow changing field values on specific transitions or states .
2020-03-09 10:11:07 +01:00
description :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- The issue description , where appropriate .
2021-04-07 08:14:03 +02:00
- Note that JIRA may not allow changing field values on specific transitions or states .
2020-03-09 10:11:07 +01:00
issuetype :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- The issue type , for issue creation .
issue :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- An existing issue key to operate on .
2020-10-31 13:53:57 +01:00
aliases : [ ' ticket ' ]
2020-03-09 10:11:07 +01:00
comment :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- The comment text to add .
2021-04-07 08:14:03 +02:00
- Note that JIRA may not allow changing field values on specific transitions or states .
2020-03-09 10:11:07 +01:00
2021-05-20 22:06:00 +02:00
comment_visibility :
type : dict
description :
- Used to specify comment comment visibility .
- See U ( https : / / developer . atlassian . com / cloud / jira / platform / rest / v2 / api - group - issue - comments / #api-rest-api-2-issue-issueidorkey-comment-post) for details.
suboptions :
type :
description :
- Use type to specify which of the JIRA visibility restriction types will be used .
type : str
required : true
choices : [ group , role ]
value :
description :
- Use value to specify value corresponding to the type of visibility restriction . For example name of the group or role .
type : str
required : true
version_added : ' 3.2.0 '
2020-03-09 10:11:07 +01:00
status :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
2021-04-07 08:14:03 +02:00
- Only used when I ( operation ) is C ( transition ) , and a bit of a misnomer , it actually refers to the transition name .
2020-03-09 10:11:07 +01:00
assignee :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
2021-04-07 08:14:03 +02:00
- Sets the the assignee when I ( operation ) is C ( create ) , C ( transition ) or C ( edit ) .
- Recent versions of JIRA no longer accept a user name as a user identifier . In that case , use I ( account_id ) instead .
- Note that JIRA may not allow changing field values on specific transitions or states .
account_id :
type : str
description :
- Sets the account identifier for the assignee when I ( operation ) is C ( create ) , C ( transition ) or C ( edit ) .
- Note that JIRA may not allow changing field values on specific transitions or states .
version_added : 2.5 .0
2020-03-09 10:11:07 +01:00
linktype :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- Set type of link , when action ' link ' selected .
inwardissue :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- Set issue from which link will be created .
outwardissue :
2020-10-31 13:53:57 +01:00
type : str
2020-03-09 10:11:07 +01:00
required : false
description :
- Set issue to which link will be created .
fields :
2020-10-31 13:53:57 +01:00
type : dict
2020-03-09 10:11:07 +01:00
required : false
description :
- This is a free - form data structure that can contain arbitrary data . This is passed directly to the JIRA REST API
( possibly after merging with other required data , as when passed to create ) . See examples for more information ,
and the JIRA REST API for the structure required for various fields .
2022-03-12 07:30:28 +01:00
- When passed to comment , the data structure is merged at the first level since community . general 4.6 .0 . Useful to add JIRA properties for example .
2021-04-07 08:14:03 +02:00
- Note that JIRA may not allow changing field values on specific transitions or states .
2022-11-01 19:25:51 +01:00
default : { }
2020-03-09 10:11:07 +01:00
2020-04-09 09:29:26 +02:00
jql :
required : false
description :
- Query JIRA in JQL Syntax , e . g . ' CMDB Hostname ' = ' test.example.com ' .
type : str
2020-06-13 15:01:19 +02:00
version_added : ' 0.2.0 '
2020-04-09 09:29:26 +02:00
maxresults :
required : false
description :
- Limit the result of I ( operation = search ) . If no value is specified , the default jira limit will be used .
- Used when I ( operation = search ) only , ignored otherwise .
type : int
2020-06-13 15:01:19 +02:00
version_added : ' 0.2.0 '
2020-04-09 09:29:26 +02:00
2020-03-09 10:11:07 +01:00
timeout :
2020-10-31 13:53:57 +01:00
type : float
2020-03-09 10:11:07 +01:00
required : false
description :
- Set timeout , in seconds , on requests to JIRA API .
default : 10
validate_certs :
required : false
description :
2022-06-22 22:43:48 +02:00
- Require valid SSL certificates ( set to C ( false ) if you ' d like to use self-signed certificates)
2020-03-09 10:11:07 +01:00
default : true
type : bool
2021-04-13 07:41:31 +02:00
attachment :
type : dict
version_added : 2.5 .0
description :
- Information about the attachment being uploaded .
suboptions :
filename :
required : true
type : path
description :
- The path to the file to upload ( from the remote node ) or , if I ( content ) is specified ,
the filename to use for the attachment .
content :
type : str
description :
- The Base64 encoded contents of the file to attach . If not specified , the contents of I ( filename ) will be
used instead .
mimetype :
type : str
description :
- The MIME type to supply for the upload . If not specified , best - effort detection will be
done .
2020-03-09 10:11:07 +01:00
notes :
2021-12-09 21:24:24 +01:00
- " Currently this only works with basic-auth, or tokens. "
2021-04-07 08:14:03 +02:00
- " To use with JIRA Cloud, pass the login e-mail as the I(username) and the API token as I(password). "
2020-03-09 10:11:07 +01:00
2020-04-09 09:29:26 +02:00
author :
- " Steve Smith (@tarka) "
- " Per Abildgaard Toft (@pertoft) "
2021-04-13 07:41:31 +02:00
- " Brandon McNama (@DWSR) "
2020-04-09 09:29:26 +02:00
"""
2020-03-09 10:11:07 +01:00
2020-12-19 17:43:41 +01:00
EXAMPLES = r """
2020-03-09 10:11:07 +01:00
# Create a new issue and add a comment to it:
- name : Create an issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
project : ANS
operation : create
summary : Example Issue
description : Created using Ansible
issuetype : Task
2020-04-09 09:29:26 +02:00
args :
fields :
customfield_13225 : " test "
2021-04-07 08:14:03 +02:00
customfield_12931 : { " value " : " Test " }
2020-03-09 10:11:07 +01:00
register : issue
- name : Comment on issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : comment
comment : A comment added by Ansible
2021-05-20 22:06:00 +02:00
- name : Comment on issue with restricted visibility
community . general . jira :
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : comment
comment : A comment added by Ansible
comment_visibility :
type : role
value : Developers
2022-03-12 07:30:28 +01:00
- name : Comment on issue with property to mark it internal
community . general . jira :
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : comment
comment : A comment added by Ansible
fields :
properties :
- key : ' sd.public.comment '
value :
internal : true
2020-03-09 10:11:07 +01:00
# Assign an existing issue using edit
- name : Assign an issue using free - form fields
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key}} '
operation : edit
assignee : ssmith
# Create an issue with an existing assignee
- name : Create an assigned issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
project : ANS
operation : create
summary : Assigned issue
description : Created and assigned using Ansible
issuetype : Task
assignee : ssmith
# Edit an issue
- name : Set the labels on an issue using free - form fields
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : edit
args :
fields :
labels :
- autocreated
- ansible
2020-04-09 09:29:26 +02:00
# Updating a field using operations: add, set & remove
- name : Change the value of a Select dropdown
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-04-09 09:29:26 +02:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : update
args :
fields :
customfield_12931 : [ { ' set ' : { ' value ' : ' Virtual ' } } ]
customfield_13820 : [ { ' set ' : { ' value ' : ' Manually ' } } ]
register : cmdb_issue
delegate_to : localhost
2020-03-09 10:11:07 +01:00
# Retrieve metadata for an issue and use it to create an account
- name : Get an issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
project : ANS
operation : fetch
issue : ANS - 63
register : issue
2020-04-09 09:29:26 +02:00
# Search for an issue
# You can limit the search for specific fields by adding optional args. Note! It must be a dict, hence, lastViewed: null
- name : Search for an issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-04-09 09:29:26 +02:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
project : ANS
operation : search
maxresults : 10
jql : project = cmdb AND cf [ 13225 ] = " test "
args :
fields :
lastViewed : null
register : issue
2020-03-09 10:11:07 +01:00
- name : Create a unix account for the reporter
become : true
user :
name : ' {{ issue.meta.fields.creator.name }} '
comment : ' {{ issue.meta.fields.creator.displayName }} '
# You can get list of valid linktypes at /rest/api/2/issueLinkType
# url of your jira installation.
- name : Create link from HSP - 1 to MKY - 1
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
operation : link
linktype : Relates
inwardissue : HSP - 1
outwardissue : MKY - 1
2021-04-07 08:14:03 +02:00
# Transition an issue
- name : Resolve the issue
2020-07-13 21:50:31 +02:00
community . general . jira :
2020-03-09 10:11:07 +01:00
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : ' {{ issue.meta.key }} '
operation : transition
2021-04-07 08:14:03 +02:00
status : Resolve Issue
account_id : 112233445566778899 aabbcc
2020-12-27 13:56:48 +01:00
fields :
2021-04-07 08:14:03 +02:00
resolution :
name : Done
description : I am done ! This is the last description I will ever give you .
2021-04-13 07:41:31 +02:00
# Attach a file to an issue
- name : Attach a file
community . general . jira :
uri : ' {{ server }} '
username : ' {{ user }} '
password : ' {{ pass }} '
issue : HSP - 1
operation : attach
attachment :
filename : topsecretreport . xlsx
2020-03-09 10:11:07 +01:00
"""
import base64
2021-04-13 07:41:31 +02:00
import binascii
2020-03-09 10:11:07 +01:00
import json
2021-04-13 07:41:31 +02:00
import mimetypes
import os
import random
import string
2020-12-27 13:56:48 +01:00
import traceback
2020-04-20 13:05:57 +02:00
2021-04-26 13:09:19 +02:00
from ansible_collections . community . general . plugins . module_utils . module_helper import StateModuleHelper , cause_changes
2020-04-20 13:05:57 +02:00
from ansible . module_utils . six . moves . urllib . request import pathname2url
2021-06-26 23:59:11 +02:00
from ansible . module_utils . common . text . converters import to_text , to_bytes , to_native
2020-03-09 10:11:07 +01:00
from ansible . module_utils . urls import fetch_url
2021-04-26 13:09:19 +02:00
class JIRA ( StateModuleHelper ) :
module = dict (
2020-03-09 10:11:07 +01:00
argument_spec = dict (
2021-04-13 07:41:31 +02:00
attachment = dict ( type = ' dict ' , options = dict (
content = dict ( type = ' str ' ) ,
filename = dict ( type = ' path ' , required = True ) ,
mimetype = dict ( type = ' str ' )
) ) ,
2020-10-31 13:53:57 +01:00
uri = dict ( type = ' str ' , required = True ) ,
2021-04-26 13:09:19 +02:00
operation = dict (
type = ' str ' ,
choices = [ ' attach ' , ' create ' , ' comment ' , ' edit ' , ' update ' , ' fetch ' , ' transition ' , ' link ' , ' search ' ] ,
aliases = [ ' command ' ] , required = True
) ,
2021-12-09 21:24:24 +01:00
username = dict ( type = ' str ' ) ,
password = dict ( type = ' str ' , no_log = True ) ,
token = dict ( type = ' str ' , no_log = True ) ,
2020-10-31 13:53:57 +01:00
project = dict ( type = ' str ' , ) ,
summary = dict ( type = ' str ' , ) ,
description = dict ( type = ' str ' , ) ,
issuetype = dict ( type = ' str ' , ) ,
issue = dict ( type = ' str ' , aliases = [ ' ticket ' ] ) ,
comment = dict ( type = ' str ' , ) ,
2021-05-20 22:06:00 +02:00
comment_visibility = dict ( type = ' dict ' , options = dict (
type = dict ( type = ' str ' , choices = [ ' group ' , ' role ' ] , required = True ) ,
value = dict ( type = ' str ' , required = True )
) ) ,
2020-10-31 13:53:57 +01:00
status = dict ( type = ' str ' , ) ,
assignee = dict ( type = ' str ' , ) ,
2020-03-09 10:11:07 +01:00
fields = dict ( default = { } , type = ' dict ' ) ,
2020-10-31 13:53:57 +01:00
linktype = dict ( type = ' str ' , ) ,
inwardissue = dict ( type = ' str ' , ) ,
outwardissue = dict ( type = ' str ' , ) ,
jql = dict ( type = ' str ' , ) ,
2020-04-09 09:29:26 +02:00
maxresults = dict ( type = ' int ' ) ,
2020-03-09 10:11:07 +01:00
timeout = dict ( type = ' float ' , default = 10 ) ,
validate_certs = dict ( default = True , type = ' bool ' ) ,
2021-04-07 08:14:03 +02:00
account_id = dict ( type = ' str ' ) ,
2020-03-09 10:11:07 +01:00
) ,
2021-12-09 21:24:24 +01:00
mutually_exclusive = [
[ ' username ' , ' token ' ] ,
[ ' password ' , ' token ' ] ,
[ ' assignee ' , ' account_id ' ] ,
] ,
required_together = [
[ ' username ' , ' password ' ] ,
] ,
required_one_of = [
[ ' username ' , ' token ' ] ,
] ,
2020-12-27 13:56:48 +01:00
required_if = (
2021-04-13 07:41:31 +02:00
( ' operation ' , ' attach ' , [ ' issue ' , ' attachment ' ] ) ,
2020-12-27 13:56:48 +01:00
( ' operation ' , ' create ' , [ ' project ' , ' issuetype ' , ' summary ' ] ) ,
( ' operation ' , ' comment ' , [ ' issue ' , ' comment ' ] ) ,
( ' operation ' , ' fetch ' , [ ' issue ' ] ) ,
( ' operation ' , ' transition ' , [ ' issue ' , ' status ' ] ) ,
( ' operation ' , ' link ' , [ ' linktype ' , ' inwardissue ' , ' outwardissue ' ] ) ,
( ' operation ' , ' search ' , [ ' jql ' ] ) ,
) ,
2020-03-09 10:11:07 +01:00
supports_check_mode = False
)
2021-04-26 13:09:19 +02:00
state_param = ' operation '
def __init_module__ ( self ) :
if self . vars . fields is None :
self . vars . fields = { }
if self . vars . assignee :
self . vars . fields [ ' assignee ' ] = { ' name ' : self . vars . assignee }
if self . vars . account_id :
self . vars . fields [ ' assignee ' ] = { ' accountId ' : self . vars . account_id }
self . vars . uri = self . vars . uri . strip ( ' / ' )
self . vars . set ( ' restbase ' , self . vars . uri + ' /rest/api/2 ' )
@cause_changes ( on_success = True )
def operation_create ( self ) :
createfields = {
' project ' : { ' key ' : self . vars . project } ,
' summary ' : self . vars . summary ,
' issuetype ' : { ' name ' : self . vars . issuetype } }
if self . vars . description :
createfields [ ' description ' ] = self . vars . description
# Merge in any additional or overridden fields
if self . vars . fields :
createfields . update ( self . vars . fields )
data = { ' fields ' : createfields }
url = self . vars . restbase + ' /issue/ '
self . vars . meta = self . post ( url , data )
@cause_changes ( on_success = True )
def operation_comment ( self ) :
data = {
' body ' : self . vars . comment
}
2021-05-20 22:06:00 +02:00
# if comment_visibility is specified restrict visibility
if self . vars . comment_visibility is not None :
data [ ' visibility ' ] = self . vars . comment_visibility
2022-03-12 07:30:28 +01:00
# Use 'fields' to merge in any additional data
if self . vars . fields :
data . update ( self . vars . fields )
2021-04-26 13:09:19 +02:00
url = self . vars . restbase + ' /issue/ ' + self . vars . issue + ' /comment '
self . vars . meta = self . post ( url , data )
@cause_changes ( on_success = True )
def operation_edit ( self ) :
data = {
' fields ' : self . vars . fields
}
url = self . vars . restbase + ' /issue/ ' + self . vars . issue
self . vars . meta = self . put ( url , data )
@cause_changes ( on_success = True )
def operation_update ( self ) :
data = {
" update " : self . vars . fields ,
}
url = self . vars . restbase + ' /issue/ ' + self . vars . issue
self . vars . meta = self . put ( url , data )
def operation_fetch ( self ) :
url = self . vars . restbase + ' /issue/ ' + self . vars . issue
self . vars . meta = self . get ( url )
def operation_search ( self ) :
url = self . vars . restbase + ' /search?jql= ' + pathname2url ( self . vars . jql )
if self . vars . fields :
fields = self . vars . fields . keys ( )
url = url + ' &fields= ' + ' &fields= ' . join ( [ pathname2url ( f ) for f in fields ] )
if self . vars . maxresults :
url = url + ' &maxResults= ' + str ( self . vars . maxresults )
self . vars . meta = self . get ( url )
@cause_changes ( on_success = True )
def operation_transition ( self ) :
# Find the transition id
turl = self . vars . restbase + ' /issue/ ' + self . vars . issue + " /transitions "
tmeta = self . get ( turl )
target = self . vars . status
tid = None
for t in tmeta [ ' transitions ' ] :
if t [ ' name ' ] == target :
tid = t [ ' id ' ]
break
else :
raise ValueError ( " Failed find valid transition for ' %s ' " % target )
fields = dict ( self . vars . fields )
if self . vars . summary is not None :
fields . update ( { ' summary ' : self . vars . summary } )
if self . vars . description is not None :
fields . update ( { ' description ' : self . vars . description } )
# Perform it
data = { ' transition ' : { " id " : tid } ,
' fields ' : fields }
if self . vars . comment is not None :
data . update ( { " update " : {
" comment " : [ {
" add " : { " body " : self . vars . comment }
} ] ,
} } )
url = self . vars . restbase + ' /issue/ ' + self . vars . issue + " /transitions "
self . vars . meta = self . post ( url , data )
@cause_changes ( on_success = True )
def operation_link ( self ) :
data = {
' type ' : { ' name ' : self . vars . linktype } ,
' inwardIssue ' : { ' key ' : self . vars . inwardissue } ,
' outwardIssue ' : { ' key ' : self . vars . outwardissue } ,
}
url = self . vars . restbase + ' /issueLink/ '
self . vars . meta = self . post ( url , data )
@cause_changes ( on_success = True )
def operation_attach ( self ) :
v = self . vars
filename = v . attachment . get ( ' filename ' )
content = v . attachment . get ( ' content ' )
if not any ( ( filename , content ) ) :
raise ValueError ( ' at least one of filename or content must be provided ' )
mime = v . attachment . get ( ' mimetype ' )
if not os . path . isfile ( filename ) :
raise ValueError ( ' The provided filename does not exist: %s ' % filename )
content_type , data = self . _prepare_attachment ( filename , content , mime )
url = v . restbase + ' /issue/ ' + v . issue + ' /attachments '
return True , self . post (
url , data , content_type = content_type , additional_headers = { " X-Atlassian-Token " : " no-check " }
)
# Ideally we'd just use prepare_multipart from ansible.module_utils.urls, but
# unfortunately it does not support specifying the encoding and also defaults to
# base64. Jira doesn't support base64 encoded attachments (and is therefore not
# spec compliant. Go figure). I originally wrote this function as an almost
# exact copypasta of prepare_multipart, but ran into some encoding issues when
# using the noop encoder. Hand rolling the entire message body seemed to work
# out much better.
#
# https://community.atlassian.com/t5/Jira-questions/Jira-dosen-t-decode-base64-attachment-request-REST-API/qaq-p/916427
#
# content is expected to be a base64 encoded string since Ansible doesn't
# support passing raw bytes objects.
@staticmethod
def _prepare_attachment ( filename , content = None , mime_type = None ) :
def escape_quotes ( s ) :
return s . replace ( ' " ' , ' \\ " ' )
boundary = " " . join ( random . choice ( string . digits + string . ascii_letters ) for dummy in range ( 30 ) )
name = to_native ( os . path . basename ( filename ) )
if not mime_type :
try :
mime_type = mimetypes . guess_type ( filename or ' ' , strict = False ) [ 0 ] or ' application/octet-stream '
except Exception :
mime_type = ' application/octet-stream '
main_type , sep , sub_type = mime_type . partition ( ' / ' )
if not content and filename :
with open ( to_bytes ( filename , errors = ' surrogate_or_strict ' ) , ' rb ' ) as f :
content = f . read ( )
else :
try :
content = base64 . b64decode ( content )
except binascii . Error as e :
raise Exception ( " Unable to base64 decode file content: %s " % e )
lines = [
" -- {0} " . format ( boundary ) ,
' Content-Disposition: form-data; name= " file " ; filename= {0} ' . format ( escape_quotes ( name ) ) ,
" Content-Type: {0} " . format ( " {0} / {1} " . format ( main_type , sub_type ) ) ,
' ' ,
to_text ( content ) ,
" -- {0} -- " . format ( boundary ) ,
" "
]
return (
" multipart/form-data; boundary= {0} " . format ( boundary ) ,
" \r \n " . join ( lines )
)
def request (
self ,
url ,
data = None ,
method = None ,
content_type = ' application/json ' ,
additional_headers = None
) :
if data and content_type == ' application/json ' :
data = json . dumps ( data )
2021-12-09 21:24:24 +01:00
headers = { }
if isinstance ( additional_headers , dict ) :
headers = additional_headers . copy ( )
2021-04-26 13:09:19 +02:00
# NOTE: fetch_url uses a password manager, which follows the
# standard request-then-challenge basic-auth semantics. However as
# JIRA allows some unauthorised operations it doesn't necessarily
# send the challenge, so the request occurs as the anonymous user,
# resulting in unexpected results. To work around this we manually
2021-12-09 21:24:24 +01:00
# inject the auth header up-front to ensure that JIRA treats
2021-04-26 13:09:19 +02:00
# the requests as authorized for this user.
2021-12-09 21:24:24 +01:00
if self . vars . token is not None :
headers . update ( {
" Content-Type " : content_type ,
" Authorization " : " Bearer %s " % self . vars . token ,
} )
else :
auth = to_text ( base64 . b64encode ( to_bytes ( ' {0} : {1} ' . format ( self . vars . username , self . vars . password ) ,
errors = ' surrogate_or_strict ' ) ) )
headers . update ( {
" Content-Type " : content_type ,
" Authorization " : " Basic %s " % auth ,
} )
2021-04-26 13:09:19 +02:00
response , info = fetch_url (
self . module , url , data = data , method = method , timeout = self . vars . timeout , headers = headers
)
if info [ ' status ' ] not in ( 200 , 201 , 204 ) :
error = None
try :
error = json . loads ( info [ ' body ' ] )
except Exception :
2021-12-17 21:47:10 +01:00
msg = ' The request " {method} {url} " returned the unexpected status code {status} {msg} \n {body} ' . format (
status = info [ ' status ' ] ,
msg = info [ ' msg ' ] ,
body = info . get ( ' body ' ) ,
url = url ,
method = method ,
)
self . module . fail_json ( msg = to_native ( msg ) , exception = traceback . format_exc ( ) )
2021-04-26 13:09:19 +02:00
if error :
msg = [ ]
for key in ( ' errorMessages ' , ' errors ' ) :
if error . get ( key ) :
msg . append ( to_native ( error [ key ] ) )
if msg :
self . module . fail_json ( msg = ' , ' . join ( msg ) )
self . module . fail_json ( msg = to_native ( error ) )
# Fallback print body, if it cant be decoded
self . module . fail_json ( msg = to_native ( info [ ' body ' ] ) )
body = response . read ( )
if body :
return json . loads ( to_text ( body , errors = ' surrogate_or_strict ' ) )
return { }
def post ( self , url , data , content_type = ' application/json ' , additional_headers = None ) :
return self . request ( url , data = data , method = ' POST ' , content_type = content_type ,
additional_headers = additional_headers )
def put ( self , url , data ) :
return self . request ( url , data = data , method = ' PUT ' )
def get ( self , url ) :
return self . request ( url )
2020-03-09 10:11:07 +01:00
2021-04-26 13:09:19 +02:00
def main ( ) :
jira = JIRA ( )
jira . run ( )
2020-03-09 10:11:07 +01:00
if __name__ == ' __main__ ' :
main ( )