1""" 2JIRA Execution module 3===================== 4 5.. versionadded:: 2019.2.0 6 7Execution module to manipulate JIRA tickets via Salt. 8 9This module requires the ``jira`` Python library to be installed. 10 11Configuration example: 12 13.. code-block:: yaml 14 15 jira: 16 server: https://jira.atlassian.org 17 username: salt 18 password: pass 19""" 20 21import logging 22 23import salt.utils.args 24 25try: 26 import jira 27 28 HAS_JIRA = True 29except ImportError: 30 HAS_JIRA = False 31 32log = logging.getLogger(__name__) 33 34__virtualname__ = "jira" 35__proxyenabled__ = ["*"] 36 37JIRA = None 38 39 40def __virtual__(): 41 return ( 42 __virtualname__ 43 if HAS_JIRA 44 else (False, "Please install the jira Python libary from PyPI") 45 ) 46 47 48def _get_credentials(server=None, username=None, password=None): 49 """ 50 Returns the credentials merged with the config data (opts + pillar). 51 """ 52 jira_cfg = __salt__["config.merge"]("jira", default={}) 53 if not server: 54 server = jira_cfg.get("server") 55 if not username: 56 username = jira_cfg.get("username") 57 if not password: 58 password = jira_cfg.get("password") 59 return server, username, password 60 61 62def _get_jira(server=None, username=None, password=None): 63 global JIRA 64 if not JIRA: 65 server, username, password = _get_credentials( 66 server=server, username=username, password=password 67 ) 68 JIRA = jira.JIRA( 69 basic_auth=(username, password), server=server, logging=True 70 ) # We want logging 71 return JIRA 72 73 74def create_issue( 75 project, 76 summary, 77 description, 78 template_engine="jinja", 79 context=None, 80 defaults=None, 81 saltenv="base", 82 issuetype="Bug", 83 priority="Normal", 84 labels=None, 85 assignee=None, 86 server=None, 87 username=None, 88 password=None, 89 **kwargs 90): 91 """ 92 Create a JIRA issue using the named settings. Return the JIRA ticket ID. 93 94 project 95 The name of the project to attach the JIRA ticket to. 96 97 summary 98 The summary (title) of the JIRA ticket. When the ``template_engine`` 99 argument is set to a proper value of an existing Salt template engine 100 (e.g., ``jinja``, ``mako``, etc.) it will render the ``summary`` before 101 creating the ticket. 102 103 description 104 The full body description of the JIRA ticket. When the ``template_engine`` 105 argument is set to a proper value of an existing Salt template engine 106 (e.g., ``jinja``, ``mako``, etc.) it will render the ``description`` before 107 creating the ticket. 108 109 template_engine: ``jinja`` 110 The name of the template engine to be used to render the values of the 111 ``summary`` and ``description`` arguments. Default: ``jinja``. 112 113 context: ``None`` 114 The context to pass when rendering the ``summary`` and ``description``. 115 This argument is ignored when ``template_engine`` is set as ``None`` 116 117 defaults: ``None`` 118 Default values to pass to the Salt rendering pipeline for the 119 ``summary`` and ``description`` arguments. 120 This argument is ignored when ``template_engine`` is set as ``None``. 121 122 saltenv: ``base`` 123 The Salt environment name (for the rendering system). 124 125 issuetype: ``Bug`` 126 The type of the JIRA ticket. Default: ``Bug``. 127 128 priority: ``Normal`` 129 The priority of the JIRA ticket. Default: ``Normal``. 130 131 labels: ``None`` 132 A list of labels to add to the ticket. 133 134 assignee: ``None`` 135 The name of the person to assign the ticket to. 136 137 CLI Examples: 138 139 .. code-block:: bash 140 141 salt '*' jira.create_issue NET 'Ticket title' 'Ticket description' 142 salt '*' jira.create_issue NET 'Issue on {{ opts.id }}' 'Error detected on {{ opts.id }}' template_engine=jinja 143 """ 144 if template_engine: 145 summary = __salt__["file.apply_template_on_contents"]( 146 summary, 147 template=template_engine, 148 context=context, 149 defaults=defaults, 150 saltenv=saltenv, 151 ) 152 description = __salt__["file.apply_template_on_contents"]( 153 description, 154 template=template_engine, 155 context=context, 156 defaults=defaults, 157 saltenv=saltenv, 158 ) 159 jira_ = _get_jira(server=server, username=username, password=password) 160 if not labels: 161 labels = [] 162 data = { 163 "project": {"key": project}, 164 "summary": summary, 165 "description": description, 166 "issuetype": {"name": issuetype}, 167 "priority": {"name": priority}, 168 "labels": labels, 169 } 170 data.update(salt.utils.args.clean_kwargs(**kwargs)) 171 issue = jira_.create_issue(data) 172 issue_key = str(issue) 173 if assignee: 174 assign_issue(issue_key, assignee) 175 return issue_key 176 177 178def assign_issue(issue_key, assignee, server=None, username=None, password=None): 179 """ 180 Assign the issue to an existing user. Return ``True`` when the issue has 181 been properly assigned. 182 183 issue_key 184 The JIRA ID of the ticket to manipulate. 185 186 assignee 187 The name of the user to assign the ticket to. 188 189 CLI Example: 190 191 .. code-block:: bash 192 193 salt '*' jira.assign_issue NET-123 example_user 194 """ 195 jira_ = _get_jira(server=server, username=username, password=password) 196 assigned = jira_.assign_issue(issue_key, assignee) 197 return assigned 198 199 200def add_comment( 201 issue_key, 202 comment, 203 visibility=None, 204 is_internal=False, 205 server=None, 206 username=None, 207 password=None, 208): 209 """ 210 Add a comment to an existing ticket. Return ``True`` when it successfully 211 added the comment. 212 213 issue_key 214 The issue ID to add the comment to. 215 216 comment 217 The body of the comment to be added. 218 219 visibility: ``None`` 220 A dictionary having two keys: 221 222 - ``type``: is ``role`` (or ``group`` if the JIRA server has configured 223 comment visibility for groups). 224 - ``value``: the name of the role (or group) to which viewing of this 225 comment will be restricted. 226 227 is_internal: ``False`` 228 Whether a comment has to be marked as ``Internal`` in Jira Service Desk. 229 230 CLI Example: 231 232 .. code-block:: bash 233 234 salt '*' jira.add_comment NE-123 'This is a comment' 235 """ 236 jira_ = _get_jira(server=server, username=username, password=password) 237 comm = jira_.add_comment( 238 issue_key, comment, visibility=visibility, is_internal=is_internal 239 ) 240 return True 241 242 243def issue_closed(issue_key, server=None, username=None, password=None): 244 """ 245 Check if the issue is closed. 246 247 issue_key 248 The JIRA iD of the ticket to close. 249 250 Returns: 251 252 - ``True``: the ticket exists and it is closed. 253 - ``False``: the ticket exists and it has not been closed. 254 - ``None``: the ticket does not exist. 255 256 CLI Example: 257 258 .. code-block:: bash 259 260 salt '*' jira.issue_closed NE-123 261 """ 262 if not issue_key: 263 return None 264 jira_ = _get_jira(server=server, username=username, password=password) 265 try: 266 ticket = jira_.issue(issue_key) 267 except jira.exceptions.JIRAError: 268 # Ticket not found 269 return None 270 return ticket.fields().status.name == "Closed" 271